Extension protocol reference

Extension wire protocol; distinct from native-host protocol 1.

Preserved reference URL; not part of the current manual navigation.

Identity boundary: octet 0.8.0 source uses only octet first-party names, including octet_version, requires_octet, OCTET_*, and octet_extension. Retained API numbers 0.1/0.2/0.3/0.4 do not imply aliases for old Ygg wire names or imports. The source SDK distributions and four official executable bundles are version 0.8.0; independent examples keep their own versions. Native assets and public-install verification are recorded in the v0.8.0 GitHub release. Native publication does not publish SDK registries.

Version scope: API 0.4 is current and uses the feature-negotiated wire retained from supported API 0.2; API 0.1 stays frozen. Versioned examples below retain their original IDs. API 0.3 is a distinct canonical wire in the generated reference; its deferred entries do not remove dynamic tools, presentation, or child sessions from the feature-negotiated wire. These low-level contracts and safety tests remain live, but only an actual host offer and frontend binding make a service available. Pi mappings below are historical implementation notes, not a shipping bridge or a release gate.

Every request and response uses the standard JSON-RPC 2.0 envelope with exactly one JSON object per line on stdout. Human diagnostics belong on stderr, which octet drains and exposes as bounded diagnostic events.

Extensions send process-to-host messages at any time after initialization. For graceful shutdown, the host sends a JSON-RPC shutdown request; the extension acknowledges it and exits promptly. Stdin EOF means the transport was lost or finally torn down and should also make the extension exit.

The manifest selects exactly one wire version. API 0.1 remains available, with its frozen initialization structure in the octet namespace, for trusted text-oriented extensions. It does not gain API 0.2 cancellation, progress, structured/media retention, parent-correlation, or terminal lifecycle guarantees. API 0.2 adds those stateful guarantees through explicit initialization negotiation; support is never inferred from the extension package version. Installable bundles also carry an exact requires_octet requirement in extension.toml; it is validated before a process can start and is packaging metadata, not an initialization field or protocol-version substitute.

This protocol supports bounded integrations in a deliberately small coding host. octet hosts model conversations, session/result persistence, permissions and approvals, process supervision/cleanup, and resource limits. MCP, browser use, computer use, web search, memory, LSP, subagent orchestration, and caffeinate are examples of separate domains, not a promise to deliver a package for every capability. Generic host services support these integrations without moving their domain protocols into the host.

Implemented limits in this reference are protocol, queue, concurrency, artifact, timeout, and process-tree cleanup bounds. octet does not yet enforce OS CPU/RSS/FD/PID quotas or sandbox trusted extensions; they run with the current user's authority.

Transport defaults#

Parameter Default
Max JSON line 1 MiB (DEFAULT_EXTENSION_MESSAGE_BYTES)
Host in-flight requests 64 (DEFAULT_PENDING_REQUESTS); API 0.2 may negotiate lower
Complete frames waiting for the serialized writer 128 (DEFAULT_WRITER_QUEUE)
Request timeout 30 s
API 0.2 cancellation grace 2 s
Cancelled-ID tombstone retention 30 s, at most 512 IDs
Normal shutdown request/ack stage 2 s (ExtensionRuntimeConfig::shutdown_timeout)
Normal post-request process-exit stage 2 s (the same per-stage timeout)
Normal product aggregate shutdown 3 s (all extension shutdowns run concurrently)
Coordinated-signal extension-shutdown cap 1.4 s in interactive, plain, print, and host modes; then force-kill registered process groups

Product discovery reads a selected extension manifest through the resource resolver's 256 KiB bound. The lower-level ExtensionManifest::load API instead defaults to 64 KiB; the product discovery path calls parse on the resolver's bounded text and does not use that lower-level default.


The host uses one bounded writer to serialize complete frames onto the child's stdin; the SDK likewise gives one writer sole ownership of extension stdout. Neither abandons a partially written frame. API 0.2 requests are also admitted through the negotiated concurrency semaphore.


1. Host-to-extension methods#

1.1 initialize#

The first host request, sent immediately after the child process starts.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "api_version": "0.1",
    "octet_version": "0.8.0",
    "extension": {
      "name": "hello-world",
      "version": "0.1.0",
      "manifest_path": "/home/user/.octet/extensions/hello-world/extension.toml",
      "source": "global"
    },
    "workspace": "/home/user/project",
    "capabilities": {
      "filesystem": "none",
      "process": false,
      "network": false
    },
    "contributes": {
      "tools": ["hello_world"],
      "commands": ["hello"],
      "hooks": ["before_prompt", "after_response"],
      "ui": ["status"],
      "context": true,
      "tool_renderers": ["hello_world"],
      "notifications": true,
      "confirmations": false
    },
    "host": {
      "session_id": null,
      "session_name": null,
      "model": "claude-sonnet-4-6",
      "reasoning": null,
      "active_skills": []
    }
  }
}

API 0.1 response:

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "api_version": "0.1",
    "tools": [
      {
        "name": "hello_world",
        "description": "Greet someone from an executable extension",
        "parameters": {
          "type": "object",
          "properties": {
            "name": { "type": "string" }
          },
          "required": ["name"],
          "additionalProperties": false
        }
      }
    ],
    "commands": [
      {
        "name": "hello",
        "description": "Show a greeting notification",
        "usage": "/hello [name]"
      }
    ]
  }
}

Common validation rules:

  • api_version in the response must exactly match the manifest-selected version, or the child is rejected.
  • Without negotiated discovery features, the duplicate-free sets of tools and commands returned must exactly equal the corresponding names declared in contributes. Negotiated dynamic_tools makes initialize's tool list authoritative epoch 0; negotiated runtime_commands makes initialize's command list authoritative for that fixed process generation. Duplicate names always reject initialization, and an initialize response may contain at most 256 command definitions.
  • Each tool must have a non-empty description and parameters must be a JSON Schema object.
  • source is one of "project", "global", or "explicit".

API 0.1 uses the request and response above and must omit protocol from both. A 0.1 tool definition cannot declare output_schema, and a 0.1 manifest cannot declare semantic presentation.

API 0.2 retains every top-level initialization field, may set contributes.presentation to true, and adds this request member:

json
{
  "protocol": {
    "version": "0.2",
    "required_features": ["request_cancellation", "content_parts"],
    "optional_features": [
      "request_progress",
      "artifacts",
      "lifecycle_events",
      "policy_intents",
      "dynamic_tools",
      "runtime_commands"
    ],
    "limits": {"max_concurrent_requests": 64}
  }
}

The response must retain top-level api_version, tools, and commands, and add:

json
{
  "protocol": {
    "version": "0.2",
    "features": [
      "request_cancellation",
      "content_parts",
      "request_progress",
      "artifacts",
      "lifecycle_events",
      "dynamic_tools",
      "runtime_commands"
    ],
    "limits": {"max_concurrent_requests": 4},
    "lifecycle_events": [
      "session/started",
      "session/settled",
      "turn/started",
      "turn/settled",
      "tool/started",
      "tool/settled"
    ]
  }
}

request_cancellation and content_parts are required. The response may add any subset of the advertised optional features, but missing required, unknown, or duplicate feature names reject initialization. The accepted max_concurrent_requests must be greater than zero and is capped by the host. runtime_commands is initialization-only: the returned command list is fixed for the generation, there are no commands/register or commands/unregister methods, and reload still requires an identical command catalog. It exists for compatibility runtimes that cannot know foreign command names before loading that runtime.

If lifecycle_events is negotiated and the subscription list is omitted or empty, all six events are subscribed. Otherwise it must be an exact subset of the six names above. A non-empty subscription without the feature is invalid.

The coding host conditionally appends agent_sessions to optional_features only for the trusted, enabled first-party octet-subagents extension when its child-session service can be bound. The service is available independently of the selected reasoning effort; Ultra is separately gated on the live provider's V2 metadata. A response may negotiate it only when it was offered. The service is bound after the Agent is constructed; calls without a bound service/resource owner fail deterministically with -32002. When agent_sessions is offered, the host also offers agent_model_selection_v1; negotiating the latter requires agent_sessions. It enables bounded configured-model discovery and explicit child model selection, not extension-supplied provider transports or credentials.

