Extension authoring
Author extensions with API 0.4 and retained compatibility contracts.
Extensions add tools and bounded, host-shaped integrations to octet's fast, small coding host. They are not a promise to run unchanged Pi extensions or to replace every host subsystem. Browse, MCP, web search, and host-owned subagents remain supported integrations with their package-specific limits; Serve remains a separate application.
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; API 0.1 stays
frozen. Exact manifest selection, host offers, and frontend bindings determine
what an extension can actually use—not the API number alone.
The generated reference retains version policy
and the live canonical schema/models. The feature-negotiated wire reference
retains commands, hooks, status, presentation, dynamic tools, artifacts,
agent_sessions, approvals, and other existing low-level services. Those
contracts and their safety/conformance tests remain live; their breadth is not
an obligation to expose every service in the coding product.
A manifest can declare options without executing 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 supplies their values in
initialize.params.flag_values (the shared generated
InitializeFlagValue shape).
See CLI flags for validation and parsing rules.
Bounded authoring path#
Use the Python API 0.4 process recipe
for a local tool. The SDK handles framing, negotiated scheduling, cancellation,
and shutdown; implement only the contributions you need. This is a source recipe
for octet 0.8.0; the SDK is installed from source, not a published registry.
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
and keeps extension version 0.1.0 and API 0.3; updating the host pin does not
translate its wire or establish publication. Do not retag it as 0.4.
Generated octet_extension.api_v03 and TypeScript bindings
remain live contract implementations, not complete process runtimes.
Earlier examples retain 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, retains the checkpoint and refuses malformed,
partial, oversized or over-budget renderings before committing. No other API
version negotiates this replacement hook.
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 selected wire.
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 does not imply a shipping capability package or make its domain part of the kernel.
Executable extensions run with the current user's operating-system authority. Capability declarations are consent metadata, not a sandbox. Message, queue, request, concurrency, artifact, shutdown, and process-tree bounds do not provide OS CPU/RSS/FD/PID isolation. Use separate OS-level isolation for full-access work.
Executable extensions are disabled by default, including installed bundles.
Discovery never executes code. Full access (unsafe_host, the default) trusts
selected extensions implicitly, but never enables them. An explicitly enabled
extension can start without an extra trust flag, subject to process-policy gates
and the existing source, compatibility, and integrity checks. This implicit
trust is calculated for the current policy; it never writes a persistent trust
grant or an invocation trust flag back into configuration.
--safe-mode removes implicit trust and never starts executable extensions,
even with explicit trust and process/shell flags enabled. Executable processes
still require unsafe_host: safe mode is not an extension sandbox, and an
approval cannot bypass that floor. Native-host protocol 1 is a
separate embedding interface; it discovers extensions but does not
start them.
Layout and discovery#
Each direct child directory contains extension.toml, and its directory name
must exactly 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 definitions win by directory name.
Project resources are ignored until the workspace is trusted. Enablement and
executable trust remain separate. Full-access trust 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 enables a selected extension for that invocation;
enabled_extensions = ["octet-web-search"] persists activation in user config.
--trust-extension NAME remains an optional explicit invocation-only trust grant,
not activation or permission to bypass safe mode. A bare persistent
trusted_extensions name applies only under ~/.octet/extensions; an exact
NAME@/absolute/path/extension.toml grant is required to persist trust for project
or explicit sources. These source-bound grants remain distinct when implicit
full-access trust is absent. Even an explicitly trusted extension stays stopped
under a controlled policy.
A trusted project config can suggest enabled_extensions but cannot create a
persistent trust grant. Persistent trust comes from user config or
OCTET_TRUSTED_EXTENSIONS, never from that project suggestion.
See the retained discovery and trust reference
for config examples, bounded manifest reads, diagnostics, and resolver APIs.
/extensions status shows discovered, enabled, trusted, and running state;
/extensions manages activation without granting trust.
Manifest#
Select api_version = "0.4" exactly; an extension's own version does not 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 four executable bundles have distribution
version 0.8.0. This does not select an extension API or publish SDK registries.
Catalog installation requires version-matched published assets; see
installation and the release notes.
Declare the entrypoint and tools for the implementation you actually supply.
Return the complete tool/command catalogs and the selected protocol features from
initialize. Only select features
actually offered by the host. The canonical API 0.3 contract negotiation is
different; do not copy it into a 0.4 process. The retained manifest
reference preserves 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 requirement for an installed
bundle. Installation 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 remain publication-gated; use a reviewed source or
local archive when matching publication has not been verified.
API 0.4 CLI flags#
Each [contributes].flags declaration needs a globally visible name, an exact
type, and a typed default. Types are exactly boolean, string, and
integer; an optional description is short plain text.
- Boolean flags accept
--index-enabledand--no-index-enabled. - Strings and integers take one value:
--index-root src,--index-limit 1000. - Integers are signed portable JSON integers; defaults and supplied strings are bounded. Names are at most 64 bytes: lowercase letter first, then lowercase letters, digits, or hyphens. Each extension may declare at most 64 flags.
- Unknown fields, duplicates, invalid identifiers, unsupported types, wrong default types, and oversized values fail manifest validation.
- Only validated metadata is read; the host never starts or imports an extension to discover options. Flags register only for selected, enabled, trusted extensions. Collisions with built-in options, another selected extension, or a generated boolean inverse reject the invocation before startup.
- Flags appear in
octet --helpfor ordinary no-subcommand startup. Authentication, package/migration, and other early-exit paths retain their static parsing. - All values, including omitted defaults, are sent as a sorted
{name, value}list ininitialize.params.flag_values.
API 0.1 manifests cannot declare these flags. Supported later manifests retain
their selected initialization wires; declaration does not execute 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 concurrency bound, not the canonical contract
object. See the wire reference for exact
messages, feature dependencies, request/owner correlation, and limits. Dynamic
tools used by MCP, agent_sessions used by subagents, and command/status/
presentation surfaces remain available subject to their existing host gates.
A documented approval or secret broker is not automatically offered by a host.
Retained canonical API 0.3 wire#
Use UTF-8 canonical JSON followed by exactly one LF, with stdout reserved for
protocol and bounded diagnostics on stderr. API 0.3 framing is stricter than
ordinary JSON-lines: duplicate keys, unknown envelope fields, noncanonical
whitespace/escapes, malformed surrogates, nonportable numbers, and excessive
depth are rejected before dispatch. The frame limit excludes LF. A frame at the
bound is accepted; one byte over terminates the stream. Negotiated limits replace
the offered limits atomically for both directions after initialization.
Implement against these exact reference sections rather than adapting 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.
On this canonical wire, only foundation methods/capabilities may be negotiated, and optional services
may be omitted when the host cannot safely bind them. dynamic_tools,
tools/register, tools/unregister, and context/collect are deferred on
API 0.3, not removed from the feature-negotiated API 0.2/0.4 wire.
ContentPart has foundation text; image and audio variants are deferred.
Legacy artifact, progress, command, presentation, secret, and child-session
methods are not implicit API 0.3 capabilities.
The schema also defines optional migration, provider catalog/auth/stream, and session-lifecycle services. A schema declaration is not proof that a particular product host offers the service. Provider acceptance ambiguity on these extension services never permits automatic replay; the separate host-qualified Codex local-function inference exception does not qualify an extension provider or its effects. Authorization uses opaque host-policy leases, not credentials or URLs in protocol fields. See the exact generated models and host offer.
Lifecycle and reload#
The host remains responsible for cleanup and persistence. Cancellation is
cooperative, not rollback, and ambiguous unsafe work is not replayed. Preserve
terminal disposition and generation ownership rather than inferring success
from a lost connection. The retained host lifecycle reference
covers candidate-first reload, bounded drain, shutdown, and supervised restart;
its legacy observational methods are not API 0.3 methods.
Provider-retry observations and advice#
The Rust ProviderRetryKind and retained API 0.2/0.4 provider-retry hook wire
add InterruptedInference / interrupted_inference and WaitingForNetwork /
waiting_for_network. max_attempts is optional (Option<usize> in the Rust
hook context, JSON null for sustained pre-send waiting), not a fabricated
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 do not roll back an
unrelated main answer or change the original failure classification. Existing
before_generation and stream_start kinds remain. Advice can decline a host-authorized retry or add bounded delay, but
cannot authorize unqualified replay, expand budgets, shorten Retry-After, or
override cancellation. These are not new API 0.3 request-path hooks; extension
API versions are unchanged. 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. Serve keeps its run owner alive during the wait
without adding a durable status item. The separate unit
AgentEvent::ProviderUsageUncertain reflects durable session accounting
uncertainty: Serve retains it through completion and prefixes completion-review
summaries with a warning that numeric usage/cost values are known subtotals.
Neither event is assistant output. See the
recovery boundary.
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; a partial pair is rejected.
Initialization must select both optional lifecycle_events and hook/run, or
neither; a declaration/selection mismatch is rejected. 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
contains only an opaque SHA-256-derived owner key, a host-created extension
instance fence, and process generation. SessionEnd adds a generated outcome,
shutdown, reload, crash, or cancelled reason, and duration in milliseconds.
It exposes no session path, mutable Session, prompt, host-state snapshot, or
request-path context.
Ownership is recorded before start to prevent duplicate pairs. Each dispatch is
capped at 250 ms; malformed results, timeouts, and remote errors become bounded
diagnostics. Valid deny or defer dispositions are recorded but cannot veto
host lifecycle ownership. Settlement is idempotent and precedes child shutdown.
On accepted reload, the old generation receives interrupted/reload end before
the replacement's start. After a detected crash, the replacement receives the
old interrupted/crash end with the old binding generation before its own start.
These hooks do not enter the prompt/tool hot path and are not legacy
session/started or session/settled observations.
The separate optional session_lifecycle capability is offered only when the
interactive product configures a safely bound active-session driver.
session/create and session/fork return durable IDs without switching;
session/switch accepts an existing workspace session ID; 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.
The following anchors preserve links from the former combined guide. The linked
maintenance references retain earlier wire examples and the services reused by
API 0.4. 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
- Separate Serve application packages
See also the legacy wire reference and project tracking.