mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-11-01 19:40:35 +00:00
The Windows plugin installer rejected intact payloads on Node 24.0.0 / libuv 1.50.0 because pathname lstat returned a zero device identifier while the open handle reported the actual volume. Full bytes, SHA-256, length, link count and inode matched. Replacing or deleting the file could never repair this metadata disagreement (#1235 follow-up). Use exact BigInt filesystem identities. When Windows pathname device metadata is missing, independently open that name and compare both handles' nonzero device identifiers and exact inodes. Recheck pathname type, link count, inode and size, retain SHA-256 and EOF checks, and report the failed invariant accurately. Keep POSIX and genuine nonzero device mismatches strict; add no version gate or dependency. Independent validation reproduced released-bundle installs failing 0/21 before and passing 21/21 after on Node 24.0.0. All 61 plugin tests pass on Node 24.0.0 and 25.2.1, including negative controls for device differences, rounded inode aliases, missing identity, links, probe substitution, replacement, handle closure and corrupt bytes. Isolated OpenCode 2.2.5 plugin provisioning, pending-request RPC/Form checks and typechecks pass. Review has no P0-P3 findings. Merge requested explicitly before overall CI completion; the run currently reports failures in Mission/native database, permission receipt and workspace ownership tests. Confirmation on the reporting machine remains required.
212 lines
20 KiB
Markdown
212 lines
20 KiB
Markdown
# OpenCode compatibility and runtime qualification
|
|
|
|
## Qualification policy
|
|
|
|
Qualify the pinned client/plugin dependencies (moved together) against the
|
|
technical minimum and `@opencode/cli@latest` before publishing; CI records the
|
|
resolved runtime. Review a release from its exact tags, published npm packages
|
|
and an authenticated isolated OpenAPI dump (the tagged
|
|
`packages/protocol/openapi.json` can lag the runtime), never GitHub's compare
|
|
view; release notes are at [opencode.ai/changelog](https://opencode.ai/changelog)
|
|
(GitHub Releases cover V1 only). Keep results in the PR and CI logs, never as
|
|
per-version sections here.
|
|
|
|
Minimum runtime **2.2.3**, recommended **2.2.5** (`runtime-support.ts`), with
|
|
**2.2.3** client/schema/plugin pins. The minimum carries the image tool contract of
|
|
upstream [#60258](https://github.com/anomalyco/opencode/pull/60258), consumed by
|
|
the Images gallery, the Design agent and skills and the Images settings: one
|
|
name, `experimental_image_generate`, for the tool, its permission action, its job
|
|
type and its completion notices' `metadata.source` (`IMAGE_TOOL` in
|
|
`session-pruning/assets-gallery-proof.ts`), and the tool offered while an image
|
|
model is available unless `experimental.image_generate` is `false`
|
|
(`image-settings.ts` reads it that way). Notices wrap their text in
|
|
`<image id="…" state="…">`; the gallery reads the `Saved image: <path> (` line.
|
|
The minimum also provides the replay contract (below) consumed by the pinned
|
|
reducer and Code Mode JavaScript run-to-completion, whose regression
|
|
`scripts/test-codemode-jobs-native.mjs` runs in CI against minimum and latest.
|
|
Daemons below the minimum are refused with `native_contract`. Plugins share the
|
|
host Effect 4.0.6/Schema runtime. Unlisted/prerelease/custom/future labels
|
|
undergo authenticated contract recognition; missing canonical APIs, session
|
|
environment or the replay shape give a concrete reason. OpenCode reads older
|
|
stored data.
|
|
|
|
Missions schedules qualify the consumed Schedule API, effective
|
|
`experimental.schedules` flag and retained plugin/provider bundle independently
|
|
of general runtime admission: [native contracts](MISSIONS_NATIVE_CONTRACTS.md#schedule-integration-qualification)
|
|
and [transport](MISSIONS_NATIVE_SCHEDULE_TRANSPORT.md). Recognition uses the
|
|
consumed capabilities, including on future versions. Schedule delivery qualification
|
|
uses the recommended 2.2.5 runtime; keep results in PR/CI evidence.
|
|
|
|
### Native Codex subscription usage
|
|
|
|
Beyond the usage rules in [PROVIDER_ACCOUNTS.md](PROVIDER_ACCOUNTS.md):
|
|
Codex uses only the owned session's integration's first connection and its
|
|
matching active ChatGPT OAuth credential (`chatgpt-browser`/`chatgpt-headless`),
|
|
never Codex CLI credentials, SQLite or another saved account; WSL uses the
|
|
selected daemon's credentials. Snapshots are keyed by native connection,
|
|
instance, session, directory, provider and credential identity and revalidate
|
|
native selection before publication; the proxy keeps blocking browser credential
|
|
export and generic RPC. Fixture: `node scripts/test-provider-usage-native.mjs <cli>`
|
|
(synthetic credentials and mocked quota HTTP only).
|
|
|
|
### Side questions (`/btw`)
|
|
|
|
`/btw` calls only native `session.generate({ sessionID, prompt })` through the
|
|
ownership-checked proxy and shows its single text answer; the session's
|
|
environment, transcript and inbox stay untouched, and closing aborts only that
|
|
request. `scripts/test-session-aside-native.mjs` (private daemon, local
|
|
provider) runs in the minimum/latest CI matrix.
|
|
|
|
### Session goal (`/goal`)
|
|
|
|
The Status tab's goal line consumes OpenCode's built-in goal plugin
|
|
(`@opencode/schema/goal`): the `opencode.goal` RPC's `get({ sessionID })`, which
|
|
answers the Session's own `Goal.State` or null, and its `changed` event
|
|
(`rpc.opencode.goal.changed`, `{ sessionID, goal }`), published in the Session's
|
|
Location on every write. The instance proxy refuses plugin RPCs, so
|
|
`GET /api/workspaces/:id/sessions/:sessionId/goal` (`server/routes/session-goal.ts`)
|
|
is the read path: it reads the Session, checks its Location's ownership, calls the
|
|
fixed method in that Location and returns only the displayed fields
|
|
(`opencode/goal-contract.ts`). An `RpcError` of type `rpc.unavailable` or
|
|
`rpc.method_not_found` answers `unsupported`; any other failure or unreadable state
|
|
answers `unavailable`, and the line shows nothing for either. Pause, Resume and
|
|
Clear run the native `goal` command with `pause`, `resume` or `clear` through the
|
|
composer's command path (`executeCustomCommand`: session admission, instructions,
|
|
profile environment at the proxy); a refused command (`No active goal to pause.`)
|
|
shows its message in a notice. The relay routes plugin RPC events to the workspace
|
|
owning their Location, so the UI applies `changed` directly and reads only on
|
|
demand, on reconnect and after its own commands ([refresh rules](CACHE_REFRESH_CONVENTIONS.md)).
|
|
`scripts/test-session-goal-native.mjs` (private daemon, local provider whose plain
|
|
reply the goal reviewer rejects, pausing the goal) runs in the minimum/latest CI matrix.
|
|
|
|
### Contract notes
|
|
|
|
- Generated client URLs keep their `baseUrl` prefix and pass through unchanged;
|
|
scheduling classifies API paths relative to the proxy prefix.
|
|
- The generated `experimental.session` methods and their `session` aliases use
|
|
identical experimental wire routes for instructions, wait, log, import/export
|
|
and stats. Stateless `experimental.generate.text` uses
|
|
`/api/experimental/generate/text`; side questions use the separate session
|
|
generation API. New client domains retain the proxy's explicit allowlist.
|
|
- The Solid reducer derives projected message IDs with
|
|
`Session.Message.ID.fromEvent`: event fixtures must use native `evt_` IDs,
|
|
producing `msg_` rows. Catalogue invalidations refresh only previously read
|
|
resources; an active-session snapshot also fetches missing running-session
|
|
metadata. Tool-file omission is opt-in; transcript and pruning reads preserve
|
|
their full native projection.
|
|
- Service discovery/header construction, RPC envelopes, `SessionLegacy`, Form
|
|
and Permission read codecs keep their consumed contracts. The private loaded
|
|
Location map canonicalizes path flavor and trailing separators; pending
|
|
recovery retains its existing-only leases and identity checks. Plugin setup
|
|
remains activation-scoped; native services are acquired in the request graph.
|
|
- The proxy allowlist stays closed to pairing (`/api/pair`, `/auth/connect/*`),
|
|
credential export/creation (`/api/credential`) and experimental voice
|
|
(`/api/experimental/voice/session`); a runtime update never widens it or
|
|
retries a mutation under another contract (`instance-proxy.test.ts`).
|
|
- Session creation needs an inspectable JSON object; the proxy rejects any
|
|
`parentID` before forwarding (native `parentID` ignores the location).
|
|
- Service lifecycle uses the selected CLI's status/start/password and the
|
|
registration fallback, never SDK `Service.ensure/stop` or an assumed default
|
|
port. A user's `opencode service set env` value overrides CodeNomad's
|
|
missing-daemon start variable of the same name; per-send
|
|
`session.environment` applies independently.
|
|
- Plugin bundles externalize Effect and the plugin SDK because two Effect
|
|
runtimes cannot share one native fiber and host Schema APIs differ across
|
|
releases. Local plugin directories with a package name and `exports` resolve
|
|
their entrypoints through that manifest (`session-pruning/package.test.ts`).
|
|
Test hosts that spread the Promise plugin context must declare the
|
|
`credential` domain. `%ProgramData%\opencode` is discovered after the global
|
|
plugin root that `config.get` consumers take first; native fixtures redirect
|
|
`ProgramData`. Package-install fixtures pass `withProductRuntime`'s `registry`
|
|
option (a loopback fake npm registry, [Plugin controls](PLUGIN_CONTROLS.md#validation)).
|
|
- Relied-on upstream behaviors keep their regressions:
|
|
`test-output-limit-native.mjs` (an output-length stop settles failed/idle with
|
|
one user prompt) and `test-promise-tool-failure-native.mjs` (rejected Promise
|
|
tools settle `error`; `permission.rejected` prevents the side effect).
|
|
- Upstream #52922 (Bus routing before Location fanout) is open: keep the
|
|
discovery and event-relay mitigations.
|
|
|
|
### Replay contract
|
|
|
|
Text/reasoning/tool/step provider data is one `native` field (on a key shared by
|
|
a tool call and its result, the result wins). Summary compactions reference
|
|
`metadata.keepID`, with kept text in `metadata.recent`; provider compactions
|
|
replay from `native.messages`. On read, the `SessionLegacy` codec
|
|
(`@opencode/schema`) upgrades stored rows/events carrying `state`,
|
|
`providerState`, `providerResultState` or `recent` without rewriting bytes, and
|
|
drops a compaction's `providerContext.version`/`provenance` and a
|
|
`providerState` stored beside a `providerContext`. The UI reduces events with
|
|
the pinned `createData` (`stores/opencode-data.ts`); mismatched pins change
|
|
reasoning identity and `stores/session-actions.ts` refuses pruning
|
|
(`session-pruning-projection.test.ts`). Admission therefore requires
|
|
`Session.Message.Assistant.Text.native` without `state` and
|
|
`Session.Message.Compaction.Completed` with `metadata` but no `recent`
|
|
(`compatibility/negotiate.ts`). `planner.ts` hashes the
|
|
`SessionLegacy` projection and writes surviving stored parts back unchanged
|
|
(`pruning.test.ts`). CodeNomad never archives (`session.archived`/`unarchived`
|
|
only invalidate); the UI compaction part keeps `native` (`normalizers.ts`). The
|
|
packed pruning plugin declares every bare import, including `@opencode/schema`
|
|
(`package.test.ts`).
|
|
|
|
### Capability-checked loaded-only pending recovery
|
|
|
|
Rules: [NATIVE_INTERRUPTION_UX.md](NATIVE_INTERRUPTION_UX.md). Pruning/history
|
|
methods have their own support and write checks.
|
|
|
|
The reader uses private native LocationServiceMap, Location, Form and Permission
|
|
services, scoped existing-only leases and repeated inventory/object-identity
|
|
checks. It preserves global/idle Forms and native numeric JSON codecs, returning
|
|
complete coverage (including empty cold directories) with the caller's actual
|
|
`originDirectory`. Bounds: 64 requested directories/selected placements, 2048
|
|
loaded keys, 1024 Forms and Permissions per placement/directory, 4 MiB UTF-8
|
|
output, a two-second reader deadline and the broker's 30-second overall deadline
|
|
(including Git ownership revalidation, eight directories at a time through the
|
|
Git worker). Overflow or unsafe serialization fails rather than truncates.
|
|
|
|
The UI sends at most eight directories per request within the escaped URL
|
|
budget, under its own ten-second deadline; recovering a whole history on a slow
|
|
filesystem can take arbitrarily long. The broker rejects overflowing raw JSON
|
|
numbers (including unknown metadata) before coverage/capability admission;
|
|
native numeric codec strings pass through unchanged.
|
|
|
|
`GET /api/workspaces/:id/pending-requests` keeps its UI wire shape and calls only
|
|
`POST /api/rpc/codenomad.pending-requests/snapshot?location[directory]=<root>`
|
|
with `{ "input": { "directories": [...] } }`. Legacy workspace identities are
|
|
rejected and never erased; workspace/connection replacement, root or candidate
|
|
deletion and changed authority fence dispatch and publication. Cold UI coverage
|
|
synthesizes a location from the validated native entry directory, never the
|
|
browser's alias. Only HTTP 400 with a strict bounded `RpcError` envelope
|
|
(`_tag: "RpcError"`, type `rpc.unavailable` or `rpc.method_not_found`, message of
|
|
at most 4,096 characters) means unsupported, and only after origin and candidate
|
|
revalidation. Plain 404s, other errors, malformed output, authentication failures,
|
|
reader errors, timeouts and overflow return non-authoritative 503s. An unverified
|
|
capability is deliberately never probed during an observed compaction.
|
|
|
|
Candidate traversal constructs no cold Locations; native HTTP middleware may
|
|
warm or rebuild the single bootstrap root, and root eviction or a config rebuild
|
|
constructs it again. Snapshots can go stale after leases release, so per-kind
|
|
mutation fences and settlement tombstones stay essential.
|
|
|
|
## Agent conventions
|
|
|
|
All managed bundle payloads (automation, session-pruning, Missions runtime and
|
|
Missions carrier provider) share `opencode/desktop-plugin-artifact.ts` for digest
|
|
verification, atomic repair and bounded lock retries. Provisioning verifies both
|
|
Missions payloads before publishing its shim or lease. Failures flow through the
|
|
shared provisioning error deduplication and pending-request recovery notice.
|
|
|
|
Artifact identities use exact BigInt device/inode values. When Windows pathname
|
|
stats report device zero, verification opens the name independently and compares
|
|
both handles' nonzero device and inode identities, then rechecks the regular,
|
|
single-link pathname and its size. The payload still passes full SHA-256 and EOF
|
|
verification; the pre-publication recheck repeats this verification for a missing
|
|
pathname device. Known device mismatches, inode changes and links fail closed.
|
|
Verification errors identify the failed file, identity, size or digest check.
|
|
|
|
- Desktop automation and pruning provisioning follows the authenticated OpenCode connection and its `config.get` global discovery directory, including reconnects. Replacing a connection aborts its in-flight provisioning: `opencode/desktop-plugin-provisioning.ts` logs that at debug as superseded (the new connection provisions again) and keeps errors for failures on the current connection, logging each distinct failure once with its consequence and repeats at debug until a provisioning succeeds. Bundle payloads are content-addressed (`<sha256>.mjs`) and shared by every backend of the namespace: `opencode/desktop-plugin-installation.ts` publishes one only by renaming a complete temporary copy, and replaces a name that holds other bytes or is not one regular file (an earlier write cut short, a link) the same way before the entry may point at it, so concurrent installers never read a partial payload and a damaged one never blocks later connections. Never derive a running daemon's roots from backend/startup environment or CLI `debug paths`. OpenCode watches the entire discovery root recursively: keep changing presence leases outside it, in the sibling `.codenomad/<root-hash>/` namespace. Retain existing outside-root storage; migrate inside-root storage while reading older backends' leases without writing there. WSL translates the daemon-reported paths through the selected distro only for filesystem access. Verify heartbeat stability with the isolated native automation fixture.
|
|
- Plugins register only at their next presence check, so a backend's first plugin RPC could arrive before registration and be refused. Followers acknowledge a lease with a `<lease>.ready` marker (current namespace only) once every follower of that directory in the native process has observed it while registered; routes calling bundled-plugin RPCs await `workspaceManager.desktopPluginsReady()` before reading or authorizing the session, so ownership checks and deletion-fence entry stay adjacent (bounded to 5 s, never rejects). The bound is paid in full only when no marker can arrive: no Location loaded yet, or pre-marker plugin copies still loaded after an upgrade. Do not replace this ordering with retries of refused reads.
|
|
- The Design agent and skills follow backend leases, never a command or RPC. A backend requests them while an installed, enabled panel extension declares `opencode.designAgent` (`opencode/design/demand.ts`, refreshed on every extension-store change). `DesktopPluginLifecycle.setLeaseContent` writes the request (`{"version":1,"design":true}`, or an empty lease) into every session-pruning lease the backend retains, one per configuration namespace, and each new installation starts from it. Writes happen only when the request changes, through a temporary name outside the lease pattern and a rename, serialized with the heartbeat so a removed lease is never resurrected; a failed write retries at the next heartbeat. The shared presence observation (`desktop-plugin-presence.ts`, versioned process key) reads at most 256 bytes of each live lease: empty, unreadable, oversized or unknown-version leases request nothing, and Design is wanted while any live lease of the plugin's presence directories requests it, so one backend's expiry withdraws only its own request. The session-pruning plugin registers its agent and skill transforms once in its `effect`, before `followPresence` (`opencode/design/register.ts`), with content conditioned on a Location-local flag; reconciliation at the presence interval only flips the flag and calls `agent.reload()` and `skill.reload()` on an actual change, and the content loads on the first request. Never register these transforms from presence, an RPC or a command: a late registration bypasses the user's permission rules and overrides the user's own `design` agent or `design-*` skills, which always win. A runtime without agent or skill transforms gets no Design agent. `opencode/design/status.ts` classifies a Location's `GET /api/agent`: `present` when the prompt starts with the content mark, `user` otherwise, or `absent`. Validate with `opencode/design/*.test.ts` and `scripts/test-design-activation-native.mjs` (build the pruning bundle first); that fixture enables project discovery, so outside CI it refuses to run under the user's home unless `CODENOMAD_NATIVE_FIXTURE_PARENT` names a parent outside it.
|
|
- The shared native event relay consumes upstream events before slow routing I/O. Preserve per-session/PTY/Shell and per-recipient FIFO, validated full-location ownership, invalidation/connection/workspace fences and bounded-backlog reconnect recovery. When an ownership lookup still fails after its retry, the relay evicts that cache entry and pulses that recipient's status alone (`error` with `event routing failed`, then `connected`), so its clients run their reconnect reconciliation (one pass per instance, with later requests coalesced into one trailing pass, `ui/src/lib/trailing-resync.ts`). Pulses are leading and trailing, at most every 5 s, doubling to 60 s while failures persist. Never fail the shared subscription for one recipient's lookup: that resyncs every instance. A slow recipient must not block another or the shared SDK subscriber. Validate with the relay regressions and isolated native location fixture.
|
|
- Native OpenCode plugins share `packages/server/scripts/native-plugin-build.mjs`: never embed Effect or the public plugin SDK into daemon-loaded bundles (version boundaries: Qualification policy above). Keep backend-side OpenTunnel isolation separate, and run the bundle regression plus isolated plugin-runtime/pruning/automation/Missions fixtures when changing this boundary.
|
|
- The Design agent and skills exist once, as native OpenCode files in `packages/server/src/opencode/design/opencode/` (`agents/design.md`, `skills/design-*/SKILL.md`, `NOTICE.md`, `LICENSE-APACHE-2.0.txt`), which also work unchanged when copied into a project's `.opencode/` or the global OpenCode configuration folder; the public help links this folder for that use (`docs/help/<locale>/extensions.md#design-without-codenomad`), so moving it means updating those links. Each file keeps its licence notice in frontmatter comments and its references inside its own body: never add sibling files, CLI or form protocols, URLs or brand names (`design/content.test.ts` checks them, with the attribution of each adapted file). `design/content.ts` embeds the files as text through the build's `.md` loader and parses them strictly: bundles call `loadDesignContent()`; `tsx` cannot import `.md`, so checkout code and tests read the files and call `parseDesignContent()`. The agent prompt starts with `codenomad-design-content: <DESIGN_CONTENT_VERSION>`; raise the version whenever the skill, direction or recipe identifiers change. Validate content changes with that test, the self-contained bundle test in `native-plugin-build.test.mjs` and `scripts/test-design-content-native.mjs`. That native test enables project discovery, which walks every ancestor directory: outside CI it refuses to run under the user's home unless `CODENOMAD_NATIVE_FIXTURE_PARENT` names a parent outside it.
|
|
- Missions provisioning installs `provider.mjs` as a separate content-addressed local provider beside its content-addressed runtime. The managed shim supplies native absolute `{provider:{file,sha256},runtime:{file,sha256}}` paths as the factory's second argument; WSL translation applies only to backend filesystem I/O. Lease release and upgrades retain both payloads for native carrier recovery. Registration paths are acquired during arming in the current native RPC graph by `missions/native-schedule-installation.ts`, from the own-process record under native Global state and current ServerInfo URLs, then authenticated with exact-file public Service discovery.
|