Skip to content

Extension, execution and observability contracts

This page records decision B03: the public Go contracts that extensions and executors implement and that the core relies on, covering ingress, route handlers, executors, the execution toolbelt and lifecycle telemetry. The contracts live in extension and observability. Shared conformance cases run against volatile reference doubles and against the real ingresses, executors, toolbelt and telemetry pipeline. These contracts supplement B02. They are in-process Go contracts; the extension subprocess protocol (B08) carries them over the wire.

Observability is a required fifth cross-cutting contract, alongside ingress, executor, toolbelt and storage. It is not a sixth primitive. The contract defines the correlation and reporting boundary without an SDK dependency; the OpenTelemetry mapping and export pipeline live in internal/telemetry (lifecycle telemetry, run history and doctor).

Ingress identity, observations and recovery

Section titled “Ingress identity, observations and recovery”

An ingress implements both Push and Reconcile, even if its only family is explicitly non-re-derivable (in that case reconcile may return an empty complete snapshot). Its registration declares each event family as recoverable or gives a concrete loss limitation. An arbitrary one-shot POST without readable backing history is not recoverable; the schedule extension’s custom webhook family schedule.webhook is declared one-shot with a loss limitation for that reason, and its reconcile returns an empty complete snapshot. Each recoverable family (for example GitHub push, release and workflow-run) must establish its own readable history, permissions, retention and recovery horizon; a service-wide recoverability claim is insufficient.

Source.Namespace is a stable configured account/resource namespace. Scope is supplied by trusted configuration. Account identifiers, not credentials, determine identity; changing credentials cannot generate new logical events. Two scopes watching the same external source must coordinate ownership rather than silently duplicate it by changing its identity. B02 correlation ownership remains global within that namespace.

An observation contains an opaque item ID, an equality-only version token and its canonical Event. Adapters normalize payloads, timestamps, event family and canonical (source, delivery_id) identically for push and reconcile. The version must distinguish recurring transitions such as A → B → A; a content hash alone cannot identify both occurrences. Occurrence time is external time; receipt time reflects actual receipt and need not be identical across real transports. Internal Event ID/receipt time are not dedup identity: B02 returns the first accepted envelope on overlap. Multiple distinct events in one delivery get distinct stable delivery IDs (for example delivery/item/change); thread correlation never serves as event identity.

Recoverable push deliveries yield observations. Non-re-derivable families yield direct Events and explicitly disclose what is lost before acceptance. All emitted families must be declared. Push normalization and reconciliation perform no coordination writes. Their return values do not acknowledge acceptance: core validates the envelope and declarations, accepts durably, and only then acknowledges the delivery. Transport authentication and HTTP response mapping belong to service implementations, not these payload contracts.

Scan carries the source, generation, page cursor and last durably stored checkpoint. Snapshot.ScanID identifies one consistent generation, with no duplicate item carrying conflicting versions. A provider must use a service snapshot token or implement an equivalent complete scan/revalidation strategy. If it cannot prove coverage (permissions, rate limit, retention, truncation, failed pagination or changing dataset), it returns an error or an incomplete scan. An empty page is not itself completion. Complete=true is permitted only on the last page, with no next cursor, and certifies coverage of the entire configured source across all pages.

Core compares opaque item/version pairs; it never examines service payload fields. It stages incomplete generations separately from the authoritative previous snapshot. No absence is inferred and no authoritative snapshot is replaced until a generation is complete. A complete scan yields changed canonical Events and the IDs of observations now absent. Absence is a generic observation change, not permission to delete or finish Work. The runtime represents it as core.ingress.absent carrying source/item/last version with a delivery identity derived from the scan generation and item. The absence envelope preserves the last canonical observation’s correlation key and explicit Work ID when present, so it can reach the same objective even when item identity differs from correlation identity; it does not attribute the absence to an earlier Execution. Domain routes decide its significance. Where exact external deletion history matters, adapters expose a canonical tombstone observation/event instead; absence cannot invent the external deletion time. Tombstones remain discoverable for the provider’s declared replay horizon.