The host likewise appends approvals only when single-use approval issuance is enabled, and appends secrets only when a secret broker is configured and the manifest's exact [capabilities].secrets allowlist is non-empty. Negotiating approvals also requires policy_intents; neither conditional service may be returned when it was not offered. The coding product currently leaves approvals disabled and configures no secret broker, so it offers neither conditional feature. Generic policy/evaluate requests return deny; the working-tree mcp.tool.call adapter permits exact active owner-scoped MCP calls under full access only, as described below.

Secret names are duplicate-free identifiers of at most 64 ASCII bytes. The first character is a letter or underscore; subsequent characters may also use digits, hyphen, or dot. The list is a broker allowlist, not launch-environment injection, and exact undeclared names remain inaccessible. An empty allowlist is omitted from the serialized capabilities object, preserving the literal API 0.1 initialize shape above. A non-empty current/API 0.2 manifest includes the exact secrets array in that object.

API 0.2 may also include [capabilities].environment, an explicit ambient broker allowlist. The only current reviewed name is SSH_AUTH_SOCK. It remains absent from the default sanitized subprocess environment and is copied from the host only when declared and present; values are not included in initialize, diagnostics, or persistence. Unsupported names and any API 0.1 declaration are invalid. Access to an agent socket grants signing authority to the trusted extension and is not a sandbox.

API 0.2 tool definitions may add output_schema, a bounded supported subset of JSON Schema used to validate structured_content. Schema nodes must be objects, nesting is capped at 32, property names at 256 bytes, and the accepted keywords are $schema, title, description, default, examples, type, properties, required, additionalProperties, items, enum, const, allOf, anyOf, oneOf, minimum, maximum, exclusiveMinimum, exclusiveMaximum, minLength, maxLength, minItems, maxItems, uniqueItems, minProperties, and maxProperties. Arbitrary JSON Schema vocabulary is rejected.


1.2 tool/call#

Invoke a model-callable tool.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tool/call",
  "params": {
    "name": "git_status",
    "arguments": {
      "include_ignored": false,
      "max_entries": 80
    },
    "catalog_revision": 2,
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "resource_owner": {
        "session_id": "durable-session-owner",
        "extension_instance_id": "host-created-instance-fence",
        "process_generation": 3
      },
      "host": {
        "session_id": "abc123",
        "session_name": null,
        "model": "claude-sonnet-4-6",
        "reasoning": null,
        "active_skills": []
      }
    }
  }
}

catalog_revision is present only for API 0.2 extensions that negotiated dynamic_tools. It selects the exact per-process catalog epoch the model saw. An extension must dispatch against that historical schema-and-handler snapshot, not whatever catalog happens to be newest when the call arrives. API 0.1 and API 0.2 without dynamic_tools omit the field.

resource_owner is present for API 0.2 model-tool/tool-hook contexts and, in the coding product, slash commands, before_prompt, after_response, and context/collect. Its host-derived session_id is the durable namespace for extension state. extension_instance_id changes across a complete process-host rebuild, even when generation numbering restarts, and process_generation rejects stale browser tabs, MCP/LSP connections, memory handles, and comparable resources after an extension reload or automatic restart within that host instance. Key state by all three fields. API 0.1 omits the field. Status, renderer, and unsolicited contribution contexts remain process-scoped and must not allocate session-owned handles. A context owner alone does not authorize reverse host services: model-tool and declared-command requests are active parents, while prompt/context handlers are not, and the negotiated service rules still apply.

API 0.1 response:

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": "branch=main\nstate=clean\ncounts=staged:0,modified:0,untracked:0,ignored:0,conflicted:0",
    "is_error": false
  }
}

Fields:

  • content — compact model-visible result text (string).
  • is_error — if true, the result is treated as a tool error.
  • metadata — optional JSON accepted by the API 0.1 decoder for compatibility. The current subprocess adapter discards it while constructing native ToolOutput; it is not sent to the model and has no frontend, renderer, or persistence guarantee.

API 0.2 response:

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {"type": "text", "text": "Found 3 sources."},
      {
        "type": "image",
        "artifact_id": "artifact-opaque-id",
        "mime_type": "image/png",
        "alt": "Search result preview"
      }
    ],
    "structured_content": {
      "sources": [{"title": "Example", "url": "https://example.com"}]
    },
    "is_error": false,
    "metadata": {"cache": "miss"}
  }
}

API 0.2 content is a non-empty ordered array of at most 256 parts and must contain at least one explicit text part. Supported parts are:

  • { "type": "text", "text": string }
  • { "type": "image", "artifact_id": string, "mime_type": string, "alt"?: string }
  • { "type": "audio", "artifact_id": string, "mime_type": string, "transcript"?: string }

Image and audio parts require the artifacts feature and must name a verified artifact from the active host-derived session owner and process generation whose MIME type and media kind match the part. Repeated references count toward a 64 MiB aggregate referenced media bound per result. If the tool declares output_schema, structured_content is required and validated; without an output schema it is forbidden. Structured content and bounded metadata are retained in native result details. Structured content is lowered to the model only by explicit host policy; metadata remains non-model-visible. Text and verified media use the normal native tool-output path. Structured content is bounded to 256 KiB; metadata to 64 KiB; both are capped at 32 levels and 16,384 nodes, and metadata keys are at most 256 bytes.


1.3 command/execute#

Invoke a slash command (/mycommand).

Request:

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "command/execute",
  "params": {
    "name": "checkpoint",
    "arguments": ["release-v2"],
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {}
    }
  }
}

Response:

json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "text": "Checkpoint preview · release-v2\nmain · clean\n\n...",
    "notifications": [
      {
        "level": "info",
        "title": "Read-only checkpoint",
        "message": "No commit or filesystem mutation was performed."
      }
    ],
    "context": []
  }
}

1.4 hook/run#

Invoke a lifecycle hook (declared in contributes.hooks).

Request:

json
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "hook/run",
  "params": {
    "hook": "before_prompt",
    "payload": { "prompt": "the user's current input text..." },
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {}
    }
  }
}

Both legacy APIs support "before_prompt", "after_response", "before_tool_call", and "after_tool_call". API 0.2 additionally supports typed provider_retry, before_persistence, and post_mutation hooks. See retry advice and bounded metadata and rescan enrichments for their payloads, non-veto boundaries, and current product coverage.

payload is hook-specific:

  • before_prompt: { "prompt": string }
  • after_response: { "response": string }
  • before_tool_call: { "name": string, "arguments": { ... } }
  • after_tool_call: { "name": string, "arguments": { ... }, "output": string, "is_error": bool }

Both API versions invoke after_response only after a successful, complete assistant response. API 0.2 uses it as a bounded content synchronization hook; it does not replace terminal lifecycle observations and is not invoked for failure, cancellation, interruption, frontend loss, or shutdown.

Response:

json
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "disposition": { "action": "continue" },
    "context": [
      {
        "label": "local-model-workflow",
        "content": "Local-model workflow is active...",
        "placement": "system_suffix"
      }
    ],
    "notifications": []
  }
}

Dispositions:

  • { "action": "continue" } — proceed normally.
  • { "action": "deny", "reason": "..." } — deny the intercepted operation (meaningful for before_prompt and before_tool_call only; other hooks continue regardless).

1.5 context/collect#

Host requests prompt context contributions.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "context/collect",
  "params": {
    "prompt": null,
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {}
    }
  }
}

Response:

json
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": [
    {
      "label": "hello-world",
      "content": "The hello-world extension is active.",
      "placement": "system_suffix"
    }
  ]
}

placement is one of: "system_prefix", "system_suffix", "prompt_prefix", "prompt_suffix" (default).


1.6 status/collect#

Host requests an optional status/header/footer contribution. The coding TUI intentionally does not request or render these as persistent chrome; the method remains protocol vocabulary for other host-owned frontends.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "status/collect",
  "params": {
    "surface": "status",
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {}
    }
  }
}

surface is one of: "status", "header", "footer".

Response:

json
{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "surface": "status",
    "text": "hello",
    "style_role": "extension.hello_world.status",
    "priority": 0
  }
}

Return null to contribute nothing.


1.7 tool/render#

Host requests semantic renderer output for a tool call.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tool/render",
  "params": {
    "name": "git_status",
    "arguments": {},
    "output": "branch=main\nstate=clean\n...",
    "is_error": false,
    "context": {
      "workspace": "/home/user/project",
      "execution_scope": null,
      "host": {}
    }
  }
}

Response:

