Skip to content

Platform service API

The HTTP contract exposed by kneo service serve. The generated OpenAPI schema is committed at openapi.json; refresh it with python docs/script/generate_reference_docs.py.

This page covers versioning, auth, redaction, governance, and the request / response shapes for each route group. The Worked examples section near the bottom has copy-pasteable curl invocations for the most common endpoints.

Versioning

The stable public HTTP API is exposed under /v1. Existing unversioned routes remain available for local development and backwards compatibility, but new service clients should prefer /v1.

Examples:

GET /v1/healthz
POST /v1/runs
GET /v1/runs/{run_id}
POST /v1/human-tasks/{continuation_id}/resume

Authentication

Authentication is disabled by default for local development. Enable it by setting API keys before starting the service:

export KNEO_SERV_AUTH_ENABLED=true
export KNEO_SERV_API_KEYS='operator:operator-token:operator;reviewer:reviewer-token:reviewer'
export KNEO_SERV_ADMIN_API_KEY='admin-token'

Clients authenticate with either header:

Authorization: Bearer <api-key>
X-Kneo-Api-Key: <api-key>

Built-in roles:

  • admin: all scopes
  • operator: runs:read, runs:write, specs:read, human:read, audit:read, credentials:read, policies:read, policies:write
  • reviewer: runs:read, human:read, human:write, audit:read
  • service: runs:read, runs:write, specs:read, human:read, human:write, audit:read, audit:write, credentials:read, policies:read
  • viewer: runs:read, specs:read, human:read, audit:read

KNEO_SERV_API_KEYS accepts semicolon-separated entries:

name:key:role_or_scope[,role_or_scope]

Example with explicit scopes:

export KNEO_SERV_API_KEYS='ci:ci-token:service;runs-reader:read-token:runs:read'

Redaction

Service responses, traces, checkpoints, and CLI JSON output are redacted before they are returned or persisted as checkpoints. Redaction covers common secret keys and inline values such as passwords, tokens, API keys, authorization headers, emails, and SSNs.

Spec governance diagnostics

Spec validation includes static governance diagnostics before deployment:

  • Unsafe tool or function implementation imports such as direct os, subprocess, shutil, socket, importlib, or builtins primitives are reported as errors.
  • Shorthand tool selection or missing tool permission policies are reported as warnings.
  • Network tools without allowed_domains, shell-capable tools, and filesystem write access are reported as warnings.
  • Specs that expose privileged tools or unsafe imports without a human workflow approval step receive a W_HUMAN_APPROVAL_MISSING warning.

These diagnostics are returned by kneo spec validate, POST /specs/validate, and strict compiler flows.

POST /specs/policy-report returns a structured policy report covering memory configuration, tool permissions, declared MCP imports, guardrail stages, human reviewers, and human approval requirements. Use it in deployment gates when a spec needs a machine-readable policy summary before signing or promotion.

GET /runs/{run_id}/policy-report returns the same shape but operates on the spec the run was started with — the service reads it out of the run's stored metadata, so operators auditing a deployed run don't need to ship the bundle to the service themselves. Same specs:read scope as the spec-bundle route.

curl -H "Authorization: Bearer $KNEO_API_KEY" \
  https://kneo.example.com/v1/runs/run-7c2f.../policy-report

Returns 404 if the run id is unknown, 400 if the run carries no spec metadata (older runs from a pre-0.3.0 store), and 200 with {"valid": <bool>, "report": {...}} otherwise. Each call records a spec.policy_reported audit event scoped to the run id with metadata.source = "run", so spec-bundle calls and run-keyed calls are distinguishable in the audit log.

Project-based CLI flows can enforce different gates per environment through environments.<name>.policy_enforcement. Enforcement runs after overlays and defaults are applied, so dev, staging, and prod can require progressively stricter tool permissions, human review, guardrails, or blocked diagnostic codes.

Redaction is a safety layer, not a replacement for secret management. Provider keys and credentials should still be supplied through deployment secret stores or environment variables rather than embedded in specs or request payloads.

Workflow specs

YAML specs can target SDK-backed workflow families while preserving service validation, tracing, cancellation, and run-result metadata:

  • sequential: ordered steps.
  • graph: keyed nodes, conditional edges, and a start node.
  • concurrent: fan-out participants executed by the SDK concurrent workflow.
  • handoff: participants plus a selector; sequence and round_robin selectors are supported.
  • group-chat or group_chat: participants repeated for rounds.

