Skip to content

Kernel API reference

The kernos binary serves the kernel and the control plane on one listener, default http://127.0.0.1:7401, base path /v1. Bodies are JSON. If KERNOS_TOKEN is set, every request carries Authorization: Bearer <token>; otherwise the listener is loopback-only.

Errors: {"error": {"code": "snake_case_code", "message": "...", "details": {}}} with a matching status. Codes are stable.

Health and keys

GET /v1/health{"ok", "version", "uptime_s", "runs": {"running", "parked"}}

GET /v1/keys{"key_id", "algorithm": "ed25519", "public_key"} (base64url, 32 bytes)

Bundles

POST /v1/bundles {"bundle": {…}, "signature": {"key_id", "signature"}}201 {"bundle_id", "name", "version"}

The signature is verified over the canonical JSON of bundle against the trusted publisher keys. Unsigned or unknown-key bundles: 422 bundle_signature_invalid. Validation failures: 422 bundle_invalid with details.path. Re-posting the same name and version with identical content returns 200; different content is 409 bundle_version_exists.

GET /v1/bundles · GET /v1/bundles/{id}

Policies

POST /v1/policies {"name", "version", "source"}201 {"policy_id", "name", "version"}; parse errors 422 policy_invalid with details.line, details.column, details.message.

GET /v1/policies · GET /v1/policies/{name} · GET /v1/policies/{name}/{version}

POST /v1/policies/test {"policy_a": {name, version}, "policy_b": {name, version} | {"source"}, "corpus": [{action, run}]}{"cases", "flips": [{"index", "a", "b", "rule_a", "rule_b"}]}

Remits

POST /v1/remits

{"tools": ["ledger.*"], "scopes": ["sql:table:ledger_entries"], "grants": [],
 "spend": {"tokens": 200000, "usd": 2.0}, "autonomy": "supervised", "ttl_seconds": 86400,
 "policy_set": ["finance-default"], "requested_by": {"id": "u-ana", "role": "ap_clerk", "manager": "u-tom"}}

201 {"remit_id", "token", "expires_at"}

POST /v1/remits/{id}/derive with any subset of the fields; every given field must narrow or the answer is 422 remit_widens with details.field. → 201 {"remit_id", "parent_id", "token", "expires_at"}

GET /v1/remits/{id} → the payload, parent_id, run_id.

Runs

POST /v1/runs {"bundle_id", "workflow", "input", "remit_id", "requested_by"}201 {"run_id", "state"}. Input is validated against the workflow schema (422 input_invalid); a remit already bound to a run is 409 remit_bound.

GET /v1/runs?state=&department=&limit=&after={"runs", "next"} GET /v1/runs/{id} → the run state GET /v1/runs/{id}/events?from_seq=&limit={"events", "next_seq"} POST /v1/runs/{id}/events {"kind", "payload", "actor"}201 {"seq", "hash"} (external kinds only; X-Kernos-Lease or X-Kernos-Remit required) POST /v1/runs/{id}/replay{"chain_valid", "events", "state_matches", "decisions", "decision_mismatches", "chain_errors", "state"} POST /v1/runs/{id}/abandon {"reason", "actor"}202 {"compensations_scheduled"} POST /v1/runs/{id}/resume {"actor"} → resumes a run parked for human, quarantine or connector_quarantined

Leases

POST /v1/leases {"worker_id", "kinds": ["model", "tool", "action", "compensation"], "ttl_seconds"}204 when idle, or 200:

{"lease_id", "run_id", "step", "attempt", "expires_at", "heartbeat_seconds",
 "step_def": {},
 "context": {"input", "steps": {"<id>": {"output"}}, "run": {"id", "bundle", "workflow", "requested_by", "department"},
             "remit_token", "remit": {"autonomy", "grants", "tools", "scopes"},
             "prompts", "mock", "tools", "pacing", "approved_actions", "prior_events"}}

POST /v1/leases/{id}/heartbeat{"expires_at"} or 410 lease_expired POST /v1/leases/{id}/complete {"output", "usage": {"tokens", "usd"}}{"run_state", "next_step"} POST /v1/leases/{id}/fail {"error": {"code", "message"}, "deterministic"}{"outcome": "retry_scheduled" | "quarantined", "delay_ms"} POST /v1/leases/{id}/actions {"action": {…}}{"action_id", "decision", "rule", "approval_id"}

On approval_required the kernel parks the run and releases the lease; the worker stops the step at once. After approval the step is re-leased with approved_actions containing the id, and the same proposal returns allow with rule approved:<approval_id>. On deny the response is 403 action_denied.

Approvals

GET /v1/approvals?state=pending&approver=role:finance_admin POST /v1/approvals/{id} {"decision": "approved" | "rejected", "actor": {"id", "role"}, "reason"}{"run_id", "run_state"}. The actor must match the approver (403 not_the_approver); deciding twice is 409 already_decided.

Metrics

GET /v1/metrics in Prometheus text: kernos_runs{state}, kernos_steps_leased_total, kernos_step_latency_seconds, kernos_usage_usd_total{department}, kernos_approvals_pending, kernos_leases_expired_total, kernos_events_total{kind}.