API Reference
API Reference
Comprehensive API documentation for the agentic-sandbox management server.
Overview
The management server exposes three network interfaces:
| Port | Protocol | Purpose |
|---|---|---|
| 8120 | gRPC | Agent bidirectional communication |
| 8121 | WebSocket | Real-time output streaming for dashboard |
| 8122 | HTTP | REST API and web dashboard |
Authentication
gRPC (Agents): Secure transport provisions authenticate agents with mTLS client identity material generated during VM provisioning. Plain TCP has no transport identity and is rejected; the legacy `x-agent-secret` compatibility path was retired in #412.
HTTP/WebSocket: No authentication required for local-host operator access. Exception: the AIWG executor-contract route `POST /api/v1/sessions/:id/dispatch` requires `Authorization: Bearer <token>` where the token is issued by `aiwg serve` at executor registration. See AIWG Executor Contract for the full integration.
Common Response Format
All HTTP endpoints return JSON. Error responses follow this structure:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message"
}
}
HTTP REST API
Base URL: `http://localhost:8122` for the default loopback-only development listener. Production remote access should use the TLS/admin listener or a trusted tunnel; plaintext non-loopback management TCP is rejected unless the operator sets an explicit unsafe override.
Activity metadata API (v2)
`POST /api/v2/activity/ingest` accepts bounded `activity.event/v1` metadata batches and returns an acknowledgement only after durable SQLite commit. Replaying an `event_id` is idempotent. Sequence jumps are recorded durably and returned in the acknowledgement. Ingest requires the admin role.
`GET /api/v2/activity/events` returns correlated metadata plus collector coverage. Filters include `event_name`, session, mission, task, tool call, command, process, trace, time bounds, and a maximum `limit` of 5,000. `GET /api/v2/activity/coverage` returns coverage without event rows.
All three endpoints require the authorized scope headers `X-Agentic-Tenant-Id`, `X-Agentic-Host-Id`, `X-Agentic-Instance-Id`, and `X-Agentic-Agent-Id`; ingest also requires `X-Agentic-Collector-Id`. The event scope must match those values. These headers carry scope, not credentials, and must be protected by the authenticated TLS/UDS management boundary.
The store is metadata-only: restricted content, secret-prohibited records, and common secret-bearing payload keys are rejected. Existing events, audit, transcript, logs, and metrics endpoints remain unchanged. The stable schema is `activity-event-v1.schema.json`, and ADR-034 documents identifier ownership and compatibility mapping.
Libvirt checkpoint and warm-pool API (v2)
The QEMU/libvirt fast-resume path is exposed as asynchronous admin operations:
| Method | Path | Purpose |
|---|---|---|
| `POST` | `/api/v2/admin/libvirt/checkpoints` | Capture a verified pre-enrollment VM after detaching virtiofs; persist RAM/device state, NVRAM, and restore metadata. |
| `POST` | `/api/v2/admin/libvirt/checkpoints/{id}/restore` | Restore the saved libvirt device identity with a fresh tenant instance id, vsock CID, disk overlay, NVRAM, bootstrap token, and mTLS enrollment. |
| `POST` | `/api/v2/admin/libvirt/warm-pools` | Reserve N distinct pre-booted checkpoints as consumable slots. |
| `POST` | `/api/v2/admin/libvirt/warm-pools/{pool}/handoff` | Atomically claim one slot, restore it, and require fresh enrollment. |
The management path only accepts `pre_enrollment: true`; it will not create a secret-bearing RAM checkpoint. One-time bootstrap material is sent to the host wrapper over stdin, staged mode `0600` in the child's isolated inbox, and removed by the guest enrollment trigger. Raw tokens are not included in the operation result or command line.
curl -X POST http://localhost:8122/api/v2/admin/libvirt/checkpoints \
-H 'content-type: application/json' \
-d '{"vm":"qemu-clean-base","checkpoint_id":"qemu-clean-v1","pre_enrollment":true}'
curl -X POST http://localhost:8122/api/v2/admin/libvirt/warm-pools \
-H 'content-type: application/json' \
-d '{"checkpoint_ids":["qemu-clean-v1","qemu-clean-v2"],"pool":"qemu-default"}'
curl -X POST http://localhost:8122/api/v2/admin/libvirt/warm-pools/qemu-default/handoff \
-H 'content-type: application/json' \
-d '{}'
Libvirt requires a saved-state restore to retain its source domain name and UUID. A direct restore's `name` must therefore match the checkpoint's source VM; a warm handoff selects that name from its consumed slot. Tenant identity is the new canonical instance id and mTLS identity, not the libvirt name.
Each response is `202 Accepted` with a `Location` header pointing to `/api/v2/admin/operations/{id}`. See `docs/contracts/admin-api.openapi.yaml` for the complete schemas.
Bootstrap readiness API (v2)
`GET /api/v2/admin/bootstrap/readiness` returns the client-safe posture needed by dashboards and automation before requiring secure agent bootstrap. It reports whether the gRPC CA backend is configured, the CA backend class, trust domain, trust-bundle freshness, bootstrap token-store counts, and the stable enrollment error taxonomy used by `/api/v1/bootstrap-enrollment/consume`.
The response deliberately omits certificate PEM bodies, token hashes, bearer values, private keys, CSRs, and filesystem paths. Clients should present `secure` as ready for mTLS bootstrap, `disabled` as an explicit plaintext/dev or unsupported state, and `degraded`/`failed` as fail-closed when secure transport is required.
Health & Monitoring
GET /healthz
Simple liveness probe. Returns 200 if server is running.
Response: `200 OK` with JSON body `{"status":"alive"}`
Example:
curl http://localhost:8122/healthz
GET /readyz
Readiness probe. Returns 200 if server is ready to accept traffic.
Response:
{
"ready": true,
"reason": "agents_connected"
}
Status Codes:
- `200` - Ready
- `503` - Not ready (returns reason)
Example:
curl http://localhost:8122/readyz
GET /healthz/deep
Detailed health check with metrics.
Response:
{
"status": "healthy",
"uptime_seconds": 0,
"agent_count": 2,
"active_tasks": 0
}
Example:
curl http://localhost:8122/healthz/deep
GET /healthz/libvirt
Bounded libvirt RPC health probe. Returns `200` when libvirt answers within the read budget, or `503` when libvirt is down, slow, or the fail-fast circuit is open.
Response:
{
"status": "healthy",
"libvirt": "alive"
}
On timeout the response uses the same structured VM error body as `/api/v1/vms` and includes `Retry-After`.
Example:
curl -i http://localhost:8122/healthz/libvirt
GET /metrics
Prometheus metrics endpoint.
Response: Prometheus text format
Example:
curl http://localhost:8122/metrics
Agents
GET /api/v1/agents
List all connected agents with their status and metrics.
Response:
{
"agents": [
{
"id": "agent-01",
"hostname": "agent-01",
"ip_address": "192.168.122.201",
"status": "Ready",
"connected_at": 1706572800000,
"last_heartbeat": 1706572830000,
"metrics": {
"cpu_percent": 2.3,
"memory_used_bytes": 536870912,
"memory_total_bytes": 8589934592,
"disk_used_bytes": 2147483648,
"disk_total_bytes": 53687091200,
"load_avg": [0.15, 0.20, 0.18],
"uptime_seconds": 3600
},
"system_info": {
"os": "Ubuntu 24.04",
"kernel": "6.8.0-generic",
"cpu_cores": 4,
"memory_bytes": 8589934592,
"disk_bytes": 53687091200
}
}
]
}
Field Descriptions:
- `status`: `"Starting"`, `"Ready"`, `"Busy"`, `"Error"`, `"ShuttingDown"`, `"Stale"`, `"Disconnected"`
- `connected_at`: Unix timestamp (milliseconds)
- `last_heartbeat`: Unix timestamp (milliseconds)
- `metrics`: Optional, current resource usage
- `system_info`: Optional, VM hardware information
Example:
curl http://localhost:8122/api/v1/agents
Virtual Machines
VM endpoints are QEMU-specific.
GET /api/v1/vms
List all VMs managed by libvirt.
Query Parameters:
- `state` (string, default: "all") - Filter by state: `"running"`, `"stopped"`, `"all"`
- `prefix` (string, default: "agent-") - Filter by name prefix. Use `"*"` for all VMs.
Response:
{
"vms": [
{
"name": "agent-01",
"state": "running",
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"vcpus": 4,
"memory_mb": 8192,
"ip_address": "192.168.122.201",
"uptime_seconds": null
}
],
"total": 1
}
States:
- `"running"`, `"stopped"`, `"paused"`, `"shutdown"`, `"crashed"`, `"suspended"`, `"unknown"`
Example:
# List all agent VMs
curl http://localhost:8122/api/v1/vms
# List only running VMs
curl http://localhost:8122/api/v1/vms?state=running
# List all VMs (including non-agent VMs)
curl http://localhost:8122/api/v1/vms?prefix=*
GET /api/v1/vms/{name}
Get detailed information about a specific VM.
Response:
{
"name": "agent-01",
"state": "running",
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"vcpus": 4,
"memory_mb": 8192,
"ip_address": "192.168.122.201",
"uptime_seconds": null,
"agent": {
"connected": true,
"connected_at": 1706572800000,
"hostname": "agent-01"
}
}
Status Codes:
- `200` - Success
- `404` - VM not found
Example:
curl http://localhost:8122/api/v1/vms/agent-01
POST /api/v1/vms
Create a new VM using the provisioning script.
Request Body:
{
"name": "agent-03",
"profile": "agentic-dev",
"vcpus": 4,
"memory_mb": 8192,
"disk_gb": 50,
"agentshare": true,
"start": true,
"ssh_key": "/home/user/.ssh/id_ed25519.pub"
}
Field Descriptions:
- `name` (string, required) - VM name (must match `^agent-[a-z0-9-]+$`)
- `profile` (string, default: "agentic-dev") - Provisioning profile: `"agentic-dev"`, `"basic"`
- `vcpus` (u32, default: 4) - Number of CPU cores
- `memory_mb` (u64, default: 8192) - Memory in megabytes
- `disk_gb` (u64, default: 50) - Disk size in gigabytes
- `agentshare` (bool, default: true) - Enable virtiofs shared storage
- `start` (bool, default: true) - Start VM after provisioning
- `ssh_key` (string, optional) - Dev/break-glass direct-runtime SSH public key.
This bypasses gateway policy/audit guarantees and is not the managed-profile SSH direction. Managed `agentic-dev` provisioning omits unmanaged direct runtime SSH keys by default; set `AGENTIC_ENABLE_DIRECT_RUNTIME_SSH=1` only for explicit dev/break-glass access. See `ADR-029`.
Response: `202 Accepted`
{
"operation": {
"id": "op-12345678-1234-1234-1234-123456789abc",
"type": "vm_create",
"status": "pending",
"target": "agent-03",
"created_at": "2024-01-30T12:00:00Z",
"progress_percent": 0
},
"vm": null
}
Status Codes:
- `202` - Accepted (provisioning started)
- `400` - Invalid request (e.g., invalid VM name)
- `409` - VM already exists
Error Codes:
- `INVALID_VM_NAME` - Name doesn't match required pattern
- `VM_ALREADY_EXISTS` - VM with this name already exists
- `PROVISIONING_ERROR` - Provisioning script failed
Example:
curl -X POST http://localhost:8122/api/v1/vms \
-H "Content-Type: application/json" \
-d '{
"name": "agent-03",
"profile": "agentic-dev",
"vcpus": 4,
"memory_mb": 8192,
"disk_gb": 50,
"agentshare": true,
"start": true
}'
# Minimal request (uses all defaults)
curl -X POST http://localhost:8122/api/v1/vms \
-H "Content-Type: application/json" \
-d '{"name": "agent-04"}'
POST /api/v1/vms/{name}/start
Start a stopped VM.
Response:
{
"vm": {
"name": "agent-01",
"state": "running"
},
"message": null
}
Status Codes:
- `200` - Success (idempotent - returns 200 even if already running)
Example:
curl -X POST http://localhost:8122/api/v1/vms/agent-01/start
POST /api/v1/vms/{name}/stop
Gracefully stop a running VM (ACPI shutdown).
Response:
{
"vm": {
"name": "agent-01",
"state": "shutdown"
},
"message": "Graceful shutdown initiated"
}
Status Codes:
- `200` - Success (idempotent)
Example:
curl -X POST http://localhost:8122/api/v1/vms/agent-01/stop
POST /api/v1/vms/{name}/destroy
Force stop a running VM (immediate termination).
Response:
{
"vm": {
"name": "agent-01",
"state": "stopped"
},
"message": "VM destroyed"
}
Status Codes:
- `200` - Success (idempotent)
Example:
curl -X POST http://localhost:8122/api/v1/vms/agent-01/destroy
POST /api/v1/vms/{name}/restart
Restart a running VM.
Request Body:
{
"mode": "graceful",
"timeout_seconds": 60
}
Field Descriptions:
- `mode` (string, default: "graceful") - Restart mode: `"graceful"` (ACPI shutdown) or `"hard"` (force destroy)
- `timeout_seconds` (u64, default: 60) - Timeout for graceful shutdown before forcing
Response: `202 Accepted`
{
"operation": {
"id": "op-12345678-1234-1234-1234-123456789abc",
"type": "vm_restart",
"status": "pending",
"target": "agent-01",
"created_at": "2024-01-30T12:00:00Z",
"progress_percent": 0
},
"vm": null
}
Status Codes:
- `202` - Accepted
- `404` - VM not found
- `409` - VM not running
Example:
# Graceful restart with default timeout
curl -X POST http://localhost:8122/api/v1/vms/agent-01/restart \
-H "Content-Type: application/json" \
-d '{"mode": "graceful", "timeout_seconds": 60}'
# Hard restart (immediate)
curl -X POST http://localhost:8122/api/v1/vms/agent-01/restart \
-H "Content-Type: application/json" \
-d '{"mode": "hard"}'
DELETE /api/v1/vms/{name}
Delete a VM definition from libvirt.
Query Parameters:
- `delete_disk` (bool, default: false) - Also delete VM disk image
- `force` (bool, default: false) - Force delete even if running
Response:
{
"deleted": true,
"name": "agent-01",
"disk_deleted": true
}
Status Codes:
- `200` - Success
- `404` - VM not found
- `409` - VM is running and force=false
Error Codes:
- `VM_NOT_FOUND` - VM doesn't exist
- `VM_RUNNING` - VM is running and force not set
Example:
# Delete VM (keep disk)
curl -X DELETE http://localhost:8122/api/v1/vms/agent-01
# Delete VM and disk
curl -X DELETE "http://localhost:8122/api/v1/vms/agent-01?delete_disk=true"
# Force delete running VM
curl -X DELETE "http://localhost:8122/api/v1/vms/agent-01?force=true&delete_disk=true"
POST /api/v1/vms/{name}/deploy-agent
Deploy agent binary to a running VM.
Response: `202 Accepted`
{
"operation": {
"id": "op-12345678-1234-1234-1234-123456789abc",
"type": "vm_create",
"status": "pending",
"target": "agent-01",
"created_at": "2024-01-30T12:00:00Z",
"progress_percent": 0
},
"vm": null
}
Status Codes:
- `202` - Accepted
- `404` - VM not found
- `409` - VM not running
Example:
curl -X POST http://localhost:8122/api/v1/vms/agent-01/deploy-agent
Operations
Long-running operations (VM create, restart, deploy) return operation IDs that can be polled for status.
GET /api/v1/operations/{id}
Get operation status.
Response:
{
"id": "op-12345678-1234-1234-1234-123456789abc",
"type": "vm_create",
"status": "completed",
"target": "agent-03",
"created_at": "2024-01-30T12:00:00Z",
"completed_at": "2024-01-30T12:05:00Z",
"progress_percent": 100,
"result": {
"vm": {
"name": "agent-03",
"state": "running"
}
}
}
Field Descriptions:
- `type`: `"vm_create"`, `"vm_delete"`, `"vm_restart"`
- `status`: `"pending"`, `"running"`, `"completed"`, `"failed"`
- `progress_percent`: 0-100
- `result`: Operation-specific result data (only on completion)
Failed Operation Response:
{
"id": "op-12345678-1234-1234-1234-123456789abc",
"type": "vm_create",
"status": "failed",
"error": "Provisioning script failed with exit code 1",
"target": "agent-03",
"created_at": "2024-01-30T12:00:00Z",
"completed_at": "2024-01-30T12:02:00Z",
"progress_percent": 20
}
Status Codes:
- `200` - Success
- `404` - Operation not found
Example:
curl http://localhost:8122/api/v1/operations/op-12345678-1234-1234-1234-123456789abc
Events
VM lifecycle and agent events are tracked and available for querying.
POST /api/v1/events
Receive event from the Rust `vm-event-bridge` service (internal use).
Request Body:
{
"event_type": "vm.started",
"vm_name": "agent-01",
"timestamp": "2024-01-30T12:00:00Z",
"details": {
"reason": "manual"
},
"agent_id": "agent-01",
"trace_id": null
}
Response:
{
"received": true
}
GET /api/v1/events
List recent events across all VMs and agents.
Response:
{
"events": [
{
"event_type": "vm.started",
"vm_name": "agent-01",
"timestamp": "2024-01-30T12:00:00Z",
"details": {
"reason": "manual"
},
"agent_id": "agent-01",
"trace_id": null
}
],
"total_count": 42,
"last_event_id": 42
}
Event Types:
VM Lifecycle:
- `vm.started`, `vm.stopped`, `vm.crashed`, `vm.shutdown`, `vm.rebooted`
- `vm.suspended`, `vm.resumed`, `vm.defined`, `vm.undefined`, `vm.pmsuspended`
Agent Events:
- `agent.connected`, `agent.disconnected`, `agent.registered`, `agent.heartbeat`
- `agent.command.started`, `agent.command.completed`
- `agent.pty.created`, `agent.pty.closed`
Session Reconciliation:
- `session.query_sent`, `session.report_received`
- `session.reconcile_started`, `session.reconcile_complete`
- `session.killed`, `session.preserved`, `session.reconcile_failed`
Example:
curl http://localhost:8122/api/v1/events
HTTP Credential Proxy
The HTTP credential proxy is the ADR-028 backend for web/API integrations that can use a broker instead of receiving raw upstream secrets. A workload submits a lease reference, matching agent/instance/session scope, and target HTTP request. The server validates the active lease and `proxy_policy`, injects the credential only into the outbound upstream request, and redacts that credential from returned headers and body.
See Credential Proxy for the full policy and runtime guidance.
POST /api/v2/credential-proxy/http
Proxy one HTTP request through an active credential lease.
Request Body:
{
"lease_id": "lease_...",
"agent_id": "agent-01",
"instance_id": "agent-01",
"session_id": "session-01",
"method": "GET",
"url": "https://api.example.test/v1/resource",
"headers": {
"accept": "application/json"
},
"body": null
}
Response:
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": "{\"ok\":true}"
}
The proxy denies missing, revoked, expired, scope-mismatched, or policyless leases. It also denies targets outside the lease policy's allowed hosts, path prefixes, methods, and workload-supplied header allowlist.
Gateway SSH Certificate Leases
The gateway SSH lease API is the #531 credential contract for gateway-mediated SSH access. It requires an authenticated operator identity and issues short-lived, principal-scoped lease records for the SSH connector and CLI path. It does not proxy SSH bytes and it does not persist private keys, certificate bodies, command payloads, or transcript data.
When `AGENTIC_GATEWAY_SSH_CA_KEY` points at an OpenSSH CA private key, lease issuance signs the submitted public key and returns the OpenSSH user certificate in the `POST` response only. List/get/audit paths retain only metadata and fingerprints.
Runtime trust is opt-in during VM provisioning. Set `AGENTIC_GATEWAY_SSH_CA_PUBLIC_KEY_HOST_PATH` to the OpenSSH CA public key, or set `AGENTIC_GATEWAY_SSH_CA_KEY` and keep the matching `.pub` file beside it. Cloud-init writes only the public CA key into the guest, configures `TrustedUserCAKeys`, and restricts accepted certificate principals through `AuthorizedPrincipalsFile /etc/ssh/agentic-authorized-principals/%u`.
By default the provisioner authorizes only the service user principal (`agent`) for the service account. Operators may override the target user with `AGENTIC_GATEWAY_SSH_AUTHORIZED_USER` and the accepted certificate principals with `AGENTIC_GATEWAY_SSH_AUTHORIZED_PRINCIPALS` (comma or space separated). Private CA key material is never written into cloud-init user-data.
Gateway SSH Connector
The gateway SSH connector is the #530 point-to-point byte-stream backend for gateway-mediated SSH. It is opt-in and separate from the terminal WebSocket and PTY fanout paths. The connector reads one newline-delimited JSON prelude from the client, resolves the requested instance to a configured runtime SSH endpoint, records session audit events, and then proxies the remaining SSH stream without retaining or rebroadcasting payload bytes.
Enable the listener with `AGENTIC_GATEWAY_SSH_LISTEN`, for example `127.0.0.1:8124`. Provide explicit runtime targets with `AGENTIC_GATEWAY_SSH_TARGETS` as a comma-separated map, and provide explicit routing policy with `AGENTIC_GATEWAY_SSH_ALLOWLIST` as `actor=instance` rules. The instance side may be `` for a controlled break-glass actor, and the actor side may be `` for a controlled target-wide rule:
AGENTIC_GATEWAY_SSH_LISTEN=127.0.0.1:8124
AGENTIC_GATEWAY_SSH_TARGETS=agent-01=127.0.0.1:2222,agent-02=127.0.0.1:2223
[email protected]=agent-01
The client prelude format is:
{"actor":"[email protected]","instance_id":"agent-01","access_mode":"ssh"}
The prelude is followed by a newline and then the raw SSH stream. Operators normally use `sandboxctl ssh`, which hides this framing behind an OpenSSH `ProxyCommand` and requests a short-lived gateway SSH lease when a local public key is available:
sandboxctl ssh agent-01
By default the CLI connects to the gateway connector at `127.0.0.1:8124`, or the address in `AGENTIC_GATEWAY_SSH_CONNECT`. Use `--gateway` to override it per call. The connector prelude actor comes from `--actor`, `AGENTIC_GATEWAY_SSH_ACTOR`, the active context role, or `$USER`, in that order, and must match the connector allowlist. Lease API actor metadata is derived from the authenticated operator identity rather than the request body.
Advanced OpenSSH tools can use generated config:
sandboxctl ssh-config agent-01 > /tmp/agent-01.ssh_config
ssh -F /tmp/agent-01.ssh_config agent-01
scp -F /tmp/agent-01.ssh_config ./artifact.txt agent-01:/tmp/artifact.txt
sftp -F /tmp/agent-01.ssh_config agent-01
Generated config routes through `sandboxctl ssh-proxy`; it is point-to-point SSH through the gateway connector, not `pty-ws` fanout, replay, observers, or multi-controller session sharing.
Managed `agentic-dev` VM provisioning omits direct-runtime `authorized_keys` by default. The `basic` profile remains the dev/break-glass direct SSH profile, and operators may explicitly opt back into direct runtime SSH keys with `AGENTIC_ENABLE_DIRECT_RUNTIME_SSH=1`.
POST /api/v2/gateway/ssh/leases
Issue a metadata-only SSH access lease.
Request Body:
{
"actor": "[email protected]",
"instance_id": "agent-01",
"principal": "agent",
"access_mode": "ssh",
"public_key": "ssh-ed25519 AAAA... [email protected]",
"ttl_seconds": 900
}
Response: `201 Created`
{
"id": "sshlease_...",
"actor": "[email protected]",
"instance_id": "agent-01",
"principal": "agent",
"access_mode": "ssh",
"public_key_sha256": "sha256:...",
"issued_at": "2026-06-22T01:00:00Z",
"expires_at": "2026-06-22T01:15:00Z",
"ttl_seconds": 900,
"state": "active",
"certificate_key_id": "sshlease_...",
"certificate_sha256": "sha256:...",
"certificate": "[email protected] AAAA...",
"revoked_at": null,
"revocation_effect": "metadata_only_until_certificate_expiry"
}
The submitted `public_key` is hashed to `public_key_sha256`; callers must not expect the key body in any response. `certificate` is present only on successful issuance when a gateway SSH CA key is configured; it is omitted from list/get responses and is not written to audit records. Lease issue and revoke operations emit `gateway_ssh_lease` security audit records when the audit logger is configured.
Revocation marks the gateway lease metadata as revoked. OpenSSH certificates that have already been returned to clients remain governed by their short certificate validity window until runtime-enforced revocation, such as KRL or a policy-backed principals command, is added.
GET /api/v2/gateway/ssh/leases
List gateway SSH lease metadata.
GET /api/v2/gateway/ssh/leases/{lease_id}
Get one gateway SSH lease.
DELETE /api/v2/gateway/ssh/leases/{lease_id}
Revoke a gateway SSH lease. Revoked records remain visible as metadata and return `revocation_effect: "metadata_only_until_certificate_expiry"`.
Tasks
Task orchestration endpoints for submitting and managing Claude Code tasks.
POST /api/v1/tasks
Submit a new task from a manifest.
Request Body:
{
"manifest_yaml": "name: example-task\
repository:\
url: https://github.com/user/repo\
prompt: 'Fix the bug in main.rs'"
}
OR
{
"manifest": {
"name": "example-task",
"repository": {
"url": "https://github.com/user/repo"
},
"prompt": "Fix the bug in main.rs"
}
}
Response: `202 Accepted`
{
"task_id": "task-12345678-1234-1234-1234-123456789abc",
"accepted": true,
"error": null
}
Status Codes:
- `202` - Accepted
- `400` - Invalid manifest
- `503` - Orchestrator not available
Example:
curl -X POST http://localhost:8122/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"manifest": {
"name": "fix-bug",
"repository": {
"url": "https://github.com/user/repo"
},
"prompt": "Fix the authentication bug"
}
}'
GET /api/v1/tasks
List all tasks with optional filtering.
Query Parameters:
- `state` (string, optional) - Comma-separated states: `pending`, `staging`, `provisioning`, `ready`, `running`, `completing`, `completed`, `failed`, `failed_preserved`, `cancelled`
- `limit` (usize, default: 50) - Max results
- `offset` (usize, default: 0) - Pagination offset
Response:
{
"tasks": [
{
"id": "task-12345678-1234-1234-1234-123456789abc",
"name": "fix-bug",
"state": "running",
"state_message": "Claude Code executing",
"created_at": "2024-01-30T12:00:00Z",
"started_at": "2024-01-30T12:01:00Z",
"state_changed_at": "2024-01-30T12:01:30Z",
"vm_name": "agent-task-abc123",
"vm_ip": "192.168.122.220",
"exit_code": null,
"error": null,
"progress": {
"output_bytes": 4096,
"tool_calls": 5,
"current_tool": "bash",
"last_activity_at": "2024-01-30T12:05:00Z"
}
}
],
"total_count": 1
}
Example:
# List all tasks
curl http://localhost:8122/api/v1/tasks
# List only running tasks
curl "http://localhost:8122/api/v1/tasks?state=running"
# List completed and failed tasks
curl "http://localhost:8122/api/v1/tasks?state=completed,failed"
GET /api/v1/tasks/{id}
Get task status.
Response:
{
"id": "task-12345678-1234-1234-1234-123456789abc",
"name": "fix-bug",
"state": "completed",
"state_message": "Task completed successfully",
"created_at": "2024-01-30T12:00:00Z",
"started_at": "2024-01-30T12:01:00Z",
"state_changed_at": "2024-01-30T12:10:00Z",
"vm_name": "agent-task-abc123",
"vm_ip": "192.168.122.220",
"exit_code": 0,
"error": null,
"progress": {
"output_bytes": 102400,
"tool_calls": 23,
"current_tool": null,
"last_activity_at": "2024-01-30T12:10:00Z"
}
}
Status Codes:
- `200` - Success
- `404` - Task not found
Example:
curl http://localhost:8122/api/v1/tasks/task-12345678-1234-1234-1234-123456789abc
DELETE /api/v1/tasks/{id}
Cancel a running task.
Request Body:
{
"reason": "User cancelled via dashboard"
}
Response:
{
"success": true,
"error": null
}
Status Codes:
- `200` - Success
- `400` - Cannot cancel (e.g., already completed)
- `404` - Task not found
Example:
curl -X DELETE http://localhost:8122/api/v1/tasks/task-12345678-1234-1234-1234-123456789abc \
-H "Content-Type: application/json" \
-d '{"reason": "User requested cancellation"}'
GET /api/v1/tasks/{id}/logs
Stream task logs via Server-Sent Events (SSE).
Response: SSE stream
Event Types:
- `stdout` - Standard output from Claude Code
- `stderr` - Standard error from Claude Code
- `event` - Structured event (JSON)
- `completed` - Task finished (data: exit code)
- `error` - Task error (data: error message)
Status Codes:
- `200` - Success (streaming)
- `404` - Task not found
Example:
curl -N http://localhost:8122/api/v1/tasks/task-12345678-1234-1234-1234-123456789abc/logs
SSE Output:
event: stdout
data: Analyzing codebase...
event: stdout
data: Running tests...
event: completed
data: 0
GET /api/v1/agent-output/stream
Stream structured agent output via Server-Sent Events (SSE). This endpoint projects the management server's agent-output bus as JSON events while preserving raw output bytes in `data_base64`.
Query Parameters:
- `agent_id` - Optional exact agent id filter
- `command_id` - Optional exact command id filter
- `stream` - Optional stream filter: `stdout`, `stderr`, or `log`
- `replay` - Set to `true` to replay buffered output for `command_id`
- `limit` - Maximum replayed chunks, capped at 1000
`command_id` is required when `replay=true` because replay is backed by the bounded per-command output buffer.
Event Types:
- `agent_output` - Live output chunk
- `agent_output.replay` - Replayed buffered output chunk
- `agent_output.error` - Stream-level error, such as a slow subscriber
Example:
curl -N 'http://localhost:8122/api/v1/agent-output/stream?agent_id=agent-1&stream=stdout'
SSE Output:
event: agent_output
data: {"schema":"agentic.agent_output.v1","event_type":"chunk","agent_id":"agent-1","command_id":"cmd-1","stream":"stdout","timestamp_ms":1783540800000,"data_base64":"SGVsbG8K","text":"Hello\
"}
GET /api/v1/agent-output/chat
Normalized, message-oriented projection of a command's `stream-json` output for Chat clients such as AIWG Cockpit (#600). Where `/agent-output/stream` carries raw output bytes, this endpoint parses Claude Code `stream-json` into assistant-message, tool-call, tool-result, and status events. It is a read-only projection — subscribing confers no controller input authority, and the raw stream stays authoritative.
The frames follow the Fortemi `POST /api/v1/chat/stream` SSE envelope for wire compatibility: named events with a JSON `data` object and monotonic `{session}-{seq}` event ids. The `delta` / `done` / `error` events carry Fortemi's exact fields as a subset, so a Fortemi-only client consumes the assistant-text projection unchanged; `tool_call` / `tool_result` / `status` / `raw` are additive events a superset client reads.
Query Parameters:
- `command_id` - Required. Command whose `stream-json` output to project.
- `session_id` - Optional override for the `{session}-{seq}` id space. Defaults
to the formal session id mapped from `command_id`.
- `replay` - Set to `true` to replay buffered output before following live.
Headers:
- `Last-Event-ID` - Resume after a `{session}-{seq}` cursor. An unknown/expired
command terminates with a Fortemi `STREAM_INTERRUPTED` error frame rather than hanging.
Event Types:
- `delta` - Assistant text chunk — `{"content": ..., "role":"assistant", ...}`
- `tool_call` - Tool invocation — `{"name": ..., "input": ..., ...}`
- `tool_result` - Tool output — `{"tool_id": ..., "status":"ok"|"error", ...}`
- `status` - Session/system status — `{"status": ..., ...}`
- `done` - Terminal completion — `{"finish_reason":"stop","model":..., "usage":...}`
- `error` - Terminal error — `{"error": ..., "code": ...}`
- `raw` - Unparsed output line preserved rather than dropped
Every `data` object carries `session_id` and a `raw_ref` (`{"command_id":..., "line":...}`) for provenance back to the raw stream.
Session capability is advertised on the session APIs via `chat_source` (`stream-json` | `none`) and a ready-to-use `chat_stream_url`. `none` means the runtime has no structured projection (e.g. an interactive shell, or Codex — tracked as follow-up); clients should show Terminal only.
Example:
curl -N 'http://localhost:8122/api/v1/agent-output/chat?command_id=cmd-1'
SSE Output:
event: delta
id: sess-1-0
data: {"role":"assistant","kind":"message","content":"Hello","session_id":"sess-1","raw_ref":{"command_id":"cmd-1","line":0}}
event: done
id: sess-1-1
data: {"role":"status","kind":"usage","finish_reason":"stop","model":"claude-fable-5","usage":{"output_tokens":50},"session_id":"sess-1","raw_ref":{"command_id":"cmd-1","line":1}}
GET /agents/{instance_id}/v1/tasks/{task_id}/artifacts
List JSON artifacts persisted for an A2A task, including stdout/stderr chunks captured from `messages:send` dispatch. This route reads the executor `TaskStore`; it is separate from the legacy filesystem artifact route under `/api/v1/tasks/{id}/artifacts`.
Response:
{
"task_id": "task-123",
"artifacts": [
{
"artifact_id": "task-123-stdout-0001",
"task_id": "task-123",
"created_at": "2026-05-21T00:00:00Z",
"artifact": {
"kind": "output_chunk",
"stream": "stdout",
"data": "hello\
",
"seq": 1
}
}
]
}
Status Codes:
- `200` - Success
- `404` - Task not found for that instance
GET /agents/{instance_id}/v1/tasks/{task_id}/artifacts/{artifact_id}
Return one persisted A2A task artifact JSON blob.
Status Codes:
- `200` - Success
- `404` - Task or artifact not found
GET /api/v1/tasks/{id}/artifacts
List artifacts produced by a task.
Response:
{
"artifacts": [
{
"name": "summary.md",
"path": "summary.md",
"size_bytes": 2048,
"content_type": "text/markdown",
"checksum": ""
}
]
}
Status Codes:
- `200` - Success
- `404` - Task not found
Example:
curl http://localhost:8122/api/v1/tasks/task-12345678-1234-1234-1234-123456789abc/artifacts
GET /api/v1/tasks/{id}/artifacts/{name}
Download a specific artifact.
Response: File download with appropriate `Content-Type` and `Content-Disposition` headers.
Status Codes:
- `200` - Success
- `404` - Task or artifact not found
Example:
curl -O http://localhost:8122/api/v1/tasks/task-12345678-1234-1234-1234-123456789abc/artifacts/summary.md
gRPC API
Address: `localhost:8120`
The gRPC API is used for bidirectional communication between agents and the management server. See `proto/agent.proto` for complete protocol definitions.
Service: AgentService
Connect (Bidirectional Stream)
Establishes a persistent connection for agent-management communication.
Agent → Management Messages:
- `AgentRegistration` - Initial registration with system info
- `Heartbeat` - Periodic status updates (every 30s)
- `OutputChunk` - stdout/stderr/log streams
- `CommandResult` - Command execution results
- `Metrics` - Resource usage snapshots
- `SessionReport` - Active sessions for reconciliation
- `SessionReconcileAck` - Reconciliation confirmation
Management → Agent Messages:
- `RegistrationAck` - Accept registration
- `CommandRequest` - Execute command
- `ConfigUpdate` - Update configuration
- `ShutdownSignal` - Graceful shutdown request
- `Ping` - Keepalive
- `StdinChunk` - Input for running command
- `PtyControl` - PTY resize/signal
- `SessionQuery` - Request session report
- `SessionReconcile` - Session cleanup instructions
Agent authentication metadata: secure transport listeners bind the verified peer identity to `x-agent-instance-id`. Plain TCP metadata-only authentication is no longer accepted. For mTLS, the verified certificate's SPIFFE URI-SAN is the peer identity, and the `/agent/<instance_id>` component must match `x-agent-instance-id`.
Exec (Server Streaming)
Execute a one-shot command and stream output.
Request:
{
"agent_id": "agent-01",
"command": "ls",
"args": ["-la", "/tmp"],
"working_dir": "/home/agent",
"env": {"DEBUG": "1"},
"timeout_seconds": 60
}
Response Stream:
{"stream": "STREAM_STDOUT", "data": "dG90YWwgNAo=", "exit_code": 0, "complete": false}
{"stream": "STREAM_STDOUT", "data": "ZHJ3eHJ3eHJ3eCA=", "exit_code": 0, "complete": false}
{"stream": "STREAM_STDOUT", "data": "", "exit_code": 0, "complete": true}
Stream Types:
- `STREAM_STDOUT` (1) - Standard output
- `STREAM_STDERR` (2) - Standard error
Example using grpcurl:
grpcurl -plaintext \
-d '{
"agent_id": "agent-01",
"command": "echo",
"args": ["Hello, World!"],
"timeout_seconds": 10
}' \
localhost:8120 agentic.sandbox.v1.AgentService/Exec
Protocol Messages
AgentRegistration
message AgentRegistration {
string agent_id = 1; // VM name (e.g., "agent-01")
string ip_address = 2; // Agent's IP
string hostname = 3; // Hostname
string profile = 4; // Profile used (basic, agentic-dev)
map<string, string> labels = 5;
SystemInfo system = 6;
}
message SystemInfo {
string os = 1; // e.g., "Ubuntu 24.04"
string kernel = 2; // e.g., "6.8.0-generic"
int32 cpu_cores = 3;
int64 memory_bytes = 4;
int64 disk_bytes = 5;
}
CommandRequest
message CommandRequest {
string command_id = 1; // Unique ID for correlation
string command = 2; // Command to execute
repeated string args = 3; // Arguments
string working_dir = 4; // Working directory
map<string, string> env = 5; // Environment variables
int32 timeout_seconds = 6; // Execution timeout (0 = no timeout)
bool capture_output = 7; // Stream stdout/stderr back
string run_as = 8; // User to run as (default: agent)
// PTY terminal options
bool allocate_pty = 9; // Spawn in pseudo-terminal
uint32 pty_cols = 10; // Terminal width (default: 80)
uint32 pty_rows = 11; // Terminal height (default: 24)
string pty_term = 12; // TERM env var (default: xterm-256color)
string session_id = 13; // Stable identity across reconnect/restart
string session_name = 14; // Managed name used during reconciliation
}
Heartbeat
message Heartbeat {
string agent_id = 1;
int64 timestamp_ms = 2;
AgentStatus status = 3; // STARTING, READY, BUSY, ERROR, SHUTTING_DOWN, STALE, DISCONNECTED
float cpu_percent = 4;
int64 memory_used_bytes = 5;
int64 uptime_seconds = 6;
}
SessionReport & SessionReconcile
Used for post-restart session cleanup.
message SessionReport {
string agent_id = 1;
repeated ActiveSession sessions = 2;
int64 timestamp_ms = 3;
}
message ActiveSession {
string command_id = 1; // UUID assigned by server
string session_name = 2; // e.g., "main", "claude"
SessionType session_type = 3; // INTERACTIVE, HEADLESS, BACKGROUND
string command = 4; // Original command
int64 started_at_ms = 5;
int32 pid = 6;
bool is_pty = 7;
string session_id = 8; // Stable identity assigned by the server
}
message SessionReconcile {
repeated string keep_session_ids = 1; // Sessions to keep
repeated string kill_session_ids = 2; // Sessions to terminate
bool kill_unrecognized = 3; // Kill all not in keep list
int32 grace_period_seconds = 4; // Grace period before SIGKILL
}
WebSocket API
Address: `ws://localhost:8121` on the default loopback-only listener.
Real-time streaming of agent output, metrics, and events to dashboard clients.
Connection
Connect to `ws://localhost:8121` using any WebSocket client on the local host. Do not expose this legacy plaintext WebSocket listener on untrusted networks; use the authenticated `pty-ws/v1`/WSS path or a trusted tunnel for remote access.
Example (JavaScript):
const ws = new WebSocket('ws://localhost:8121');
ws.onopen = () => {
console.log('Connected to WebSocket');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log('Received:', message);
};
ws.onclose = () => {
console.log('Disconnected');
};
Message Types
Messages are JSON with a `type` field indicating the message type.
Agent Output
Stdout, stderr, and log streams from agents.
{
"type": "output",
"agent_id": "agent-01",
"stream_id": "cmd-12345",
"stream_type": "stdout",
"data": "SGVsbG8sIFdvcmxkIQo=",
"timestamp": 1706572800000
}
Stream Types: `"stdout"`, `"stderr"`, `"log"`
Agent Metrics
Periodic resource usage updates.
{
"type": "metrics",
"agent_id": "agent-01",
"cpu_percent": 2.3,
"memory_used_bytes": 536870912,
"memory_total_bytes": 8589934592,
"disk_used_bytes": 2147483648,
"disk_total_bytes": 53687091200,
"load_avg": [0.15, 0.20, 0.18],
"timestamp": 1706572800000
}
Agent Status
Agent connection state changes.
{
"type": "agent_status",
"agent_id": "agent-01",
"status": "Ready",
"timestamp": 1706572800000
}
Status Values: `"Starting"`, `"Ready"`, `"Busy"`, `"Error"`, `"ShuttingDown"`, `"Stale"`, `"Disconnected"`
Code Examples
Python Client
import requests
import json
from typing import Optional
class AgenticClient:
def __init__(self, base_url: str = "http://localhost:8122"):
self.base_url = base_url
self.session = requests.Session()
def list_agents(self):
"""List all connected agents."""
resp = self.session.get(f"{self.base_url}/api/v1/agents")
resp.raise_for_status()
return resp.json()["agents"]
def list_vms(self, state: str = "all"):
"""List VMs with optional state filter."""
resp = self.session.get(
f"{self.base_url}/api/v1/vms",
params={"state": state}
)
resp.raise_for_status()
return resp.json()["vms"]
def create_vm(
self,
name: str,
profile: str = "agentic-dev",
vcpus: int = 4,
memory_mb: int = 8192,
disk_gb: int = 50,
start: bool = True
):
"""Create a new VM."""
resp = self.session.post(
f"{self.base_url}/api/v1/vms",
json={
"name": name,
"profile": profile,
"vcpus": vcpus,
"memory_mb": memory_mb,
"disk_gb": disk_gb,
"agentshare": True,
"start": start
}
)
resp.raise_for_status()
return resp.json()["operation"]["id"]
def get_operation(self, op_id: str):
"""Poll operation status."""
resp = self.session.get(f"{self.base_url}/api/v1/operations/{op_id}")
resp.raise_for_status()
return resp.json()
def wait_for_operation(self, op_id: str, timeout: int = 300):
"""Poll until operation completes."""
import time
start = time.time()
while time.time() - start < timeout:
op = self.get_operation(op_id)
if op["status"] == "completed":
return op
elif op["status"] == "failed":
raise Exception(f"Operation failed: {op.get('error')}")
time.sleep(2)
raise TimeoutError("Operation timed out")
def start_vm(self, name: str):
"""Start a VM."""
resp = self.session.post(f"{self.base_url}/api/v1/vms/{name}/start")
resp.raise_for_status()
return resp.json()
def stop_vm(self, name: str):
"""Stop a VM gracefully."""
resp = self.session.post(f"{self.base_url}/api/v1/vms/{name}/stop")
resp.raise_for_status()
return resp.json()
def delete_vm(self, name: str, delete_disk: bool = False, force: bool = False):
"""Delete a VM."""
resp = self.session.delete(
f"{self.base_url}/api/v1/vms/{name}",
params={"delete_disk": delete_disk, "force": force}
)
resp.raise_for_status()
return resp.json()
# Usage
client = AgenticClient()
# List agents
agents = client.list_agents()
print(f"Connected agents: {len(agents)}")
# Create VM and wait for completion
op_id = client.create_vm("agent-05")
print(f"Provisioning started: {op_id}")
result = client.wait_for_operation(op_id)
print(f"VM created: {result['result']}")
# Start/stop VM
client.stop_vm("agent-05")
client.start_vm("agent-05")
# Delete VM
client.delete_vm("agent-05", delete_disk=True, force=True)
JavaScript/Node.js Client
const axios = require('axios');
class AgenticClient {
constructor(baseUrl = 'http://localhost:8122') {
this.baseUrl = baseUrl;
this.client = axios.create({ baseURL: baseUrl });
}
async listAgents() {
const resp = await this.client.get('/api/v1/agents');
return resp.data.agents;
}
async listVMs(state = 'all') {
const resp = await this.client.get('/api/v1/vms', {
params: { state }
});
return resp.data.vms;
}
async createVM(options) {
const {
name,
profile = 'agentic-dev',
vcpus = 4,
memoryMb = 8192,
diskGb = 50,
start = true
} = options;
const resp = await this.client.post('/api/v1/vms', {
name,
profile,
vcpus,
memory_mb: memoryMb,
disk_gb: diskGb,
agentshare: true,
start
});
return resp.data.operation.id;
}
async getOperation(opId) {
const resp = await this.client.get(`/api/v1/operations/${opId}`);
return resp.data;
}
async waitForOperation(opId, timeout = 300000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const op = await this.getOperation(opId);
if (op.status === 'completed') {
return op;
} else if (op.status === 'failed') {
throw new Error(`Operation failed: ${op.error}`);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
throw new Error('Operation timed out');
}
async startVM(name) {
const resp = await this.client.post(`/api/v1/vms/${name}/start`);
return resp.data;
}
async stopVM(name) {
const resp = await this.client.post(`/api/v1/vms/${name}/stop`);
return resp.data;
}
async deleteVM(name, options = {}) {
const { deleteDisk = false, force = false } = options;
const resp = await this.client.delete(`/api/v1/vms/${name}`, {
params: { delete_disk: deleteDisk, force }
});
return resp.data;
}
}
// Usage
(async () => {
const client = new AgenticClient();
// List agents
const agents = await client.listAgents();
console.log(`Connected agents: ${agents.length}`);
// Create VM
const opId = await client.createVM({ name: 'agent-06' });
console.log(`Provisioning started: ${opId}`);
const result = await client.waitForOperation(opId);
console.log(`VM created:`, result.result);
})();
curl Examples
# Health check
curl http://localhost:8122/healthz
# List agents
curl http://localhost:8122/api/v1/agents | jq
# List running VMs
curl "http://localhost:8122/api/v1/vms?state=running" | jq
# Get VM details
curl http://localhost:8122/api/v1/vms/agent-01 | jq
# Create VM
curl -X POST http://localhost:8122/api/v1/vms \
-H "Content-Type: application/json" \
-d '{"name":"agent-07"}' | jq
# Poll operation status
curl http://localhost:8122/api/v1/operations/op-12345 | jq
# Start VM
curl -X POST http://localhost:8122/api/v1/vms/agent-07/start | jq
# Stop VM
curl -X POST http://localhost:8122/api/v1/vms/agent-07/stop | jq
# Restart VM
curl -X POST http://localhost:8122/api/v1/vms/agent-07/restart \
-H "Content-Type: application/json" \
-d '{"mode":"graceful","timeout_seconds":60}' | jq
# Delete VM
curl -X DELETE "http://localhost:8122/api/v1/vms/agent-07?delete_disk=true&force=true" | jq
# List events
curl http://localhost:8122/api/v1/events | jq
# Submit task
curl -X POST http://localhost:8122/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"manifest": {
"name": "analyze-repo",
"repository": {"url": "https://github.com/user/repo"},
"prompt": "Analyze code quality"
}
}' | jq
# List tasks
curl http://localhost:8122/api/v1/tasks | jq
# Stream task logs
curl -N http://localhost:8122/api/v1/tasks/task-12345/logs
Error Codes
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | OK - Request successful |
| 202 | Accepted - Async operation started |
| 400 | Bad Request - Invalid input |
| 404 | Not Found - Resource doesn't exist |
| 409 | Conflict - Resource state conflict |
| 500 | Internal Server Error - Server error |
| 503 | Service Unavailable - Service not ready |
Application Error Codes
| Code | Description |
|---|---|
| `VM_NOT_FOUND` | VM doesn't exist in libvirt |
| `VM_RUNNING` | VM is running (when stopped required) |
| `VM_STOPPED` | VM is stopped (when running required) |
| `VM_NOT_RUNNING` | VM is not running |
| `VM_ALREADY_EXISTS` | VM name already in use |
| `INVALID_VM_NAME` | VM name doesn't match pattern |
| `PROVISIONING_ERROR` | VM provisioning failed |
| `LIBVIRT_ERROR` | libvirt operation failed |
| `OPERATION_NOT_FOUND` | Operation ID not found |
Endpoints not yet integrated above
The following routes are wired up in `management/src/http/server.rs` but were absent from the canonical reference. They are documented here in summary form so callers can discover them; the reference sections above will absorb these on the next documentation pass.
Agent lifecycle (extended)
POST /api/v1/agents/{id}/reprovision
Triggers a reprovision of the named agent VM via `reprovision-vm.sh`.
Response: `202 Accepted` with `{"operation_id": "...", "status": "queued"}`
POST /api/v1/agents/{id}/rotate-secret
Retired with the legacy shared-secret path in #412.
Response: `410 Gone` with an error explaining that agents must use transport identity credentials.
AIWG bridge
GET /api/v1/aiwg/status
Returns current AIWG bridge connection state.
Response:
{ "connected": true, "session_count": 3, "last_event_secs": 12 }
POST /api/v1/aiwg/reconnect
Forces a reconnect of the AIWG bridge.
Response: `200 OK` with `{"ok": true}`
Legacy AIWG direct-runtime SSH proxy
The companion manifest and `aiwg exec` routes remain wired for dev/break-glass diagnostics only:
- `GET /api/v1/agents/{id}/manifests/{platform}`
- `GET /api/v1/agents/{id}/manifests/{platform}/{name}`
- `POST /api/v1/agents/{id}/manifests/{platform}/{name}`
- `POST /api/v1/agents/{id}/aiwg/exec`
These routes shell out to direct runtime SSH and bypass the gateway-mediated SSH policy/audit boundary from ADR-029 (now available via the SSH certificate lease API above). They are disabled by default and return `403 Forbidden` unless `AGENTIC_ENABLE_DIRECT_SSH_AIWG_PROXY=1` is set for a dev/break-glass diagnostic session. Managed-profile SSH access should use the gateway-mediated path tracked by #531.
Sessions (agent-scoped)
POST /api/v1/agents/{id}/sessions
Creates a new interactive PTY session on the agent. The response preserves the legacy websocket fields for older clients and also includes the current v2 PTY and orchestrator attach metadata for #321-style TUI orchestration. Clients that retry this mutating request after a network timeout should send an `Idempotency-Key` header. Reusing the same key returns the cached successful create response instead of creating a duplicate terminal.
Request body:
{
"command": "bash",
"session_name": "codex-tui",
"session_backend": "tmux",
"session_class": "managed"
}
All fields are optional. When omitted, the server launches `bash` with a generated `terminal-*` session name using the current managed `tmux` session host. `session_backend` is the #461 session-host selector. This endpoint currently supports `tmux` only; `screen`, `zellij`, and `native` are rejected with `501 session_backend.not_implemented` until their backends land. `session_class` currently supports `managed` only; direct ad-hoc session control is exposed through the `pty-ws/v1` capability surface first.
Response:
{
"session_id": "<stable-session-id>",
"instance_id": "<routable-a2a-instance-id>",
"command_id": "<agent-command-correlation-id>",
"session_name": "codex-tui",
"ws_endpoint": "ws://{host}:8121/",
"join_message": {
"type": "join_session",
"session_id": "<stable-session-id>",
"role": "controller"
},
"pty_ws_url": "wss://{host}/agents/<instance_id>/sessions/<session_id>/attach",
"pty_ws_subprotocol": "pty-ws.v1",
"orchestrator_observer_url": "/ws/sessions/<session_id>/orchestrate?role=observer",
"orchestrator_controller_url": "/ws/sessions/<session_id>/orchestrate?role=controller",
"default_role": "observer",
"controller_policy": "controller input is policy-gated",
"session_backend": "tmux",
"session_class": "managed",
"supported_session_backends": ["tmux"],
"supported_session_classes": ["managed"],
"observe_supported": true,
"drive_supported": true,
"reattach_supported": true
}
For new orchestration clients, use `default_role: observer` first. Controller attachment is intended only for policy-approved bounded input. The legacy `ws_endpoint` / `join_message` fields remain for compatibility with older path-agnostic websocket clients.
The `pty-ws/v1` `binding_hello` frame also includes `session_host` capability metadata from the active PTY bridge. The default no-op bridge reports native/direct observe, drive, and reattach support. The real agent PTY bridge reports native/direct plus screen/managed, zellij/managed, and tmux/managed support. Clients select the backend on `pty.join_session`:
{
"op": "pty.join_session",
"payload": {
"session_backend": "native",
"session_class": "direct",
"argv": ["/bin/bash", "-l"],
"cwd": "/workspace",
"env": { "TERM": "xterm-256color" },
"terminal_size": { "cols": 132, "rows": 43 }
}
}
GET /api/v1/agents/{id}/sessions
Lists active sessions for one agent. Each entry includes legacy command/session fields plus enough metadata for clients to attach without inferring routes from AgentCard or hard-coded URL templates.
Response:
{
"agent_id": "agent-01",
"sessions": [
{
"session_id": "<stable-session-id>",
"command_id": "<agent-command-correlation-id>",
"session_name": "codex-tui",
"session_type": "interactive",
"command": "bash",
"created_at_secs": 42,
"has_screen": true,
"pty_ws_url": "wss://{host}/agents/<instance_id>/sessions/<session_id>/attach",
"pty_ws_subprotocol": "pty-ws.v1",
"orchestrator_observer_url": "/ws/sessions/<session_id>/orchestrate?role=observer",
"orchestrator_controller_url": "/ws/sessions/<session_id>/orchestrate?role=controller",
"default_role": "observer",
"controller_policy": "controller input is policy-gated",
"membership": {
"controllers": ["<client-id>"],
"observers": ["<client-id>"],
"attachment_count": 2
},
"liveness": {
"agent_connected": true,
"has_screen": true,
"replay_newest_seq": 128,
"max_client_lag": 0
},
"session_backend": "tmux",
"session_class": "managed"
}
]
}
`membership.controllers` is empty when the controller lease is available and has at most one entry for the formal session registry. A controller request can be downgraded to observer when another live controller holds the lease; clients must trust the server-returned role.
Unsupported join selections fail closed with `session_backend.not_implemented` or `session_class.not_implemented` before a PTY bridge session is started.
DELETE /api/v1/agents/{id}/sessions/{session}
Kills a session.
Query params:
- `signal` (optional, default `TERM`) — one of `TERM | KILL | INT | HUP`.
Response: `200 OK` with `{"killed": true}`
Container images (curated catalog)
GET /api/v1/container-images
Returns the curated agent-image catalog used to populate the dashboard's Create Instance image picker (#179). The list mirrors the Dockerfiles under `images/container/` and is updated when new images land in CI.
Response:
{
"images": [
{ "ref": "agentic/claude:latest", "label": "Claude", "description": "Anthropic Claude Code agent", "default": true },
{ "ref": "agentic/codex:latest", "label": "Codex", "description": "OpenAI Codex agent" },
{ "ref": "agentic/opencode:latest","label": "OpenCode","description": "OpenCode agent" },
{ "ref": "agentic/automation-control:latest", "label": "Automation Control", "description": "Orchestrator-ready control image with Codex, Aider, dev tools, and credential-free probes" }
]
}
AIWG executor contract
POST /api/v1/sessions/{id}/dispatch
AIWG `aiwg serve` calls this route to dispatch a mission to this sandbox. See AIWG Executor Contract for the full integration (registration, capabilities, event vocabulary, persistence, lifecycle).
Auth: `Authorization: Bearer <token>` — token issued at executor registration. Constant-time comparison.
Request body:
{
"mission_id": "<UUID>",
"objective": "<command/prompt>",
"completion": "<optional completion criteria>",
"long_running": false,
"executor_filter": { "agent_id": "agent-01" },
"metadata": { }
}
Response: `202 Accepted`
{
"mission_id": "<echo>",
"executor_id": "<sandbox instance_id>",
"status": "assigned",
"estimated_start": "<RFC3339>"
}
Failure: `401` (bad token), `404` (agent not found), `503` (no agents available / executor not registered), `500` (dispatcher error — emits `mission.failed` with reason).
Storage downloads
These complement the upload/list endpoints already documented under Storage:
| Endpoint | Method | Description |
|---|---|---|
| `/api/v1/storage/global/_download` | GET | Stream a file from the read-only global share |
| `/api/v1/storage/inbox/{agent_id}/_download` | GET | Stream a file from a per-agent inbox |
| `/api/v1/storage/outbox/{task_id}/_download` | GET | Stream a file from a per-task outbox |
All three accept `?path=<relative-path>` and respond with the raw file bytes (`Content-Type` inferred from extension).
Rate Limits
Currently no rate limiting is enforced. For production deployments, consider implementing:
- Per-IP rate limits on HTTP endpoints
- Connection limits on WebSocket
- gRPC flow control for agent streams
Versioning
API version is included in the path: `/api/v1/...`
Current version: v1
Breaking changes will increment the version number. Legacy endpoints are maintained for backwards compatibility where possible.