Orchestration workflow participants use the same step shape as sequential workflow steps:

workflow:
  type: handoff
  name: review-handoff
  participants:
    - id: researcher
      kind: agent
      ref: research_agent
    - id: reviewer
      kind: agent
      ref: review_agent
  selector:
    type: sequence
    sequence: [researcher, reviewer]

Participant ids must be unique, participant refs must resolve to declared components, handoff selector entries must reference participant ids, and group-chat rounds must be at least 1.

Declarative tools, MCP servers, and composition

A spec wires tools and composes agents declaratively. A tool is backed by exactly one of three sources (the validator rejects zero or multiple with E_TOOL_NO_BACKING / E_TOOL_MULTIPLE_BACKINGS):

  • implementation: a Python import path.
  • mcp: a reference {server: <name>, name?: <remote-tool>} to a declared MCP server (the hybrid lazy-binding path — the connection is opened on first call, not at compile time).
  • agent: the agent-as-tool pattern — names a components.agents entry, exposed to the parent agent as a tool. Its input schema is fixed to a single input string; author-declared parameters are ignored (W_AGENT_TOOL_PARAMETERS_IGNORED).

MCP servers are declared under a top-level mcp_servers block; each entry sets a transport (stdio needs command; http needs url; sse needs sse_url) plus optional TLS material (verify, ca_bundle, client_cert, client_key / client_key_ref). Setting verify: false disables TLS verification and emits W_MCP_TLS_VERIFY_DISABLED. Supply secrets by reference (client_key_ref) so they never land in persisted spec state.

