skill
Curium
UnreviewedUse when the user wants to create, delete, pause, resume, stop, start, or manage Adom containers via the Curium API.
{
"schema_version": 1,
"type": "skill",
"slug": "curium",
"title": "Curium",
"brief": "Use when the user wants to create, delete, pause, resume, stop, start, or manage Adom containers via the Curium API.",
"version": "1.0.0",
"tags": [],
"license": "MIT",
"source_path": "SKILL.md",
"readme": "# Curium API -- Adom Container Management\n\nCurium is the Adom container orchestration service. It manages Docker containers, Traefik routing, SSH portal access, and USB/IP device attachment for user project containers.\n\n**Base URL**: `https://curium.adom.inc` (or wherever your Curium instance is hosted)\n\n## Authentication\n\nCurium 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:\n\n```bash\nAPI_KEY=$(cat /var/run/adom/api-key)\n# Then use: -b \"session_token=$API_KEY\" on curl calls if auth is added\n```\n\n## Container ID Format\n\nAll containers use the format: `{owner}-{repository}-{hex_hash}`\n\nExample: `john-gallia-f280e93ffec7e79d`\n\n- `owner`: project owner username\n- `repository`: project/repo name\n- `hex_hash`: 8-byte random hex identifier\n\n---\n\n## Container Endpoints\n\n### List Containers\n\n```\nGET /containers\n```\n\n**Query Parameters** (optional):\n| Param | Type | Description |\n|-------|------|-------------|\n| `owner` | string | Filter by owner username |\n| `repository` | string | Filter by repository name |\n\n**Response** (200):\n```json\n[\n {\n \"id\": \"john-gallia-f280e93ffec7e79d\",\n \"owner\": \"john\",\n \"repository\": \"gallia\",\n \"unique_hash\": \"f280e93ffec7e79d\",\n \"status\": \"running\",\n \"services\": {\n \"coder_url\": \"https://coder.john-gallia-f280e93ffec7e79d.containers.adom.inc\",\n \"s3_url\": \"https://s3.john-gallia-f280e93ffec7e79d.containers.adom.inc\",\n \"ssh_credentials\": {\n \"hostname\": \"ssh.containers.adom.inc\",\n \"port\": 2222,\n \"username\": \"john-gallia-f280e93ffec7e79d\"\n }\n }\n }\n]\n```\n\n### Create Container\n\n```\nPOST /containers\n```\n\n**Request Body**:\n```json\n{\n \"owner\": \"john\",\n \"repository\": \"gallia\",\n \"actor\": \"john\",\n \"resource_limits\": {\n \"cpu_shares\": 512,\n \"vcpu_count\": 2.0,\n \"memory_soft_limit\": 536870912,\n \"memory_hard_limit\": 1073741824\n }\n}\n```\n\n| Field | Type | Required | Default | Description |\n|-------|------|----------|---------|-------------|\n| `owner` | string | yes | -- | Project owner username |\n| `repository` | string | yes | -- | Project/repo name |\n| `actor` | string | yes | -- | User performing the action |\n| `resource_limits.cpu_shares` | int | no | 512 | Relative CPU priority |\n| `resource_limits.vcpu_count` | float | no | 2.0 | vCPU allocation (0.1-8) |\n| `resource_limits.memory_soft_limit` | int | no | 536870912 | Memory reservation in bytes (512 MiB) |\n| `resource_limits.memory_hard_limit` | int | no | 1073741824 | Memory max in bytes (1 GiB). Range: 128 MiB - 8 GiB |\n\n**Response** (201):\n```json\n{\n \"id\": \"john-gallia-f280e93ffec7e79d\",\n \"owner\": \"john\",\n \"repository\": \"gallia\",\n \"unique_hash\": \"f280e93ffec7e79d\",\n \"services\": { \"...\" }\n}\n```\n\n**What happens internally**:\n1. Fetches user orgs from Carbon (`/users/{actor}/orgs`)\n2. Creates API key via Carbon (`/internal/containers/create-api-key`)\n3. Copies project files from `{DATA_DIR}/public/projects/{owner}/{name}/latest`\n4. Creates isolated Docker bridge network named `{container_id}`\n5. Connects Traefik to the new network\n6. Creates container with Traefik labels for Coder (port 8080) and S3 (port 9000)\n7. Mounts project dir at `/home/adom/project`, API key at `/var/run/adom/api-key`\n8. Starts the container\n\n### Get Container\n\n```\nGET /containers/{id}\n```\n\n**Response** (200): Container object with status. Returns 404 if not found.\n\n### Delete Container\n\n```\nDELETE /containers/{id}?commit_state={bool}\n```\n\n| Param | Type | Required | Description |\n|-------|------|----------|-------------|\n| `commit_state` | bool | yes | If true, saves container state before deletion |\n\n**Response** (204)\n\nWhen `commit_state=true`:\n- Backs up current `latest/` to `history/{uuid}.tar.gz`\n- Replaces `latest/` with current container filesystem\n\nThen: force-kills container, removes volumes, disconnects Traefik, deletes network.\n\n### Pause Container\n\n```\nPOST /containers/{id}/pause\n```\n\n**Response** (204). Suspends all processes without removing the container.\n\n### Resume Container\n\n```\nPOST /containers/{id}/resume\n```\n\n**Response** (204). Resumes a paused container.\n\n### Stop Container\n\n```\nPOST /containers/{id}/stop\n```\n\n**Response** (204). Stops container without removing it.\n\n### Start Container\n\n```\nPOST /containers/{id}/start\n```\n\n**Response** (204). Starts a stopped container.\n\n### Commit Container State\n\n```\nPOST /containers/{id}/commit\n```\n\n**Response** (204). Creates a backup archive and replaces `latest/` with current filesystem.\n\n### Stream Container Stats\n\n```\nGET /containers/{id}/stats\n```\n\n**Without SSE**: Returns a single JSON stats snapshot (Docker `ContainerStatsResponse` format: CPU, memory, network I/O, block I/O, PIDs).\n\n**With SSE** (set `Accept: text/event-stream`): Streams stats as SSE events:\n```\nevent: stats\ndata: {\"cpu_stats\": {...}, \"memory_stats\": {...}, ...}\n```\n\n### Prune Containers\n\n```\nPOST /containers/prune\n```\n\n**Response** (204). Cleans up:\n- Non-existent containers labeled `adom-user-container`\n- Disconnects Traefik from orphaned networks\n- Deletes orphaned networks\n- Removes stale working directories, Traefik configs, and TLS certs\n\n---\n\n## Container Lifecycle\n\n```\ncreated --[start]--> running\nrunning --[pause]--> paused\npaused --[resume]--> running\nrunning --[stop]--> exited\nexited --[start]--> running\nany --[delete]--> removed\n```\n\n---\n\n## Workcell Endpoints\n\nWorkcells are physical hardware units (e.g., Raspberry Pis) with USB devices that can be associated with containers.\n\n### List Workcells\n\n```\nGET /workcells\n```\n\n**Response** (200):\n```json\n[\n {\n \"workcell_id\": \"raspberry-pi-001\",\n \"ip_address\": \"192.168.1.100\",\n \"connected_at\": \"2026-03-06T12:15:30Z\",\n \"container_id\": \"john-gallia-f280e93ffec7e79d\",\n \"associated_at\": \"2026-03-06T12:16:45Z\",\n \"bound_devices\": [\n {\n \"bus_id\": \"1-1\",\n \"bound_at\": \"2026-03-06T12:16:50Z\",\n \"dev_node\": \"/dev/ttyUSB0\",\n \"subsystem\": \"tty\",\n \"manufacturer\": \"Silicon Labs\",\n \"product\": \"CP2102 USB to UART Bridge Controller\",\n \"serial_number\": \"0001\",\n \"id_vendor\": 1659,\n \"id_product\": 6015,\n \"...\": \"additional device metadata\"\n }\n ]\n }\n]\n```\n\n### Get Workcell\n\n```\nGET /workcells/{id}\n```\n\n**Response** (200): Workcell object with bound devices. Returns 404 if not found.\n\n### Associate Container with Workcell\n\n```\nPUT /workcells/{id}/association\n```\n\n**Request Body**:\n```json\n{\n \"container_id\": \"john-gallia-f280e93ffec7e79d\"\n}\n```\n\n**Response** (200): Updated workcell object.\n\nAttaches all bound USB devices to the container via USB/IP.\n\n### Disassociate Container from Workcell\n\n```\nDELETE /workcells/{id}/association\n```\n\n**Response** (200): Updated workcell object (container_id becomes null).\n\nDetaches all USB devices and cleans up USB/IP ports.\n\n### Workcell WebSocket (Device Notifications)\n\n```\nGET /workcells/usbip-notify (WebSocket upgrade)\n```\n\nUsed by workcells to report device hot-plug events:\n\n**Initial handshake** (client sends):\n```json\n{\n \"workcell_id\": \"raspberry-pi-001\",\n \"ip_address\": \"192.168.1.100\"\n}\n```\n\n**Device bound** (client sends):\n```json\n{\n \"type\": \"device_bound\",\n \"bus_id\": \"1-1\",\n \"metadata\": { \"dev_node\": \"/dev/ttyUSB0\", \"manufacturer\": \"...\", \"...\" }\n}\n```\n\n**Device removed** (client sends):\n```json\n{\n \"type\": \"device_removed\",\n \"bus_id\": \"1-1\"\n}\n```\n\n---\n\n## Networking & Routing\n\nEach container gets its own Docker bridge network. Traefik connects to each network and routes traffic based on hostname:\n\n| Service | Hostname Pattern | Port |\n|---------|-----------------|------|\n| Coder (VS Code) | `coder.{container_id}.{BASE_HOSTNAME}` | 8080 |\n| S3 | `s3.{container_id}.{BASE_HOSTNAME}` | 9000 |\n| SSH | `ssh.{BASE_HOSTNAME}:2222` (username = container_id) | 2222 |\n\nDefault `BASE_HOSTNAME`: `containers.adom.inc` (production) or `containers.adom.localhost` (dev).\n\nTLS via Let's Encrypt on the `websecure` entrypoint (port 443).\n\n---\n\n## Directory Structure on Host\n\n```\n{DATA_DIR}/\n public/\n containers/{owner}/{repo}/{hash}/ # Working dir -> /home/adom/project\n projects/{owner}/{repo}/\n latest/ # Current project files\n history/{uuid}.tar.gz # Committed snapshots\n molecules/{scope}/ # Shared molecule dirs\n private/\n container-api-keys/{owner}/{repo}/{hash} # -> /var/run/adom/api-key\n traefik/dynamic-config/ # Per-container routing rules\n traefik/dynamic-certificates/ # Per-container TLS certs\n```\n\n---\n\n## Resource Defaults\n\n| Resource | Default | Range |\n|----------|---------|-------|\n| CPU shares | 512 | -- |\n| vCPU count | 2.0 | 0.1 - 8 |\n| Memory soft limit | 512 MiB | -- |\n| Memory hard limit | 1 GiB | 128 MiB - 8 GiB |\n\n---\n\n## Configuration (Environment Variables)\n\n| Var | Default | Description |\n|-----|---------|-------------|\n| `BIND_ADDR` | `0.0.0.0` | Server bind address |\n| `PORT` | `3000` | Server port |\n| `DATA_DIRECTORY_EXTERNAL` | -- | External data path |\n| `DATA_DIRECTORY_INTERNAL` | -- | Internal data path |\n| `BASE_HOSTNAME` | `containers.adom.localhost` | Base hostname for routing |\n| `USER_CONTAINER_IMAGE` | `adom/user-containers/default:latest` | Docker image for containers |\n| `DOCKER_SOCKET_PATH` | `/var/run/docker.sock` | Docker socket |\n| `KEEP_CONTAINER_WORKING_DIRECTORY` | -- | Set to `1` to preserve dirs on delete |\n\n---\n\n## Error Responses\n\n```json\n{\n \"code\": 500,\n \"error\": \"INTERNAL_SERVER_ERROR\",\n \"message\": \"descriptive error message\"\n}\n```\n\nStandard HTTP status codes: 200, 201, 204, 404, 500.\n\n---\n\n## Quick Examples\n\n**Create a container**:\n```bash\ncurl -s -X POST https://curium.adom.inc/containers \\\n -H 'Content-Type: application/json' \\\n -d '{\n \"owner\": \"john\",\n \"repository\": \"my-project\",\n \"actor\": \"john\",\n \"resource_limits\": {\n \"vcpu_count\": 4.0,\n \"memory_hard_limit\": 4294967296\n }\n }'\n```\n\n**Get container stats**:\n```bash\ncurl -s https://curium.adom.inc/containers/john-my-project-abc123def456/stats\n```\n\n**Pause and resume**:\n```bash\ncurl -s -X POST https://curium.adom.inc/containers/john-my-project-abc123def456/pause\ncurl -s -X POST https://curium.adom.inc/containers/john-my-project-abc123def456/resume\n```\n\n**Delete with state commit**:\n```bash\ncurl -s -X DELETE 'https://curium.adom.inc/containers/john-my-project-abc123def456?commit_state=true'\n```\n\n**List containers for a user**:\n```bash\ncurl -s 'https://curium.adom.inc/containers?owner=john'\n```\n\n**Associate a workcell**:\n```bash\ncurl -s -X PUT https://curium.adom.inc/workcells/raspberry-pi-001/association \\\n -H 'Content-Type: application/json' \\\n -d '{\"container_id\": \"john-gallia-f280e93ffec7e79d\"}'\n```",
"author": {
"id": "695820315b5f1e4db2fcf602",
"name": "Kyle Bergstedt",
"email": "[email protected]"
},
"visibility": {
"public": true
},
"hero": null,
"sample_prompts": [],
"discovery_triggers": [],
"discovery_pitch": null,
"metadata": {},
"created_at": "2026-05-28T05:29:57.439Z",
"updated_at": "2026-05-28T05:29:57.439Z",
"sub_skills": [],
"parent_app": null
}