{
  "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
}