An agent can itself be backed by a workflow (the workflow-as-agent pattern) via as_agent: <workflow-name>; only name / description / system_prompt are legal alongside as_agent (anything else is the workflow's job and is rejected with E_AS_AGENT_FIELDS).

mcp_servers:
  search:
    transport: http
    url: https://mcp.example.com/api
    client_key_ref: MCP_SEARCH_KEY
tools:
  web_search:
    description: Search the web via MCP.
    mcp: {server: search, name: search}
  ask_specialist:
    description: Delegate to the specialist agent.
    agent: specialist            # agent-as-tool
components:
  agents:
    specialist: {name: specialist}
    pipeline_agent:
      name: pipeline_agent
      as_agent: review_pipeline   # workflow-as-agent
  workflows:
    review_pipeline:
      type: sequential
      steps:
        - {id: s1, kind: agent, ref: specialist}

Components are built in dependency order (a topological sort over tool → agent → workflow references); a dependency cycle is rejected at validation with E_BUILD_CYCLE.

Secret management

kneo_serv resolves provider keys, MCP credentials, service tokens, and runtime-specific values through named environment-variable references. Project config stores only env-var names, never raw secret values:

secrets:
  provider_env:
    openai: OPENAI_API_KEY
  extra_env:
    mcp_default: MCP_API_KEY

The default provider mappings include openai/openai-agents, anthropic, google, and google-adk. The CLI can show a redacted inventory for deployment checks:

kneo config secrets --json

Native provider startup can fail fast when a required provider secret is missing:

export KNEO_SERV_REQUIRE_PROVIDER_SECRETS=true

Service API keys remain in KNEO_SERV_API_KEY, KNEO_SERV_API_KEYS, and KNEO_SERV_ADMIN_API_KEY; the secret inventory reports whether they are present without exposing values.

The service exposes the same redacted inventory for operators:

GET /v1/security/credentials?providers=openai&include_service_tokens=false

This endpoint requires credentials:read. The response reports configured provider, extra, and service-token references as a typed inventory (relay #10) — each entry carries present, a redacted value, a derived health status (present | missing), and reserved expires_at / last_checked slots (env-var secrets carry no rotation metadata, so these are null):

{
  "inventory": {
    "providers": {
      "openai": {
        "name": "provider:openai",
        "env_var": "OPENAI_API_KEY",
        "present": true,
        "value": "[REDACTED]",
        "status": "present",
        "expires_at": null,
        "last_checked": null
      }
    },
    "extra": {}
  }
}

Every successful credential inventory request records a credential.inventory_accessed audit event. Audit metadata includes counts and which reference names were present; raw secret values are never included.

Environment policy management

Environment policy enforcement can be managed through the service when a deployment needs operator-controlled gates outside checked-in project config:

GET  /v1/policies/environment
GET  /v1/policies/environment/prod
PUT  /v1/policies/environment/prod
POST /v1/policies/environment/prod/preview

Reads require policies:read; writes require policies:write. A policy update stores validated EnvironmentPolicyEnforcement settings in the run state store's project_metadata table/key-value area:

{
  "enabled": true,
  "fail_on_warnings": false,
  "blocked_diagnostic_codes": [],
  "require_human_review": true,
  "require_tool_permissions": true,
  "deny_unrestricted_tools": true,
  "require_guardrails": false
}

The response includes the current policy and, for updates, the previous policy when one existed. Each successful update records a policy.changed audit event with the policy surface, environment, previous/current redacted policy payloads, and changed field names.

POST /v1/policies/environment/{environment}/preview (scope policies:read, relay #9) evaluates a candidate policy without persisting it: it returns the diff versus the stored policy, the affected_run_ids, which of those become newly_blocking under the candidate (by replaying them through the enforcement engine), and runs_evaluated. Only the control plane can answer this honestly — it owns the policy engine and the run corpus — so a dashboard can't compute it client-side.

Request limits

The service rejects oversized request bodies before route handling and applies strict request-model validation for inline payloads. Unknown request fields are rejected with 422, and bodies above the configured transport limit return 413.

Default limits (environment.md § Service limits is canonical for these values):

  • KNEO_SERV_MAX_BODY_BYTES: 1048576
  • KNEO_SERV_MAX_INPUT_CHARS: 20000
  • KNEO_SERV_MAX_HUMAN_CONTENT_CHARS: 20000
  • KNEO_SERV_MAX_INLINE_SPEC_BYTES: 262144
  • KNEO_SERV_MAX_OVERRIDES_BYTES: 65536
  • KNEO_SERV_MAX_METADATA_BYTES: 32768
  • KNEO_SERV_MAX_LIST_ITEMS: 100
  • KNEO_SERV_MAX_PATH_CHARS: 4096

Structured logging

API requests emit redacted JSON log records on the kneo_serv.service logger. Each request record includes event=http_request, request_id, method, path, status code, duration, client IP when available, and route-supplied run, continuation, or trace IDs when known.

Clients can send X-Request-ID; otherwise the service generates one. The response always includes the effective X-Request-ID.

Configuration:

  • KNEO_SERV_REQUEST_LOGS: defaults to true
  • KNEO_SERV_LOG_LEVEL: defaults to INFO

SDK OpenTelemetry tracing

When SDK telemetry support is installed, set KNEO_SERV_OTEL_ENABLED=true to attach kneo_agent.observability.OpenTelemetryMiddleware to SDK-backed agents. The middleware uses the OpenTelemetry global tracer provider, so exporters and resources can be configured with standard OTEL_* environment variables in the deployment environment.

Service defaults keep potentially sensitive span attributes disabled:

  • KNEO_SERV_OTEL_RECORD_ARGUMENTS: defaults to false
  • KNEO_SERV_OTEL_RECORD_RESULTS: defaults to false

Enable those only for trusted deployments where tool arguments and results are safe to emit to telemetry backends.

Idempotency

POST /runs, POST /specs/run, and POST /human-tasks/{continuation_id}/resume support the Idempotency-Key header. When the same key is reused with the same request payload, the service returns the original response without creating a duplicate run or submitting a second human decision.

Idempotency-Key: <stable-client-generated-key>

Reusing a key with a different payload returns 409 with idempotency_key_conflict.

The CLI service client can send a key per call in code, or read one from:

export KNEO_SERV_IDEMPOTENCY_KEY=<stable-client-generated-key>

Human-task resume also takes a store-backed continuation lock. If another process is already resuming the same continuation, the service returns 409 with resource_locked.

Run cancellation

POST /runs/{run_id}/cancel marks a pending or running run as cancelled. Background execution receives a cooperative cancellation token through the SDK run config extra payload, so service workflows, agents, runtimes, and wrapped workflow steps check cancellation before and after unit-of-work boundaries. A cancelled run is not overwritten as completed if execution returns after cancellation was requested.

Provider calls that do not expose an interrupt primitive can only stop at the next cooperative boundary after the provider returns.

Retry, timeout, and backoff

Service-client retries are configured with KNEO_SERV_CLIENT_* variables. Provider/runtime and MCP calls use the same conservative policy shape:

export KNEO_SERV_PROVIDER_RETRIES=2
export KNEO_SERV_PROVIDER_RETRY_BACKOFF_SECONDS=0.25
export KNEO_SERV_PROVIDER_TIMEOUT_SECONDS=120

export KNEO_SERV_MCP_RETRIES=2
export KNEO_SERV_MCP_RETRY_BACKOFF_SECONDS=0.25
export KNEO_SERV_MCP_TIMEOUT_SECONDS=30

Workflow steps can also set on_error: retry, max_retries, and timeout_seconds in YAML specs. Cancellation is never retried.

Health checks

This section is the API contract. For an on-call triage tree mapping each /readyz check to recovery actions, see incident_response.md.

  • GET /healthz: lightweight API health.
  • GET /livez: process liveness.
  • GET /readyz: readiness for API wiring, run state store, continuation store, durable run queue, runtime registry, tool registry, and configured provider or MCP secret dependencies.

Provider and MCP dependency checks are opt-in so local development does not fail when no real upstream credentials are configured:

export KNEO_SERV_HEALTH_PROVIDERS=openai,anthropic
export KNEO_SERV_HEALTH_MCP_SECRETS=mcp_default

If a configured readiness dependency is missing or unhealthy, /readyz returns 503 with a structured not_ready detail payload.

Background worker queue

Async run creation enqueues run IDs into the configured run state store before worker execution. SQLite and file stores persist queue records with status, attempt count, lease owner, lease expiry, and error details; in-memory stores keep the same contract for tests and local ephemeral use.

Workers claim queued or expired leased records, execute the run through the same PlatformManager.execute_run path, and then mark the queue record completed or failed. On service startup the default manager starts a worker so previously queued records can be resumed.

Recovery and continuation

Workflow execution stores live execution context on run state and persists step/node completion and failure checkpoints. For interrupted non-human sequential workflows, the service can report the completed steps, failed step, resume input, and next step index:

GET /runs/{run_id}/recovery

When replay_context.can_continue is true, the run can continue from the last completed step boundary:

POST /runs/{run_id}/continue

Graph workflows expose replay context from node checkpoints, but automatic continuation is limited to sequential workflows until graph edge state is persisted at each routing decision.

Replay and checkpoint diff

Operators can inspect a compact replay timeline without reading full checkpoint payloads:

GET /runs/{run_id}/replay

The response includes checkpoint sequence, type, step/node IDs, status, current execution position, pending human request ID, error summary, and the same replay context used by /runs/{run_id}/recovery.

Checkpoint diffs compare checkpoint state and metadata. By default the latest two checkpoints are compared:

GET /runs/{run_id}/checkpoints/diff
GET /runs/{run_id}/checkpoints/diff?from_sequence=1&to_sequence=3

The diff response reports added, removed, and changed flattened paths. Values are redacted before returning.

Audit events

The service records redacted audit events in the configured run state store for successful spec operations, run creation, run cancellation, run continuation, spec-run execution, and human-in-the-loop decisions.

GET /audit-events
GET /audit-events?event_type=run.created
GET /audit-events?run_id=<run_id>
GET /audit-events?limit=50&offset=50&sort_by=created_at&sort_order=desc

The audit list endpoint requires audit:read and returns events newest first. Each event includes event_type, actor, optional run_id and continuation_id, redacted metadata, and created_at. The response carries the same pagination metadata block as the other list endpoints — count (items on this page), total, limit, offset, sort_by, and sort_order (limit 1–1000, default 100; sort_order defaults to desc).

Error responses

Every 4xx/5xx response uses the envelope {"detail": {"error": "<code>", "message": "<human-readable>", ...}}, where error is a stable, snake_case machine code (e.g. not_found, invalid_request, internal_error, queue_full, resource_locked, unauthorized, forbidden) decoupled from internal exception names. Some errors carry extra context keys (e.g. resource, queue_depth, required_scope). 500 responses are opaque (internal_error with a generic message); the real cause is logged server-side, never returned to the client. These shapes are published in the OpenAPI schema as ErrorResponse / ErrorDetail.

SQLite migrations

SQLite state stores apply versioned migrations on startup. The migration table is schema_migrations, and the current schema covers run state, checkpoints, idempotency records, locks, durable run queue records, continuation records, audit event records, and project metadata records.

Existing unversioned SQLite databases are upgraded in place with CREATE TABLE IF NOT EXISTS and CREATE INDEX IF NOT EXISTS statements, so existing run payloads remain readable after migration.

Project metadata is used by service-managed environment policies. Upgrade coverage verifies that existing SQLite databases can create, persist, and reload policy metadata after migrations have applied.

Retention and pruning

RetentionManager provides an operator-callable pruning job for run state, checkpoints, completed or failed queue records, file-backed continuations, audit events, artifacts, and logs. It can be configured directly or through environment variables:

export KNEO_SERV_RETENTION_RUNS_DAYS=30
export KNEO_SERV_RETENTION_CHECKPOINTS_DAYS=30
export KNEO_SERV_RETENTION_QUEUE_DAYS=14
export KNEO_SERV_RETENTION_CONTINUATIONS_DAYS=30
export KNEO_SERV_RETENTION_AUDIT_DAYS=90
export KNEO_SERV_RETENTION_ARTIFACTS_DAYS=30
export KNEO_SERV_RETENTION_LOGS_DAYS=30

The platform manager exposes prune_retention() for embedded operators and future scheduled jobs.

Checkpoint payload limits

SQLite and file stores transparently compress large checkpoint payloads before writing them. If a checkpoint remains above the hard cap after compression, the store persists a bounded checkpoint preview that keeps run ID, checkpoint type, step/node IDs, timestamps, limited trace previews, and metadata describing the size reduction.

Defaults:

  • KNEO_SERV_CHECKPOINT_COMPRESS_BYTES: 65536
  • KNEO_SERV_CHECKPOINT_MAX_BYTES: 1048576
  • KNEO_SERV_CHECKPOINT_PREVIEW_CHARS: 1200
  • KNEO_SERV_CHECKPOINT_MAX_LIST_ITEMS: 20
  • KNEO_SERV_CHECKPOINT_MAX_DICT_ITEMS: 50

Backup and restore

This section documents the Python backup API. For the operator-facing production procedure (PostgreSQL pg_dump, off-site rotation, restore verification, DR checklist), see backup_and_recovery.md.

The default SQLite store can be backed up online with SQLite's backup API:

from kneo_serv.maintenance import backup_sqlite_database, restore_sqlite_database

backup_sqlite_database(".kneo/kneo_runs.sqlite", ".kneo/backups/kneo_runs.sqlite")
restore_sqlite_database(".kneo/backups/kneo_runs.sqlite", ".kneo/kneo_runs.restored.sqlite")

The smoke test covers run state and checkpoint restore from the copied database. File-backed continuations, artifacts, and logs should be included in deployment-level filesystem backups when those paths are used.

Runs

  • POST /v1/runs
  • GET /v1/runs
  • GET /v1/runs/{run_id}
  • POST /v1/runs/{run_id}/cancel
  • GET /v1/runs/{run_id}/policy-report
  • GET /v1/runs/{run_id}/recovery
  • GET /v1/runs/{run_id}/replay
  • GET /v1/runs/{run_id}/graph
  • POST /v1/runs/{run_id}/continue
  • GET /v1/runs/{run_id}/checkpoints
  • GET /v1/runs/{run_id}/checkpoints/diff
  • GET /v1/runs/{run_id}/trace

Legacy aliases:

  • GET /runs
  • POST /runs
  • GET /runs/{run_id}
  • POST /runs/{run_id}/cancel
  • GET /runs/{run_id}/recovery
  • GET /runs/{run_id}/replay
  • POST /runs/{run_id}/continue
  • GET /runs/{run_id}/checkpoints
  • GET /runs/{run_id}/checkpoints/diff
  • GET /runs/{run_id}/trace

Human tasks

  • GET /v1/human-tasks
  • GET /v1/human-tasks/{continuation_id}
  • POST /v1/human-tasks/{continuation_id}/resume

Legacy aliases:

  • GET /human-tasks
  • GET /human-tasks/{continuation_id}
  • POST /human-tasks/{continuation_id}/resume

Specs

  • POST /v1/specs/validate
  • POST /v1/specs/compile
  • POST /v1/specs/explain
  • POST /v1/specs/graph
  • POST /v1/specs/policy-report
  • POST /v1/specs/run

The five read-only endpoints (validate, compile, explain, graph, policy-report) accept the same envelope (spec_path or inline spec, plus environment, overlays, and overrides); compile additionally honors strict; graph (scope specs:read) returns the static workflow DAG. The overlays/overrides layer the effective spec the same way a run does. An invalid spec returns 400 spec_invalid carrying the diagnostic list (it is not a 500).

POST /specs/run takes the POST /runs envelope and, as of 0.12.0, honors async_mode: with async_mode=true it dispatches the run to the worker queue and returns 202 Accepted with the queued run_id (poll GET /runs/{run_id}), exactly like POST /runs; the synchronous default (async_mode=false) runs inline and returns 200.

Spec-path confinement. spec_path, overlays, and skills[].source are filesystem paths the service reads at compile time. They are confined to the spec rootKNEO_SERV_SPEC_ROOT when set, otherwise the process working directory — and anything resolving outside it (absolute path, ..-traversal, or symlink escape) is rejected 422 spec_path_confined. Confinement is default-on as of 1.0.0 (it was opt-in through 0.12.x, where an out-of-root absolute path only logged a deprecation warning). Set KNEO_SERV_SPEC_ROOT explicitly when your specs / overlays / skill bundles live outside the working directory. See security_hardening.md.

Legacy aliases:

  • POST /specs/validate
  • POST /specs/compile
  • POST /specs/explain
  • POST /specs/graph
  • POST /specs/policy-report
  • POST /specs/run

Skills

  • GET /v1/skills

Read-only catalog of skills discovered in the service's default locations (name / description / path), paginated. Requires specs:read. No compilation or spec is needed. A run can toggle the root agent's skills per-request with the skills overlay on POST /v1/runs ({add, disable}); add may only enable skills already declared in the spec — a request cannot inject an undeclared skill.

Legacy alias:

  • GET /skills

Audit

  • GET /v1/audit-events

Legacy alias:

  • GET /audit-events

Security and policies

  • GET /v1/security/credentials
  • GET /v1/policies/environment
  • GET /v1/policies/environment/{environment}
  • PUT /v1/policies/environment/{environment}
  • POST /v1/policies/environment/{environment}/preview

Legacy aliases:

  • GET /security/credentials
  • GET /policies/environment
  • GET /policies/environment/{environment}
  • PUT /policies/environment/{environment}

Worked examples

Concrete curl invocations and abbreviated response shapes for the most common endpoints. The full schema is in openapi.json; these are illustrative.

All examples assume:

export BASE=http://127.0.0.1:8000
export KEY=operator-token   # an entry from KNEO_SERV_API_KEYS

Health

curl -sf "$BASE/livez"     # {"ok": true, "metadata": {"status": "alive"}}
curl -sf "$BASE/readyz"    # 200 with checks: {} or 503 with not_ready details

/livez and /readyz are intentionally unauthenticated. See troubleshooting.md § 1.2 for the failure shape.

Create a run

Required scope: runs:write.

curl -sf -X POST "$BASE/v1/runs" \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "input": "Summarize Nvidia AI strategy",
    "spec_path": "examples/research_agent.yaml",
    "target": "workflow",
    "environment": "prod",
    "async_mode": false
  }' | jq

Synchronous response (run finished within the request):

{
  "run_id": "run_2026-05-10T12:34:56_a1b2c3d4",
  "status": "completed",
  "output_text": "Nvidia's AI strategy hinges on …",
  "human_intervention_required": false,
  "continuation_id": null,
  "metadata": {"workflow_kind": "sequential", "trace_event_count": 7}
}

If the workflow pauses on a human step:

{
  "run_id": "run_…",
  "status": "blocked",
  "output_text": null,
  "human_intervention_required": true,
  "continuation_id": "cont_…",
  "metadata": {"continuation_id": "cont_…", "request": {"id": "req_…", "prompt": "Approve the draft?"}}
}

The pending human request rides the run's metadata.request (its id is under request.id). On GET /v1/runs/{run_id} a blocked run also exposes a top-level pending_human_request object (populated from run state, redacted) — see Get run state.

For retry-safe submissions, send an Idempotency-Key header. Reusing the same key with the same body replays the original response; mismatched bodies return 409 idempotency_key_conflict.

Get run state

Required scope: runs:read.

curl -sf "$BASE/v1/runs/run_…" \
  -H "Authorization: Bearer $KEY" | jq
{
  "run_id": "run_…",
  "status": "running",
  "agent_name": "research-copilot",
  "workflow_name": "research-pipeline",
  "workflow_kind": "sequential",
  "current_step_index": 1,
  "current_node_id": "analyze",
  "visited_steps": ["retrieve"],
  "visited_nodes": ["retrieve"],
  "trace_event_count": 4,
  "metadata": {"environment": "prod"}
}

For terminal status:

{
  "run_id": "run_…",
  "status": "completed",
  "output_text": "…",
  "visited_steps": ["retrieve", "analyze", "summarize"],
  "trace_event_count": 11,
  "usage": {"input_tokens": 1840, "output_tokens": 320, "total_tokens": 2160}
}

The first-class usage object (relay #2) carries per-run token counts once the run has produced them (null until then). Tokens only — cost is a pricing-sheet concern for the dashboard. The same counts are also mirrored under metadata.usage.

List runs (paginated)

curl -sf "$BASE/v1/runs?status=running&limit=20&sort_by=created_at&sort_order=desc" \
  -H "Authorization: Bearer $KEY" | jq
{
  "runs": [
    {"run_id": "run_…", "status": "running", "workflow_name": "research-pipeline", "created_at": "2026-05-10T12:30:00Z"},
    {"run_id": "run_…", "status": "running", "workflow_name": "approval-workflow", "created_at": "2026-05-10T12:28:11Z"}
  ],
  "count": 2,
  "total": 2,
  "limit": 20,
  "offset": 0,
  "sort_by": "created_at",
  "sort_order": "desc"
}

Cancel a run

curl -sf -X POST "$BASE/v1/runs/run_…/cancel" \
  -H "Authorization: Bearer $KEY"

The run transitions to cancelled; cancellation is cooperative — in-flight steps stop at unit-of-work boundaries. See troubleshooting.md § 5.2.

Validate a spec

Required scope: specs:read.

curl -sf -X POST "$BASE/v1/specs/validate" \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"spec_path": "examples/research_agent.yaml", "environment": "prod"}' | jq
{
  "valid": true,
  "diagnostics": [],
  "report": {
    "agent_name": "research-copilot",
    "workflow_name": "research-pipeline"
  }
}

For an invalid spec, valid is false and diagnostics is populated:

{
  "valid": false,
  "diagnostics": [
    {
      "severity": "error",
      "code": "E_TOOL_REF",
      "message": "Tool 'web_search' is referenced but not defined.",
      "path": "agent.tools"
    }
  ]
}

List human tasks

Required scope: human:read.

curl -sf "$BASE/v1/human-tasks?run_id=run_…" \
  -H "Authorization: Bearer $KEY" | jq
{
  "tasks": [
    {
      "id": "cont_…",
      "run_id": "run_…",
      "workflow_name": "research-pipeline",
      "workflow_kind": "sequential",
      "pending_human_request_id": "req_…",
      "pending_human_request": {"id": "req_…", "prompt": "Approve the draft?"},
      "expires_at": 1715432400.0
    }
  ],
  "count": 1,
  "total": 1,
  "limit": 100,
  "offset": 0
}

Resume a human task

Required scope: human:write. Pair with Idempotency-Key for safe retries.

curl -sf -X POST "$BASE/v1/human-tasks/cont_…/resume" \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'Content-Type: application/json' \
  -d '{
    "request_id": "req_…",
    "decision": "approved",
    "content": "Looks good. Ship it."
  }' | jq
{
  "run_id": "run_…",
  "status": "completed",
  "output_text": "Published. https://…",
  "human_intervention_required": false,
  "continuation_id": null,
  "metadata": {}
}

decision is one of approved, rejected, edited, selected, provided. See human_in_the_loop.md.

List audit events

Required scope: audit:read. Audit payloads are redacted; secret and PII patterns never appear.

curl -sf "$BASE/v1/audit-events?event_type=human.decision" \
  -H "Authorization: Bearer $KEY" | jq
{
  "events": [
    {
      "id": "evt_…",
      "event_type": "human.decision",
      "actor": "reviewer",
      "created_at": "2026-05-10T12:35:01Z",
      "metadata": {
        "request_id": "req_…",
        "decision": "approved",
        "selected_option": null,
        "status": "completed",
        "has_content": true
      }
    }
  ],
  "count": 1
}

Inspect credential references

Required scope: credentials:read. Returns presence metadata only; secret values never appear.

curl -sf "$BASE/v1/security/credentials" \
  -H "Authorization: Bearer $KEY" | jq
{
  "inventory": {
    "providers": {
      "openai": {"name": "provider:openai", "env_var": "OPENAI_API_KEY", "present": true, "value": "[REDACTED]"},
      "anthropic": {"name": "provider:anthropic", "env_var": "ANTHROPIC_API_KEY", "present": false, "value": null}
    },
    "extra": {},
    "service_tokens": {
      "KNEO_SERV_API_KEYS": {"name": "service:KNEO_SERV_API_KEYS", "env_var": "KNEO_SERV_API_KEYS", "present": true, "value": "[REDACTED]"}
    }
  }
}

Each access records a credential.inventory_accessed audit event.

Read or update environment policy

Read requires policies:read; write requires policies:write.

curl -sf "$BASE/v1/policies/environment/prod" \
  -H "Authorization: Bearer $KEY" | jq
{
  "environment": "prod",
  "policy": {
    "enabled": true,
    "fail_on_warnings": false,
    "blocked_diagnostic_codes": ["E_UNSAFE_TOOL_IMPORT"],
    "require_human_review": false,
    "require_tool_permissions": true,
    "deny_unrestricted_tools": true,
    "require_guardrails": false
  }
}
curl -sf -X PUT "$BASE/v1/policies/environment/prod" \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "enabled": true,
    "require_tool_permissions": true,
    "deny_unrestricted_tools": true,
    "blocked_diagnostic_codes": ["E_UNSAFE_TOOL_IMPORT", "E_UNSAFE_FUNCTION_IMPORT"]
  }' | jq

