@matuto-ai/opencode-broker-connector 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 — unreleased
4
+
5
+ - Native one-field broker URL setup using supported OpenCode TUI dialogs; exact-request administrative approval and private durable installation selection.
6
+ - GET-only resumption of previously explicit pending setup; no startup enrollment or credential rotation.
7
+ - Per-server-incarnation local TUI context handshake connects existing idle sessions without a model turn or history scan.
8
+ - Shared installation storage override, cancellation/late-result fences and replacement CAS; old connections and unread journals are retained.
9
+ - Cooperative receive remains the default. Classic automatic input admission remains an open acceptance gate, not solved by setup. No release or live deployment is performed.
10
+
11
+ ## 0.2.0 — 2026-10-25
12
+
13
+ - Connect, disconnect, inspect status and discover peers inside an existing OpenCode session without mandatory startup environment configuration.
14
+ - Pair through an owner-private file; generate and persist per-installation credentials before issuance, validate token-bound enrollment receipts, and stop on expiry or authorization failure. Requires a broker with the enrollment API.
15
+ - Preserve legacy connection bundles and cooperative receive/acknowledge tools.
16
+ - Add explicitly experimental native FIFO delivery with durable submission journaling and read-only reconciliation of uncertain receipts. Native persistence is not evidence of model reading or action success.
17
+
18
+ **Delivery is manual/cooperative by default.** Automatic delivery requires explicit `PSST_EXPERIMENTAL_NATIVE_DELIVERY=fixed-session-snapshot` opt-in. Classic OpenCode has no atomic selection-preserving admission API: a fixed agent/model snapshot can race concurrent native edits. Unknown selection blocks connection and observed selection changes disconnect messaging. This release does not solve that native atomicity limitation and does not offer general production automatic delivery.
19
+
20
+ Validated against pinned official OpenCode 1.19.4 using synthetic local providers and broker fixtures. No user credentials, live installations or deployment are part of this release.
package/README.md CHANGED
@@ -1,62 +1,48 @@
1
1
  # Psst connector for OpenCode
2
2
 