Generation staging, checkpoint advancement, resulting Event acceptance and observation publication use B02 transactions/KV. Core may durably stage pages and their page cursors before completion, but incomplete staging cannot trigger absence. After a crash it resumes the saved generation if valid or discards only staging and starts a new one. Failed pages cannot advance a checkpoint. Generations are serialized per source/scope. Push events can be accepted during a scan but cannot rewrite its staged snapshot; subsequent reconcile resolves newer external state. Delivery dedup prevents duplicate Events from overlap. Reconcile is not a total order of external history and cannot recover transitions that the provider no longer exposes. The runtime implements these rules and crash boundaries (event ingress); the reference ObservationSet is only a pure diff oracle with caller-owned staging/checkpoint persistence.

The GitHub extension and the Slack extension settle these points for every adapter:

  • An adapter may derive the version from the whole normalized envelope, as long as occurrence-bearing content distinguishes lifecycle recurrences (for GitHub, closed_at and the reopen generation). Attribute-only recurrences and the adapter’s own projection writes are then deliberately not separate observations.
  • The runtime publishes a generation’s changes in lexical item order, so adapters nest dependent items (comments) under the item that may create Work.
  • Transport authentication wraps header-borne delivery metadata into the Delivery body at the webhook boundary; the payload contract is unchanged.
  • A push transport whose authenticated connection, not a signature, is the transport-authentication boundary (Slack Socket Mode) acknowledges envelopes only after durable acceptance, exactly as an HTTP status is.
  • A non-re-derivable family that never accompanies an observation in one delivery (Slack’s push-only deletions) is tested through the optional OneShot delivery of conformance.IngressFixture: the canonical delivery must then yield exactly the canonical observation and no direct Event, and the one-shot family is checked from OneShot.

Run starts one attempt and returns a RunHandle; Wait observes its structured Result. Four outcomes are valid: completed, failed, waiting and handoff. An executor transport/process error is distinct from a declared failed outcome, and neither silently completes Work. B02 defines persisted state mapping; runtime retry/wake decisions are B04. Shell exit status and agent result parsing must translate into this common surface. Usage has optional token counters and optional finite nonnegative cost with currency. Missing measurements stay absent, rather than becoming a false zero.

RunRequest.Inputs is the acceptance-ordered batch presented to this attempt, including ordinary context before a wake. Trigger remains the selected event (the wake winner on Resume); recovery restores the same durable batch. Immutable Execution.TriggerID records the original attempt trigger. Runtime retains separate causal continuation ancestry when a parked attempt resumes.

Executor errors may implement the optional public RetryHintProvider, returning RetryHint{ResetAt}. Executors can implement it using extension alone. A nil reset means unknown; core validates the timestamp, applies generic backoff and retains a bounded failure budget. The callback runs outside coordination transactions. Provider-specific classification and error text stay in the executor. A structured failed result can also carry a hint in extension.Result.RetryHint (for example a usage limit’s reset). Core applies it like a RetryHintProvider error, but the observed result also establishes that the attempt finished, so no uncertain hold remains. ValidateResult rejects a hint on any other outcome. A failed result can instead say it is Permanent: the binding, its resource or the executor’s own config is wrong, and the next attempt would fail the same way. Core then arms no retry and spends the Work’s failure budget at once (outcomes and retry). ValidateResult rejects Permanent on any other outcome or together with a hint. On the wire it is the result’s permanent field.

extension.Result.Workspace (WorkspaceRecord: kind, lease reference, directory, summary, description) lets every executor report the resource it ran in for history, and extension.Resource.Summary is a provider’s one-line description of it. domain.Binding.TimeoutSeconds carries a binding’s deadline.

Capabilities advertise mechanical interrupt, inject, resume, workspace and exactly one toolbelt transport (mcp, cli, sdk, none). ValidateBinding rejects unavailable requirements, unknown transports and advertised controls lacking their optional Go interfaces before any execution is scheduled. An empty required transport means no transport constraint; none explicitly requires none. A reporting route must require its selected transport. An active inject/interrupt policy implies the corresponding capability, or an explicit fallback (B04). No inferred fallback or semantic “coding ability” label exists.