The response includes previous_policy so you can audit what changed. Each write records a policy.changed audit event.

Error response shape

All error paths use the same envelope:

{
  "error": "forbidden",
  "message": "Missing required scope: runs:write",
  "required_scope": "runs:write"
}

Common error codes: unauthorized (401), forbidden (403), invalid_request (400), not_found (404), idempotency_key_conflict (409), payload_too_large (413), spec_path_confined (422), not_ready (503). Errors map through service/errors.py.

Pagination, filtering, and sorting

List-style endpoints return the original collection field plus pagination metadata:

{
  "count": 25,
  "total": 91,
  "window": 10000,
  "limit": 25,
  "offset": 50,
  "sort_by": "updated_at",
  "sort_order": "desc"
}

total is the store-wide count; window is the newest-rows window a single list request fetches and pages over. On deployments where total > window, rows older than the window are not reachable through the list endpoint (use retention pruning or direct store queries for archival access), and sort_order=asc orders within the window — it does not surface the oldest rows overall.

Supported query parameters:

  • GET /v1/runs: status, workflow_kind, workflow_name, session_id, has_error, created_after, created_before, q, limit, offset, sort_by, sort_order. The filters AND-combine; q is a bounded, case-insensitive substring search over each run's output_text + error.message (relay #4); has_error is a tri-state boolean; created_after/created_before compare against the stored ISO-8601 created_at. The same filter is applied to both the page and its reported total, so the count can't drift from the window.
  • GET /v1/runs/{run_id}/checkpoints: type, limit, offset, sort_by, sort_order
  • GET /v1/runs/{run_id}/trace: event_type, limit, offset, sort_by, sort_order
  • GET /v1/human-tasks: run_id, workflow_kind, status, limit, offset, sort_by, sort_order

sort_order is asc or desc; limit is capped at 1000.

Only the documented query parameters above are honored. 0.11.0 breaking change: an unrecognized query parameter is now rejected with 422 (error: "unknown_query_parameters", naming the offending keys) on the authenticated /v1 (and root) routes — through 0.10.x it was silently ignored. This mirrors the request-body contract (unknown body fields are already rejected). The /healthz, /readyz, and /metrics endpoints are exempt (monitoring tooling may pass arbitrary scrape params). See upgrade.md § 0.11.0.