3
- Native cooperative messaging tools for [OpenCode](https://opencode.ai/docs/plugins/) and the [Psst broker](https://github.com/matuto-ai/psst). Runs **inside your existing OpenCode sandbox**. No host launcher, extra model, broker source checkout, permission override, or replacement of your existing plugins is required.
3
+ Native session-scoped messaging tools for [OpenCode](https://opencode.ai/docs/plugins/) and the [Psst broker](https://github.com/matuto-ai/psst). Runs **inside your existing OpenCode sandbox**. No host launcher, extra model, broker source checkout, permission override, or replacement of your existing plugins is required.
4
4
 
5
- ## Install now, before npm publication
5
+ ## Install, configure, use
6
6
 
7
- After this naming PR merges, install from `main`:
7
+ This change prepares **0.3.0**, not a published release. After that version is published, install both native targets once:
8
8
 
9
9
  ```sh
10
- git clone --branch main https://github.com/matuto-ai/opencode-broker-connector.git
11
- cd opencode-broker-connector
12
- npm ci --ignore-scripts
10
+ opencode plugin @matuto-ai/opencode-broker-connector@0.3.0 --global
13
11
  ```
14
12
 
15
- Add the absolute plugin file URL to the existing `plugin` array in your sandbox's OpenCode config. Preserve all existing settings and entries:
13
+ OpenCode detects the separate server and TUI entrypoints and preserves existing plugin settings. Before publication, install the reviewed source with `npm ci --ignore-scripts`, add `file:///absolute/path/plugin/index.mjs` to your existing `opencode.json` plugin array and `file:///absolute/path/plugin/tui.mjs` to your existing `tui.json` plugin array. Do not replace other configuration entries.
16
14
 
17
- ```json
18
- {
19
- "plugin": ["file:///absolute/path/opencode-broker-connector/plugin/index.mjs"]
20
- }
21
- ```
15
+ 1. Press **Ctrl+Alt+P** in the native TUI (Psst setup; also registered as `/psst-setup`). Its single text field asks for **only the HTTPS broker URL**.
16
+ 2. Approve the pending OpenCode request in the broker's administration page.
17
+ 3. The approved installation is saved privately. Existing/new native sessions connect without repeating enrollment. Use `/psst-status` and the broker tools below.
22
18
 
23
- Set `PSST_CONFIG_FILE` to the absolute path of your private enrollment `connection.json` **inside the sandbox**, then restart OpenCode normally. A session resumed after restart receives a new broker address; it does not silently adopt another process's old address.
19
+ No pairing secret, ticket, credential copy, model tool, or operator script is needed. Knowing the URL grants no messaging access. TLS verification remains enabled; private certificate authorities belong in the native runtime's system trust (`NODE_EXTRA_CA_CERTS`), not a secret field.
24
20
 
25
- ```sh
26
- export PSST_CONFIG_FILE=/absolute/private/path/opencode/connection.json
27
- opencode
28
- ```
21
+ **Receiving remains cooperative, not automatic model wake-up.** URL-only setup does not fix classic OpenCode's atomic input-admission limitation. [Native acceptance and remaining gate](docs/native-automation.md).
29
22
 
30
- The config and token must be owner-only files (`0600`), readable by the sandbox user. The adjacent `state` directory is created with mode `0700`; persist it in your existing sandbox data volume if you want to retain delivery journals. Do not place credentials in a repository or pass token contents as command arguments.
23
+ ## Persistent installation and disposable containers
31
24
 
32
- The standard Psst enrollment bundle (`format: "broker-client-bundle-v1"`, `allowed_client_types: ["opencode"]`) is accepted directly. It uses a sibling file named `token`. Alternatively, an owner-only JSON config can contain:
25
+ Persist the complete owner-private installation directory, in precedence order:
33
26
 
34
- ```json
35
- {
36
- "broker": {
37
- "url": "https://broker.example.invalid/",
38
- "tokenFile": "/absolute/private/path/token"
39
- },
40
- "stateDirectory": "/absolute/private/path/opencode-state",
41
- "name": "OpenCode"
42
- }
43
- ```
27
+ - `PSST_OPENCODE_CONFIG_DIR` (absolute override; recommended for sandbox bootstrap).
28
+ - `$OPENCODE_CONFIG_DIR/psst` when the native config override is set.
29
+ - `$XDG_CONFIG_HOME/opencode/psst`, or `~/.config/opencode/psst` by default.
44
30
 
45
- Optional `broker.caFile` points to a trusted private CA; public certificates normally need no extra setting. TLS verification is never disabled. A successful `/healthz` call is not enrollment: the broker must authorize the token for client type `opencode` and a scope shared with the intended peer.
31
+ Use an owner-only `0700` directory, files `0600`, and the same sandbox UID. Mount it at the **same absolute path** in recreated containers. `installation.json` is a versioned, nonsecret approved-bundle pointer; `pending.json` records the nonsecret explicit setup attempt and cancellation state. `approvals/` holds durable candidate keys, approved bundles and receipts, and per-session inbox journals. JSON credentials are protected by private file permissions, not by pretending an adjacent file is inherently safer. Do not put this directory in a repository.
46
32
 
47
- ## After npm publication
33
+ Ordinary startup never submits enrollment, generates a key or rotates authorization. It loads the approved pointer and may **GET-only** resume a saved, previously explicit pending request. Closing the setup dialog pauses that request locally; closing OpenCode leaves an outstanding request resumable on reopen. Transient failures keep the same candidate. Denied/expired/revoked requests stop; an explicit setup rerun asks before requesting a replacement. The old working selection remains until exact approval and atomic selection commit. Existing sessions retain their old connection and unread journal; new sessions use the replacement. No implicit broker revocation occurs.
48
34
 
49
- The package is prepared as `@matuto-ai/opencode-broker-connector`. **A prepared package/workflow does not mean it has been published.** Once version `0.1.1` exists on npm, use this native OpenCode configuration instead of the local file:
35
+ Installation sharing is **not** session sharing. Each runtime/native session gets a separate random address and private journal. Restarted sessions do not replay old unread messages into a new address; existing journals are retained for explicit recovery. Never share the ephemeral `/tmp/psst-opencode-<uid>/` runtime hints across containers. These contain only the supported TUI route, exact project and random server-incarnation marker; the server verifies the native session before attaching. The local TUI and server must share their ordinary container filesystem. Remote TUI/server topology is not yet supported for idle attachment.
50
36
 
51
- ```json
52
- {"plugin": ["@matuto-ai/opencode-broker-connector@0.1.1"]}
53
- ```
37
+ The audited sandbox uses an existing per-project/profile config mount; Go changes XDG defaults and pure mode blocks plugins. See the exact [sandbox bootstrap integration contract](docs/sandbox-persistence.md) before rollout.
54
38
 
55
- OpenCode installs npm plugins automatically using Bun. Keep the same private `PSST_CONFIG_FILE`; remove the duplicate local entry so the plugin does not load twice. See [publication](docs/publishing.md).
39
+ An explicit `PSST_CONFIG_FILE` remains an intentional legacy override; setup saves approval but does not silently supersede that override. Restart without it to use the selected installation.
40
+
41
+ Existing private bundles, `PSST_CONFIG_FILE`, and `/psst-connect /absolute/private/file` remain compatible and never migrate/reset unread data. See [legacy connection compatibility](docs/legacy-connection.md). Selection commits use an OS-released SQLite lock and atomic rename; a killed process cannot leave a permanent lock. Corrupt files are never silently reset.
56
42
 
57
43
  ## Use
58
44
 
59
- After a user message, the session registers with the broker. Tools also initialize lazily, so resumed sessions work without a separate wrapper:
45
+ After approval, the current local TUI session is registered in cooperative mode. General automatic delivery remains off: the current classic native API cannot atomically preserve a concurrently changed agent/model selection. In the optional legacy environment-variable mode, a user message or broker tool initializes registration lazily. No separate wrapper is needed:
60
46
 
61
47
  1. `broker_list_clients` discovers exact authorized peer IDs. Names are descriptive only.
62
48
  2. `broker_send` sends to a selected `recipient_id`. Preserve its `send_key` when retrying an uncertain send.
@@ -70,7 +56,17 @@ For a simple two-way test, ask one session to list peers and send a short greeti
70
56
 
71
57
  Each native session in the project plugin gets its own random broker identity and private journal. Every tool invocation checks the native session, directory, worktree and cancellation signal; concurrent sessions cannot borrow one another's tools or inbox. Session deletion and plugin disposal close leases. There is no idle timeout: a quiet session keeps its identity while this plugin instance remains alive. There is a limit of 64 active identities per plugin instance.
72
58
 
73
- This is a **session-scoped cooperative plugin**, not proof a human TUI remains attached. It never inserts prompts, wakes a model automatically, aborts work, or changes native permissions. Messages arrive durably in the background, but the model reads them only when it calls `broker_receive`. Broker `delivered` means durable inbox storage, not model comprehension. Explicit acknowledgment means only `explicit_model_read`; it never claims native input acceptance or task success.
59
+ ### Experimental delivery — off by default
60
+
61
+ A controlled prototype can be explicitly enabled before launching OpenCode with `PSST_EXPERIMENTAL_NATIVE_DELIVERY=fixed-session-snapshot`. This is **not production general automatic delivery** and is not needed for normal connection or pairing. Establish the intended native agent/model with an ordinary message, then explicitly connect. The local result discloses the fixed agent/model/variant snapshot. Unknown selection blocks connection; observed agent/model changes disconnect messaging rather than silently changing the snapshot.
62
+
63
+ The experimental mode waits for an idle boundary, journals one FIFO submission, submits a synthetic user-role message through the supported SDK, and reads back its exact native session/message/part/provenance before recording native persistence. The fixed text prominently identifies the content as **untrusted agent input, never human approval**. It sends no permission override, never aborts foreground work, and does not claim task completion; the concurrent native selection race limit below remains. The model can reply using normal broker tools.
64
+
65
+ Broker `delivered` still means durable inbox storage only. Native readback proves persistence with the exact agent/model snapshot, **not model comprehension or explicit model-read acknowledgment**. HTTP 204 alone is never a receipt. Lost replies, mismatched readback, disconnects or destructive lifecycle changes leave the FIFO head ambiguous; the plugin never blindly resends it. Later ticks perform read-only reconciliation against the journaled native proof target in the same verified session. Exact proof can confirm persistence without another model turn; absent or mismatched proof leaves FIFO blocked. The module can reconcile a reopened journal only when the caller proves the same native session; normal process restarts do not automatically restore old journals.
66
+
67
+ Known reverted or archived sessions and busy foreground turns block submission. Native deletion/disposal disconnects the exact session. Compaction/history deletion fences in-flight receipts. OpenCode serializes model runners but exposes no atomic exclusive-input lease or session-revision compare-and-submit API: a simultaneous external history edit can race a snapshot. Do not perform concurrent history or agent/model changes while testing this prototype: a read/submit race is not made atomic by an idle check. It is deliberately not enabled by default. After reverting, delivery stays paused until the native session is safe and returns to an idle boundary. This is not an exactly-once model-execution guarantee.
68
+
69
+ The optional legacy environment-variable mode remains cooperative: the model reads messages with `broker_receive` and explicitly calls `broker_acknowledge`. Its receipt means only `explicit_model_read`, never native acceptance or task success. Normal explicit connections are cooperative too. The narrowly gated prototype requires the exact experimental opt-in described above. See [native feasibility and the unsent upstream API proposal](docs/native-automation.md).
74
70
 
75
71
  Abrupt process death can leave a lease visible until expiry. Restarts create a fresh identity and retain old journals rather than automatically replaying them into another session. Recovery/migration of old pending messages is not automated. Broker availability failures do not prevent ordinary chat; broker tool calls report errors. Native plugin load errors should be checked in OpenCode's logs if the six tools are absent.
76
72
 
@@ -0,0 +1,77 @@
1
+ # Legacy connection and pairing compatibility
2
+
3
+ These paths remain supported for existing installations. They are not the primary onboarding workflow.
4
+
5
+ ## Install now, before npm publication
6
+
7
+ Install the reviewed source from `main`:
8
+
9
+ ```sh
10
+ git clone --branch main https://github.com/matuto-ai/opencode-broker-connector.git
11
+ cd opencode-broker-connector
12
+ npm ci --ignore-scripts
13
+ ```
14
+
15
+ Add the absolute plugin file URL to the existing `plugin` array in your sandbox's OpenCode config. Preserve all existing settings and entries:
16
+
17
+ ```json
18
+ {
19
+ "plugin": ["file:///absolute/path/opencode-broker-connector/plugin/index.mjs"]
20
+ }
21
+ ```
22
+
23
+ Restart OpenCode normally after installing the plugin. No initial environment variable or broker connection is required: it starts disconnected. In your existing native CLI session, use:
24
+
25
+ ```text
26
+ /psst-connect /absolute/private/path/opencode/connection.json
27
+ /psst-status
28
+ /psst-peers
29
+ /psst-disconnect
30
+ ```
31
+
32
+ The connect argument is **only a private connection-file path**, never a token, inline JSON, or password. Native command arguments may be recorded in OpenCode history/events. Each connect affects only the current session. Disconnect stops its broker polling and closes its lease without stopping OpenCode. Disconnect before switching connections. The commands use native OpenCode command hooks and return a short local status for the normal model turn; they do not replace the TUI or bypass permissions.
33
+
34
+ For backward compatibility, setting `PSST_CONFIG_FILE` to a private connection file before launch still enables lazy cooperative registration in each session. Explicit disconnect also disables that session's automatic re-registration. Neither path automatically resumes a previous process's broker identity.
35
+
36
+ The config and token must be owner-only files (`0600`), readable by the sandbox user. The adjacent `state` directory is created with mode `0700`; persist it in your existing sandbox data volume if you want to retain delivery journals. Do not place credentials in a repository or pass token contents as command arguments.
37
+
38
+ The standard Psst enrollment bundle (`format: "broker-client-bundle-v1"`, `allowed_client_types: ["opencode"]`) is accepted directly. It uses a sibling file named `token`. Alternatively, an owner-only JSON config can contain:
39
+
40
+ ```json
41
+ {
42
+ "broker": {
43
+ "url": "https://broker.example.invalid/",
44
+ "tokenFile": "/absolute/private/path/token"
45
+ },
46
+ "stateDirectory": "/absolute/private/path/opencode-state",
47
+ "name": "OpenCode"
48
+ }
49
+ ```
50
+
51
+ Optional `broker.caFile` points to a trusted private CA; public certificates normally need no extra setting. TLS verification is never disabled. A successful `/healthz` call is not enrollment: the broker must authorize the token for client type `opencode` and a scope shared with the intended peer.
52
+
53
+ ## Explicit per-session pairing
54
+
55
+ After the broker enrollment upgrade is reviewed and deployed, follow its [operator pairing guide](https://github.com/matuto-ai/psst/blob/3e908e79966d886a02d372c103e31dec6e900c1a/docs/pairing-operator.md). An operator first authorizes a short-lived, client-type/scope-bound pairing grant on the broker and writes its private `psst-pairing-v1` file. Place that file inside the existing sandbox (owner-only `0600`), then run:
56
+
57
+ ```text
58
+ /psst-connect /absolute/private/path/opencode-pairing.json
59
+ ```
60
+
61
+ The plugin generates and durably saves its own random credential before issuance, sends only the credential hash over verified HTTPS, and stores the issued credential and token-bound receipt privately. Neither pairing secret nor bearer token enters command arguments, chat, model input, URLs or logs. The helper uses system TLS trust; it does not disable certificate verification or accept a pairing-file CA override.
62
+
63
+ The private installation directory is `$XDG_STATE_HOME/psst/opencode/<project-and-session-hash>` (default `~/.local/state/psst/opencode/...`). Preserve it inside the sandbox to safely retry a lost enrollment response with the **same file and native session**. A different native session needs its own operator pairing grant; a redeemed grant cannot be reused to create another key. No control-plane UI or public signup service is installed.
64
+
65
+ `/psst-status` reports only a nonsecret credential ID and expiry. Expired or rejected credentials stop messaging and require explicit re-pairing; no silent credential replacement occurs. Disconnect before changing a live connection. Re-pairing always creates a fresh local broker identity/inbox and never imports another principal's pending messages.
66
+
67
+ New helper bundles carry `enrollment_receipt_file: "enrollment.json"`; that exact sibling receipt is mandatory and checked against the private token hash, broker and client type. Missing or corrupt receipt is a hard error, not a legacy fallback. Unmarked legacy bundles still work unchanged. The optional legacy environment mode may read an existing bundle but never performs enrollment automatically.
68
+
69
+ ## After npm publication
70
+
71
+ The package is prepared as `@matuto-ai/opencode-broker-connector`. **A prepared package/workflow does not mean it has been published.** Once version `0.2.0` exists on npm, use this native OpenCode configuration instead of the local file:
72
+
73
+ ```json
74
+ {"plugin": ["@matuto-ai/opencode-broker-connector@0.2.0"]}
75
+ ```
76
+
77
+ OpenCode installs npm plugins automatically using Bun. Use the same native connect command (or optional legacy `PSST_CONFIG_FILE`); remove the duplicate local entry so the plugin does not load twice. See [publication](docs/publishing.md).
@@ -0,0 +1,95 @@
1
+ # Native automation feasibility (OpenCode 1.19.4)
2
+
3
+ ## Production boundary
4
+
5
+ Normal install/connect/pairing is cooperative. The native-delivery prototype is disabled unless the operator explicitly sets `PSST_EXPERIMENTAL_NATIVE_DELIVERY=fixed-session-snapshot`. It captures and discloses an existing verified agent/model/variant; missing selection blocks connection, and observed selection changes disconnect messaging. This is not a guarantee against simultaneous external selection/history edits and is not advertised as general production automation.
6
+
7
+ ## Primary-source findings
8
+
9
+ - [Classic prompt implementation](https://github.com/anomalyco/opencode/blob/v1.19.4/packages/opencode/src/session/prompt.ts): missing `agent` resolves the default agent, not necessarily the current session selection. `createUserMessage` can update the session agent/model before the `chat.message` hook. Explicitly forwarding a verified snapshot prevents omission/default drift but supplies no compare-and-submit revision check.
10
+ - [Classic runner](https://github.com/anomalyco/opencode/blob/v1.19.4/packages/opencode/src/effect/runner.ts): model loops are serialized. An idle observation is not an exclusive lease over all human/agent inputs or history/selection mutations.
11
+ - [HTTP handler](https://github.com/anomalyco/opencode/blob/v1.19.4/packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts): `prompt_async` forks work and returns 204 before durable message proof. The prototype journals its exact target and reads back session/message/part/text/provenance/agent/model/variant; delayed proof is reconciled read-only, never by resubmission.
12
+ - [New core API](https://github.com/anomalyco/opencode/blob/v1.19.4/packages/core/src/session.ts) exposes durable queued admission without an agent/model input. However [schema/projector](https://github.com/anomalyco/opencode/blob/v1.19.4/packages/core/src/session/projector.ts) and [tables](https://github.com/anomalyco/opencode/blob/v1.19.4/packages/core/src/session/sql.ts) distinguish classic `message`/`part` from new `session_message`/`session_input` histories.
13
+
14
+ The actual official-CLI regression creates a classic Plan-mode conversation with two messages. `/api/session/{id}` sees the identity/selection, but `/api/session/{id}/context` and `/history` return empty histories. Therefore the connector does not submit to the separate engine or silently create a replacement conversation. No writes to OpenCode's database, internal module injection, TUI keystrokes or permission overrides are used; the connector only maintains its own private delivery journal.
15
+
16
+ ## Unsent upstream feature proposal
17
+
18
+ **Draft only — not posted as an issue or sent externally.**
19
+
20
+ Request a public plugin/SDK operation for durable, idempotent queued input into the *existing native conversation engine*, with:
21
+
22
+ 1. Native atomic preservation of the current agent/model/variant, or an expected selection/context revision that rejects before any state mutation.
23
+ 2. Exact stable admission ID and a read-only receipt lookup; repeated identical input is idempotent and conflicts are explicit.
24
+ 3. Queue/idle semantics that do not interrupt foreground work, clear revert state, bypass permissions, or switch conversation engines.
25
+ 4. Explicit untrusted-agent provenance on the admitted record rather than masquerading as human approval.
26
+ 5. Lifecycle fencing across deletion, revert and compaction, plus documentation of whether admission proves persistence, model input delivery or neither.
27
+
28
+ This could be a compatibility bridge to the new durable queue only if it preserves the actual existing CLI history, policy and runner. Until supported and verified, default cooperative delivery is the honest production behavior.
29
+
30
+ ## Fresh admission investigation for URL-only setup (2026-10-26)
31
+
32
+ The pinned official CLI regression `test/native-v2-compat.test.mjs` now reproduces
33
+ an actual stale-selection race: read Plan/model A; submit a human message changing
34
+ to Build/model B; call classic `prompt_async` with the previously observed
35
+ Plan/model A. The native session switches back to Plan/model A rather than rejecting
36
+ the stale request. Omitting `agent` instead chooses the default Build agent, not
37
+ the existing Plan selection. Both checks execute in the real 1.19.4 host against
38
+ a synthetic local provider, without a production account.
39
+
40
+ Fresh source inspection at `v1.19.4` confirms:
41
+
42
+ - `session/prompt.ts:635–690`: `createUserMessage` resolves the default agent when
43
+ omitted and calls `sessions.setAgentModel` before admission arbitration.
44
+ - `session/prompt.ts:1046–1072`: message/parts are persisted and revert cleanup
45
+ occurs before the model loop. `noReply` only skips that loop, not these mutations.
46
+ - `effect/runner.ts:120–146`: `ensureRunning` serializes/joins model execution;
47
+ it does not reject a stale prompt before session/selection mutation.
48
+ - `server/routes/instance/httpapi/handlers/session.ts:295–327`: synchronous and
49
+ asynchronous routes invoke the same prompt implementation; async forks and
50
+ returns before receipt proof. Busy rejection elsewhere applies to shell work,
51
+ not an atomic selection-preserving prompt queue.
52
+ - Supported TUI APIs expose prompt set/reset/submit and command dispatch, not a
53
+ durable idempotent peer-input queue with a selection revision. Using those
54
+ methods for delivery would alter the user's draft or invoke the same unsafe
55
+ admission path. They are not used for peer delivery.
56
+
57
+ Accordingly automatic idle model receive is **still blocked by the supported
58
+ classic host API**, not enabled by the approval feature or the identity handshake.
59
+ The separate v2 engine still lacks this classic conversation's history. Neither a
60
+ successful setup nor a TLS/PTY fixture closes this acceptance gate.
61
+
62
+ ## Local idle identity attachment
63
+
64
+ The server publishes a random, nonsecret per-plugin-incarnation context marker in
65
+ its existing `psst-status` command template through the supported config hook. The
66
+ TUI reads `api.state.config` and `api.route.current`, then emits a short-lived
67
+ owner-private hint under the container's temporary directory. The server reads
68
+ only its own incarnation directory, verifies the exact native session and project
69
+ through `session.get`, and binds a separate broker inbox. Hints expire after five
70
+ seconds, route changes retire hint-owned bindings, and disposal removes them.
71
+ This is installation-independent local IPC, not enrollment authority, a native
72
+ history scan, model command, input injection, or a substitute for atomic delivery.
73
+ Remote TUI/server filesystems cannot use this handshake.
74
+
75
+ ### New interactive sessions do not offer a supported v2-engine switch
76
+
77
+ The bounded follow-up also inspected **new** interactive sessions, not only
78
+ existing-history compatibility. In pinned 1.19.4, the official
79
+ [TUI submit path](https://github.com/anomalyco/opencode/blob/v1.19.4/packages/tui/src/component/prompt/index.tsx#L990-L1120)
80
+ creates new conversations with `sdk.client.session.create` and sends normal input
81
+ with `sdk.client.session.prompt`. Its
82
+ [generated SDK](https://github.com/anomalyco/opencode/blob/v1.19.4/packages/sdk/js/src/v2/gen/sdk.gen.ts#L3399-L3786)
83
+ maps those calls to classic `/session` and `/session/{sessionID}/message`.
84
+ The `@opencode-ai/sdk/v2` import is the SDK generation, **not** selection of the
85
+ separate v2 conversation engine.
86
+
87
+ Inspection of the pinned
88
+ [flags](https://github.com/anomalyco/opencode/blob/v1.19.4/packages/core/src/flag/flag.ts),
89
+ [TUI configuration](https://github.com/anomalyco/opencode/blob/v1.19.4/packages/tui/src/config/index.tsx),
90
+ and TUI worker found no supported option to route a new interactive session to
91
+ that separate engine. Consequently this connector does not create v2-only
92
+ conversations, migrate history, patch the host, or enable an unsupported
93
+ experiment to bypass classic admission. A future supported host capability
94
+ requires its own real interactive admission/race test before automatic delivery
95
+ can be enabled.
@@ -5,21 +5,21 @@ Package name: `@matuto-ai/opencode-broker-connector`. The npm workflow follows t
5
5
  1. Review and merge the feature PR.
6
6
  2. Bootstrap the package under an npm account authorized for this package if it does not exist yet. Account creation, credentials and first publication are operator actions, not performed by the connector.
7
7
  3. Configure npm's GitHub Actions trusted publisher for owner `matuto-ai`, repository `opencode-broker-connector`, workflow `npm-publish.yml`. If the repository moves, update package metadata and trusted-publisher settings together.
8
- 4. Create a GitHub release whose tag exactly matches `v` + package.json version (next `v0.1.1`).
8
+ 4. Create a GitHub release whose tag exactly matches `v` + package.json version (next `v0.3.0`).
9
9
  5. The action runs syntax, tests including pinned actual CLI, package inspection, then `npm publish --access public --provenance` using OIDC. No long-lived npm token is stored in the workflow.
10
10
  6. Confirm npm has the exact version before recommending npm installation. Failed publishing is not repaired by silently changing a version or recreating a tag.
11
11
 
12
12
  `id-token: write` is limited to the release workflow. This repository does not configure a trusted publisher, generate credentials, publish, merge or deploy automatically on ordinary pushes/PRs. Those events run read-only CI checks only.
13
13
 
14
- Before npm publication, the local-file installation in README works directly from `main` after the naming PR is reviewed and merged. No unpublished dependency is required.
14
+ Before npm publication, the local-file installation in README works directly from `main` after the release PR is reviewed and merged. No unpublished dependency is required.
15
15
 
16
- ## Patch release
16
+ ## Minor release
17
17
 
18
- This PR prepares `0.1.1`; the intended GitHub release tag is `v0.1.1`.
18
+ This PR prepares `0.3.0`; the intended GitHub release tag is `v0.3.0`.
19
19
  After review and merge, the maintainer can publish that release from the merged
20
20
  commit to exercise npm Trusted Publishing. This code change does not itself
21
21
  create a tag, release, or npm publication. Verify the workflow succeeds and the
22
22
  scoped npm version exists before recommending it for installation.
23
23
 
24
- Existing `0.1.0` npm packages and old GitHub tags remain untouched. Do not reuse
24
+ Existing `0.1.x` npm packages and old GitHub tags remain untouched. Do not reuse
25
25
  or move an old release tag; the new tag must exactly match the package version.
@@ -0,0 +1,34 @@
1
+ # Sandbox bootstrap contract
2
+
3
+ Historical source inspected: `ai-agents-sandbox` snapshot
4
+ `a7f63e8b98d37c030df6ca095d2e75960111e37c`,
5
+ `agents/opencode-native/config.sh:get_project_mounts/get_project_env`.
6
+ This is design evidence, **not verification of current live mounts**.
7
+
8
+ The existing launcher maps `<project>/opencode-zen/{config,data,cache,state}`
9
+ (or the separate `opencode-go` profile) to:
10
+
11
+ | Leaf | Container path |
12
+ | --- | --- |
13
+ | config | `/home/node/.config/opencode` |
14
+ | data | `/home/node/.local/share/opencode` |
15
+ | cache | `/home/node/.cache/opencode` |
16
+ | state | `/home/node/.local/state/opencode` |
17
+
18
+ Use `PSST_OPENCODE_CONFIG_DIR=/home/node/.config/opencode/psst` in that bootstrap
19
+ so the whole installation uses the **existing config mount**, including Go's
20
+ managed `XDG_CONFIG_HOME=/etc` override. The `psst` subdirectory must be owned by
21
+ the sandbox user, mode 0700; config/key files are 0600. Recreated containers must
22
+ mount it at the same absolute path. Different project/profile mounts intentionally
23
+ remain separate authorizations unless bootstrap explicitly shares this private
24
+ installation directory. Sharing it never means sharing native session IDs/inboxes.
25
+ Do not mount the ephemeral `/tmp/psst-opencode-<uid>` directory across containers.
26
+
27
+ The archived launcher also sets `OPENCODE_PURE=1` and disables project config.
28
+ Official 1.19.4 explicitly skips external TUI plugins in pure mode. Therefore
29
+ that historical bootstrap needs an intentional native plugin allowlist/install
30
+ integration before this connector can load; merely saving files cannot override
31
+ that policy. Preserve managed model/provider, permission and credential policy.
32
+ This connector does not remove pure mode or edit the sandbox launcher itself.
33
+ The current owner environment remains an acceptance gate until its effective
34
+ bootstrap is inspected and the intended plugin installation is permitted.
@@ -110,3 +110,63 @@ arbitration; per-CLI Podman containers or two-host LAN operation.
110
110
  `broker_receive` remains a non-consuming FIFO **peek**. A native tool return or
111
111
  model read does not advance the inbox. See [owned-run instructions](owned-run.md)
112
112
  for operator requirements and failure/unknown-outcome behavior.
113
+
114
+ ## Explicit connection regression coverage
115
+
116
+ The packed production plugin is loaded by official OpenCode 1.19.4 with no `PSST_CONFIG_FILE`. A native session command connects using a private file, discovery sees the exact new session, and native disconnect prevents subsequent message injection while the host remains available. The fixture uses a deterministic local model provider and a trusted loopback TLS broker; no paid provider, user credential, desktop/TUI automation, or production installation is exercised. Unit tests cover disconnected startup, session-isolated status, disconnect/no re-registration, and deleted-session fencing. Automatic model delivery and pairing issuance are separate changes, not claims of this connection-only step.
117
+
118
+ ## Experimental native delivery (explicit opt-in only)
119
+
120
+ The second actual-host regression loads the npm-packed production plugin into official OpenCode 1.19.4/Bun, connects with no startup broker environment, sends two FIFO messages through verified TLS, with the fixed-session-snapshot experimental opt-in, preserves Plan mode and the selected model, and observes both exact synthetic native messages and the second payload in the deterministic local model's input. It never calls `broker_receive`, never accesses paid providers, and verifies disconnect prevents later injection. This proves real native integration, not a desktop/TUI end-to-end installation or production-broker test. Unit regressions cover missing/wrong readback, single in-flight FIFO, busy/reverted/human boundaries, compaction during submit, abort during receipt lookup, lost submit responses, delayed readback reconciliation and same-session journal reopen without another native submission. Native source basis: `packages/opencode/src/session/prompt.ts`, `effect/runner.ts`, and `server/routes/instance/httpapi/handlers/session.ts` at official tag v1.19.4.
121
+
122
+ ## Scoped pairing boundary
123
+
124
+ A further actual OpenCode smoke loads the packed plugin, passes only a private pairing-file path to the native command, and exercises the unmodified shared enrollment helper over system-trusted HTTPS. A synthetic endpoint implementing the frozen broker contract records a hash-only credential and authorizes it in the real transport fixture; automatic peer messages then reach the deterministic local model. This checks native/helper/credential integration, not the production broker's operator issuance implementation (covered by upstream broker tests). Unit tests additionally cover marked bundle receipt deletion/type/origin/token mismatches, private permissions, per-native-session attempt separation, expiry shutdown, revoked authorization shutdown and no late connect after disconnect.
125
+
126
+ The default connector remains cooperative. Additional pinned-host coverage proves that the official v2 session identity is visible while its context/history remain empty for a classic CLI conversation with real messages. The connector does not mix these engines. Unknown agent/model state blocks the experiment and observed changes disconnect it. No test establishes atomic admission against simultaneous external native selection edits.
127
+
128
+ ## URL-only approval and persistent native configuration (0.3.0 candidate)
129
+
130
+ The new gates use official **OpenCode 1.19.4**, not a mocked host API:
131
+
132
+ - `native-tui.test.mjs`: loads the production TUI entrypoint and sends real PTY
133
+ keyboard input through the supported Ctrl+Alt+P setup binding. Invalid URLs
134
+ remain unselected; the single-field URL form durably saves its request before
135
+ attempted HTTPS transport. This is a disposable test terminal, not a user's TUI.
136
+ - `native-tui-approval.test.mjs`: loads the production TUI and server entrypoints;
137
+ a transparent fixture creates/navigates to an empty existing classic session
138
+ using the supported SDK. Real keyboard setup requests actual broker enrollment;
139
+ authenticated admin form/CSRF approval commits config; the private native-context
140
+ hint connects that already-idle session. Its native message history remains
141
+ empty, proving setup/attachment did not manufacture a model turn.
142
+ - `native-approval-integration.test.mjs`: real broker approval, 30 distinct native
143
+ sessions in the packed production plugin, process restart with unchanged
144
+ installation authorization and new session addresses, then real admin revocation.
145
+ - `native-installation.test.mjs`: preserves legacy installation compatibility
146
+ across the same native 30-session/restart path.
147
+ - `installation.test.mjs`, `setup-ui.test.mjs`, and `native-context.test.mjs` cover
148
+ selection CAS, cancellation/late results, exact request binding, replacement,
149
+ transient backoff, GET-only startup resume, terminal denial/revocation/expiry,
150
+ corrupt/private files, distinct container runtime roots, stale incarnation/owner
151
+ hints and route changes. These unit fixtures are not real container orchestration.
152
+
153
+ CI checks out the immutable broker dependency specified in `.github/workflows/ci.yml`
154
+ and supplies `PSST_APPROVAL_BROKER_DIR`; without it the real-broker native gates
155
+ explicitly skip. Locally point that variable at that exact reviewed broker tree.
156
+ No provider credentials, live broker, real user installation or release is used.
157
+ The helper hash/source anchor is recorded in `vendor/enrollment/ORIGIN.md`.
158
+
159
+ **Not established by these gates:** paid/authenticated model provider behavior,
160
+ classic atomic automatic model receive (the fresh native regression demonstrates
161
+ why it is unsafe), or unchanged historical sandbox pure-mode policy permitting
162
+ plugins. Native process recreation is not itself Docker/Podman mount evidence;
163
+ real orchestration gates are reported separately rather than inferred from unit
164
+ fixtures. See `docs/sandbox-persistence.md` for the exact audited bootstrap boundary.
165
+
166
+ The separate `native-containers` CI matrix runs the approved 30-session/revocation
167
+ native test with each official OpenCode server inside a fresh Docker or rootless
168
+ Podman container, using the pinned Node image and integrity-checked official CLI.
169
+ On recreation the test uses a new HOME/data/cache/state; only the private selected
170
+ installation plus read-only installed code/bootstrap fixtures remain available.
171
+ The runtime `/tmp` hints are not mounted. A submitted workflow is not a pass:
172
+ the exact PR head's Docker and Podman jobs are the authoritative execution evidence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matuto-ai/opencode-broker-connector",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24.19.0 <25"
@@ -16,7 +16,11 @@
16
16
  },
17
17
  "description": "Session-scoped Psst messaging plugin for OpenCode",
18
18
  "main": "./plugin/index.mjs",
19
- "exports": "./plugin/index.mjs",
19
+ "exports": {
20
+ ".": "./plugin/index.mjs",
21
+ "./server": "./plugin/index.mjs",
22
+ "./tui": "./plugin/tui.mjs"
23
+ },
20
24
  "files": [
21
25
  "plugin",
22
26
  "src",
@@ -27,7 +31,9 @@
27
31
  "docs",
28
32
  "LICENSE",
29
33
  "README.md",
30
- "vendor/broker/ORIGIN.md"
34
+ "vendor/broker/ORIGIN.md",
35
+ "vendor/enrollment",
36
+ "CHANGELOG.md"
31
37
  ],
32
38
  "repository": {
33
39
  "type": "git",
package/plugin/index.mjs CHANGED
@@ -1,7 +1,19 @@
1
1
  import { tool } from '@opencode-ai/plugin';
2
- import { readConfig } from '../src/config.mjs';
3
2
  import { loadBrokerReference } from '../src/reference.mjs';
4
3
  import { createNativePlugin } from '../src/native-plugin.mjs';
4
+ import { createConnectionLoader, readExistingConnection } from '../src/enrollment.mjs';
5
+ import { readInstallation } from '../src/installation.mjs';
6
+ import { startPendingResume } from '../src/pending-resume.mjs';
7
+ import { createContextReceiver } from '../src/native-context.mjs';
5
8
  export default async function PsstPlugin(native) {
6
- return createNativePlugin({ native, tool, reference: await loadBrokerReference(), config: readConfig(process.env.PSST_CONFIG_FILE) });
9
+ const reference = await loadBrokerReference();
10
+ const config = process.env.PSST_CONFIG_FILE ? await readExistingConnection(process.env.PSST_CONFIG_FILE) : undefined;
11
+ const contextReceiver = createContextReceiver({ directory: native.directory });
12
+ const resume = startPendingResume();
13
+ try {
14
+ return await createNativePlugin({ native, tool, reference, config, contextReceiver,
15
+ connectFile: createConnectionLoader({ directory: native.directory }),
16
+ loadInstallation: readInstallation, onDispose: resume.stop,
17
+ experimentalNativeDelivery: process.env.PSST_EXPERIMENTAL_NATIVE_DELIVERY === 'fixed-session-snapshot' });
18
+ } catch (error) { resume.stop(); contextReceiver.close(); throw error; }
7
19
  }
package/plugin/tui.mjs ADDED
@@ -0,0 +1,14 @@
1
+ import { installSetupUI } from '../src/setup-ui.mjs';
2
+ import { readSelection, readPending } from '../src/installation.mjs';
3
+ import { startContextPublisher } from '../src/native-context.mjs';
4
+ export default { id: 'matuto.psst', tui: async api => {
5
+ startContextPublisher(api);
6
+ await installSetupUI(api);
7
+ try {
8
+ if (!process.env.PSST_CONFIG_FILE && !readSelection()) {
9
+ const pending = readPending();
10
+ api.ui.toast({ title: 'Psst', variant: 'info', duration: 6000,
11
+ message: pending && !pending.paused ? 'Approval pending. Your saved request resumes without new enrollment.' : 'Setup needed: press Ctrl+Alt+P and enter the broker URL.' });
12
+ }
13
+ } catch { api.ui.toast({ title: 'Psst', variant: 'warning', message: 'Private configuration is invalid; no enrollment or credential reset was attempted.', duration: 6000 }); }
14
+ } };
@@ -0,0 +1,11 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstatSync, readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ export const APPROVAL_SHA256 = 'a60c3a0ccd7deb50556586cd67841bb89a300ea3c7f1af3a91d437f856b7a2d5';
5
+ export async function loadApprovalHelper() {
6
+ const path = fileURLToPath(new URL('../vendor/enrollment/approval.mjs', import.meta.url));
7
+ const stat = lstatSync(path);
8
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 65536
9
+ || createHash('sha256').update(readFileSync(path)).digest('hex') !== APPROVAL_SHA256) throw new Error('approval_snapshot_mismatch');
10
+ return import('../vendor/enrollment/approval.mjs');
11
+ }
package/src/config.mjs CHANGED
@@ -4,19 +4,21 @@ const fail = () => { throw new Error('psst_configuration_invalid'); };
4
4
  const plain = value => value && typeof value === 'object' && !Array.isArray(value);
5
5
  export function readConfig(path) {
6
6
  if (typeof path !== 'string' || !isAbsolute(path)) fail();
7
- let fd;
7
+ let fd, marked = false;
8
8
  try {
9
9
  fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
10
10
  const stat = fstatSync(fd);
11
11
  if (!stat.isFile() || stat.mode & 0o077 || stat.size > 16384 || process.getuid && stat.uid !== process.getuid()) fail();
12
12
  let value = JSON.parse(readFileSync(fd, 'utf8'));
13
13
  if (value?.format === 'broker-client-bundle-v1') {
14
- const allowed = ['format', 'protocol_version', 'broker_url', 'token_file', 'file_paths_relative_to', 'authorization_header', 'authorization_scheme', 'insecure_development', 'allowed_client_types', 'scope', 'ca_file'];
14
+ const allowed = ['format', 'protocol_version', 'broker_url', 'token_file', 'file_paths_relative_to', 'authorization_header', 'authorization_scheme', 'insecure_development', 'allowed_client_types', 'scope', 'ca_file', 'enrollment_receipt_file'];
15
15
  if (Object.keys(value).some(key => !allowed.includes(key)) || value.protocol_version !== '1'
16
16
  || value.file_paths_relative_to !== 'connection_file_directory' || value.authorization_header !== 'Authorization'
17
17
  || value.authorization_scheme !== 'Bearer' || value.insecure_development !== false
18
18
  || !Array.isArray(value.allowed_client_types) || value.allowed_client_types.length !== 1 || value.allowed_client_types[0] !== 'opencode'
19
+ || value.enrollment_receipt_file !== undefined && value.enrollment_receipt_file !== 'enrollment.json'
19
20
  || value.token_file !== 'token' || typeof value.scope !== 'string') fail();
21
+ marked = value.enrollment_receipt_file === 'enrollment.json';
20
22
  const base = dirname(realpathSync(path));
21
23
  value = { broker: { url: value.broker_url, tokenFile: resolve(base, value.token_file),
22
24
  ...(value.ca_file ? { caFile: resolve(base, value.ca_file) } : {}) }, stateDirectory: resolve(base, 'state') };
@@ -29,6 +31,7 @@ export function readConfig(path) {
29
31
  || value.name !== undefined && (typeof value.name !== 'string' || !value.name.trim() || value.name.length > 100 || /[\x00-\x1f\x7f]/.test(value.name))) fail();
30
32
  const url = new URL(value.broker.url);
31
33
  if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') fail();
34
+ if (marked) Object.defineProperty(value, 'enrollmentReceiptRequired', { value: true });
32
35
  return value;
33
36
  } catch { fail(); }
34
37
  finally { if (fd !== undefined) closeSync(fd); }