HTTP API
agentkernel includes a REST API for programmatic sandbox management.
Starting the Server
# As a background service (recommended — survives reboots)
brew services start agentkernel
# Or start manually on default port (18888)
agentkernel serve
# Custom port
agentkernel serve --port 3000
# With API key authentication
agentkernel serve --api-key your-secret
# Multiple API keys
agentkernel serve --api-key key1 --api-key key2
# Load keys from a file (one per line, # comments supported)
agentkernel serve --api-key-file /path/to/keys.txt
# Or via environment variable
AGENTKERNEL_API_KEY=your-secret agentkernel serve
# With OpenTelemetry trace export
agentkernel serve --otel-endpoint http://localhost:4318
# With webhook notifications
agentkernel serve --webhook-url http://localhost:9999/hooks
# Multiple webhooks + OTel
agentkernel serve --otel-endpoint http://localhost:4318 \
--webhook-url http://hook1.example.com \
--webhook-url http://hook2.example.com
Private local control socket
For CLI/MCP delegation alongside a TLS-only TCP listener, use
--control-socket /absolute/private/path/api.sock or [api].control_socket.
Set AGENTKERNEL_CONTROL_SOCKET to that same path in clients. See the
Firecracker control transport example
for directory permissions, TLS setup, and authentication.
Authentication
When API key authentication is enabled (via --api-key, --api-key-file, AGENTKERNEL_API_KEY, or [api].api_key in config), all requests require an Authorization header, except for the public GET /health, GET /status, GET /metrics, and GET /stats endpoints:
Multiple keys can be configured for key rotation or multi-tenant setups. Any valid key will authenticate the request.
Resource quota status (enterprise)
Returns only the authenticated principal's user and organization scopes. It is
safe for dashboards to poll this endpoint. Usage is reported even when
enabled is false; limits are informational and no lifecycle request is
denied. A server without enterprise quota support returns 404, which
clients should present as “quota reporting unavailable.”
{
"success": true,
"data": {
"enabled": true,
"user": {
"id": "user-123",
"limits": {"max_running_sandboxes": 4, "max_total_vcpus": 16},
"usage": {"total_sandboxes": 2, "running_sandboxes": 1, "total_vcpus": 4, "total_memory_mb": 2048}
},
"organization": {
"id": "acme",
"limits": {"max_total_sandboxes": 100},
"usage": {"total_sandboxes": 12, "running_sandboxes": 5, "total_vcpus": 24, "total_memory_mb": 16384}
}
}
}
Quota denials return HTTP 429 with a dimension-specific message and emit a
quota_denied audit event. The check is serialized with the lifecycle
mutation inside one daemon process, so concurrent HTTP create requests cannot
oversubscribe a limit. Running multiple daemons against the same sandbox state
is not yet a supported quota-enforcement topology.
The quota-enforced surface is the persistent HTTP sandbox API: create, list,
get, start, stop, pause, resume, fork, resize, delete, logs, restore, and config import. Restore
and import consume total capacity (restore is created stopped; its running
slot is charged only when started). Resume and fork charge a running slot; a
successful pause releases one. All sandbox-scoped HTTP routes—including
exec, files, detached commands, browser, Git, and config export—are filtered
to the authenticated user and organization. An explicit JWT admin role may
cross owners. Requests for an unauthorized or legacy unowned sandbox return
the same 404 as a missing sandbox; local CLI access is unaffected.
Fast /run pool/ephemeral execution, CLI/MCP direct VmManager calls,
scheduler autostart, and background object/task workers remain outside quota
accounting. Multiple daemons sharing one sandbox state directory are not a
supported quota-enforcement topology.
Endpoints
Health Check
Server Status
{
"success": true,
"data": {"version": "0.15.0", "backend": "docker", "api_key_configured": false}
}
Server Statistics
{
"success": true,
"data": {
"sandbox_count": 12,
"sandbox_limit": 0,
"backend": "docker",
"uptime_seconds": 3600,
"version": "0.15.0",
"resource_usage": {
"cpu_percent": 65.2,
"memory_used_mb": 8192,
"memory_total_mb": 16384,
"disk_used_mb": 4096
}
}
}
The resource_usage field provides host-level CPU, memory, and disk metrics for fleet load-balancing.
Event Stream (SSE)
Streams sandbox lifecycle events via Server-Sent Events. Requires authentication when API keys are configured. Requires --webhook-url or --otel-endpoint to enable the event bus.
# Stream all events
curl -N http://localhost:18888/events
# Filter to a specific sandbox
curl -N http://localhost:18888/events?sandbox=my-sandbox
Events:
- sandbox.created — sandbox was created and started
- sandbox.exec.completed — command execution finished (includes exit_code, duration_ms)
- sandbox.deleted — sandbox was removed
Event payload:
{
"event": "sandbox.exec.completed",
"timestamp": "2026-02-23T12:00:00Z",
"sandbox": "my-sandbox",
"labels": {},
"metadata": {
"command": "echo hello",
"duration_ms": 42,
"success": true,
"exit_code": 0
}
}
Observability Flags
| Flag | Description |
|---|---|
--otel-endpoint URL |
OTLP/HTTP endpoint for trace export (e.g. http://localhost:4318) |
--webhook-url URL |
POST events to this URL (can be repeated) |
When --otel-endpoint is set, every HTTP request creates an OTel span with W3C traceparent propagation. Pass a traceparent header on incoming requests to link sandbox operations to your existing traces.
When executing commands (POST /sandboxes/{name}/exec), the TRACEPARENT and TRACESTATE environment variables are automatically injected into the sandbox, enabling code inside the sandbox to continue the distributed trace.
Garbage Collection
Removes sandboxes that have exceeded their time-to-live. In enterprise mode, this fleet-wide destructive operation requires an administrator identity.
Run Command
Execute a command in a temporary sandbox.
curl -X POST http://localhost:18888/run \
-H "Content-Type: application/json" \
-d '{"command": ["python3", "-c", "print(1+1)"], "fast": false}'
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
command |
array | Yes | Command and arguments |
image |
string | No | Docker image on the non-fast path (auto-detected if omitted) |
profile |
string | No | Security profile on the non-fast path |
fast |
bool | No | Use container pool (default: true) |
Run Command (Streaming)
Execute a command with Server-Sent Events (SSE) streaming.
curl -X POST http://localhost:18888/run/stream \
-H "Content-Type: application/json" \
-d '{"command": ["python3", "-u", "-c", "import time; print(1); time.sleep(1); print(2)"], "fast": false}'
Response (SSE stream):
event: started
data: {"sandbox":"sandbox-abc123"}
event: progress
data: {"stage":"creating"}
event: progress
data: {"stage":"starting"}
event: progress
data: {"stage":"executing"}
event: output
data: {"content":"Processing step 1...\n"}
event: output
data: {"content":"Processing step 2...\n"}
event: done
data: {"exit_code":0}
Event types:
| Event | Data | Description |
|---|---|---|
started |
{"sandbox": "name"} |
Command execution started |
progress |
{"stage": "..."} |
Execution stage (creating, starting, executing) |
output |
{"content": "..."} |
Command output (stdout/stderr) |
done |
{"exit_code": 0} |
Command completed successfully |
error |
{"message": "..."} |
Error occurred |
Request body: Same as /run
Use cases: - Long-running commands - Real-time output display - Progress tracking
List Sandboxes
# List all sandboxes
curl http://localhost:18888/sandboxes
# Filter by labels (multiple labels are ANDed)
curl "http://localhost:18888/sandboxes?label=env:prod&label=team:ml"
{
"success": true,
"data": [
{"name": "my-sandbox", "uuid": "019abc12-1234-7def-89ab-0123456789ab", "status": "running", "backend": "docker", "ip": "172.17.0.3"},
{"name": "test", "uuid": "019abc12-2345-7def-89ab-0123456789ab", "status": "stopped", "backend": "docker"}
]
}
The ip field contains the container's Docker bridge network IP address. It is only present for running sandboxes.
Create Sandbox
curl -X POST http://localhost:18888/sandboxes \
-H "Content-Type: application/json" \
-d '{"name": "my-sandbox", "image": "python:3.12-alpine"}'
{
"success": true,
"data": {"name": "my-sandbox", "uuid": "019abc12-1234-7def-89ab-0123456789ab", "status": "running", "backend": "docker"}
}
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Sandbox name |
image |
string | No | Docker image on the non-fast path (auto-detected if omitted) |
vcpus |
integer | No | Number of vCPUs (default: 1) |
memory_mb |
integer | No | Memory in MB (default: 512) |
profile |
string | No | Security profile: permissive, moderate, restrictive |
volumes |
string[] | No | Persistent mounts in slug:/container/path or slug:/container/path:ro format; volumes must already exist (Docker/Podman backends) |
network |
object | No | AgentKernel-managed Docker/Podman bridge; see fields below |
labels |
object | No | Key-value labels for fleet management and filtering |
description |
string | No | Human-readable description |
lifecycle |
object | No | Lifecycle policy (auto_stop_after_seconds, auto_archive_after_seconds, auto_delete_after_seconds) |
The optional network object is only supported with Docker and Podman:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Runtime bridge name (1-63 letters, digits, ., _, or -) |
subnet |
string | No | IPv4 CIDR, default 172.30.0.0/24 |
gateway |
string | No | IPv4 gateway inside the subnet; defaults to the first host address |
dns |
string[] | No | IPv4 DNS servers passed to the container at run time |
static_ip |
string | No | Fixed IPv4 address for this sandbox; it cannot be changed after allocation |
curl -X POST http://localhost:18888/sandboxes \
-H "Content-Type: application/json" \
-d '{
"name": "networked-sandbox",
"backend": "docker",
"network": {
"name": "agentkernel-dev",
"subnet": "172.30.0.0/24",
"gateway": "172.30.0.1",
"dns": ["1.1.1.1"],
"static_ip": "172.30.0.10"
}
}'
AgentKernel validates the network values, persists address ownership with a lock, and restores leases across restart. Existing networks must have AgentKernel ownership labels and compatible bridge settings; external networks are never adopted or removed. Managed bridge networking is not available for Firecracker, Apple Container, Kubernetes, Nomad, or remote backends.
With labels and description:
curl -X POST http://localhost:18888/sandboxes \
-H "Content-Type: application/json" \
-d '{
"name": "eval-sandbox",
"image": "python:3.12-alpine",
"volumes": ["my-data:/data", "cache:/cache:ro"],
"labels": {"scenario": "drift_s3", "model": "sonnet", "eval_run": "pr-123"},
"description": "Drift scenario evaluation"
}'
Persistent named volumes are currently supported by the Docker and Podman
backends. Other backends reject a create request that includes volumes
instead of silently dropping the mounts.
Update Sandbox
curl -X PATCH http://localhost:18888/sandboxes/my-sandbox \
-H "Content-Type: application/json" \
-d '{"labels": {"env": "staging"}, "description": "Updated description"}'
{
"success": true,
"data": {"name": "my-sandbox", "uuid": "019abc12-...", "status": "running", "backend": "docker"}
}
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
labels |
object | No | Replace all labels |
description |
string | No | Update description |
lifecycle |
object or null | No | Set lifecycle policy or clear it with null |
Get Sandbox
{
"success": true,
"data": {
"name": "my-sandbox",
"uuid": "019abc12-1234-7def-89ab-0123456789ab",
"status": "running",
"backend": "docker",
"ip": "172.17.0.3",
"image": "python:3.12-alpine",
"vcpus": 1,
"memory_mb": 512,
"created_at": "2026-01-30T12:00:00Z"
}
}
The response includes resource limits and metadata when available. The ip field is only present for running sandboxes. Fields that are unknown are omitted.
Get Sandbox by UUID
Execute in Sandbox
curl -X POST http://localhost:18888/sandboxes/my-sandbox/exec \
-H "Content-Type: application/json" \
-d '{"command": ["ls", "-la"]}'
Start Sandbox
An empty request body starts with the default moderate permissions and no file injections, preserving the original API contract:
The local CLI can select a private start configuration it previously persisted for this sandbox:
curl -X POST http://localhost:18888/sandboxes/my-sandbox/start \
-H "Content-Type: application/json" \
-d '{
"configuration": {
"source": "persisted",
"token": "<64-character one-shot nonce from the local CLI>"
}
}'
The server derives this configuration from a private, atomic host manifest. Its unguessable nonce is one-shot and the manifest is bound to the sandbox UUID, persisted owner, and authenticated request identity. It is consumed on the first attempt, expires after five minutes, is superseded by the next local start configuration, and is scrubbed on sandbox removal. This prevents replay after a config change or remove/recreate cycle. The HTTP request cannot contain permission or file-injection values, so an authenticated API caller cannot use start to enable host mounts, environment passthrough, privileged mode, or other capabilities. The CLI creates this reference automatically; API clients normally omit the body.
For an unowned sandbox created by the local CLI, successful validation of this
one-shot reference also authorizes the server's first ownership claim. This
continues to work after a server restart or a prior GET imported the state;
merely discovering or refreshing an unowned sandbox never authorizes a claim.
Stop Sandbox
Pause a Firecracker Sandbox
Capture a durable full-VM checkpoint containing guest memory, process state,
virtual-device state, and disk state. The resulting sandbox status is
paused.
Pause is supported only for Firecracker sandboxes on x86_64 Linux/KVM. Unsupported
backends return 422; a lifecycle state conflict, such as pausing an already
paused sandbox, returns 409. Enterprise policy requires the Run action.
Resume a Firecracker Sandbox
Resume restores the captured memory and process state. Enterprise deployments
require the Run action and return 429 if resuming would exceed a
running-sandbox or resource quota.
Fork a Paused Firecracker Sandbox
Create a new running sandbox from the paused source. The source remains paused and can be resumed or forked again.
curl -X POST http://localhost:18888/sandboxes/experiment-a/fork \
-H "Content-Type: application/json" \
-d '{"as_name":"experiment-b"}'
{
"success": true,
"data": {
"sandbox": {
"name": "experiment-b",
"status": "running",
"backend": "firecracker"
},
"security_warning": "Forking duplicates userspace memory. Rotate cached identifiers and cryptographic tokens in each child; prefer proxy-managed secrets that never enter the VM."
}
}
The request body accepts exactly one required string field, as_name. A name
collision or unpaused source returns 409; a non-Firecracker source returns
422; enterprise quota denial returns 429. The child atomically inherits the
source ownership metadata. A request whose authenticated owner or tenant does
not match the source is rejected with 403 before any child VM is restored.
Enterprise policy requires Run on the source plus both Create and Run for the
child; Create permission alone cannot authorize access to the source memory or
disk state.
Security warning: Forking duplicates guest memory and filesystem state, including credentials captured in the checkpoint. Rotate or revoke cloned credentials when appropriate.
Delete Sandbox
Extend TTL
Extend a sandbox's time-to-live.
curl -X POST http://localhost:18888/sandboxes/my-sandbox/extend \
-H "Content-Type: application/json" \
-d '{"by": "1h"}'
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
by |
string | No | Duration to extend (default: "1h"). Examples: "30m", "2h", "1d" |
Recover Archived Sandbox
Clears archive metadata so an archived sandbox can be started again.
Reconcile Lifecycle Policies
Applies lifecycle policies across all sandboxes (or previews actions). In enterprise mode, both apply and dry-run requests require an administrator identity because evaluation spans every tenant.
# Apply lifecycle actions
curl -X POST http://localhost:18888/lifecycle/reconcile
# Dry run (preview only)
curl -X POST http://localhost:18888/lifecycle/reconcile \
-H "Content-Type: application/json" \
-d '{"dry_run": true}'
File Operations
Read, write, and delete files inside a running sandbox.
Write File
curl -X PUT http://localhost:18888/sandboxes/my-sandbox/files/tmp/hello.txt \
-H "Content-Type: application/json" \
-d '{"content": "hello world"}'
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
content |
string | Yes | File content (text or base64-encoded) |
encoding |
string | No | utf8 (default) or base64 |
Binary file (base64):
curl -X PUT http://localhost:18888/sandboxes/my-sandbox/files/tmp/data.bin \
-H "Content-Type: application/json" \
-d '{"content": "aGVsbG8=", "encoding": "base64"}'
Read File
Binary files are returned as base64 with "encoding": "base64".
Delete File
Sandbox Logs
Retrieve audit log entries for a specific sandbox.
{
"success": true,
"data": [
{
"timestamp": "2026-01-30T12:00:00Z",
"event": "sandbox_created",
"sandbox": "my-sandbox"
}
]
}
Returns all audit events associated with the sandbox, sorted by timestamp. See audit logging for event types.
Batch Execution
Run multiple commands in parallel, each in its own temporary sandbox.
curl -X POST http://localhost:18888/batch/run \
-H "Content-Type: application/json" \
-d '{
"commands": [
{"command": ["echo", "hello"]},
{"command": ["python3", "-c", "print(2+2)"]}
]
}'
{
"success": true,
"data": {
"results": [
{"output": "hello\n", "error": null},
{"output": "4\n", "error": null}
]
}
}
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
commands |
array | Yes | List of commands to run |
commands[].command |
array | Yes | Command and arguments |
Each command runs in an isolated container from the pool. Results are returned in the same order as the input commands.
Snapshots
List Snapshots
{
"success": true,
"data": [
{
"name": "checkpoint-1",
"sandbox": "my-sandbox",
"image_tag": "agentkernel-snap:checkpoint-1",
"backend": "docker",
"base_image": "python:3.12-alpine",
"vcpus": 2,
"memory_mb": 512,
"created_at": "2026-02-05T12:00:00Z"
}
]
}
Take Snapshot
curl -X POST http://localhost:18888/snapshots \
-H "Content-Type: application/json" \
-d '{"sandbox": "my-sandbox", "name": "checkpoint-1"}'
{
"success": true,
"data": {
"name": "checkpoint-1",
"sandbox": "my-sandbox",
"image_tag": "agentkernel-snap:checkpoint-1",
"backend": "docker",
"base_image": "python:3.12-alpine",
"vcpus": 2,
"memory_mb": 512,
"created_at": "2026-02-05T12:00:00Z"
}
}
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
sandbox |
string | Yes | Name of the sandbox to snapshot |
name |
string | Yes | Name for the snapshot |
Get Snapshot
Returns snapshot details (same format as list).
Delete Snapshot
Restore Snapshot
curl -X POST http://localhost:18888/snapshots/checkpoint-1/restore \
-H "Content-Type: application/json" \
-d '{"as_name": "restored-sandbox"}'
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
as_name |
string | No | Name for the restored sandbox (defaults to {original}-restored) |
Browser Automation
Control a persistent headless browser inside a sandbox via ARIA snapshots.
The browser server starts automatically on first use. It runs Chromium via Playwright inside the sandbox and exposes an internal HTTP API on port 9222.
Start Browser Server
Starts the in-sandbox browser server. Called automatically by other browser endpoints if needed.
List Pages
Navigate (Goto)
curl -X POST http://localhost:18888/sandboxes/my-browser/browser/pages/default/goto \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
{
"snapshot": "- document \"Example Domain\":\n - heading \"Example Domain\" [level=1] [ref=e1]\n ...",
"url": "https://example.com/",
"title": "Example Domain",
"refs": ["e1", "e2"]
}
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | URL to navigate to |
Get ARIA Snapshot
Returns the current ARIA snapshot without navigating. Same response format as goto.
Click Element
curl -X POST http://localhost:18888/sandboxes/my-browser/browser/pages/default/click \
-H "Content-Type: application/json" \
-d '{"ref": "e2"}'
Returns a new ARIA snapshot after clicking.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
ref |
string | No | Ref ID from ARIA snapshot |
selector |
string | No | CSS selector (fallback) |
Fill Input
curl -X POST http://localhost:18888/sandboxes/my-browser/browser/pages/default/fill \
-H "Content-Type: application/json" \
-d '{"ref": "e3", "value": "search query"}'
Returns a new ARIA snapshot after filling.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
value |
string | Yes | Text to type |
ref |
string | No | Ref ID from ARIA snapshot |
selector |
string | No | CSS selector (fallback) |
Screenshot
Returns a PNG screenshot as base64.
Evaluate JavaScript
curl -X POST http://localhost:18888/sandboxes/my-browser/browser/pages/default/evaluate \
-H "Content-Type: application/json" \
-d '{"expression": "document.title"}'
Get Page Content
Returns raw page content (title, text, links) — similar to v1 goto format.
Close Page
Browser Events
[
{"seq": 1, "type": "page.navigated", "page": "default", "ts": "2026-02-10T12:00:00Z"},
{"seq": 2, "type": "page.clicked", "page": "default", "ts": "2026-02-10T12:00:01Z"}
]
Query parameters:
| Field | Type | Required | Description |
|---|---|---|---|
offset |
integer | No | Start from this sequence number (default: 0) |
limit |
integer | No | Max events to return (default: 100) |
Durable Objects
List Objects
{
"success": true,
"data": [
{
"id": "019abc12-...",
"class": "Counter",
"object_id": "counter-1",
"status": "active",
"sandbox": "my-sandbox",
"storage": {"count": 42},
"idle_timeout_seconds": 300,
"created_at": "2026-02-18T12:00:00Z",
"updated_at": "2026-02-18T12:00:00Z"
}
]
}
Create Object
curl -X POST http://localhost:18888/objects \
-H "Content-Type: application/json" \
-d '{"class": "Counter", "object_id": "counter-1", "sandbox": "my-sandbox"}'
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
class |
string | Yes | Object class name |
object_id |
string | Yes | Unique object identifier within the class |
sandbox |
string | No | Sandbox to bind to (validated if provided) |
storage |
object | No | Initial storage state |
idle_timeout_seconds |
integer | No | Seconds before hibernation (default: 300) |
Get Object
Update Object
curl -X PATCH http://localhost:18888/objects/019abc12-... \
-H "Content-Type: application/json" \
-d '{"storage": {"count": 99}}'
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
storage |
object | No | Replace storage state |
status |
string | No | Set status (active, hibernating) |
Delete Object
Call Object Method
curl -X POST http://localhost:18888/objects/Counter/counter-1/call/increment \
-H "Content-Type: application/json" \
-d '{"amount": 1}'
Auto-creates the object if it does not exist. Wakes from hibernation if needed. The request body is passed as method arguments.
Schedules
When [[schedule]] entries are present in agentkernel.toml, the
/schedules/configured endpoints list the daemon-integrated user jobs and
expose their truthful execution state. The config jobs use stable IDs and run
in UTC. GET /schedules/configured/{id} and GET
/schedules/configured/{id}/status return enabled, status, last_run_at,
last_error, and the derived next_run_at. POST
/schedules/configured/{id}/trigger executes one configured job immediately;
it does not require the current minute to match cron.
curl http://localhost:18888/schedules/configured
curl http://localhost:18888/schedules/configured/refresh-index/status
curl -X POST http://localhost:18888/schedules/configured/refresh-index/trigger
The existing POST /schedules CRUD form remains available for durable-object
schedule records. Config-defined jobs are validated at daemon startup and are
not mutated by the HTTP API.
List Schedules
{
"success": true,
"data": [
{
"id": "019abc12-...",
"name": "daily-cleanup",
"cron": "0 0 * * *",
"method": "cleanup",
"args": {},
"status": "active",
"created_at": "2026-02-18T12:00:00Z",
"updated_at": "2026-02-18T12:00:00Z"
}
]
}
Create Schedule
curl -X POST http://localhost:18888/schedules \
-H "Content-Type: application/json" \
-d '{"name": "daily-cleanup", "cron": "0 0 * * *", "method": "cleanup"}'
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Schedule name |
method |
string | Yes | Method to invoke |
cron |
string | No | Cron expression (mutually exclusive with fire_at) |
fire_at |
string | No | One-shot fire time in RFC3339 format |
args |
object | No | Method arguments |
target_class |
string | No | Target durable object class |
target_object_id |
string | No | Target durable object ID |
target_orchestration |
string | No | Target orchestration |
Get Schedule
Delete Schedule
Durable Stores
List Stores
{
"success": true,
"data": [
{
"id": "019abc12-...",
"name": "my-db",
"kind": "sqlite",
"sandbox": "my-sandbox",
"config": {},
"created_at": "2026-02-18T12:00:00Z",
"updated_at": "2026-02-18T12:00:00Z"
}
]
}
Create Store
curl -X POST http://localhost:18888/stores \
-H "Content-Type: application/json" \
-d '{"name": "my-db", "kind": "sqlite", "sandbox": "my-sandbox"}'
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Store name |
kind |
string | Yes | Engine type: sqlite, kv, queue |
sandbox |
string | No | Sandbox to bind to (validated if provided) |
config |
object | No | Engine-specific configuration |
Get Store
Delete Store
Query Store
Run a read-only query against a store.
curl -X POST http://localhost:18888/stores/019abc12-.../query \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT * FROM users WHERE active = ?", "params": [true]}'
{
"success": true,
"data": {
"columns": ["id", "name", "active"],
"rows": [[1, "Alice", true]],
"row_count": 1
}
}
Execute Store
Run a write statement against a store.
curl -X POST http://localhost:18888/stores/019abc12-.../execute \
-H "Content-Type: application/json" \
-d '{"sql": "INSERT INTO users (name, active) VALUES (?, ?)", "params": ["Bob", true]}'
LLM Spend
Query daily token aggregates from the LLM proxy/interceptor at GET /llm/spend. This endpoint requires a validated JWT or configured API key, including on servers where other routes allow anonymous local access. Non-admin callers are restricted to their authenticated organization and user. The project filter is descriptive only and does not grant access.
Example:
curl 'http://localhost:18888/llm/spend?from=2026-08-01&to=2026-08-31&limit=100' \
-H "Authorization: Bearer $AGENTKERNEL_API_KEY"
The response is daily, bounded aggregate data containing no prompt, response,
header, API-key, or monetary-cost fields. Provider pricing is not configured
by AgentKernel, so monetary cost is reported as unavailable. The aggregate
SQLite database retains at most 180 days of daily rows. Existing JSONL proxy
file hooks can be replayed once with the LlmSpendStore::ingest_jsonl library
API; legacy events without trusted identity remain unknown and are not visible
to ordinary tenant scopes. Storage is also capped at 10,000 aggregate rows per
tenant/user and UTC day; additional distinct dimensions are combined in an
access-preserving __overflow__ bucket. limit must be 1-200 and offset must
be at most 100,000; malformed values are rejected with HTTP 400.
Error Responses
| Status Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad request (validation error) |
| 401 | Unauthorized (missing/invalid API key) |
| 404 | Not found |
| 500 | Internal server error |
SCIM 2.0 provisioning
The server exposes an authenticated SCIM 2.0 base URL at /scim/v2. SCIM
always requires an API key, including when the legacy sandbox API is running
without optional authentication. Configure the IdP with the same Bearer API
key used by the HTTP API. SCIM responses and errors use
application/scim+json.
The served tenant is selected at server startup, never from request input:
AGENTKERNEL_SCIM_TENANT_IDis authoritative when set.- Otherwise
[enterprise].org_idis read from the config path passed to the server (oragentkernel.tomlfor the plain server entry point). - The fallback tenant is
default.
Every user, group, and membership row stores this tenant ID and all reads and
writes scope it. SCIM users and groups are durable records in the same SQLite
database as orchestration state. User IDs and group IDs are generated UUIDv7
values; IdP externalId and uniqueness are tenant-scoped.
Supported discovery endpoints are ServiceProviderConfig, ResourceTypes,
and Schemas. User and group create/replace bodies must include exactly their
core SCIM schema URN (...:User or ...:Group); unsupported extension URNs
are rejected. Users support create, list with equality filters and one-based
pagination, get, replace, PATCH (including non-deleting deactivation with
active: false), and DELETE. Groups support create, list, get, replace, PATCH
membership synchronization (including members[value eq "<user-id>"]), and
DELETE. DELETE keeps an internal tombstone for audit/history, returns 404 for
the deleted ID, and releases unique names and external IDs for a replacement.
SCIM records are also materialized into durable, tenant-scoped authorization
bindings when group membership changes. Grants are opt-in: configure
[[enterprise.scim_group_mappings]] with tenant_id, exactly one of
group_id or group_external_id, and one or more roles and/or a team_id.
Group IDs are server-generated UUIDs, so group_external_id is normally the
stable IdP selector. Unknown or unmapped groups grant nothing, and invalid
mapping configuration disables SCIM storage rather than granting a partial
mapping.
For Cedar evaluation, the validated JWT sub must equal the SCIM user's
externalId, and the JWT org_id must equal the mapping's tenant_id.
There is no email, userName, or generated-UUID fallback. Inactive or deleted
SCIM users have no materialized grants; restarting the server reloads the
durable bindings and the configured mappings.