# Curium API -- Adom Container Management

Curium is the Adom container orchestration service. It manages Docker containers, Traefik routing, SSH portal access, and USB/IP device attachment for user project containers.

**Base URL**: `https://curium.adom.inc` (or wherever your Curium instance is hosted)

## Authentication

Curium itself currently has minimal auth (TODOs in codebase). It integrates with **Carbon** for user/org lookups and API key creation. When calling Curium from within an Adom container, use the standard Adom API key pattern:

```bash
API_KEY=$(cat /var/run/adom/api-key)
# Then use: -b "session_token=$API_KEY" on curl calls if auth is added
```

## Container ID Format

All containers use the format: `{owner}-{repository}-{hex_hash}`

Example: `john-gallia-f280e93ffec7e79d`

- `owner`: project owner username
- `repository`: project/repo name
- `hex_hash`: 8-byte random hex identifier

---

## Container Endpoints

### List Containers

```
GET /containers
```

**Query Parameters** (optional):
| Param | Type | Description |
|-------|------|-------------|
| `owner` | string | Filter by owner username |
| `repository` | string | Filter by repository name |

**Response** (200):
```json
[
  {
    "id": "john-gallia-f280e93ffec7e79d",
    "owner": "john",
    "repository": "gallia",
    "unique_hash": "f280e93ffec7e79d",
    "status": "running",
    "services": {
      "coder_url": "https://coder.john-gallia-f280e93ffec7e79d.containers.adom.inc",
      "s3_url": "https://s3.john-gallia-f280e93ffec7e79d.containers.adom.inc",
      "ssh_credentials": {
        "hostname": "ssh.containers.adom.inc",
        "port": 2222,
        "username": "john-gallia-f280e93ffec7e79d"
      }
    }
  }
]
```

### Create Container

```
POST /containers
```

**Request Body**:
```json
{
  "owner": "john",
  "repository": "gallia",
  "actor": "john",
  "resource_limits": {
    "cpu_shares": 512,
    "vcpu_count": 2.0,
    "memory_soft_limit": 536870912,
    "memory_hard_limit": 1073741824
  }
}
```

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `owner` | string | yes | -- | Project owner username |
| `repository` | string | yes | -- | Project/repo name |
| `actor` | string | yes | -- | User performing the action |
| `resource_limits.cpu_shares` | int | no | 512 | Relative CPU priority |
| `resource_limits.vcpu_count` | float | no | 2.0 | vCPU allocation (0.1-8) |
| `resource_limits.memory_soft_limit` | int | no | 536870912 | Memory reservation in bytes (512 MiB) |
| `resource_limits.memory_hard_limit` | int | no | 1073741824 | Memory max in bytes (1 GiB). Range: 128 MiB - 8 GiB |

**Response** (201):
```json
{
  "id": "john-gallia-f280e93ffec7e79d",
  "owner": "john",
  "repository": "gallia",
  "unique_hash": "f280e93ffec7e79d",
  "services": { "..." }
}
```

**What happens internally**:
1. Fetches user orgs from Carbon (`/users/{actor}/orgs`)
2. Creates API key via Carbon (`/internal/containers/create-api-key`)
3. Copies project files from `{DATA_DIR}/public/projects/{owner}/{name}/latest`
4. Creates isolated Docker bridge network named `{container_id}`
5. Connects Traefik to the new network
6. Creates container with Traefik labels for Coder (port 8080) and S3 (port 9000)
7. Mounts project dir at `/home/adom/project`, API key at `/var/run/adom/api-key`
8. Starts the container

### Get Container

```
GET /containers/{id}
```

**Response** (200): Container object with status. Returns 404 if not found.

### Delete Container

```
DELETE /containers/{id}?commit_state={bool}
```

| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `commit_state` | bool | yes | If true, saves container state before deletion |

**Response** (204)

When `commit_state=true`:
- Backs up current `latest/` to `history/{uuid}.tar.gz`
- Replaces `latest/` with current container filesystem

Then: force-kills container, removes volumes, disconnects Traefik, deletes network.

### Pause Container

```
POST /containers/{id}/pause
```

**Response** (204). Suspends all processes without removing the container.

### Resume Container

```
POST /containers/{id}/resume
```

**Response** (204). Resumes a paused container.

### Stop Container

```
POST /containers/{id}/stop
```

**Response** (204). Stops container without removing it.

### Start Container

```
POST /containers/{id}/start
```

**Response** (204). Starts a stopped container.

### Commit Container State

```
POST /containers/{id}/commit
```

**Response** (204). Creates a backup archive and replaces `latest/` with current filesystem.

### Stream Container Stats

```
GET /containers/{id}/stats
```

**Without SSE**: Returns a single JSON stats snapshot (Docker `ContainerStatsResponse` format: CPU, memory, network I/O, block I/O, PIDs).

**With SSE** (set `Accept: text/event-stream`): Streams stats as SSE events:
```
event: stats
data: {"cpu_stats": {...}, "memory_stats": {...}, ...}
```

### Prune Containers

```
POST /containers/prune
```

**Response** (204). Cleans up:
- Non-existent containers labeled `adom-user-container`
- Disconnects Traefik from orphaned networks
- Deletes orphaned networks
- Removes stale working directories, Traefik configs, and TLS certs

---

## Container Lifecycle

```
created --[start]--> running
running --[pause]--> paused
paused  --[resume]--> running
running --[stop]--> exited
exited  --[start]--> running
any     --[delete]--> removed
```

---

## Workcell Endpoints

Workcells are physical hardware units (e.g., Raspberry Pis) with USB devices that can be associated with containers.

### List Workcells

```
GET /workcells
```