Run context contains a scope configuration snapshot, resolved secrets for this execution, an optional host-bound ResourceBroker (nil without a binding resource), an optional host-bound SandboxBroker (nil without a binding sandbox, B09), and the endpoint/token. No workspace is required. Resource acquisition, copy/setup and cleanup semantics are B07: the public ResourceProvider contract gives idempotent per-owner acquisition recorded before creation, Lookup without creation, and host-driven Reconcile from durable owner state (terminal plus confirmed exclusion). Core only passes the host-bound broker through SchedulerOptions.Resources. A sandbox is a launch wrapper (B09): an executor advertising Capabilities.Sandbox hands the argv it would run to extension.WrapLaunch with its needs, and proves a recovered segment with extension.AdoptSandboxed; core passes the broker through SchedulerOptions.Sandbox and records only the plan’s digest and summary with the segment. Shell presents OS_ENDPOINT, OS_TOKEN and os on PATH; MCP and SDK present the same operations and authority. none receives no usable toolbelt credentials. Context secrets/tokens and resource internals must never enter logs or lifecycle attributes.

Executor continuity is private, keyed by Work ID, and must be separated by executor/connection namespace. The executor owns resume-versus-fresh, session identifiers and handoff economics. Core persists small opaque recovery metadata through the fenced PersistRecovery callback; this must succeed before acknowledging a launched handle. Executors must close any launch/persist crash window by recording intent before launch, using a deterministic discoverable handle, or cleaning up an unacknowledged launch. Large transcripts use artifacts. Callback failure cannot return successful launch acknowledgement.

Recover receives the same Work/Execution identity, executor-owned metadata and newly claimed authority. It adopts an existing detached attempt or returns its already finished result via a handle. Unknown/dead/unverifiable metadata returns ErrNotAdoptable. It never launches a replacement or interprets metadata in core. Core decides retry/failure after that result; new attempts use Run. The executor must replace old process credentials with the new token before allowing further mutations. Re-acknowledging identical metadata under the new lease is permitted; replacement metadata is rejected. Old credentials remain fenced even if an old process lives. Capability resume concerns continuing a parked wait; it is independent of detached adoption. Resume uses the accepted wake event as trigger, same attempt and new lease/token. A non-resuming wait terminates the attempt and wake creates N+1. Cancellation of Wait only cancels observation; explicit interruption/cancellation policy controls the process.

InjectExecutor has three answers: nil (accepted for delivery), extension.ErrNotAccepting (definitely not delivered) and any other error (ambiguous). Inject success means accepted for delivery and must not be treated as proof of consumption. A finished result carries consumption proof in Result.ConsumedInputs (the Events the running process demonstrably took in) on every outcome. When the result is recorded, core consumes each named input only if it holds an accepted inject intent for that execution and the input is still pending; other IDs are ignored, so the field grants no authority. Acknowledgement and completion races are settled in generic inject acknowledgement, and the shared executor suite checks them with CheckInjectExecutor.

Two further optional executor interfaces are public: StopExecutor (physical-stop evidence before ConfirmStopped) and AttachExecutor with AttachSpec and ErrNothingToAttach (the operator attach control; the management wire code is nothing_to_attach).

Toolbelt.Call(token, call) is the one execution-facing surface. TokenIssuer and Registrar are trusted registration/runtime interfaces. Registrations declare in-process ContractVersion=1, never a storage-capability flag; protocol.Version versions the subprocess wire separately. Registration is atomic: names and event types are declared before binding, duplicate owners and reserved builtin names fail without partial registration. Extension operations require a registered handler, an explicit token operation allowlist and their user-bound route/executor; registering an operation grants no privilege. Extension operation handlers receive validated authority and opaque input, never the raw token. Domain behavior stays in the handler. Any emitted Events must pass the same declared-family, authority and causation checks as builtin emit; no handler gets trusted control-plane authority by virtue of registration. Remote side effects run outside storage transactions, use adapter markers for ambiguous writes and reconcile their results. The reference double supports pure operation handlers only.

Tokens are unpredictable, per execution and lease epoch, bounded in lifetime, revocable and never logged/persisted as plaintext coordination payloads. Issue a fresh token after claim/recovery/resume. Expiry is exclusive. Validate scope, Work, Execution, operation allowlist, revocation and B02 lease on every call including reads; token validity alone is insufficient. Park, termination, cancellation, lease release/expiry or supersession invalidates access immediately. Optional request IDs are assertions narrowing the token’s target; they cannot switch target. Tokens grant neither neighboring Work in the same scope nor another scope. Transport and local endpoint authentication are B05. No unauthenticated request becomes a nil storage Guard.

