API reference
The kobe HTTP API is served by the operator. All endpoints except /v1/status,
/healthz, and /metrics require an Authorization header using a method the
service advertises: a Bearer token for OIDC/static-token auth, or an SSH request
signature.
Quick reference
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST | /v1/leases | Required | Create a lease from a pool |
GET | /v1/leases | Required | List your active leases |
GET | /v1/leases/:id | Required | Get a specific lease |
DELETE | /v1/leases/:id | Required | Release a lease |
PATCH | /v1/leases/:id | Required | Extend a lease TTL |
GET | /v1/leases/:id/diagnostics | Required | Get diagnostics bundle URL |
GET | /v1/pools | Required | List available pools |
GET | /v1/pools/:name | Required | Get a specific pool's status |
GET | /v1/pools/:name/leases | Required | List leases for a specific pool |
POST | /v1/sandbox-leases | Required | Create a Sandbox lease from a SandboxPool |
GET | /v1/sandbox-leases | Required | List your Sandbox leases |
GET | /v1/sandbox-leases/:id | Required | Get one Sandbox lease |
PATCH | /v1/sandbox-leases/:id | Required | Extend a Sandbox lease TTL |
DELETE | /v1/sandbox-leases/:id | Required | Request Sandbox release |
GET | /v1/sandbox-leases/:id/logs | Required | Read the Sandbox log tail |
POST | /v1/sandbox-leases/:id/executions | Required | Run or reserve an idempotent execution |
GET | /v1/sandbox-leases/:id/executions/:execution | Required | Read execution state |
DELETE | /v1/sandbox-leases/:id/executions/:execution | Required | Cancel an execution |
GET | /v1/sandbox-leases/:id/executions/:execution/logs | Required | Read retained stdout/stderr windows |
GET | /v1/sandbox-leases/:id/attach | Required | Upgrade to an attach WebSocket |
GET | /v1/sandbox-leases/:id/port-forward | Required | Upgrade to a declared-port WebSocket |
GET | /v1/status | Optional | Endpoint status, auth methods, pool summary |
GET | /healthz | None | Liveness probe |
GET | /metrics | None | Prometheus metrics |
POST /v1/leases
Creates a lease from the specified pool. Returns 202 Accepted immediately with the lease ID and Pending phase. Poll GET /v1/leases/:id until the phase is Bound and kubeconfig is populated.
Request body
{
"profile": "ci-small",
"ttl": "30m",
"alias": "pr-3835",
"metadata": {
"github": {
"actor": "lenij",
"pullRequest": 3835,
"runId": 123456789
},
"purpose": "e2e"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
profile | string | Yes | Pool name to lease from |
ttl | string | No | Requested TTL (e.g. "30m", "1h"). Defaults to the pool's spec.ttl. Capped by AccessPolicy maxTtl. |
alias | string | No | DNS-label name unique among the requester's active leases. |
metadata | object | No | Opaque caller context. Maximum 8 KiB encoded JSON, 32 top-level keys, four nesting levels, and 128 JSON values. |
metadata is descriptive, untrusted, and creation-time only: the lease API
does not expose an update operation. It never affects authorization, quota,
priority, scheduling, or lifecycle behavior. Do not include credentials or
secrets: the lease owner and cluster administrators can read it. Shared pool
listings redact another tenant's metadata together with its requester identity
and alias.
Response — 202 Accepted
{
"id": "lease-a1b2c3d4e5f6",
"phase": "Pending",
"profile": "ci-small",
"queue_position": 0,
"effective_ttl": "30m",
"alias": "pr-3835",
"metadata": {
"github": {
"actor": "lenij",
"pullRequest": 3835,
"runId": 123456789
},
"purpose": "e2e"
}
}
effective_ttl reports the TTL the server accepted after policy clamping.
Once bound, GET /v1/leases/:id returns:
{
"id": "lease-a1b2c3d4e5f6",
"phase": "Bound",
"profile": "ci-small",
"kubeconfig": "apiVersion: v1\nclusters:\n...",
"expires_at": "2026-04-09T15:00:00Z"
}
Errors
| Status | Meaning |
|---|---|
400 | Invalid profile name, alias, TTL, or metadata |
403 | Profile not allowed for your identity |
429 | Concurrent lease limit reached, or server overloaded |
GET /v1/leases
Lists all active leases belonging to your identity.
Response — 200 OK
[
{
"id": "lease-a1b2c3d4e5f6",
"phase": "Bound",
"profile": "ci-small",
"expires_at": "2026-04-09T15:00:00Z"
}
]
GET /v1/leases/:id
Returns the full lease, including kubeconfig once the phase is Bound.
Response — 200 OK
{
"id": "lease-a1b2c3d4e5f6",
"phase": "Bound",
"profile": "ci-small",
"kubeconfig": "apiVersion: v1\n...",
"expires_at": "2026-04-09T15:00:00Z"
}
Possible values for phase: Pending, Bound, Released, Expired,
Recycling, Quarantined.
DELETE /v1/leases/:id
Releases a lease immediately. The cluster is destroyed and a fresh replacement
is queued. Returns 204 No Content. A lease already in Quarantined returns
409 Conflict with reason TeardownQuarantined; release cannot downgrade or
discard incomplete cleanup evidence.
PATCH /v1/leases/:id
Extends a lease TTL. Only allowed up to maxExtensions times per lease (configured in AccessPolicy, default: 2).
Request body
{
"extend_ttl": "30m"
}
Response — 200 OK
{
"expires_at": "2026-04-09T16:00:00Z"
}
GET /v1/leases/:id/diagnostics
Returns a pre-signed S3 URL to the diagnostics bundle for the lease, if diagnostics are configured on the pool and a bundle was captured.
Returns 404 if no bundle exists.
GET /v1/pools
Lists every pool accessible to your identity, regardless of resource kind.
resourceKind identifies what a lease allocates and capabilities identifies
the operations available to the caller.
Response — 200 OK
[
{
"name": "ci-small",
"resourceKind": "Cluster",
"capabilities": ["lease", "kubeconfig", "extend", "release"],
"phase": "Healthy",
"ready": 3,
"leased": 1,
"creating": 0,
"recycling": 0,
"unhealthy": 0,
"quarantined": 0,
"queueDepth": 0,
"policy": {
"mode": "fixed",
"ttl": "2h",
"warmTarget": 3
}
}
]
GET /v1/pools/:name
Returns status for a single pool.
Response — 200 OK
{
"name": "ci-small",
"resourceKind": "Cluster",
"capabilities": ["lease", "kubeconfig", "extend", "release"],
"phase": "Healthy",
"ready": 3,
"leased": 1,
"creating": 0,
"recycling": 0,
"unhealthy": 0,
"quarantined": 0,
"queueDepth": 0,
"policy": {
"mode": "fixed",
"ttl": "2h",
"warmTarget": 3
}
}
GET /v1/pools/:name/leases
Lists leases currently associated with one pool. This is useful for operational visibility when debugging queueing, stuck leases, or tenant usage.
Response - 200 OK
[
{
"id": "lease-a1b2c3d4e5f6",
"phase": "Bound",
"profile": "ci-small",
"cluster_name": "pool-ci-small-0",
"requester": "github:org/repo",
"expires_at": "2026-04-09T15:00:00Z"
}
]
Sandbox leases
SandboxPool and SandboxLease are alpha APIs. Sandbox HTTP endpoints are
mounted only when agentSandbox.mode=external and the operator-installed
Agent Sandbox v0.5.6 APIs pass read-only compatibility validation. Startup
writes no validation Claim. Disabled mode serves no Sandbox API, so it cannot
admit a lease that no controller will reconcile.
Sandbox callers select an administrator-owned SandboxPool; they cannot send a
Pod spec, namespace, RuntimeClass, environment values, mounts, PVCs, or target
credentials. Responses never contain a management or child-cluster kubeconfig.
Sandbox error reasons
Every Sandbox denial body is { "error": string, "detail"?: string, "reason"?: string }. reason carries a bounded machine-readable code — the
same condition reports the same code on the cluster and Sandbox paths (a
quarantined release is teardown_quarantined everywhere) — so an agent or
script can branch on why without parsing free text. It is omitted where no
bounded reason exists. The closed set:
reason | Retryable |
|---|---|
not_found | no |
not_ready | yes, once Ready or placed |
expired | no — request a new lease |
target_unresolved | yes, after placement completes |
provenance_incomplete | needs operator attention |
pool_unresolvable | needs operator attention |
not_declared | no |
ambiguous_alias | no — disambiguate first |
backend_error | yes, transient |
extension_budget_exhausted | no |
max_ttl_ceiling | no |
expiry_derivation_mismatch | no |
conflict_retryable | yes, against current state |
teardown_quarantined | no — resolve cleanup first |
POST /v1/sandbox-leases
Creates caller-safe Sandbox intent and returns 202 Accepted. The response
includes Location: /v1/sandbox-leases/:id; poll that URL until the lease is
ready or terminal.
{
"pool": "agent-small",
"ttl": "30m",
"alias": "review-142",
"idempotencyKey": "one-random-key-per-deliberate-create"
}
ttl defaults to the pool's defaultTtl and is capped by both the pool and the
caller's Sandbox policy. alias is optional and must be unique among that
identity's active Sandbox leases. Current clients should generate one random
idempotencyKey (1–253 bytes) for each deliberate create and reuse it only
while recovering that request. It remains optional for older clients.
{
"id": "sandbox-a1b2c3d4e5f60718293a4b5c",
"phase": "Pending",
"pool": "agent-small",
"ttl": "30m",
"alias": "review-142",
"provisioning_deadline": "2026-08-20T12:10:00Z"
}
When a requested TTL is reduced, effective_ttl reports the accepted value.
provisioning_deadline is anchored to the API-server creation time and already
includes queueing or controller downtime; retrying reconciliation never resets
it.
If Kobe is shutting down or cannot safely classify a lost Kubernetes response, it preserves the same legacy fields and adds an explicit non-retry handoff:
{
"id": "sandbox-a1b2c3d4e5f60718293a4b5c",
"phase": "Pending",
"pool": "agent-small",
"ttl": "30m",
"provisioning_deadline": "2026-08-20T12:10:00Z",
"status": "admission_pending",
"retry": false,
"statusUrl": "/v1/sandbox-leases/sandbox-a1b2c3d4e5f60718293a4b5c"
}
This response is not confirmation that placement was accepted. Do not repeat
the create: retain id and poll Location/statusUrl. The admission arbiter
will either admit that exact lease or cancel it; a later 404 means
cancellation won. phase: "Pending" remains present so older clients also keep
polling the handle instead of treating it as placeable.
A keyed retry returns the same durable lease and Location. Reusing a key with
different pool, TTL, or alias intent returns 409. If response headers arrived
but the body was lost, recover with GET Location; do not repeat the POST. If
the connection failed before headers, the same keyed POST may be repeated.
| Status | Meaning |
|---|---|
400 | Invalid pool name, alias, or TTL |
403 | Missing Sandbox lease permission or resource ceiling exceeded |
404 | An allowed SandboxPool does not exist |
409 | Alias is already active, or an idempotency key is bound to different intent |
429 | Sandbox concurrency limit reached, or the per-principal admission rate limit was hit |
503 | Pool configuration or Kubernetes admission cannot be verified |
Admission is rate limited per principal, independently of the concurrency
limit. Each attempt that passes the limiter spends one token before admission
work starts. An already-throttled request is refused without another charge or
admission mutation. A keyed retry may perform one exact-name GET solely to
recover an already committed lease; a miss remains 429. Rate-limit 429
includes Retry-After in seconds, while concurrency 429 does not. The limit
is per API replica, and kobe_sandbox_admission_rate_limited_total counts every
refusal.
GET /v1/sandbox-leases
Lists only leases owned by the authenticated identity and still permitted by its Sandbox pool scope. Requester identity and target credentials are omitted.
GET /v1/sandbox-leases/:id
Returns the typed lifecycle state for an owned lease. A lease belonging to
another identity returns 404.
Possible phases are Pending, Provisioning, Ready, Releasing, Released,
Expired, and Quarantined. Quarantined means cleanup is uncertain and the
capacity remains unavailable; it is not a successful terminal state.
When teardown starts, release_cause is recorded once as Requested,
RuntimeTtl, or ProvisioningDeadline. The first cause is immutable: a later
release request cannot turn an already-expired lease into a caller-requested
Released outcome. After cleanup is verified, both automatic causes finish as
Expired and Requested finishes as Released; unverifiable cleanup finishes
as Quarantined without returning capacity.
When resolved, the response may include non-secret placement and target object references with exact Kubernetes UIDs. It never includes bearer tokens or kubeconfigs.
PATCH /v1/sandbox-leases/:id
Extends the runtime TTL of a Ready lease. The body takes extendTtl (or
extend_ttl, so one body shape works against the cluster endpoint too):
{ "extendTtl": "30m" }
The response reports the new expiry and the remaining budget:
{ "expiresAt": "2026-08-10T01:32:00Z", "extensionsCount": 1, "maxExtensions": 2 }
Authorized by the lease verb rather than one of its own: an extension can
never move expiry past maxTtl measured from readiness, which is the same
ceiling the caller could have requested at creation. Set maxExtensions: 0 on
the Sandbox grant to forbid extension and require callers to commit up front.
Refusals are all 409 Conflict except a malformed or non-positive duration,
which is 400 Bad Request: a lease that is not Ready, an exhausted
extension budget, an extension that would exceed the ceiling, or a lease that
changed concurrently — the last is safe to retry against the current lease.
DELETE /v1/sandbox-leases/:id
Requires the independent Sandbox release verb. For an admitted active lease,
the API records server-owned release intent and returns 204 No Content; it
does not write lifecycle status. The placement controller observes that intent,
moves the lease to Releasing, revokes access, and verifies cleanup.
Repeated release of an already requested, Releasing, Released, or Expired
lease returns 204 No Content. A Quarantined lease returns 409 Conflict:
cleanup is still uncertain, so its capacity remains unavailable rather than
being reported as released.
POST /v1/sandbox-leases/:id/executions
Reserves the execution before starting its process, so retrying the same
request cannot run a command twice. idempotencyKey is required, contains
1–253 bytes, and must be new for each deliberate execution. Reusing it with
different command, working directory, timeout, container, or detach intent
returns 409 Conflict.
{
"command": ["/agent", "analyse", "./repo"],
"cwd": "/workspace",
"timeout": "5m",
"container": "agent",
"idempotencyKey": "execution-018f8f4f",
"detach": false
}
command is an argv array and is never interpreted by a shell. cwd and
container are optional. timeout defaults to 60 seconds, is capped at one
hour, and can never extend past the lease expiry.
Wait mode (detach: false) returns 200 OK after the supervised command ends:
{
"id": "sbxe-a1b2c3d4e5f6",
"state": "Succeeded",
"exitCode": 0,
"startedAt": "2026-08-20T12:01:00Z",
"finishedAt": "2026-08-20T12:01:12Z",
"stdout": "analysis complete\n",
"stderr": "",
"truncated": false
}
Detached mode returns 202 Accepted once the durable reservation exists. Poll
the execution URL; a keyed retry returns the same execution rather than
starting another process.
Execution states are Queued, Running, Succeeded, Failed, Cancelled,
TimedOut, and Unknown. exitCode is present only when the remote process
reported one. Unknown means Kobe could not establish the outcome and must not
be treated as permission to rerun effects automatically.
Common failures are 400 for an invalid command, timeout, cwd, or idempotency
key; 403/404 for unavailable lease access; 409 for key reuse with changed
intent and for the per-lifetime execution bound (reason: execution_limit_exhausted — retrying cannot succeed, request a new lease);
501 when the pool has no Kobe runner; and 503 when the execution store or
target cannot be verified.
GET /v1/sandbox-leases/:id/executions/:execution
Returns the current execution object in the same response shape, without
inline stdout/stderr. It returns 404 unless both the lease and execution
belong to the authenticated caller and exact lease instance.
GET /v1/sandbox-leases/:id/executions/:execution/logs
Reads retained output with independent resume positions:
?stdoutOffset=0&stderrOffset=0
{
"id": "sbxe-a1b2c3d4e5f6",
"state": "Running",
"stdout": {
"data": "building\n",
"nextOffset": 9,
"more": true,
"truncated": false
},
"stderr": {
"data": "",
"nextOffset": 0,
"more": false,
"truncated": false
}
}
Pass each returned nextOffset unchanged on the next request. Offsets count
the runner's retained raw bytes and are opaque resume tokens; do not derive
them from the length of data. The API represents each returned window as a
string, replacing invalid UTF-8 when necessary. more means another window is
already available. truncated means earlier or excess output was irreversibly
dropped at the retention cap.
A follower is drained only when the execution state is terminal and both
windows report more: false. Transport/body retry uses the same offsets so it
cannot duplicate an already acknowledged window. 401 and 403 are terminal
authorization results, not retry signals.
DELETE /v1/sandbox-leases/:id/executions/:execution
Requests cancellation and returns 200 OK with the state that actually
applies. Runner-managed execution cancellation signals the whole process group.
An execution already terminal remains in that terminal state; the response does
not claim it was cancelled after the fact. Repeating cancellation is safe.
GET /v1/status
Public endpoint — no auth required, but providing a token enriches the response with accessible pools.
Response — 200 OK
{
"version": "0.1.0",
"auth": {
"methods": ["oidc", "ssh"],
"sessions": []
},
"pools": [
{ "name": "ci-small", "ready": 3, "leased": 1, "creating": 0 }
]
}
This is the endpoint kobe status queries.