Extension authoring
Author extensions with API 0.4 and retained compatibility contracts.
Manual for v0.9.5. Stable release: v0.9.5.
Extensions run as subprocesses over JSON-RPC. The optional Pi adapter implements the reviewed subset of Pi 1.0.2's public extension API that the 27-row ledger records; Pi defines that API's semantics, and rows the adapter does not implement are marked partial or missing there. The adapter is not a general Pi runtime. Third-party package internals, the Pi CLI and child SDK are not compatibility targets. Rust owns agent execution, sessions, policy, persistence and the terminal; domain integrations remain optional.
Write new process extensions against API 0.4, the current working-tree
version. It uses the feature-negotiated JSON-RPC wire retained from API 0.2.
API 0.3 remains supported on its distinct canonical wire, and API 0.1 stays
frozen. What an extension can actually use depends on its exact manifest
selection, the host's offers and the frontend bindings, not on the API number
alone.
The generated reference keeps the version
policy and the live canonical schema and models. The feature-negotiated wire
reference keeps commands, hooks, status,
presentation, dynamic tools, artifacts, agent_sessions, approvals and the
other existing low-level services. Those contracts and their safety and
conformance tests stay live, but their breadth isn't an obligation to expose
every service in the coding product.
A manifest can declare options without running extension code. This is a manifest fragment, not a runnable extension:
api_version = "0.4"
[contributes]
flags = [
{ name = "index-enabled", type = "boolean", default = true },
{ name = "index-root", type = "string", default = "." },
{ name = "index-limit", type = "integer", default = 500 },
]The host resolves these options before startup and passes their values in
initialize.params.flag_values (the shared generated
InitializeFlagValue
shape). The rules are under CLI flags.
Bounded authoring path#
This guide mixes two kinds of extension work. First-party material — the
octet SDK, generated bindings and the shipped bundles listed in
extensions/release-catalog.txt — is built
and tested with octet. Community compatibility material — the optional Pi
adapter, retained legacy API examples and generic single-file helpers — is
third-party work that octet loads or exports without owning it.
Start with the build, install and maintenance quickstart for single-file Python/JS/TS tools, narrow Rust/C/C++ helpers, reviewed local packages, explicit activation, lifecycle tests and whole-closure upgrades.
Use the Python API 0.4 process
recipe for a local tool. The SDK
handles framing, negotiated scheduling, cancellation and shutdown, so implement
only the contributions you need. It's a source recipe for octet 0.9.5: the
SDK is installed from source, not from a published registry.
An authoring smoke check covers discovery and explicit enablement, exact negotiation, one real tool call, cooperative cancellation and clean shutdown. Run the generated-contract, SDK and conformance checks for the wire you selected.
The retained 0.3 example, and the compaction hook
The retained ordinary-process API 0.3
example uses only Python's
standard library and exercises canonical negotiation, an echo tool,
cancellation and shutdown. Its source manifest pins octet 0.8.0, extension
version 0.1.0 and API 0.3. Updating the host pin doesn't translate its wire
or establish publication, so don't retag it as 0.4. The generated
octet_extension.api_v03 and TypeScript bindings
stay live contract implementations, not complete process runtimes. Earlier
examples keep their exact API
versions.
For a host-owned replacement of local compaction (not a model-callable tool),
see the source octet-snap-compact
extension. API 0.4 offers its
compaction_strategy feature only to manifests declaring
hooks = ["compaction_strategy"]. The hook receives {model_id, text} in
hook/run batches and returns {compaction_frames: [base64_png, ...]}. The
host selects it only for vision models, keeps the checkpoint, and refuses
malformed, partial, oversized or over-budget renderings before committing. No
other API version negotiates this replacement hook.
Tool composition#
The optional first-party octet-codemode
bundle is one Rust executable embedding Pi's pinned offline QuickJS/WASM runtime,
so it needs no Node.js and no Python. It adds single-shot JavaScript
batching/chaining/filtering of the host's existing tools, not a new model, shell,
filesystem or network authority. Scripts run in a fresh isolated Wasmi store per
script inside a warm runner, so nothing survives between scripts; the trusted
launcher remains an ordinary OS process subject to the extension trust rules
below. An installed bundle starts directly; a source checkout either has a build
or builds the crate once with cargo --locked --release.
Enable explicitly with --enable-extension octet-codemode. Startup flags
--codemode-mode on|only and --codemode-inline-budget 0..16000 control
presentation, not policy. The default on preserves ordinary direct tools;
only keeps them in a separate frozen nested registry. Excluded/disabled tools
stay unavailable in either mode. /codemode status and the menu are read-only
help surfaces.
Embedders opt into the generic API 0.4 tool_composition_v1 service through
ExtensionRuntimeConfig.tool_composition; it defaults false. The coding host
offers it to enabled trusted API 0.4 processes. The host binds dispatch only
to live model-tool parents, preserving schema validation, effects, approvals,
hooks, cancellation, session budgets, private durable receipts and branch-store
ancestry. Commands, hooks, stale/foreign parents and recursive compositions
cannot use it. See the wire contract
for declarations, limits and secure oversized JSON transport.
Kernel boundary#
The host owns model conversations, sessions and tool-result persistence, permissions and approvals, process supervision and cleanup, and resource limits. Extensions own domain behavior such as MCP, search, browser use, LSP or subagent orchestration. A generic host service doesn't mean a capability package ships, or that its domain joins the kernel.
Executable extensions run with your OS authority. Capability declarations are consent metadata, not a sandbox. Message, queue, request, concurrency, artifact, shutdown and process-tree bounds don't isolate CPU, memory, file descriptors or PIDs. For full-access work, add OS-level isolation.
Capability declarations decide which services the host routes and which requests the extension may make; they do not confine the process. OS-level enforcement is future work.
Executable extensions are disabled by default, including installed bundles.
Discovery never runs code. Full access (unsafe_host, the default) trusts
selected extensions implicitly, but never enables them. An explicitly enabled
extension can start without an extra authority grant, subject to the
--no-process and --no-shell capability gate and the existing source,
compatibility and integrity checks. This implicit authority is calculated for
the current policy, and never writes a persistent grant back into config.
--safe-mode (and other controlled policies) removes implicit host authority.
An enabled extension with an explicit source-bound grant can start even in
safe mode, and an ungranted one stays stopped. A grant lets its code run as a
host process with your OS permissions outside the tool-effect broker. Safe
mode still governs brokered tool effects, but it isn't an OS sandbox for the
extension, so use OS-level isolation for untrusted work. Native-host protocol
1 is a separate embedding interface. It discovers extensions and never
starts them.
Layout and discovery#
Each direct child directory holds an extension.toml, and the directory name
must match the manifest name:
.octet/extensions/workspace-index/extension.toml
~/.octet/extensions/workspace-index/extension.tomlPrecedence is global, then trusted project, then explicit --extension-dir
directories in command-line order. Later wins by directory name. Project
resources are ignored until the workspace is trusted. On/off and host authority
stay separate. Full-access implicit authority applies to the selected, validated
source, without changing discovery precedence or enabling other installed
extensions. For example, with a reviewed bundle installed:
octet --enable-extension octet-web-search--enable-extension NAME is the same as choosing Enable in /extensions: it
adds NAME to enabled_extensions in user config, so the extension stays on
for later launches and the menu (and /reload) can still turn it off or turn
others on. When a trusted project config or OCTET_EXTENSIONS sets the
activation list, or user config can't be written, the flag applies to that
invocation only and the menu can't change activation.
--trust-extension NAME grants host authority to the selected source for this
invocation, even in safe mode, and doesn't turn the extension on. An explicit
--extension-dir grants host authority for that invocation without an extra
flag, but doesn't enable the extension. trusted_extensions is the existing
user-config list of persistent host authority grants: a bare name applies
only under ~/.octet/extensions, while NAME@/absolute/path/extension.toml
grants one exact project or other source. A same-named project shadow never
inherits a global grant, and the workspace must be trusted first. Revoking an
explicit grant doesn't override full-access implicit authority.
A trusted project config can suggest enabled_extensions but can't create a
persistent trust grant. Persistent trust comes from user config or
OCTET_TRUSTED_EXTENSIONS, never from that project suggestion.
/extensions status shows what's discovered, enabled, trusted and running.
/extensions shows On/off and Host authority per row and offers Grant and
Revoke. Enabling an ungranted extension under safe mode prompts for host
authority, and an off extension never starts, even with authority. Config
examples, manifest reads, diagnostics and resolver APIs are in the discovery
and trust reference.
Manifest#
Select api_version = "0.4" exactly. An extension's own version doesn't
select the wire. Current source uses octet_version, requires_octet,
OCTET_* and octet_extension, with no aliases for earlier first-party wire
names or imports. The local host, SDK source packages and five shipped executable
bundles have distribution version 0.9.5. That doesn't select an extension
API or publish SDK registries. Catalog installation needs version-matched
published assets: see installation and the release
notes.
Declare the entrypoint and the tools you actually supply. Return the complete
tool and command catalogs and the selected protocol features from
initialize, and select only
features the host actually offers. The canonical API 0.3 contract
negotiation is different, so don't copy it into a 0.4 process. The retained
manifest reference keeps earlier
examples and shared host operations, not a license to retag their
implementations.
requires_octet is optional for an unpackaged local extension, enforced when
present, and mandatory (as an exact running version) for an installed bundle.
Installing never enables, records a trust grant, starts or runs setup code.
Full-access trust is a runtime policy, not an installer side effect. See bundle
validation and
commands.
Published-catalog examples need matching published assets, so use a reviewed
source or local archive when matching publication hasn't been verified.
API 0.4 CLI flags#
Each [contributes].flags entry needs a globally visible name, an exact
type and a typed default. Types are boolean, string and integer. A
description is optional short plain text.
- Booleans accept
--index-enabledand--no-index-enabled. Strings and integers take one value:--index-root src,--index-limit 1000. - Integers are signed portable JSON integers. Names are at most 64 bytes: a lowercase letter first, then lowercase letters, digits or hyphens. Defaults and supplied strings are bounded. Each extension can declare at most 64 flags.
- Unknown fields, duplicates, invalid identifiers, unsupported types, wrong default types and oversized values fail manifest validation.
- The host reads only validated metadata and never starts or imports an extension to find options. Flags register only for selected, enabled, trusted extensions. A collision with a built-in option, another selected extension or a generated boolean inverse rejects the invocation before startup.
- Flags show in
octet --helpfor ordinary no-subcommand startup. Authentication, package, migration and other early-exit paths keep their static parsing. - All values, including omitted defaults, go to
initialize.params.flag_valuesas a sorted{name, value}list.
API 0.1 manifests can't declare these flags. Supported later manifests keep
their selected initialization wires. Declaring a flag doesn't run third-party
code or imply Pi registerFlag compatibility.
Transport contract#
API 0.4 uses UTF-8 JSON-RPC, one compact object per line, with stdout reserved
for protocol and bounded diagnostics on stderr. Initialization negotiates a
protocol feature list and a concurrency bound, not the canonical contract
object. See the wire reference for exact
messages, feature dependencies, request and owner correlation, and limits.
Dynamic tools used by MCP, agent_sessions used by subagents, and command,
status and presentation surfaces stay available subject to their existing host
gates. A documented approval or secret broker isn't automatically offered by a
host.
Typed values, native resources and immutable bulk (API 0.4)#
The Python and Rust SDKs
support generated typed JSON inputs/outputs, explicit model-facing text and
validated diagnostics. Native objects opt into resource_refs_v1 plus
operation_descriptors_v1: the extension keeps the object while the host
mediates opaque ResourceRefs, exact nominal types, exclusive admission and
explicit cleanup. Ordinary tools, including Pi tools, need no annotations.
Hosts may opt into bounded applicable-operation discovery and lazy model-schema
projection without changing their full registered/active catalogs. Configured
bulk_objects_v1 storage transports numerical bytes through immutable verified
local-file.v1 snapshots; model results contain only BlobRefs and summaries.
ResourceRefs do not survive owner/generation/host retirement. Durable blobs
require explicit host retention/recovery; transport tickets and locators are
never durable domain data.
See the wire methods,
approved contract and
required conformance cases.
Implementation is not a full conformance or Pi compatibility claim; actual
SPICE execution additionally requires ngspice. These services do not sandbox
a trusted subprocess's operating-system access.
Tool prompt metadata (API 0.4)#
The optional tool_prompt_metadata_v1 feature carries presentation metadata in
initialized and dynamically registered tool definitions:
{"name":"inspect","description":"Inspect a project","parameters":{"type":"object"},"prompt_snippet":"Inspect project structure","prompt_guidelines":["Report only verified findings"]}prompt_snippet is optional; prompt_guidelines defaults to an empty array.
Each string is limited to 1,024 UTF-8 bytes, with at most 16 guidelines per tool.
Newline and tab are allowed; other C0/C1 controls are rejected. Metadata also
counts toward the existing aggregate catalog byte budget. A snippet or nonempty
guidelines array requires this negotiated feature on API 0.4; older wires are
unchanged.
The Agent includes explicit metadata only from its model-visible, policy-filtered
active tool snapshot. Guidelines work without a snippet. Empty/whitespace-only
contributions add nothing. The section retains the Agent's existing byte/count
bounds and is frozen for that run, just like the system prefix; changes take
effect on the next run. Tool-free runs omit it. Ordinary tools without metadata
retain their existing behavior, including the default-disabled legacy built-in
prompt section. Embedders assembling their own prompts can read the owned
Tool::prompt_metadata() contribution. Metadata never changes effects,
authorization, schemas, handlers, or active-tool selection.
The Pi adapter maps promptSnippet/promptGuidelines to these fields. This is
one binding, not a full Pi compatibility claim.
Autocomplete edits (API 0.4)#
The optional autocomplete_edit_v1 profile requires negotiated autocomplete
and uses the existing ui/autocomplete/complete RPC, not a new editor service.
Choices may include replace_after_bytes (additional original bytes after the
cursor to replace, absent = zero, at most 262,144) and cursor_offset_bytes
(cursor within the inserted value, absent = its UTF-8 byte length). Both are
optional u32 fields; explicit null or any unnegotiated presence, including
zero, is invalid. Prefix/value may contain LF, TAB and CR under this profile;
labels/descriptions remain control-free. Each string stays within 1,024 UTF-8
bytes and each response within 32 items.
The host validates the exact snapshot, UTF-8 edit range and resulting editor budget at response, display and explicit acceptance. Live provider instance, generation, session/resource owner and text/cursor/revision/focus fences prevent stale edits or fallback into a new draft. The native editor also requires representable grapheme boundaries: it refuses an incompatible edit/cursor rather than silently moving the requested cursor. See the wire profile and example. This documents the native contract, not Pi adapter support, full parity or completed feature qualification.
Retained canonical API 0.3 wire#
Use UTF-8 canonical JSON followed by exactly one LF, with stdout for protocol
only and bounded diagnostics on stderr. API 0.3 framing is stricter than
ordinary JSON lines: duplicate keys, unknown envelope fields, noncanonical
whitespace and escapes, malformed surrogates, nonportable numbers and excessive
depth are rejected before dispatch. The frame limit excludes the LF. A frame at
the limit is accepted, and one byte over ends the stream. After initialization,
the negotiated limits replace the offered ones, in both directions, atomically.
Build against these reference sections, not a legacy JSON example:
- Canonical envelopes and all bounds.
- Host offer, selection and initialization fields.
- Capabilities, method directions and terminal semantics and exact error code/message pairs.
- Tool arguments, results, cancellation and shutdown.
What's deferred on the 0.3 wire
On this canonical wire, only foundation methods and capabilities can be
negotiated, and optional services can be omitted when the host can't bind them
safely. dynamic_tools, tools/register, tools/unregister and
context/collect are deferred on API 0.3, not removed from the
feature-negotiated API 0.2 and 0.4 wire. ContentPart has foundation text,
with image and audio variants deferred. Legacy artifact, progress, command,
presentation, secret and child-session methods aren't implicit API 0.3
capabilities.
The schema also defines optional migration, provider catalog, auth and stream, and session-lifecycle services. A declaration in the schema doesn't prove a particular product host offers the service. An ambiguous provider acceptance on these extension services never allows automatic replay, and the separate host-qualified Codex local-function inference exception doesn't qualify an extension provider or its effects. Authorization uses opaque host-policy leases, not credentials or URLs in protocol fields. The generated models and the host offer are exact.
Lifecycle and reload#
The host stays responsible for cleanup and persistence. Cancellation is
cooperative, not rollback, and ambiguous unsafe work isn't replayed. Keep
terminal disposition and generation ownership. Don't infer success from a lost
connection. The host lifecycle
reference covers
candidate-first reload, bounded drain, shutdown and supervised restart. Its
legacy observational methods aren't API 0.3 methods.
Provider-retry observations and advice#
The Rust ProviderRetryKind and the retained API 0.2 and 0.4 provider-retry
hook wire add InterruptedInference / interrupted_inference and
WaitingForNetwork / waiting_for_network. Hooks can veto only the proposed
retry for their operation, or add bounded delay. They can't authorize
unqualified replay, expand budgets, shorten Retry-After or override
cancellation. Extension API versions are unchanged.
Hook context, operations and events
max_attempts is optional (Option<usize> in the Rust hook context, JSON
null for sustained pre-send waiting), not a made-up finite denominator.
ProviderRetryContext.operation: Option<ProviderOperation> is None for main
sampling, serialized as an explicit "operation": null (not omitted). Auxiliary
hooks receive "operation": "local_compaction", "native_compaction" or
"terminal_gate", including manual native compaction. These hooks can veto only
the proposed retry for that operation, or add delay (cumulative additional delay
capped at five seconds). They don't roll back an unrelated main answer or change
the original failure classification. The existing before_generation and
stream_start kinds remain. Advice can decline a host-authorized retry or add
bounded delay, but can't authorize unqualified replay, expand budgets, shorten
Retry-After or override cancellation. These aren't new API 0.3 request-path
hooks. The session schema version is also unchanged, but the additive
usage_uncertainty record evolves its record contract.
AgentEvent::ProviderWaitingForNetwork is live recovery telemetry, not
assistant content or run completion. The separate unit
AgentEvent::ProviderUsageUncertain reflects durable session accounting
uncertainty: numeric usage and cost values remain known subtotals.
Neither event is assistant output. See the recovery
boundary.
Cache-warming decision advice (API 0.4)#
API 0.4 manifests may declare hooks = ["cache_warming_decision"] and
negotiate the matching optional feature. It is offered only to declarations on
API 0.4, not on frozen 0.1, retained 0.2, or canonical 0.3.
Before every due refresh, hook/run supplies a content-free payload:
{"decision":{"phase":"idle","warm_cost_microdollars":100,"miss_cost_microdollars":1000,"continuation_probability":0.15,"expected_savings_microdollars":50,"economics_available":true,"action":"stop"},"model":"cache-model"}phase is streaming or idle; costs and signed expected savings are in
microdollars. The separate execution context carries the host-issued resource
owner, instance and process-generation fence. No prompt, credentials, provider
endpoint, or mutable session is disclosed. Return
{"cache_warming_decision": "warm"} or {"cache_warming_decision": "stop"};
an absent or null field is no opinion. Other hook dispositions, prompt contributions and notifications do
not control a refresh.
Hooks run in registration order; the last returned action wins. Invalid responses, remote failures, stale generations and no opinion leave the preceding host/hook decision unchanged. The process adapter caps each wait at 200 ms; the host also enforces one aggregate hook budget capped by the original refresh deadline. Advice cannot enable an off, unsupported or ineligible refresh, expand any spending/attempt budget, override cancellation, or authorize provider replay. Keep handlers fast and local: the host alone performs provider I/O.
Native Rust extensions register CacheWarmingDecisionHook through
ExtensionHost::cache_warming_decision_hook; the read-only
CacheWarmingDecisionContext contains the typed decision, model and durable
resource_owner. The Python SDK offers @ext.cache_warming_decision, returning
"warm", "stop" or None, plus typed payload/result models. See the
SDK recipe and
local example. This is bounded
advice, not Pi ABI compatibility or ownership of the cache scheduler.
Declared API 0.3 session hooks#
The optional cleanup hooks are an all-or-nothing pair:
api_version = "0.3"
[contributes]
hooks = ["session_start", "session_end"]Under canonical API 0.3 the hooks are declared as a pair, and a partial pair
is rejected. Initialization must select both optional lifecycle_events and
hook/run, or neither. The host sends session_start when the product
activates its durable session owner, and session_end when that binding
settles.
Hook rules, ordering and the separate session service
These hooks are distinct from the optional session_lifecycle service. One
hook/run with hook: "session_start" is delivered when the coding product
activates its durable session owner, and one with hook: "session_end" when
that binding settles.
SessionBinding holds only an
opaque SHA-256-derived owner key, a host-made instance fence and the process
generation. SessionEnd adds an outcome, a shutdown, reload, crash or
cancelled reason, and a duration in milliseconds. Neither exposes a session
path, a mutable Session, a prompt, a host-state snapshot or request-path
context.
Ownership is recorded before start to prevent duplicate pairs. session_start
runs off the startup path with the ordinary request deadline, as Pi awaits it;
session_end is capped at 250 ms so exit and reload never wait on an extension. Malformed results, timeouts and remote errors become bounded
diagnostics. A valid deny or defer is recorded but can't veto the host's
lifecycle ownership. Settlement is idempotent and happens before child shutdown.
On an accepted reload, the old generation gets its interrupted/reload end
before the replacement's start. After a detected crash, the replacement first
gets the old interrupted/crash end, with the old binding generation, before
its own start. These hooks stay out of the prompt and tool hot path, and they
aren't the legacy session/started or session/settled observations.
The separate optional session_lifecycle capability is offered only when the
interactive product has a safely bound active-session driver. session/create
and session/fork return durable IDs without switching. session/switch takes
an existing workspace session ID, and session/reload rereads the active
durable session at an idle boundary. See the generated request
models.
Retained reference topics#
Retained feature-negotiated hook enrichments document bounded progress decoration, namespaced pre-persistence metadata and PostMutation rescans, including current product integration limits.
These anchors keep links from the former combined guide working. The linked
maintenance references keep earlier wire examples and the services API 0.4
reuses; they are first-party octet material. Version-specific payloads and
host-availability limits still apply.
- Runtime lifecycle and sharing
- Live tool catalogs
- Semantic presentation
- Child model-session service
- Brokers, correlation, input, and approvals
- Python SDK status and legacy runtime
- Capability ownership boundaries
- Bundle installation, update, removal, and validation
See also the legacy wire reference and project tracking.
Moved sections
These earlier section links now continue in the references below.