Operation Required behavior
emit Only declared/allowed event types; preserve client idempotency delivery key; trusted execution source, Work, Execution and trigger causation; reject forged parents/targets and unowned correlations; commit before acknowledgement. Root Events cannot be submitted through execution authority.
create_work Create child with current Work as immutable parent and current scope; reject foreign scope/parent and conflicting keys; preserve causation via an accompanying accepted core.work.created Event in the same transaction. IDs are allocated before commit for retry safety.
wait Current Work/Execution only; B02 match/timeout registration and state/authority release are atomic; resume-capable executor parks, other executors end waiting. Event/wake race semantics and cross-work subscriptions are B04.
declare_outcome Validate structured outcome; fence persistence and release attempt authority. Work completion/retry is runtime policy. Waiting requires the wait operation’s predicate/timeout and cannot be declared without registration.
read_context Return only accumulated context authorized for this Work; never neighboring Work, raw secrets, tokens or executor continuity metadata.

The production toolbelt (toolbelt authority and local management) records wait/declare_outcome as fenced per-epoch declarations that release toolbelt authority immediately. The predicate/timeout registration or outcome is applied when the trusted host observes the segment finish or suspend, because a client request alone is not exclusion proof. The declared exclusive watermark ensures registration backscans everything accepted in between.

The shell executor presents this surface as OS_ENDPOINT/OS_TOKEN (plus a replaceable OS_TOKEN_FILE) and the os CLI. The Claude Code executor is an mcp toolbelt executor: os mcp presents the same five builtins plus call over MCP stdio, through the CLI’s authority path, with a replaceable credential file. Its session, resume-versus-fresh, handoff note and attachment are executor-private continuity in namespaced KV, keyed by Work ID.

Core derives causation from the current trusted trigger, never from client-selected roots. Child creation and timeout Events carry that ancestry. Emission does not bypass the core chain-depth/per-chain-rate controls (B04). Production implementations join all coordination changes in the fenced B02 transaction. Extension operations performing external writes must revalidate authority for each subsequent coordination mutation; a successful initial check cannot grant indefinitely valid authority.