json
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "segments": [
      { "text": "git · clean", "style_role": "extension.git_tools.clean" },
      { "text": "\n", "style_role": null },
      { "text": "...", "style_role": "extension.git_tools.detail" }
    ]
  }
}

1.8 shutdown#

Sent when the host wants the extension to exit gracefully.

Request:

json
{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "shutdown",
  "params": {}
}

Response:

json
{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {}
}

On normal shutdown, the host first waits shutdown_timeout (2 seconds by default) for the JSON-RPC reply. Whether the request is acknowledged or times out, it then waits up to the same per-stage timeout for the child to exit. If the child does not exit, octet terminates the child process group. Normal product shutdown runs these per-connection sequences concurrently inside a separate 3-second aggregate deadline; dropping a remaining connection also terminates its process group.

Interactive, plain, print, and host coordinated-signal exits instead cap the whole extension-shutdown attempt at 1.4 seconds, then force-kill all registered process groups. That outer cap may cut either normal 2-second stage short. Closing stdin is not the graceful shutdown signal; it is a transport-loss/final-teardown fallback.

API 0.2 enters draining before shutdown: it admits no new work, cancels remaining requests at the bounded deadline, then performs the same bounded request/ack, exit, and process-group cleanup sequence. API 0.1 keeps its legacy resident-process behavior.


1.9 $/cancelRequest (API 0.2)#

The host cooperatively cancels a host request, or an extension-originated child request owned by it:

json
{
  "jsonrpc": "2.0",
  "method": "$/cancelRequest",
  "params": {"id": 42, "reason": "user"}
}

Before the serialized writer starts the original frame, cancellation skips the frame and sends nothing. Once writing has begun, that complete frame is sent, followed by at most one cancellation notification. A cooperative extension observes its request token and answers the original ID with JSON-RPC error code -32800; a normal result may instead win the race.

The host drops the cancelled waiter, cancels its unresolved child requests, and tombstones the ID. A late response to a tombstone is ignored and diagnosed without closing unrelated calls. If the extension does not settle within the 2-second default grace period, its generation becomes degraded and is terminated. Cancellation requests cooperation; it does not imply rollback of external side effects and an ambiguous unsafe operation is never replayed.


1.10 Lifecycle notifications (API 0.2)#

When lifecycle_events and the specific method are subscribed, the host sends best-effort JSON-RPC notifications named:

  • session/started, session/settled
  • turn/started, turn/settled
  • tool/started, tool/settled

For example:

json
{
  "jsonrpc": "2.0",
  "method": "turn/settled",
  "params": {
    "session_id": "abc123",
    "run_id": "extension-run-7",
    "turn_id": "extension-turn-7",
    "outcome": "completed",
    "duration_ms": 942,
    "reason": null
  }
}

Start events carry their stable session/run/turn identifiers. Tool events also carry tool_call_id and tool_name. Settled events add outcome, duration_ms, and an optional reason bounded to 4 KiB UTF-8. Outcomes are completed, failed, cancelled, interrupted, frontend_disconnected, shutdown, or limit_reached. These observations are non-veto and bounded: product session/turn delivery uses a 250 ms deadline and tool observations use the bounded writer queue. Host finalizers remain authoritative if delivery fails.

after_response remains success-only in both API versions. It is not sent on failure, cancellation, interruption, frontend loss, shutdown, or a turn limit; API 0.2 lifecycle settlement covers those terminal outcomes.

1.11 Wave-1 notifications (API 0.2, negotiated)#

These notifications are additive and optional. Each is gated by one negotiated feature; the host emits it only after initialize accepted that feature, and an extension that never negotiated it sees nothing. Every payload is a bounded plain object: the frame is a JSON-RPC notification (no id), and an unnegotiated method, a non-object payload, or an oversized payload is refused with a bounded diagnostic instead of being delivered.

Notification Feature Pi surface
shortcut/trigger shortcuts Dispatches the admitted runtime shortcut whose id matches params.id
message/started lifecycle_events_v2 message_start
message/updated lifecycle_events_v2 message_update
message/settled lifecycle_events_v2 message_end
compaction/started lifecycle_events_v2 session_before_compact
compaction/settled lifecycle_events_v2 session_compact
compaction/failed lifecycle_events_v2 session_compact_failed
session/info_changed lifecycle_events_v2 session_info_changed
dialog/started lifecycle_events_v2 ui_prompt_start
dialog/settled lifecycle_events_v2 ui_prompt_end
model/selected lifecycle_events_v2 model_select
reasoning/selected lifecycle_events_v2 thinking_level_select
bash/user lifecycle_events_v2 user_bash
terminal/grant-lost terminal_handoff Revokes a ceded terminal grant: the holder must stop owning the tty and restore its own state

shortcut/trigger carries {"id": "<shortcut id>"}. The id must match a shortcut whose shortcut/register was admitted; a trigger for an unknown, refused, or stale registration fails closed and never runs a handler.

message/updated carries exactly one coalesced host batch: {"message_id": "<bounded id>", "delta": "<coalesced text>", "deltas": <count>} — delta is the coalesced text and deltas is how many extension-visible deltas it represents (a batch is emitted at 4096 accumulated bytes, 64 deltas, or 50 ms, whichever comes first, and always before message/settled). The bridge forwards each batch once, in arrival order, as a single message_update event: it never splits, re-buffers, or reorders it and never opens a per-delta round trip. It also accepts an SDK-shaped batch as a JSON array under deltas (or the alias updates); an array longer than 1024 deltas is refused as bounds_exceeded and no event is emitted. Every other Wave-1 notification forwards its bounded payload as the matching Pi event and is never silently dropped: it is either dispatched or refused with a typed error name in the diagnostic.

These notifications do not change the extension API version. They remain API 0.2; there is no API 0.3 variant, and API 0.3 provider manifests do not negotiate any of them.


2. Extension-to-host messages#