**Response** (200):
```json
[
  {
    "workcell_id": "raspberry-pi-001",
    "ip_address": "192.168.1.100",
    "connected_at": "2026-03-06T12:15:30Z",
    "container_id": "john-gallia-f280e93ffec7e79d",
    "associated_at": "2026-03-06T12:16:45Z",
    "bound_devices": [
      {
        "bus_id": "1-1",
        "bound_at": "2026-03-06T12:16:50Z",
        "dev_node": "/dev/ttyUSB0",
        "subsystem": "tty",
        "manufacturer": "Silicon Labs",
        "product": "CP2102 USB to UART Bridge Controller",
        "serial_number": "0001",
        "id_vendor": 1659,
        "id_product": 6015,
        "...": "additional device metadata"
      }
    ]
  }
]
```

### Get Workcell

```
GET /workcells/{id}
```

**Response** (200): Workcell object with bound devices. Returns 404 if not found.

### Associate Container with Workcell

```
PUT /workcells/{id}/association
```

**Request Body**:
```json
{
  "container_id": "john-gallia-f280e93ffec7e79d"
}
```

**Response** (200): Updated workcell object.

Attaches all bound USB devices to the container via USB/IP.

### Disassociate Container from Workcell

```
DELETE /workcells/{id}/association
```

**Response** (200): Updated workcell object (container_id becomes null).

Detaches all USB devices and cleans up USB/IP ports.

### Workcell WebSocket (Device Notifications)

```
GET /workcells/usbip-notify  (WebSocket upgrade)
```

Used by workcells to report device hot-plug events:

**Initial handshake** (client sends):
```json
{
  "workcell_id": "raspberry-pi-001",
  "ip_address": "192.168.1.100"
}
```

**Device bound** (client sends):
```json
{
  "type": "device_bound",
  "bus_id": "1-1",
  "metadata": { "dev_node": "/dev/ttyUSB0", "manufacturer": "...", "..." }
}
```

**Device removed** (client sends):
```json
{
  "type": "device_removed",
  "bus_id": "1-1"
}
```

---

## Networking & Routing

Each container gets its own Docker bridge network. Traefik connects to each network and routes traffic based on hostname:

| Service | Hostname Pattern | Port |
|---------|-----------------|------|
| Coder (VS Code) | `coder.{container_id}.{BASE_HOSTNAME}` | 8080 |
| S3 | `s3.{container_id}.{BASE_HOSTNAME}` | 9000 |
| SSH | `ssh.{BASE_HOSTNAME}:2222` (username = container_id) | 2222 |

Default `BASE_HOSTNAME`: `containers.adom.inc` (production) or `containers.adom.localhost` (dev).

TLS via Let's Encrypt on the `websecure` entrypoint (port 443).

---

## Directory Structure on Host

```
{DATA_DIR}/
  public/
    containers/{owner}/{repo}/{hash}/    # Working dir -> /home/adom/project
    projects/{owner}/{repo}/
      latest/                            # Current project files
      history/{uuid}.tar.gz              # Committed snapshots
    molecules/{scope}/                   # Shared molecule dirs
  private/
    container-api-keys/{owner}/{repo}/{hash}  # -> /var/run/adom/api-key
    traefik/dynamic-config/              # Per-container routing rules
    traefik/dynamic-certificates/        # Per-container TLS certs
```

---

## Resource Defaults

| Resource | Default | Range |
|----------|---------|-------|
| CPU shares | 512 | -- |
| vCPU count | 2.0 | 0.1 - 8 |
| Memory soft limit | 512 MiB | -- |
| Memory hard limit | 1 GiB | 128 MiB - 8 GiB |

---

## Configuration (Environment Variables)

| Var | Default | Description |
|-----|---------|-------------|
| `BIND_ADDR` | `0.0.0.0` | Server bind address |
| `PORT` | `3000` | Server port |
| `DATA_DIRECTORY_EXTERNAL` | -- | External data path |
| `DATA_DIRECTORY_INTERNAL` | -- | Internal data path |
| `BASE_HOSTNAME` | `containers.adom.localhost` | Base hostname for routing |
| `USER_CONTAINER_IMAGE` | `adom/user-containers/default:latest` | Docker image for containers |
| `DOCKER_SOCKET_PATH` | `/var/run/docker.sock` | Docker socket |
| `KEEP_CONTAINER_WORKING_DIRECTORY` | -- | Set to `1` to preserve dirs on delete |

---

## Error Responses

```json
{
  "code": 500,
  "error": "INTERNAL_SERVER_ERROR",
  "message": "descriptive error message"
}
```

Standard HTTP status codes: 200, 201, 204, 404, 500.

---

## Quick Examples

**Create a container**:
```bash
curl -s -X POST https://curium.adom.inc/containers \
  -H 'Content-Type: application/json' \
  -d '{
    "owner": "john",
    "repository": "my-project",
    "actor": "john",
    "resource_limits": {
      "vcpu_count": 4.0,
      "memory_hard_limit": 4294967296
    }
  }'
```

**Get container stats**:
```bash
curl -s https://curium.adom.inc/containers/john-my-project-abc123def456/stats
```

**Pause and resume**:
```bash
curl -s -X POST https://curium.adom.inc/containers/john-my-project-abc123def456/pause
curl -s -X POST https://curium.adom.inc/containers/john-my-project-abc123def456/resume
```

**Delete with state commit**:
```bash
curl -s -X DELETE 'https://curium.adom.inc/containers/john-my-project-abc123def456?commit_state=true'
```

**List containers for a user**:
```bash
curl -s 'https://curium.adom.inc/containers?owner=john'
```

**Associate a workcell**:
```bash
curl -s -X PUT https://curium.adom.inc/workcells/raspberry-pi-001/association \
  -H 'Content-Type: application/json' \
  -d '{"container_id": "john-gallia-f280e93ffec7e79d"}'
```