Extensions extend the toolbelt through extension-neutral contract pieces; none adds a domain type (engineering tools, waits and playbooks):

  • Operation.Wait: an operation whose handler returns a WaitPlan, declared like the builtin wait. Dependency is a B04 dependency subscription with an opaque filter and FromNow, and Registration.DependencyFilter is a pure filter core evaluates inside the reconcile transaction.
  • CallRequestID(ctx): the caller’s request identity for stable intent IDs.
  • OperationError: a handler-authored safe refusal (code plus message) returned verbatim instead of a generic failure.
  • Operation.Serves: on a handler, a capability the operation answers, routed by name rather than by extension. GET /v1/toolbelt/serving?capability= answers which operations the caller’s token is granted and current config still enables serve it, in grant order, and the client calls each in turn until one answers; each call is authorized as usual. The one capability so far is git.credential (extension.CapabilityGitCredential): input {protocol, host, path?}, git’s credential request, and output {username, password, expires_at?} for a remote the extension owns, or {} for any other. os git-credential, the credential helper a run’s git calls, asks for it, so GitHub (github.git.credential), another code host or an installed package can each serve git for its own remotes (issue #357).
  • Operation.Guidance: optional Markdown (at most MaxGuidance, 4 KiB) telling an agent when and how to use the operation. GET /v1/toolbelt/guidance answers the guidance of the operations the caller’s token is granted and current config still enables, and os mcp appends it to its MCP instructions. An extension describes its own operations this way, so a playbook never has to describe another extension’s (introspection.correction, introspection.problem, introspection.report-issue and human.ask do; issue #208).
  • Operation.Summary and Operation.Input: what the operation does, in one line (at most MaxSummary, 200 bytes), and the JSON Schema of its input’s data, an object. extension.InputSchema[T]() derives the schema from the struct a handler decodes (the same derivation sdk.Handler uses; a field’s doc tag is its description), so the declaration follows the code. An in-process extension declares them once, on the declarations its inproc.Declarer returns (an optional interface beside inproc.Operator, which still names them), and the host gives each registered operation of the same name whatever its registration leaves unset (inproc.Describe); an installed package declares them in its manifest. The same guidance endpoint answers the summary and input of each granted operation, and os mcp lists them in its instructions; the generated operations reference renders them for every bundled extension and package. The toolbelt does not check calls against Input; each handler still decodes its own input (issue #392).

The reference toolbelt double supports pure handlers only.

extension.Operation.Emit is a translated emission, set instead of Handler or Wait. The handler returns an EmitPlan: an Event giving only type, delivery identity, optional occurrence time, key and data, plus a pure Commit callback. The toolbelt accepts that Event exactly like the builtin emit:

  • a family the same registration declared, which the token allows;
  • trusted execution/<id> source and trigger causation;
  • AcceptDerivedInTx chain-depth and per-chain rate limits.

In the same transaction it writes the commit’s Records, which must stay in namespaces equal to the registration name or under name., and only when the Event is newly added. It returns the commit’s output (Emitted carries the accepted Event and its root and depth). A key the current Work does not own must lie in the registration’s own source namespace (name or name/…). The Event is then addressed to that key rather than to the emitting Work, so the registration’s own route can give it Work, while source and causation still record the emitter. Presetting identity, source, causation or target is refused, and nothing a refused plan names is committed. The issue-reporting extension uses it; see issue reporting.

Cited causation. EmitPlan.Cause may name an accepted Event the emission derives from instead of the attempt’s trigger. The cited Event must be in the emitting Work’s scope and have a key in the registration’s own source namespace; anything else, a missing Event included, is refused (ErrUnauthorized). The new Event’s causation is the cause and it joins the cause’s chain. Its depth is one more than the deeper of the cause and the trigger, checked against the stricter limit, and it is charged to both chains’ per-minute budgets. Citing therefore never shortens a chain or escapes the trigger’s limits (runtime semantics). The introspection extension’s fix request uses it to cite the report it fixes, so a fix traces through causation to the report and its origin (introspection).

Identity across executions. EmitPlan.Once names the emission’s identity across executions, at most extension.MaxOnce (1024) bytes. The toolbelt records it, per registration and scope, with the Event it first accepts under it, in the accepting transaction (core.toolbelt.once). A later plan with the same Once and the same Event (type, key and payload) is that first Event again: nothing is accepted, no causal budget is spent, no records are written, and Commit sees Added false. A plan with the same Once and a different Event is refused with code once_conflict. Once therefore names a revision rather than something that changes, and content is never hashed to tell edits apart: an extension recording edits puts the revision in Once. A translated emission’s own identity includes the execution, so without Once the same thing emitted by two executions is two Events. The observe extension uses it so repeated reads of a source give one Event per item revision.

RouteInput.Route carries the route being evaluated, including its opaque handler config, so one handler can serve several routes (one per binding). config.Registry.HandlerConfig lets a registered handler validate that config at load/apply time. The schedule extension uses both; it also derives schedule.run.* Events for job: waits, which GitHub’s tools resolve through the host’s wait targets (Host.WaitTarget, served by inproc.Targeter) without importing it.

The optional extension.Setup hook lets an extension own its setup. SetupInfo names a type and Setup(ctx, SetupHost, args) returns connections plus one scope. Core open-conduit init <type> hosts it with prompts, a browser opener, the secret store, a helper-command runner, offline validation and the merge, and it never interprets service details. GitHub (init github), Slack (init slack), Linear (init linear) and Jira (init jira) implement it, each served from its package; see operator setup.

The required lifecycle hooks cover acceptance/dedup/rejection, reconcile start/page/complete/partial/failure, route/binding decisions (including drop), Work transitions, Execution queued/start/park/resume/outcome/cancel, lease renew/loss/recovery/adoption, wait register/wake/cancel, timer arm/fire/cancel and toolbelt calls/emissions/rejections. Every actual transition emits a record after its corresponding durable commit, with retries/duplicate exporter delivery identifiable; logs/metrics/traces must not imply a rolled-back transition happened. The runtime instruments every committed transition from the durable writes themselves and exports through a non-blocking, drop-accounted pipeline with an OpenTelemetry mapping (lifecycle telemetry, run history and doctor).

Every record includes name, trusted timestamp, scope, causal Event and trace. Once associated with Work, include Work ID. Every execution.* record additionally requires Work/Execution IDs and executor binding. Related route/emission/wait/timer records retain these fields whenever an attempt exists. Before association those IDs are legitimately absent; the system must not fabricate them. An ingress scan or pre-acceptance rejection uses a trusted ingress/control trigger Event to anchor the trace. Host-polled ingress scan records (ingress. prefix) have no trigger Event, so they are anchored by their source attribute and a scan-derived trace; Validate accepts that anchor only for these records. From/To describe generic lifecycle states; attributes explain route choice/reason and safe diagnostics. Runtime supplies correlation, never an executor’s unverified payload. Recovery retains the original trace/attempt and records the new lease epoch; accepted wake/emission Events link the chain. High-cardinality IDs belong in logs/traces, not metric labels.

Executors report usage/cost on that same execution correlation; shared Measurement carries the optional counters and currency. Outcome quality and per-turn telemetry are extension metrics, with no core semantic interpretation. Opaque payloads, recovery state, request bodies, secret config and bearer tokens are excluded from telemetry. Redact copies attributes, filters sensitive keys and replaces known secret values before any exporter sees them. Identifiers/state names must be secret-free at their source; redaction cannot repair identities without breaking correlation. Errors must be sanitized before becoming attributes. Artifact streams need their own producer redaction policy.

The Observer contract is required even with no exporter configured. Exporters are optional and must support shutdown. Export failures produce safe diagnostics and cannot roll back durable transitions, stop scheduling or change execution outcomes. Passing the reference observer proves validation/redaction only, not OpenTelemetry coverage; the export pipeline has its own buffering, drop accounting and exporter fault tests.

External extensions run behind internal/extproc, which adapts a supervised process back onto these same contracts (runtime, trust model and schema evolution). The extension subprocess protocol (B08, provisional) is JSON-RPC 2.0 over stdio, with a validated manifest declaring mechanical capabilities and mandatory namespaced KV, and a method catalogue in package protocol that maps every wire method to the contract it carries and its first-party users. In-process ContractVersion and wire protocol.Version are versioned separately.

Every extension runs as a package, out of the daemon’s process. Most first-party extensions are written as Go libraries on extension/inproc, which protocol/sdk/inprocsdk serves as a package: it declares the manifest from the surfaces the extension implements and carries each over the protocol. Each surface is an optional interface: config types and validators, toolbelt operations and the toolsets that grant them by role, route handlers, start and stop, Configure with the same protocol.ConfigureParams an installed package receives, toolbelt registration, action providers, annotations, rendering of its own Event families, paced background tasks, doctor checks, playbooks, a telemetry exporter and a telemetry-event sink. A route handler’s config type can also check the binding each route with that handler binds (its executor, operations and executor config), so a handler can refuse an agent it must not run; the refusal is the diagnostic binding_refused with the handler’s fixed reason. An extension can also declare the lifecycle of its own Work kinds (Timed marks a kind that runs on a clock, which a dry run never starts) and its timer recurrences, and have its toolbelt registration join only generations with one of its entries. It can register executors bindings name, each with the capacity pool it counts against (inproc.Executing; GitHub’s github-hold is a job). It can register ingress sources after each configure (inproc.Ingester; the host takes only sources named after the extension that produce its own Event families, as for an installed package), serve the webhook paths under /<name>/ on the webhook listener (inproc.Webhooker), and resolve named wait targets such as job:<name> (inproc.Targeter). Configure also carries its entries in disabled scopes (ConfigureParams.Disabled), so it can keep what it holds for them. Each is a manifest declaration with its protocol methods (manifest), so a package written on protocol/sdk has each of these too, and bootstrap composes every package, bundled or installed, one way: internal/bootstrap/compose.go iterates the running packages and reads each surface from the package’s manifest and its process (extproc.Remote), naming no extension. The host gives each one its store, clock, logger, diagnostics and secrets, scoped to its own entries and connections as for an installed package (B05). It also gives a loop nudge, the state directory and dry-run flag, root acceptance of the extension’s own Event families (types named <name>.*) inside the extension’s transaction, derived acceptance of them from a parent Event in the same scope under the generation’s causal limits, the scope an accepted Event belongs to, delivery to its own ingress sources, the operator’s authorization for a cross-scope read or filing (Authorize), the wait targets other extensions resolve (WaitTarget), the artifact store, toolbelt and playbook directories, per-Event rendering through the owning extension (Render) and a rotating log of its own (OpenLog) for executors, word when a container segment it issued something for ends (SegmentEnder), timers for its own recurrence kinds, cancellation of its own Work kinds, capture into the host’s telemetry events log, and a config service (extproc.ConfigHost behind config.*, which inprocsdk presents to a library as inproc.ConfigHost; the config host) that keeps the config file’s text behind handles and hands out only redacted copies, makes config set’s order-keeping pointer edits against a handle, and validates and traces routes against a candidate, but never applies. The host also gives the setup probes (setup sessions): every probe another extension or executor offers through the optional extension.SetupProbes (a named, read-only check with declared arguments that takes no command line or reference, such as claude-code.status and github.accounts), and its own open-conduit.init (init TYPE --dry-run for every setup but the first-run one, registered or installed, on a host that never prompts, stores nothing and runs nothing) and open-conduit.doctor (doctor’s checks that need no secret). They are named after the host, not the extension that runs them. An extension whose Work proposes configuration implements extension.ConfigCandidate, which GET /work/{id}/config-candidate serves to the operator: the changes, with the extension’s actions that decide each, for the web view’s review, and the candidate’s text to apply. An extension can also contribute a page of its own to the web view (inproc.Paged, extension.Page), which core draws from its panels and actions (pages). Every first-party extension runs as a package bundled with the release (posthog, human, observe, linear, jira, bitbucket, schedule, introspection, slack, github and codex; setup is its own module’s package), and so do the shell and claude-code executors and the git, srt, docker, ssh, ngrok and cloudflared providers. Nothing registers an extension in process: scripts/check-boundaries refuses an import of an extension in bootstrap’s production code, and of extension/inproc anywhere but extensions/*, protocol/sdk and integration’s tests.

Installed extension packages are loaded by the daemon and composed exactly as the bundled first-party ones are, through these contracts, with no domain branch in core. Slack, GitHub and the shell and claude-code executors run only as packages, and pass the public certification runner. The GitHub extension (extensions/github/pkg) is served unchanged by protocol/sdk/inprocsdk, which serves any extension written against the in-process contract as a package: that it certifies is the proof that the contract is fully carried by the wire (#357). See extension packaging, SDK and certification (provisional).

  • B04 runtime semantics resolves route order and first match, explicit active-policy fallback, completion/inject races, wait arbitration, adoption and causal bounds, and generic consumption acknowledgement.
  • B05 configuration, secrets and local management specifies inheritance and tombstones, pinned generations with current authorization ceilings, secret resolution and local HTTP-over-Unix-socket bearer authentication.

Run go test -race ./..., go vet ./... and go build ./.... Shared ingress fixtures check declarations/loss, stable normalization, pagination and canonical overlap; they use synthetic state-change and one-shot families, with identical timestamps to check deterministic normalization, and run against the GitHub, Slack, Linear, Jira and Bitbucket ingresses. Diff fixtures check incomplete scan safety, mixed generations, absence only on completion, unchanged snapshots and dedup against public storage. Executor fixtures cover every structured outcome, recovery metadata persistence failure, adoption without replacement and unknown metadata. Binding tests reject unsupported/misadvertised capabilities and transport mismatches. Toolbelt cases cover each builtin, custom operation, registration conflicts, allowlists, forged causation, cross-target authority, exact token expiry, revocation, lost lease and authority release after wait/outcome; they also run against the production toolbelt. Telemetry fixtures reject missing correlation and require redaction, and run against the export pipeline. Negative controls weaken canonical identity, loss declarations, toolbelt authority and telemetry validation and must fail the actual shared cases.

The shared ingress, executor, inject and toolbelt cases also run unchanged against the example external extension and executor, both in-process and as subprocesses. The shared suite conformance.RunExtensionKV covers the namespaced KV reached through the host: own namespaces only, version tokens, atomic precondition batches and prefix order.

  • 01-03 defined these contracts against volatile reference doubles. It implemented no runtime, service integration or subprocess protocol. The planning document’s sequence listed four interfaces, but its cross-cutting contracts also required observability, so 01-03 made observability the required fifth contract. OpenTelemetry instrumentation and export wiring were left to 02-08, and no SDK dependency was added.
  • At 01-03 several decisions were assigned to later tickets: ordering, active-policy acknowledgements and fallbacks, wake races, causal bounds and retry/adoption policy to 01-04 (resolved by B04, noted here as the “B04 update”); configuration, secret resolution and local authentication to 01-05 (resolved by B05, noted as the “B05 update”, with runtime enforcement in 02-06); the combined contracts gate to 01-06; generic injection acknowledgement to 04-02; third-party schema evolution, manifest and transport to stage 07. The GitHub push, release and workflow-run recoverability requirements were assigned to 03-01 and 05-01.
  • When 01-03 closed, service transport correctness, process kill/restart, SQLite durability, actual secret transport and OTel instrumentation were still later work.
  • 02-02 implemented the ingress staging and crash-boundary rules in the runtime (event ingress).
  • 02-04 added RetryHintProvider. 02-05 added RunRequest.Inputs.
  • 02-06 implemented the production toolbelt’s fenced per-epoch wait/declare_outcome declarations.
  • 02-07 added the shell executor’s OS_TOKEN_FILE and os CLI presentation, and relaxed the shared executor case to permit Recover re-acknowledging identical metadata.
  • 02-08 added the telemetry pipeline and the ingress. scan-record anchor clarification.
  • 03-01 made GitHub the first real ingress and established the version, item-order and webhook-boundary rules.
  • 03-02 replaced the run context’s resource descriptor list with the optional ResourceBroker and added ResourceProvider (B07).
  • 03-03 added the first mcp executor, os mcp, and extension.Result.RetryHint.
  • 03-04 added Operation.Wait, Dependency, Registration.DependencyFilter, CallRequestID and OperationError.
  • 03-06 introduced Result.ConsumedInputs as consumption proof on completed and handoff results; further acknowledgement and completion races were left to 04-02.
  • 03-08 added translated emissions (Operation.Emit), first used by issue reporting.
  • 04-01 made Slack the second real ingress, added the OneShot ingress fixture delivery and the Socket Mode acknowledgement rule.
  • 04-02 added the extension.Setup hook, ErrNotAccepting, ConsumedInputs on every outcome and CheckInjectExecutor.
  • 05-01 added RouteInput.Route, config.Registry.HandlerConfig and the schedule.webhook one-shot family.
  • 05-02 added Result.Workspace, Resource.Summary, Binding.TimeoutSeconds and the schedule.run.* Events for job: waits.
  • 05-03 added cited causation (EmitPlan.Cause).
  • 07-01 extracted the subprocess protocol (B08, provisional) and made StopExecutor and AttachExecutor public with no behaviour change; the management wire code no_session became nothing_to_attach.
  • 07-02 ran the shared suites against the external examples and added conformance.RunExtensionKV.
  • 07-03 loaded installed packages in the daemon and ran the Slack extension and shell executor as certified subprocess packages.
  • #318 added EmitPlan.Once, an emission’s identity across executions, for the observe extension.
  • #357 added Operation.Serves and GET /v1/toolbelt/serving, so git credentials route to whichever granted operation serves git.credential instead of to github.git.credential by name.
  • #357 also put installed packages’ executors, Work kinds, recurrences, ingress sources and webhook paths on the in-process surfaces (Executing, WorkKinder, Recurring, Ingester, Webhooker) instead of bootstrap’s own path for packages; an installed executor naming an unknown capacity pool is now reported as executor_pool, like an in-process one.
  • #496 closed #477: with every extension a package, bootstrap’s in-process list (firstParty()) and the boundary checker’s compositionExceptions went. Bootstrap composes the running packages directly instead of adapting each onto extension/inproc, the config host’s interface moved to internal/extproc on the protocol’s types, and make boundaries now keeps extension/inproc to the extensions and the SDKs that serve them.