Extensions send these after initialize completes. API 0.2 operation-scoped requests (confirmation/request, input/request, artifact/publish, policy/evaluate, and secret/get, plus every agent/* method) must include the active numeric host parent_request_id.

Optional Wave-1 resource_owner#

The ten Wave-1 requests (composer/get, composer/set, composer/insert, shortcut/register, session/append_entry, session/set_name, session/set_label, session/send_message, session/send_user_message, tools/set_active) additionally accept one optional resource_owner object: { "session_id": "...", "extension_instance_id": "...", "process_generation": 3 }. It is additive: older hosts ignore it, a request without it stays valid, and no feature or API version changes because of it.

Precedence is fixed. If parent_request_id names a live host request, that request's resource owner wins and the explicit field is ignored. If the parent request has already settled — a deferred caller such as a later HTTP callback — an explicit resource_owner that validates (matching process generation, the instance id issued to this process, and a session id the host already issued) is admitted and answered independently of the settled parent. Anything else, any foreign or stale owner, or an absent field with a settled parent is refused with -32002 not_foreground_owner. An invalid owner is never coerced into a valid one, and no other Wave-1 request admits a caller-supplied owner. Global notifications, context, status, and presentation contributions, plus process-scoped tool-catalog mutations, do not include it. When the parent settles, the host cancels every unresolved operation-scoped child and ignores late child replies.

Every extension-originated request ID is either an unsigned 64-bit integer or a string of at most 256 UTF-8 bytes. IDs are unique among outstanding child requests and cannot be reused within a process generation. At most 128 child requests may be outstanding and at most 65,536 distinct child IDs may be used in one generation.

2.1 notification#

Emit a user-visible notification.

json
{
  "jsonrpc": "2.0",
  "method": "notification",
  "params": {
    "level": "success",
    "title": "Hello",
    "message": "hello_world greeted tinkerer"
  }
}

level is one of: "info" (default), "success", "warning", "error".

This is a JSON-RPC notification (no id); the host does not reply.


2.2 confirmation/request#

Ask the user a yes/no question.

json
{
  "jsonrpc": "2.0",
  "id": "confirm-1",
  "method": "confirmation/request",
  "params": {
    "parent_request_id": 2,
    "prompt": "Push to origin/main?",
    "detail": "5 commits, +120 −30 lines",
    "destructive": false,
    "default": false
  }
}

The host answers the same id:

json
{
  "jsonrpc": "2.0",
  "id": "confirm-1",
  "result": {
    "confirmed": true
  }
}
  • The id must be a string no longer than 256 UTF-8 bytes, or an unsigned 64-bit integer.
  • parent_request_id is required and must name an active host request in API 0.2; frozen API 0.1 omits it.
  • The extension must wait for the answer before proceeding with the confirmed action.
  • Dropping the request or using a non-interactive frontend denies it.
  • Pending confirmation IDs carry a process generation; they cannot be answered against a replacement child after reload.

2.3 context/contribution#

Unsolicited prompt context pushed by the extension at any time.

json
{
  "jsonrpc": "2.0",
  "method": "context/contribution",
  "params": {
    "label": "file-watcher",
    "content": "src/main.rs was modified at 14:32.",
    "placement": "system_suffix"
  }
}

Notification (no id).


2.4 status/contribution#

Unsolicited semantic status contribution. It does not force any frontend to create persistent chrome; the coding TUI keeps extension state in explicit views.

json
{
  "jsonrpc": "2.0",
  "method": "status/contribution",
  "params": {
    "surface": "status",
    "text": "watching src/",
    "style_role": "extension.watcher.status",
    "priority": 10
  }
}

Notification (no id).


2.5 presentation/update (API 0.2)#

Requires contributes.presentation = true. Publish one complete semantic state snapshot as a JSON-RPC notification:

json
{
  "jsonrpc":"2.0",
  "method":"presentation/update",
  "params": {
    "parent_request_id":2,
    "snapshot": {
      "revision":4,
      "status":{"state":"active","label":"1 worker"},
    "activities":[{
      "id":"worker:1","kind":"delegation","state":"running",
      "summary":"Reviewing tests","provenance":"local child",
      "started_at_ms":1721000000000,
      "metrics":{"tool_calls":4,"input_tokens":12000,"cache_read_tokens":800,
        "cache_write_tokens":0,"output_tokens":220,"reasoning_tokens":60,
        "cost_microdollars":7200},
      "references":[]
    }],
    "collection":{
      "kind":"tree","title":"Workers",
      "nodes":[{
        "id":"worker:1","parent_id":null,"state":"running",
        "label":"test-review","secondary":"running",
        "action_ids":["stop"],"references":[]
      }],
      "selected_node_id":"worker:1",
      "detail":{
        "node_id":"worker:1","title":"test-review",
        "body":"Running in a bounded child session.","references":[]
      }
    },
    "actions":[{
      "id":"stop","label":"Stop worker","command":"workers",
      "arguments":["stop","worker:1"],"destructive":true
    }]
    }
  }
}

parent_request_id correlates a handler-time snapshot to the active host request; octet derives its owner rather than accepting a session name from the extension. A background publisher instead supplies the complete host-issued resource_owner triple it previously received; octet accepts it only if that exact triple was issued to this process generation. The fields are mutually exclusive. Omitting both declares process-scoped state, which must contain no session-owned data. Stale/foreign triples and snapshots for another active product owner are dropped.

revision is an unsigned monotonic process-generation revision (zero is valid); a newer process generation may restart it. status is optional. activities and actions default to empty arrays; collection is optional. Collection kind is list or tree. IDs are stable extension-scoped identifiers. Nodes use optional parent_id; lists cannot have parents, trees cannot contain a missing parent, cycle, or depth over 16. Activities may include optional metrics with tool_calls, disjoint input_tokens, cache_read_tokens, and cache_write_tokens, output_tokens, its reasoning_tokens subset, and optional cost_microdollars. Every counter uses the portable JSON integer bound and reasoning cannot exceed output. A detail node_id must match the current selected_node_id. Generic states are empty, loading, pending, active, running, succeeded, failed, cancelled, degraded, stopped, and unavailable.

References contain {kind,id,label?}. Kinds session, artifact, and resource carry opaque identifiers. A session reference is only a lookup key: frontends may open it read-only after the host separately verifies the issuing parent session, path-free extension principal, and resource owner; mutation continues through agent_sessions. Kind url carries a sanitized absolute HTTP(S) URL; credentials, localhost/.local, and private/loopback/link-local/ unspecified/multicast literal IP targets are rejected and frontends expose it only after a user click. Each action must route to an existing command declared by this manifest. Labels and detail are plain data: ANSI/control sequences, HTML, scripts, CSS, and frontend layout coordinates are rejected or rendered as text.

A snapshot is capped at 256 KiB encoded, 128 activities, 256 nodes, 64 actions, 16 tree levels, 8 references per item, 1,024 bytes per compact label/ID, and 64 KiB for detail body. Revisions and timestamps are capped at the largest exactly representable JSON integer. One generation emits at most 32 snapshots in a one-second window; excess valid notifications are coalesced last-wins and the newest complete snapshot is emitted when the next window opens. Throttling produces at most one diagnostic for that window. The host validates the complete snapshot atomically, attaches manifest identity, a non-repeating process-instance fence, generation, and active resource owner, ignores or diagnoses stale updates, and retains the latest accepted replacement for explicit TUI views, Serve, and bounded headless fallbacks. Generic snapshots do not become ambient chrome. The coding TUI recognizes owner-fenced octet-subagents activities as a first-party observed surface and updates one bounded tool-like Subagents transcript block in place from native AgentEvent::DelegationUpdated events, including between root turns. Its heading counts worker states and up to four active child lines show tasks and input/output tokens; /subagents retains the complete roster, metrics, and cost. The TUI does not poll a status command, and the extension cannot supply footer text or terminal rows. It clears stale state on owner/process replacement; Serve action identity includes the instance fence, generation, and revision before routing the selected manifest process's command. The notification never invokes an action, repeats work, mutates a tool result, or grants authority.


2.6 $/progress (API 0.2)#

Requires request_progress. Progress is correlated to one active host request and sequences must increase strictly for that request:

json
{
  "jsonrpc": "2.0",
  "method": "$/progress",
  "params": {
    "request_id": 2,
    "sequence": 7,
    "event": {
      "type": "status",
      "message": "Fetched 3 of 10 results",
      "current": 3,
      "total": 10,
      "unit": "results"
    }
  }
}

Event variants are:

  • status {message, current?, total?, unit?}
  • output {stream: "stdout"|"stderr", encoding: "utf8"|"base64", data}
  • decoration {label, detail?} additionally requires progress_decoration; bounded ephemeral annotation rules.

Inactive-request and non-monotonic progress is ignored with a diagnostic. Accepted output uses octet's existing 8 KiB chunking and bounded progress sink, which may coalesce/drop under pressure. Progress is ephemeral: it is not a model result and is not persisted in the conversation transcript.


2.7 input/request (API 0.2)#

Request ephemeral text from the frontend while an operation is active:

json
{
  "jsonrpc": "2.0",
  "id": "input-1",
  "method": "input/request",
  "params": {
    "parent_request_id": 2,
    "prompt": "Password:",
    "secret": true
  }
}

The host answers the same ID with { "value": string|null }. null means the frontend cancelled or could not answer. Prompts must contain non-whitespace text and are bounded to 16 KiB UTF-8; answers are bounded to 256 KiB UTF-8; the 1 MiB full-frame bound also applies. secret: true suppresses echo and ordinary editor handling in an interactive frontend. Secret answers stay on the private reply channel and are never placed in diagnostics, progress, session state, or persistence. Headless/unavailable input is cancelled rather than guessed. Parent settlement cancels the pending input request.


2.8 artifact/publish (API 0.2)#

Requires artifacts. Publish either small inline base64 data:

json
{
  "jsonrpc": "2.0",
  "id": "artifact-1",
  "method": "artifact/publish",
  "params": {
    "parent_request_id": 2,
    "mime_type": "image/png",
    "size": 1234,
    "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "data": {"encoding": "base64", "data": "..."}
  }
}

or a relative path under OCTET_EXTENSION_SCRATCH by replacing data with "path": "screenshots/result.png". Exactly one source is required. Success returns { "artifact_id": "..." }; malformed or rejected publication uses -32602, while a request without an active host-owned session context uses -32002.

The host securely reads a regular no-follow scratch file, snapshots immutable bytes, and verifies exact size, lowercase SHA-256, canonical MIME, and media signature. Defaults are 256 KiB inline, 20 MiB per artifact, 64 MiB and 64 artifacts per process generation, and a 4096-byte/64-component relative path. The 1 MiB JSON-line bound still applies. Supported media are PNG, JPEG, GIF, WebP, WAV, MPEG audio, FLAC, Opus, AAC, and MP4 audio. IDs are opaque and valid only for the host-derived session owner that published them and that process generation. Tool-result media resolution supplies the same owner internally; another owner sees an unknown artifact even if it learns the opaque ID. Reload, generation settlement, or an owner mismatch makes the handle unavailable.


2.9 policy/evaluate (API 0.2)#

Requires policy_intents:

json
{
  "jsonrpc": "2.0",
  "id": "policy-1",
  "method": "policy/evaluate",
  "params": {
    "parent_request_id": 2,
    "intent": {
      "kind": "external_side_effect",
      "operation": "browser.submit_form",
      "target": {"origin": "https://example.com", "label": "Publish comment"},
      "data_classes": ["user_text"],
      "adapter_hints": {"read_only": false, "destructive": false}
    }
  }
}

The response is { "decision": "allow"|"ask"|"deny", "approval_token"?: string }. The host owns classification. Adapter hints can only increase caution and do not authorize an action. confirmation/request remains cooperative UI, not policy enforcement.

The optional approvals feature adds a single-use retry boundary. If the host classifies an intent as ask and a trusted frontend approves it, the response remains ask but includes a 64-character lowercase hexadecimal approval_token. A token is valid only on ask; allow and deny responses must omit it. The extension must repeat policy/evaluate with the exact same intent, still-active parent_request_id, and that token:

json
{
  "jsonrpc": "2.0",
  "id": "policy-2",
  "method": "policy/evaluate",
  "params": {
    "parent_request_id": 2,
    "intent": {
      "kind": "external_side_effect",
      "operation": "browser.submit_form",
      "target": {"origin": "https://example.com", "label": "Publish comment"},
      "data_classes": ["user_text"],
      "adapter_hints": {"read_only": false, "destructive": false}
    },
    "approval_token": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  }
}

The host atomically consumes the token and returns allow only when it still matches the canonical original intent, active owner/parent, and process generation before its bounded expiry (at most five minutes). Expiry, reuse, or intent/parent/generation mismatch returns deny; a recognized mismatched token is consumed as well. Supplying a token without negotiated approvals is rejected with -32602. Approval capability state is invalidated on generation replacement. The coding product leaves approval-token issuance off. Its working-tree mcp.tool.call adapter permits the admitted octet-mcp process's exact active, owner-scoped tool call under unsafe_host, including mutations. It verifies the generation, exact published tool identity, and arguments against the host-issued call; commands, ownerless/settled parents, changed targets, and controlled policies cannot authorize it. Generic operations still return deny. This is not an annotation-based read-only exemption or an automatic replay grant.

An extension may send $/cancelRequest for one of its own outstanding child request IDs. The host also sends it automatically when the owning parent settles.


2.10 secret/get (API 0.2)#

Requires the conditionally offered secrets feature and an exact name from the manifest's [capabilities].secrets allowlist:

json
{
  "jsonrpc": "2.0",
  "id": "secret-1",
  "method": "secret/get",
  "params": {
    "parent_request_id": 2,
    "name": "browser.api_token"
  }
}

Success returns { "value": string }. Values are UTF-8, may be empty, and are capped at 64 KiB. octet derives the extension identity and the complete {session_id, extension_instance_id, process_generation} resource owner from the active parent rather than accepting either from child JSON. The broker receives that identity, owner, parent request ID, and exact logical name.

An undeclared/invalid name returns -32602; no active owner or service returns -32002. A broker returning no value or failing always returns the same -32004 secret is unavailable response, keeping provider details host-side. octet does not persist or log the value and best-effort wipes the host broker buffer and serialized writer frame after use. Once delivered, however, the extension holds an ordinary process-memory string: API 0.2 does not promise end-to-end zeroization. The coding product currently configures no broker and therefore does not offer secrets.


2.11 tools/register (API 0.2)#

Requires dynamic_tools. Add new tools or replace complete definitions for existing extension-owned names:

json
{
  "jsonrpc": "2.0",
  "id": "catalog-1",
  "method": "tools/register",
  "params": {
    "tools": [{
      "name": "ableton_tracks",
      "description": "List tracks in the active Ableton set",
      "parameters": {"type": "object", "properties": {}},
      "output_schema": null
    }]
  }
}

The request contains complete ToolDefinition objects, not patches. Names in one request must be unique. The request and complete prospective catalog are capped at 256 tools. octet validates the complete result, reserves names against host tools and other extensions, applies the active host tool policy, and publishes the extension group atomically. After a parseable request ID, malformed parameters, a schema error, duplicate, or name conflict return -32602 and leave the previously published group unchanged.

Success returns the new per-process epoch and the complete set octet accepted:

json
{
  "jsonrpc": "2.0",
  "id": "catalog-1",
  "result": {
    "revision": 1,
    "tools": ["ableton_tracks", "ableton_transport"]
  }
}

The returned list is authoritative: policy may omit requested tools. Revision 0 is the initialize catalog, and every accepted mutation increments it once. The epoch resets to 0 for a new process generation. If acknowledgement cannot be delivered after publication, octet removes the extension's dynamic group and terminates the generation so host and extension cannot continue with different catalogs.


2.12 tools/unregister (API 0.2)#

Requires dynamic_tools. Remove extension-owned names transactionally:

json
{
  "jsonrpc": "2.0",
  "id": "catalog-2",
  "method": "tools/unregister",
  "params": {"names": ["ableton_tracks"]}
}

Names must be unique valid tool identifiers; missing names are ignored. The same 256-name bound, revision rules, transactional publication, authoritative response shape, and acknowledgement-failure handling as tools/register apply. The Python SDK rejects an empty mutation locally; callers should not use no-op catalog changes as revision clocks.

The exact initialize catalog is authoritative epoch 0 and is the only deterministic first-request catalog. Once a post-initialize catalog change is accepted, octet advertises it at the next model-request boundary after publication. It does not infer catalog quiescence, so a registration sent immediately after initialize is not guaranteed to appear on turn one; put turn-one tools in the manifest and initialize response. A provider request already in flight retains the schema and tool implementation it was given. Calls therefore carry tool/call.catalog_revision; extensions must keep enough historical dispatch state for overlapping turns and reject an unknown or retired epoch with -32602. The Python SDK retains eight committed catalog snapshots and temporarily exposes its staged next epoch during the publication-before-ack window. A replacement generation likewise begins from its initialize catalog at epoch 0; its subsequent mutations obey the same next-boundary rule.


2.13 agent/spawn (API 0.2)#

Requires the conditionally offered agent_sessions feature and an active host model-tool or declared-command parent. Create one bounded in-harness child model session:

json
{
  "jsonrpc": "2.0",
  "id": "agent-1",
  "method": "agent/spawn",
  "params": {
    "parent_request_id": 2,
    "task_name": "inspect-midi-tools",
    "profile": "review",
    "fingerprint": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "message": "Inspect the current Ableton MCP catalog.",
    "idempotency_key": "catalog-audit-2026-08-16",
    "policy": {
      "tools": ["read", "search"],
      "max_depth": 1,
      "max_concurrent_children": 8,
      "max_turns": null,
      "max_tokens": null,
      "max_cost_microdollars": null,
      "max_output_bytes": 8192,
      "timeout_ms": null
    }
  }
}

The host derives the resource owner from parent_request_id; the extension cannot submit an owner. policy is mandatory. Its tools are a non-empty, duplicate-free subset of read, search, edit, write, and bash (the first-party extension defaults to all five; explicitly select read/search for read-only work); depth is exactly one; concurrency is 1..=8; returned UTF-8 bytes are 512..=16,384. The turn, cost, and wall-time ceilings are optional per child: max_turns: null, max_cost_microdollars: null, and timeout_ms: null inherit the parent session's ceilings (an unlimited parent produces an unlimited child); explicit values are 1..=256 turns, 1..=50,000,000 microdollars, and 5,000..=86,400,000 milliseconds. max_tokens: null means exact inheritance of the parent's optional cumulative session-token setting, so a parent with no ceiling produces a child with no ceiling; a non-null 1,000..=64,000 value may request a stricter cap. Every child starts with a fresh context using the selected model's context window and an output limit capped by both the parent's resolved per-request limit and the selected model's output capacity. octet freezes a detached effective tool snapshot containing only the granted tools (no collaboration or agent tools), applies the requested ceilings or inherits the parent's ceilings when they are omitted, and owns limit settlement even when the extension is idle or restarted.

The idempotency key is 1..=256 bytes and scoped to the extension principal plus that owner. Retrying identical task_name/profile/fingerprint/message/ policy while the owning-run child record is retained returns the same result. Reuse with different input fails with -32002. At the next owning run, stale idempotency entries are pruned with their missing child records rather than returning a nonexistent session. task_name and optional profile are 1..=48 lowercase ASCII letters, digits, underscores, or hyphens. Optional fingerprint is one lowercase SHA-256 digest used only as opaque recovery metadata. The message is capped at 128 KiB. Success includes agent_id, agent_path, caller-visible task_name, optional profile/fingerprint, idempotency_key, status, effective policy, host-owned created_at_ms/started_at_ms/ completed_at_ms, and deadline_at_ms, plus the path-free extension principal and durable session resource_owner string.

Child model selection (agent_model_selection_v1)#

With the feature negotiated, policy.model_selection accepts {"provider":"inherit","model":"inherit","reasoning":"inherit"}. Omitting the object (or using null) preserves default inheritance; each omitted member defaults to "inherit". Explicit members are non-empty strings of at most 256 UTF-8 bytes without control characters; unknown members are rejected. An explicit provider requires an explicit model. Model/provider identifiers must resolve through the host's configured inventory, not an arbitrary URL, endpoint, or credential supplied by the extension. Supplying a non-null selection without negotiating the feature is rejected.

The host owns reasoning parsing, compatibility checks, normalization, and child lowering. Same-route inheritance preserves parent reasoning; a different route uses the host's supported translation or rejects incompatible reasoning rather than silently substituting a default. Discovery's reasoning labels are choices, not permission to bypass that policy. Astra V2 Ultra child reasoning is lowered to xhigh by the host. Unsupported routes or reasoning fail before child admission. Without a configured resolver, only the inherited binding is available.

Spawn and list records expose host-confirmed resolved_model, also included in effective policy.resolved_model, with provider, model, and reasoning. Unlike the requested reasoning string, resolved reasoning is a serialized ReasoningConfig object (for example {"type":"off"}). This secret-free metadata contains no credentials, headers, or transport URLs. The host pins resolved model/provider/reasoning identity for continuation and recovery; a saved selection that no longer resolves to that identity fails closed. This is not a promise to detect arbitrary transport configuration changes behind those IDs.

agent/models (API 0.2, agent_model_selection_v1)#

Requires both agent_sessions and agent_model_selection_v1. Request:

json
{"jsonrpc":"2.0","id":"models-1","method":"agent/models",
 "params":{"parent_request_id":2,"query":"sonnet","limit":50}}

The active host model-tool or declared-command parent supplies the resource owner; callers cannot submit an owner. Discovery is root-owner-only and retains the same principal, process-generation, and active-parent fences as other agent/* requests. Missing, foreign, or inactive owners fail closed.

query is optional/null, a case-insensitive model/provider/display-name search of at most 128 UTF-8 bytes without control characters. limit is optional/null, defaults to 50, and must be 1..=100. Success returns {"models":[...],"truncated":false}; each row contains model, provider, nullable display_name, reasoning (supported string labels), context_window, and max_output_tokens. The host bounds discovery and marks excess matches with truncated; this is not an unbounded catalog dump. IDs are capped at 256 bytes, display names at 512 bytes, and reasoning at 32 labels of at most 256 bytes each. Rows are secret-free configured-route metadata, not transport or authentication configuration. Without a configured resolver the view contains only the inherited model (subject to the query).


2.14 agent/message (API 0.2)#

Send steering input to an owned child while preserving the child session:

json
{
  "jsonrpc": "2.0",
  "id": "agent-2",
  "method": "agent/message",
  "params": {
    "parent_request_id": 2,
    "target": "agent-1",
    "message": "Also check resource tools."
  }
}

Success returns { "delivered_to": string, "delivery": "steering"|"queued" }. The target may be the ID or path returned by agent/spawn, but must belong to this extension principal and resource owner's child-session trees. The message is capped at 128 KiB.


2.15 agent/follow_up (API 0.2)#

Queue a subsequent run on an owned child:

json
{
  "jsonrpc": "2.0",
  "id": "agent-3",
  "method": "agent/follow_up",
  "params": {
    "parent_request_id": 2,
    "target": "agent-1",
    "message": "Now summarize only callable tools."
  }
}

Success returns agent_id, agent_path, and delivery: follow_up when the child is active (the message is queued on the running session) and new_run when the child is settled (the child's durable session resumes as a new run). Ownership checks match agent/message; the follow-up is capped at 128 KiB. Follow-ups reject shut-down targets, targets with an interrupt in flight, and a full follow-up queue. A resumed run re-enters the host's turn/cost accounting; when the child's wall deadline has already elapsed the host re-anchors it from the child's requested timeout so the new run owns a fresh budget instead of starting an already-expired one, a still-future deadline is preserved across the resume, and a child without a timeout stays unlimited.


2.16 agent/list (API 0.2)#

Request { "parent_request_id": 2 }. Success returns the extension principal, derived resource_owner, persistence_error, and agents. Each agent record contains agent_id, agent_path, parent_id, public task_name, optional profile, durable idempotency_key/fingerprint, depth, opaque agent-session:* resource reference, tagged status, effective policy, host-owned created_at_ms/started_at_ms/completed_at_ms, deadline_at_ms, turn_count, host-observed tool_call_count, structured phase and optional tool_name, a bounded rolling recent_tools array of the last six tool calls (each with name, a flattened single-line args summary capped at 160 UTF-8 bytes, host started_at_ms, optional finished_at_ms, and an error flag) so owner-facing pickers can answer what a worker is doing without opening its transcript, cumulative disjoint usage, optional cost_microdollars, and principal/owner provenance. States are pending, running, completed (with host-byte-capped output), interrupted, timed_out, failed (with bounded error), and shutdown. Private delegation JSONL paths are never returned. A current owner-scoped presentation may route the opaque reference into Serve, /extensions inspect, or the native /subagents arrow-key browser as a locked read-only transcript; the resolver separately verifies host-written parent-session, extension-principal, and resource-owner provenance. The TUI transcript panel starts at the live tail, supports bounded scrolling, and returns to the worker list on Escape or Left. All mutation continues through the owner-bound agent_sessions methods. The list contains only roots spawned by this principal/owner and their descendants. Child contexts remain independent and their tokens never become parent prompt context. Before the root run settles, octet stops and briefly joins these children, aggregates each child session's durable usage and exact cost (including picodollar remainder), and appends one delegated_agent usage record per child to the root session. The live TUI adds the current presentation cost only until those records are committed, so the cumulative footer never double-counts delegated spend; this ledger mirror is accounting rather than context sharing and is not charged to the parent's own-context token ceiling. Owner-scoped agent/list/agent/wait observation remains available after the owning root becomes inactive so the final terminal snapshot and transcript picker can settle; spawn/message/follow-up/interrupt mutation still requires an active owner and fails closed.


2.17 agent/wait (API 0.2)#

Request { "parent_request_id": 2, "timeout_ms": 30000 }. The timeout defaults to 30 seconds and is clamped to 1..=60,000 ms. The request returns immediately when no owned child is pending/running; otherwise it waits until all owned children settle or the deadline expires. Success is { "timed_out": bool, "snapshot": <agent/list result> }. Parent settlement cancels the wait.


2.18 agent/interrupt (API 0.2)#

Request { "parent_request_id": 2, "target": "agent-1" }. Success returns agent_id, agent_path, previous_status, and interrupt_requested. The host cancels the owned descendant tree when an active interrupt is requested.

All agent/* methods share the child-request bounds; child creation retains the eight-worker bound. After a parseable request ID, malformed parameters return -32602. Unavailable service/owner, invalid ownership, exhausted delegation limits, persistence failure, or an invalid operation return -32002. The extension's stable principal is derived from its manifest name plus a SHA-256 manifest-identity digest; the manifest path itself is never returned to the extension. A different extension or resource owner cannot list, message, follow up, wait on, or interrupt its child trees. Ownership uses that principal plus the durable session-owner string, not the extension process generation, so supervised restart/reload can resume an existing tree. Process shutdown requests shutdown of the service's owned trees; a complete process-host rebuild creates a new service boundary. Hosted agents are a separate capability; these methods create octet child conversations. Observe their state through agent/list/agent/wait. Delegated child turns do not currently emit extension session/* or turn/* lifecycle notifications; that notification stream covers the owning/root product session.

2.19 composer/get, composer/set, composer/insert (API 0.2, feature composer)#

Host-owned composer access. composer/get requests { "parent_request_id": 2 } and returns { "text": "<bounded composer text>" }. composer/set requests { "parent_request_id": 2, "text": "..." } and replaces the whole composer; composer/insert requests the same shape and inserts the text at the host composer cursor. Both mutations return {}.

json
{ "jsonrpc": "2.0", "id": "pi:4", "method": "composer/set",
  "params": { "parent_request_id": 7, "text": "hello" } }

The host resolves the foreground resource owner from parent_request_id; any other owner is refused with -32002 not_foreground_owner. Composer text is bounded UTF-8 without lone surrogates or terminal control characters, at the same 256 KiB cap as the editor handoff. A local refusal is reported to the extension with the bounds_exceeded or invalid_request token before anything is written, so an oversized composer mutation never reaches the host. ctx.ui.editor is a separate, declared reduction: it seeds the requested prefill and focuses the host editor through the negotiated editor_handoff surface, the edited text stays host-owned, and the bridge never fabricates or returns one.

2.20 shortcut/register (API 0.2, feature shortcuts)#

Request { "parent_request_id": 2, "id": "pi:0:ctrl+shift+p", "key": "ctrl+shift+p", "description": "..." }. Success returns {}. Registration is written inside the initialize request scope, so parent_request_id is the initialize request and the id is stable for the process generation. At most 64 runtime shortcuts are admitted per process; the id is at most 128 UTF-8 bytes, the key at most 128 bytes, and the description at most 4 KiB. A shortcut without a handler, a duplicate id, a refused registration, or a registration arriving after the initialize request settled leaves the shortcut unavailable; a shortcut/trigger for it fails closed.

2.21 session/append_entry, session/set_name, session/set_label (API 0.2, feature session_entries)#

session/append_entry requests { "parent_request_id": 2, "entry_type": "custom", "data": <bounded JSON> } and returns { "entry_id": "..." }, the host-assigned durable entry identifier. session/set_name requests { "parent_request_id": 2, "name": "..." }; an empty name clears it. session/set_label requests { "parent_request_id": 2, "entry_id": "...", "label": "..." }; an empty label clears it. Both mutations return {}.

entry_type is bounded to 128 UTF-8 bytes, the opaque data payload to 64 KiB of bounded plain JSON (no functions, exotic prototypes, non-finite numbers, or nesting beyond the protocol limit), the entry id to 256 bytes, and the name and label to 4 KiB. Every method is owner-scoped through parent_request_id and is refused with -32002 not_foreground_owner for any other owner.

2.22 session/send_message, session/send_user_message (API 0.2, feature message_injection)#

session/send_message requests { "parent_request_id": 2, "role": "assistant" | "system", "text": "..." } and session/send_user_message requests { "parent_request_id": 2, "text": "..." }; both return {} after the host admits the bounded injection. Any other role, including user, is refused locally with invalid_request rather than coerced. Injected text is bounded at 256 KiB and both methods are owner-scoped through parent_request_id.

2.23 tools/set_active (API 0.2, feature active_tools)#

Request { "parent_request_id": 2, "names": ["tool", ...] }; success returns {}. names is the complete replacement active tool set, bounded like tools/register (at most 256 names, each at most 128 UTF-8 bytes). A host build that cannot apply dynamic tool policy refuses the request instead of accepting an unapplied set. The method is owner-scoped through parent_request_id.


2.23b pi.sendMessage mapping (bridge surface)#

pi.sendMessage has three shapes in the installed corpus, and the bridge maps each one onto a real host op instead of a single coercion:

call octet path notes
pi.sendMessage({ role: "user", ... }) session/send_user_message the ordinary user-turn path
pi.sendMessage({ customType, content, display, details }, { triggerTurn }) session/append_entry (entry type = customType) plus session/send_user_message when a turn is requested the dominant corpus form (13 of 28 installed packages); needs both session_entries and, for the turn, message_injection
pi.sendMessage({ role: "assistant" | "system", ... }) refused: unsupported_feature octet never fabricates a provider turn, so an extension-authored assistant/system message is refused rather than coerced

The custom-message form's display: true is recorded in the entry payload but not yet honored: the shell does not project extension entries into the transcript, so rendering a custom message is the entry-renderer tier of P9. This is a declared divergence, not a silent drop.

2.24 terminal/acquire, terminal/release (API 0.2, feature terminal_handoff)#

Foreground terminal handoff. terminal/acquire requests { "parent_request_id": 2 } and returns { "grant_id": "<bounded id>", "columns": <u16>, "rows": <u16> } once the frontend that owns the foreground session's tty has actually ceded it. terminal/release requests the same shape and returns {} once the frontend has re-entered its own terminal and input loop.

json
{ "jsonrpc": "2.0", "id": "pi:9", "method": "terminal/acquire",
  "params": { "parent_request_id": 7 } }

The terminal stays host-owned. The grant is exclusive, owner-scoped, and revocable at any moment: the host refuses acquire while another grant is live (invalid_request), refuses release from a caller that does not hold the current grant (invalid_request), and refuses both when no foreground terminal is available (not_foreground_owner/invalid_request). grant_id is minted by the frontend and bounded to 128 UTF-8 bytes; the extension never supplies it. The host may take the grant back without being asked — the holder crashed, the session or process generation moved, or a coordinated shutdown force-restored the terminal — in which case the holder receives terminal/grant-lost with a bounded reason and must stop writing to the tty. A holder that keeps writing after a revocation is writing to a terminal the host has already restored.

While a grant is live the host emits no ui/terminal-input or ui/resize observation to the holder (it owns the tty for the duration), and the same process must not be handed a second grant. The dispatch-level refusal is typed: unsupported_feature when terminal_handoff was not negotiated, and invalid_request/bounds_exceeded/not_foreground_owner for everything else.

3. Standard JSON-RPC errors#

Code Message Meaning
-32700 Parse error Invalid JSON was received
-32600 Invalid Request JSON is not a valid Request object
-32601 Method not found Method does not exist / is not implemented
-32602 Invalid params Method arguments are invalid
-32603 Internal error Internal JSON-RPC error
-32800 Request cancelled API 0.2 cooperative cancellation won
-32000 to -32099 Server error Reserved for implementation-defined errors

Extensions should use -32601 for unknown methods and -32602 for invalid parameters. Custom errors in the -32000 to -32099 range are reserved for extension-specific server errors.

Wave-1 contract error names#

The Wave-1 composer, shortcut, session-entry, message-injection, and active-tool requests share one refusal vocabulary. A host refusal carries the numeric code and the contract name as the first token of error.message:

Code Contract name Meaning
-32601 unsupported_feature The method is not negotiated by this host build
-32602 invalid_request The params are malformed or outside the admitted shape
-32602 bounds_exceeded A string, payload, or batch exceeds its byte/element cap
-32002 not_foreground_owner The resource owner of parent_request_id is not the foreground owner

Refusals stay typed at the Pi surface: the extension sees an error whose first token is the wire contract it called followed by the contract name, so a void Pi setter can never hide a refusal. API 0.3 providers are unaffected and do not negotiate these names.


4. Type reference#

ExtensionIdentity#

Field Type Description
name string Stable extension name (matches manifest)
version string Semantic version
manifest_path string Absolute path to extension.toml
source string "project", "global", or "explicit"

ExtensionHostState#

Field Type Description
session_id string | null Stable session identifier
session_name string | null User-assigned session name
model string | null Canonical current model ID
reasoning value | null Reasoning configuration
active_skills array Explicitly active skills

ActiveSkill#

Field Type Description
id string Stable skill identifier
name string Human-readable skill name
version string | null Skill version

ExtensionExecutionContext#

Field Type Description
workspace string Active workspace root
execution_scope string | null Tool execution scope ID
resource_owner object | null API 0.2 host-derived {session_id, extension_instance_id, process_generation} on every session-owned tool, hook, command, context, status, or renderer boundary; omitted only for process-scoped/unsolicited contributions and in 0.1
host object Current ExtensionHostState

ExtensionResourceOwner (API 0.2)#

Field Type Description
session_id string SHA-256-derived durable canonical-session-path namespace; never supplied by model arguments
extension_instance_id string Host-created instance fence that changes across a complete process-host rebuild, including when generation numbering restarts
process_generation unsigned integer Reload/automatic-restart fence within one host instance for rejecting stale extension handles

ToolDefinition#

Field Type Description
name string Tool name; initialize names match the manifest, while negotiated dynamic_tools may publish post-initialize names
description string Model-facing description
parameters object JSON Schema (must be an object type)
output_schema object | null API 0.2 schema for required structured_content; forbidden in 0.1

CommandDefinition#

Field Type Description
name string Command name without leading /
description string User-facing summary
usage string | null Compact usage string

ContextContribution#

Field Type Description
label string Stable label for context inspection
content string Plain text sent to the model
placement string "system_prefix", "system_suffix", "prompt_prefix", "prompt_suffix"

ExtensionNotification#

Field Type Description
level string "info", "success", "warning", "error"
title string | null Concise title
message string Notification body

ConfirmationRequest#

Field Type Description
parent_request_id unsigned integer Active owning host request; required in API 0.2, absent in 0.1
prompt string Short action-oriented question
detail string | null Additional scope detail
destructive bool Potentially destructive action
default bool Suggested default

ExtensionInputRequest (API 0.2)#

Field Type Description
parent_request_id unsigned integer Active owning host request
prompt string Non-whitespace frontend prompt, at most 16 KiB UTF-8
secret bool Suppress echo and ordinary editor handling

The response contains exactly value: string|null, with non-null values capped at 256 KiB UTF-8.

ExtensionSecretGetRequest (API 0.2)#

Field Type Description
parent_request_id unsigned integer Active owner-scoped host request
name string Exact manifest-allowlisted identifier, at most 64 ASCII bytes

Success contains exactly value: string, capped at 64 KiB UTF-8. A broker no-value result and provider failure share the generic -32004 unavailable result.

ToolRenderSegment#

Field Type Description
text string Plain text content
style_role string | null Semantic theme role

StatusContribution#

Field Type Description
surface string "status", "header", "footer"
text string Plain display text
style_role string | null Semantic theme role
priority int Higher = retained first when constrained

ExtensionPresentationSnapshot (API 0.2)#

Field Type Description
revision unsigned integer Monotonic within one process generation
status object | null Generic state, compact label, optional detail
activities array Stable ID, kind, state, summary, provenance/timing/references
collection object | null list/tree, stable nodes, selection, selected detail
actions array Stable ID/label routed to a manifest-declared command and literal arguments

See presentation/update for exact state, reference, safety, parentage, and bound rules.

API 0.2 protocol features#

Feature Required Enables
request_cancellation yes Cooperative $/cancelRequest, cancellation errors, tombstones
content_parts yes Ordered text/media tool-result parts and native result details
request_progress no Request-scoped $/progress
progress_decoration no Bounded ephemeral semantic annotations; also requires request_progress
artifacts no artifact/publish and image/audio content parts
lifecycle_events no Subscribed session/turn/tool observations
policy_intents no Correlated policy/evaluate requests
dynamic_tools no Transactional tools/register, tools/unregister, and revision-pinned tool/call
runtime_commands no Initialize-time authoritative fixed command catalog for compatibility runtimes; no live mutations
agent_sessions conditional Principal/owner-scoped agent/* child model-session service
agent_model_selection_v1 conditional Bounded agent/models, policy.model_selection, and host-confirmed resolved_model; also requires agent_sessions
delegation_telemetry_v1 conditional first-party requirement Native owner-run AgentEvent::DelegationUpdated child telemetry; required by octet-subagents when agent_sessions is offered
approvals conditional Original-intent/active-owner-bound single-use policy/evaluate retry tokens; also requires policy_intents
secrets conditional Owner-scoped secret/get for exact manifest-allowlisted names
composer no Host-owned composer/get, composer/set, and composer/insert; the extension API stays 0.2
shortcuts no Runtime shortcut/register plus admitted shortcut/trigger dispatch; at most 64 per process
session_entries no session/append_entry, session/set_name, and session/set_label durable entry/session metadata
message_injection no Bounded session/send_message (assistant
lifecycle_events_v2 no Coalesced message/started, message/updated, message/settled, compaction/*, session/info_changed, dialog/*, model/selected, reasoning/selected, and bash/user fan-out
active_tools no Host-owned tools/set_active replacement active tool set
terminal_handoff no Exclusive, revocable foreground tty grant (terminal/acquire/terminal/release) with terminal/grant-lost revocation

Parent correlation (including input/request), serialized writes, bounded drain, and health tracking are base API 0.2 invariants rather than optional feature strings.


5. Lifecycle#

starting -> initializing -> ready -> draining -> stopped
                              |          |
                              +-> degraded/crashed

/extensions status inspection exposes each running process generation, negotiated features, pending request count, health state, and bounded last error. The supervisor uses backoff after an unexpected exit or terminal transport failure and parked after its retry budget or a permanent manifest/version/re-registration error.

Reload: octet starts and fully negotiates candidate generation N+1 while N remains ready. A process negotiating dynamic_tools may replace its tool catalog; otherwise changed tools, or any changed command/hook/UI contribution, are rejected with re-registration required and require a full host rebuild. octet reserves candidate tool names, marks N draining, waits the bounded drain, cancels the remainder, emits remaining lifecycle terminals, and waits for shutdown acknowledgement or timeout. It then seeds lifecycle state and atomically routes new calls to N+1. If candidate launch or negotiation fails, the old process remains active. Stale progress, child requests, confirmation IDs, approvals, secret lookups, artifacts, presentation snapshots, catalog epochs, and resource handles cannot cross the generation boundary. Unresolved unsafe calls are never replayed. /extensions reload replaces running children; general /reload rebuilds discovery and the product boundary.

Unexpected exit: after one successful initialization, the supervisor removes the dead generation's tool group, enters backoff, and attempts the same candidate-first generation-checked reload. Delay uses full jitter with a 250 ms exponential base and 30 s cap. Eight failed restart attempts park the extension; 30 seconds continuously ready resets that budget. Permanent manifest/version/re-registration errors park immediately. Explicit shutdown cancels supervision, and the reload lock prevents a supervisor/manual reload race. A manual generation change revives the parked watcher. Initial spawn/initialize failures are reported as generation-0 parked discovery entries and are not retried by this post-initialization supervisor. The supervisor does not heartbeat an otherwise live process. A full product rebuild creates a new extension instance and supervisor, resetting in-memory retry/parked state.

API 0.1 has one resident contact policy and no manifest contact-policy field. An enabled, trusted extension is started while the product extension host is constructed only when the resolved product policy is UnsafeHost and the independent process gate permits startup; Controlled retains discovery without a resident child. Each admitted generation remains resident until reload, shutdown, or connection failure; the host supervisor may create a replacement after failure. On reload, the replacement is initialized before the active generation stops admission and shuts down; the replacement is then swapped in. Its wire contract remains frozen and does not emulate API 0.2 stateful guarantees.

API 0.2 lifecycle notifications cover the shared terminal outcome boundary used by interactive, plain, print, RPC, native-host, and Serve execution paths. Every admitted turn is settled across completion, abort, failure, turn limit, frontend stream loss, and shutdown. Observations do not own cleanup.

Sleep inhibition is not a protocol/kernel responsibility, and no core sleep inhibitor remains. The example caffeinate package is an API 0.2, version 0.2.0 extension. It observes owning/root turn/started, turn/settled, and session/settled, reference-counts overlapping turns, runs one bounded macOS helper, and relies on extension shutdown plus process-group cleanup as its final fence.


6. Environment variables#

Every extension child receives:

Variable Value
OCTET_EXTENSION_API_VERSION Exact manifest-selected version ("0.1", "0.2", "0.3", or "0.4")
OCTET_EXTENSION_NAME Extension manifest name
OCTET_EXTENSION_DIR Extension directory (beside manifest)
OCTET_EXTENSION_MANIFEST Absolute path to extension.toml
OCTET_WORKSPACE Active workspace root
OCTET_EXTENSION_SCRATCH Host-owned scratch directory for the active process generation
SSH_AUTH_SOCK Forwarded only for an API 0.2 manifest that explicitly declares it in [capabilities].environment and only when present in the host