OpenCode Host Adapter v1
September 13, 2026 ยท View on GitHub
Status: LOCAL private-host contract; no hosted provider evidence. Audience: local adapter maintainers and compiler contributors.
Status and boundary
This is a LOCAL private-host contract for issue #112. It binds one
explicitly configured OpenCode command to live_invocation::ModelHandler in
semaprax-toolchain. The standalone semaprax compiler and SDK remain
offline. A local source-driver smoke reached Complete with OpenCode 1.18.27;
this contract does not claim hosted evidence, production support or OS isolation. The source adapter implements the existing
Agent lifecycle v2 feedback callback; broader live-kernel source/HIR integration
remains tracked separately in #177.
The only admitted profile is opencode/muse-spark-1.3-contributor-free. There
is no fallback model, provider, endpoint or paid route. The adapter adds no
automatic transport retry; OpenCode subprocess work remains deadline-bounded.
Host credentials are not added to model context or runtime journals. Provider
error bodies and headers are discarded from the closed diagnostic categories.
Authority and route
A deployment supplies both an OpenCodeHostConfig (absolute executable,
empty non-symlink absolute workspace, positive deadline, and an
OpenCodeGrammar derived from the exact CompiledInteractionSchema) and the
existing per-call ModelInvokeCapability. OpenCodeGrammar carries the
canonical interaction schema and its existing provider JSON Schema projection;
the handler rejects a request whose grammar digest differs, then passes those
compiler-derived guidance bytes to the configured process. It does not decode
or admit the response. The existing SourceInteractionProposalDecoder still
decodes the raw response after this handler returns it. Neither source data nor a response can construct
those values. The production ProcessOpenCodeRunner uses no shell or inherited
stdin and invokes exactly:
opencode run --pure --agent semaprax-live \
--model opencode/muse-spark-1.3-contributor-free --format json --dir WORKSPACE PROMPT
It writes an opencode.json agent policy with "*":"deny" and
"snapshot":false before the call. Filesystem snapshot tracking is disabled.
The child receives a cleared environment with private home, configuration,
cache, temporary and database paths. Project/Claude instructions, external
skills, default plugins and automatic updates are disabled. The explicit
policy is the sole admitted local configuration. Existing host credentials
remain in the original XDG data directory; their bytes never enter arguments,
context or receipts. Remote-configuration auth entries and managed settings
cause a generic pre-spawn refusal. Run and export share this private context.
These controls are required because --pure alone only disables plugins.
--dir and that policy limit OpenCode's own tools only; they are not an
operating-system sandbox. The process output and session export each have a
1 MiB ceiling. The runner polls both an explicit host cancellation handle and
its deadline, kills and reaps its child on cancellation,
overflow, deadline, and process errors, and captures no stderr. On Unix it starts a dedicated process group and uses same-thread nonblocking
stdout polling; it kills that group after direct-child exit, so a descendant
retaining stdout cannot make the call unbounded. This v1 adapter refuses before
spawning on non-Unix platforms because it does not yet implement an equivalent
bounded nonblocking pipe loop there.
Settling and replay evidence
The runner requires exactly ordered step_start, text, and step_finish
NDJSON events with one session/message pair and a stop finish. It then runs
opencode export SESSION --pure and requires the fixed provider/model self-report,
the matching user and assistant sessionID, matching user prompt, and matching
assistant text before returning the raw text bytes. The receipt records the bound session/message, fixed model, supplied token
counters and reported cost; it proves neither identity, billing, provider authorization, nor
exclusive execution.
The handler returns Settled(raw_bytes) only after that transport validation.
It deliberately performs no proposal/schema decoding. The existing
SourceInteractionProposalDecoder runs in the live driver after the handler,
so compiler-derived grammar admission remains authoritative.
Before process start, a cancellation returns Cancelled. After start, any
transport/export uncertainty is ProviderError unless the injected runner has
observed cancellation; timeout, capacity, malformed output, and policy refusal
remain their respective closed ModelFailure cases. A cancelled call does not
prove that the provider stopped work or billing.
Regression obligations
Offline tests cover ordered-event/export binding including hostile session
substitution, receipt usage preservation, cancellation and overflow reaping of
local stubs, and the rule that malformed post-start output is not cancellation. A local
stub executable is the required integration seam for process command,
deadline, bounded-output, and export coverage; it must never call a provider.
The existing agent_interaction_schema::live_bridge kernel test remains the
compiled-schema decoder gate. A separately recorded free-model call is required for live-provider evidence;
it does not establish hosted CI or production support.
Source-feedback smoke embedding
crates/semaprax-toolchain/examples/opencode_live_smoke.rs compiles a frozen,
pure one-turn source lifecycle and calls its existing #111 run_live route.
With no arguments it uses one canonical offline proposal. A real invocation is
opt-in and requires all of:
cargo run -p semaprax-toolchain --example opencode_live_smoke -- \
--live --opencode /absolute/path/to/opencode --scratch /absolute/empty/dir \
--evidence /absolute/new/evidence-dir
The --live branch keeps the source lifecycle's own proposal decode and
bounded retry loop, but its host runner permits only one actual provider run.
It archives the prompt, raw events and session export in the new explicit
evidence directory. It swaps only the ProposalSource callback for the
explicit OpenCode bridge, uses the free configured profile, and has no paid
fallback. This example is local host evidence only; it does not establish a
hosted support claim.
Implementation references checked 2026-09-13: OpenCode CLI
(run, model/agent/JSON flags and session export) and permissions
(the deny-all agent policy). The observed wire profile is OpenCode 1.18.27;
future CLI/export changes must be admitted deliberately rather than silently
accepted. The receipt validator compares all streamed parts to their exported
counterparts, admits the observed empty reasoning marker, and rejects model,
finish, prompt, session, part-order, and typed-usage drift. Missing token usage
remains unknown. It resets the prior receipt before every attempted request.
Local execution evidence (2026-09-13)
The explicit source smoke returned live status=Complete using the frozen
source fixture, real run_live, compiler-derived proposal grammar and unchanged
canonical decoder. Its in-memory read callback returned a fixed fixture value;
no external effect mutation or deployment occurred. OpenCode 1.18.27 reported
session ses_f67f10b32ffeSsQKpC96WHphgi, assistant
msg_0980ef5b2001CpH4AZG9d5JL7N, model
opencode/muse-spark-1.3-contributor-free, and a stop finish.
Reported counters: total 3188, input 1780, output 110, reasoning 1298, cache read
and write 0; reported cost 0. These are provider self-reports, not billing proof.
The smoke runner allowed one actual OpenCode run and archived raw prompt,
events and exported session under local .agent-logs/0913-opencode-source-live-v6/.
The exercised binary SHA-256 was
c1ab0401fd926d9250fc8d446bdf47bd6a8f71a98807d9c6538d9f8a832a0a24.
Raw event SHA-256:
039b8955d5aaa6bebe5bc20173631d70ac43d693eaf7fc7e4068a5fc58dc32e1.
Export SHA-256:
e14ef1b0cef6f7ef95370f9aad9cc3d123af3e1f88e299b7173c59e3a9640aac.
Earlier unsuccessful attempts were rejected for multiple steps, the CLI's
positional-prompt quoting, absent/literal-escaped terminal LF, and a snapshot
patch record. The final call used scratch outside Git with snapshots disabled. The final prompt requests the LF explicitly;
the host neither appends it nor repairs the proposal. Those failures remain
separate local observations, not successful executions.
OpenCode's tagged CLI emitter
also owns the positional-prompt quoting and error envelopes used here.
last_provider_failure distinguishes rate limiting, authentication, refusal,
server failure and incomplete output without retaining provider error text.
The source driver still owns proposal decoding, bounded malformed retries and
all checked authorization/reduction stages. Local focused gate:
cargo test --locked -p semaprax-toolchain --lib opencode_host -- --test-threads=1
passed all 29 tests. The canonical-context regression (1), existing source-driver
tests (5), bundle pin (1), module-size (1), and source-contract coverage (1) also
passed. The offline smoke returned Complete. These are focused local checks;
the full/hosted quality profile was not rerun.
The environment profile was checked against OpenCode v1.18.27's tagged instruction loader and configuration loader. Offline environment tests use synthetic auth/settings paths; normal tests neither read the developer's credentials nor contact a provider.
OpenCode may install runtime dependencies in its private configuration/cache
directories even with --pure; those writes are not compiler build-time work.
The smoke archives its receipts before removing disposable scratch storage.
Source attempt accounting
The source adapter requires an explicit OpenCodeSourceAccounting wrapper
around the existing InvocationBudgetHook. The host supplies a positive fixed
reservation in the deployment's units and a bounded attempt capacity. Each
attempt reserves before dispatch and retains that reservation through malformed
proposals, uncertain provider failure and cancellation in flight. Validated
reported token counters are observations; they never refund or determine the
reservation. A cancellation observed before reservation makes no provider call.
The wrapper retains bounded host-only receipts with turn/attempt identity, reserved units, measured request/response bytes and optional reported counters. The source grammar decoder still owns admission, so a transport-settled but noncanonical document remains charged when the driver retries. Receipt capacity is checked before a new reservation. The shared absolute deadline is checked before dispatch and again before settled proposal bytes reach the decoder.
The existing source driver checks the same policy through
ProposalSource::check_deadline at stage, proposal/decode, effect and result
publication boundaries. The OpenCode source delegates that read-only check to
its accounting wrapper; fixture sources retain an accepting default. Checks do
not reserve again. An expired successful read blocks reduction and publication;
an already selected effect failure or reducer Fail remains selected.
These receipts and the source wrapper are in-memory. A deadline diagnostic from
run_live does not return its partial run or durably retain an observed effect.
Durable source recovery, migration, restart-stable deadline binding and complete
failure evidence remain open #113 requirements. The smoke clock is process-local.
Source Live Journal v1 records the proposed
source durability contract and required recovery tests; it is unimplemented.
The earlier local provider evidence above is bound to its recorded executable;
these extensions have separate offline regression evidence.
Focused accounting validation (2026-09-13): 39 OpenCode host tests and 103 live-invocation library tests passed, including exact-deadline rejection, just-before-deadline admission, nonrefundable malformed retries, prior-failure selection and causal journal replay. Bundle pin and both structural checks also passed. No additional provider call was used for this extension.
Source-boundary extension validation (2026-09-13): 8 source-driver tests and 41 OpenCode host tests passed, including expiry inside stage/effect/transition callbacks, actual adapter reads finishing late, cancellation before Complete publication and sticky reducer Fail. These are local offline regressions.