@matuto-ai/opencode-broker-connector 0.1.1 → 0.4.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +61 -43
  3. package/bin/doctor.mjs +5 -0
  4. package/docs/candidate-verification.md +25 -0
  5. package/docs/integration.md +8 -6
  6. package/docs/legacy-connection.md +77 -0
  7. package/docs/message-lifecycle.md +24 -0
  8. package/docs/native-automation.md +155 -0
  9. package/docs/native-v2.md +121 -0
  10. package/docs/owned-run.md +4 -2
  11. package/docs/publishing.md +5 -5
  12. package/docs/reference-snapshot.sha256 +5 -5
  13. package/docs/sandbox-persistence.md +43 -0
  14. package/docs/verification.md +62 -0
  15. package/package.json +15 -4
  16. package/plugin/index.mjs +14 -2
  17. package/plugin/tui.mjs +17 -0
  18. package/plugin/v2.mjs +18 -0
  19. package/src/approval-helper.mjs +11 -0
  20. package/src/capabilities.mjs +13 -0
  21. package/src/config.mjs +5 -2
  22. package/src/cooperative-inbox.mjs +13 -8
  23. package/src/enrollment.mjs +52 -0
  24. package/src/installation.mjs +123 -0
  25. package/src/message-format.mjs +26 -0
  26. package/src/native-context.mjs +57 -0
  27. package/src/native-delivery.mjs +151 -0
  28. package/src/native-plugin.mjs +135 -13
  29. package/src/opencode-adapter.mjs +31 -15
  30. package/src/pending-resume.mjs +38 -0
  31. package/src/reference-pin.mjs +6 -6
  32. package/src/reference.mjs +1 -1
  33. package/src/setup-ui.mjs +100 -0
  34. package/src/tool-catalog.mjs +7 -2
  35. package/src/tool-scope.mjs +26 -0
  36. package/src/v2-delivery.mjs +149 -0
  37. package/src/v2-inbox.mjs +33 -0
  38. package/src/v2-lock.mjs +17 -0
  39. package/src/v2-plugin.mjs +192 -0
  40. package/src/v2-setup.mjs +133 -0
  41. package/v2/LICENSE +21 -0
  42. package/v2/README.md +17 -0
  43. package/v2/index.mjs +2 -0
  44. package/v2/package-lock.json +420 -0
  45. package/v2/package.json +13 -0
  46. package/vendor/broker/ORIGIN.md +11 -4
  47. package/vendor/broker/reference/client.mjs +90 -15
  48. package/vendor/broker/reference/inbox.mjs +55 -10
  49. package/vendor/broker/reference/mcp-stdio.mjs +5 -1
  50. package/vendor/broker/reference/tools.mjs +38 -14
  51. package/vendor/broker/src/protocol.mjs +19 -0
  52. package/vendor/enrollment/LICENSE +21 -0
  53. package/vendor/enrollment/ORIGIN.md +20 -0
  54. package/vendor/enrollment/approval.mjs +181 -0
  55. package/vendor/enrollment/enrollment.mjs +137 -0
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,78 +1,92 @@
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
+ > **Unreleased 0.4.0 candidate.** The new commands/behavior below are not in the existing npm release. Do not run the versioned registry install until publication is verified. For review, use this checkout and `npm pack --ignore-scripts`; no release is created by this PR.
4
4
 
5
- ## Install now, before npm publication
5
+ 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.
6
6
 
7
- After this naming PR merges, install from `main`:
7
+ ## Install, configure, use
8
+
9
+ This change prepares **0.4.0**, not a published release. After that version is published, install both native targets once:
8
10
 
9
11
  ```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
12
+ opencode plugin @matuto-ai/opencode-broker-connector@0.4.0 --global
13
13
  ```
14
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:
15
+ 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
16
 
17
- ```json
18
- {
19
- "plugin": ["file:///absolute/path/opencode-broker-connector/plugin/index.mjs"]
20
- }
21
- ```
17
+ 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**.
18
+ 2. Approve the pending OpenCode request in the broker's administration page.
19
+ 3. The approved installation is saved privately. Existing/new native sessions connect without repeating enrollment. Use `/psst-status` and the broker tools below.
22
20
 
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.
21
+ 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
22
 
25
- ```sh
26
- export PSST_CONFIG_FILE=/absolute/private/path/opencode/connection.json
27
- opencode
28
- ```
23
+ **Production requires automatic receive. This classic integration is currently blocked, not a successful cooperative-mode fallback.** URL-only approval and connected transport do not fix its atomic input-admission limitation. Manual receive exists only for explicit diagnostics/tests. [Native acceptance and remaining gate](docs/native-automation.md).
29
24
 
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.
25
+ ## Diagnose readiness without changing policy
31
26
 
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:
27
+ `psst-opencode` (the packaged doctor) is read-only and works independently of
28
+ native plugin loading. From a reviewed source checkout use `node bin/doctor.mjs`.
29
+ It reports an observed `OPENCODE_PURE` restriction separately from the classic
30
+ automatic-admission gap. Exit 2 is an actionable blocked state, not a ready client.
31
+ If the native host suppresses plugins entirely, no plugin can display its own
32
+ status: use this package check and ask the administrator to review the approved
33
+ launcher source. The connector never unsets pure mode or edits native policy.
33
34
 
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
- ```
35
+ When loaded, `/psst-status` separately reports `connected` transport, this exact
36
+ session's `client_id`, `ready`, and the automatic-delivery blocker. Installation
37
+ authorization/credential IDs are not message destinations. Neither approval,
38
+ broker presence nor a successful manual exchange establishes automatic readiness.
44
39
 
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.
40
+ ## Persistent installation and disposable containers
46
41
 
47
- ## After npm publication
42
+ Persist the complete owner-private installation directory, in precedence order:
48
43
 
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:
44
+ - `PSST_OPENCODE_CONFIG_DIR` (absolute override; recommended for sandbox bootstrap).
45
+ - `$OPENCODE_CONFIG_DIR/psst` when the native config override is set.
46
+ - `$XDG_CONFIG_HOME/opencode/psst`, or `~/.config/opencode/psst` by default.
50
47
 
51
- ```json
52
- {"plugin": ["@matuto-ai/opencode-broker-connector@0.1.1"]}
53
- ```
48
+ 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.
49
+
50
+ 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.
51
+
52
+ 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.
54
53
 
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).
54
+ 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.
55
+
56
+ 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.
57
+
58
+ 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
59
 
57
60
  ## Use
58
61
 
59
- After a user message, the session registers with the broker. Tools also initialize lazily, so resumed sessions work without a separate wrapper:
62
+ After approval, transport may register the current local TUI session, but `/psst-status` reports `mode: automatic_required`, `ready: false` and `delivery: automatic_blocked`: 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. Do not use a snapshot override as a production workaround. The following manual tools are **diagnostic/test-only** until safe automatic admission is implemented:
60
63
 
61
64
  1. `broker_list_clients` discovers exact authorized peer IDs. Names are descriptive only.
62
- 2. `broker_send` sends to a selected `recipient_id`. Preserve its `send_key` when retrying an uncertain send.
63
- 3. `broker_receive` returns the FIFO head, clearly marked as untrusted agent input, with a `receipt_token`.
64
- 4. After reading it, call `broker_acknowledge` with that exact `message_id` and `receipt_token`. This advances the local inbox, enabling repeated conversations.
65
+ 2. `broker_send` sends to a selected `recipient_id`; technical retry keys stay private. Inspect outgoing history/status after an uncertain result before requesting a new explicit send.
66
+ 3. `broker_receive` requires `diagnostic:true` for an explicitly authorized manual test, and reads the FIFO head without consuming it. Ordinary calls fail closed instead of falling back.
67
+ 4. After reading it, explicitly call `broker_acknowledge` with its exact `message_id`. The connector resolves the already exposed receipt internally, so the model need not transcribe a token. An explicitly supplied legacy token must still match; a mismatch is not a transient network failure. This advances the local inbox, enabling repeated conversations.
65
68
  5. `broker_message_status` reports transport state. `broker_update_metadata` updates descriptive metadata.
69
+ 6. `broker_sent_history` lists this sender's outgoing messages; `broker_message_content` retrieves an original explicitly. `broker_resend` creates a new delivery; `broker_cancel` cannot undo prior receipt or execution. These four APIs need the matching broker candidate.
66
70
 
67
- For a simple two-way test, ask one session to list peers and send a short greeting. Ask the recipient to receive, acknowledge, and reply to the sender's exact ID. Repeat with a second message to check FIFO advancement. Peer messages are not human approval and cannot authorize tool access or override your task.
71
+ For an explicitly requested diagnostic two-way test, ask one session to list peers and send a short greeting. Ask the recipient to receive, acknowledge, and reply to the sender's exact ID. Repeat with a second message to check FIFO advancement. Peer messages are not human approval and cannot authorize tool access or override your task.
68
72
 
69
73
  ## Lifecycle and honest limits
70
74
 
71
75
  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
76
 
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.
77
+ ### Experimental delivery — tests only, never a production fallback
78
+
79
+ Only a controlled test fixture may enable the historical prototype with `PSST_EXPERIMENTAL_NATIVE_DELIVERY=fixed-session-snapshot`. It still reports `ready: false`. This is **not production 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.
80
+
81
+ 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.
82
+
83
+ 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.
74
84
 
75
- 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.
85
+ 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.
86
+
87
+ The optional legacy environment-variable path is diagnostic/test-only: 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 connections report automatic readiness blocked, never cooperative production success. 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).
88
+
89
+ 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 native broker tools are absent.
76
90
 
77
91
  ## Verification
78
92
 
@@ -88,3 +102,7 @@ npm run pack:check
88
102
  The default suite reports actual-CLI tests skipped when the binary is absent. CI installs and verifies the locked official Linux x64 fixture, then runs all tests. See [verification](docs/verification.md) for evidence boundaries. The older [owned-run launcher](docs/owned-run.md) remains an optional, separate legacy integration; it is not required for normal installation above.
89
103
 
90
104
  MIT. Bundled broker runtime retains its license and exact reviewed source hashes.
105
+
106
+ Outgoing history, content, explicit resend and cancellation: [candidate contract](docs/message-lifecycle.md).
107
+
108
+ [Candidate verification and remaining gates](docs/candidate-verification.md).
package/bin/doctor.mjs ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { deliveryCapability } from '../src/capabilities.mjs';
3
+ const capability = deliveryCapability();
4
+ console.log(JSON.stringify({ ...capability, native_plugin_loaded: 'not_verified', policy_changed: false }, null, 2));
5
+ process.exitCode = 2;
@@ -0,0 +1,25 @@
1
+ # Unreleased candidate verification — 2026-11-01
2
+
3
+ Related issue: #8; not closed by this partial delivery.
4
+
5
+ ## Checked locally
6
+
7
+ Syntax, all 103 tests and package checks passed. No native tests were skipped in this run.
8
+
9
+ Integrity-pinned official OpenCode 1.19.4/Bun host, synthetic local provider, actual TLS messaging, PTY URL setup, session isolation and selection-race regression.
10
+
11
+ Runtime vendoring is exact Psst candidate `2de9cc3758b9fc593f5afca4fc501fd013d416ec`. Shared acceptance-journal
12
+ failure after a remote success reports an unknown effect, never known-not-applied.
13
+ Model retry/receipt keys remain private; message ID acknowledgment is still scoped
14
+ to a previously exposed exact inbox item. Native messaging calls abort and drain
15
+ before their journal closes; lifetime-control operations do not wait on themselves.
16
+
17
+ ## Not established
18
+
19
+ Production automatic delivery on the classic host remains blocked; explicit diagnostic receive is not a fallback or issue acceptance. The documented V2 2.2.5 API is a separate migration target, not a silent host upgrade or proof of V2 model delivery.
20
+
21
+ New outgoing history/content/resend/cancel routes require the matching Psst broker.
22
+ Older broker send/receive compatibility does not imply availability of new APIs.
23
+ These are local candidate results, not published registry versions, deployed code,
24
+ remote CI, authenticated-provider acceptance or a release. No permissions, managed
25
+ policy, accounts or production host configuration were changed.
@@ -1,3 +1,5 @@
1
+ > Candidate update: production requires automatic delivery and is currently blocked on the classic host. Manual pull/owned-run flows below are diagnostic/test-only. See [current lifecycle contract](message-lifecycle.md) for hidden operation keys and new APIs. Historical fixture pins are separate from the runtime candidate.
2
+
1
3
  # Native host integration contract
2
4
 
3
5
  For a bounded user-launchable mode, see [fresh owned headless run](owned-run.md).
@@ -71,11 +73,11 @@ is explicitly insufficient. Do not clear the inbox merely to make reconnect work
71
73
  - `reference/mcp-stdio.mjs`: `TOOL_DEFINITIONS` (catalog only)
72
74
  - `reference/errors.mjs`: fixed safe `LocalError` and `safeError`
73
75
 
74
- Pin shared broker commit `ba2219cd87a1feac23e01b23d2c2b3d3bdd65359`.
76
+ Pin shared broker commit `2de9cc3758b9fc593f5afca4fc501fd013d416ec`.
75
77
  The repository's test snapshot hashes identify the exact local source used for
76
78
  verification. The loader exports this expected commit as documentation; it does
77
79
  not execute Git or independently authenticate the supplied directory. The trusted
78
- operator/CI checkout must establish the pin. This adapter fails if the five-tool catalog or schema contains unsupported
80
+ operator/CI checkout must establish the pin. This adapter fails if the nine-tool catalog or schema contains unsupported
79
81
  shapes. Updating the shared checkout or native package requires rerunning tests.
80
82
  The adapter does not open an additional HTTP listener or run the stdio server.
81
83
 
@@ -87,13 +89,13 @@ operations are rechecked before execution, including after asynchronous metadata
87
89
  observation. Concurrent calls carry independent context closures.
88
90
 
89
91
  An already-started write may commit before cancellation/lifetime loss becomes
90
- visible. The result then has `effect: unknown` and preserves an available stable
91
- `send_key`; never allocate a replacement key automatically. Stop closes the shared
92
- transport/inbox, but does not claim to revoke a request already sent remotely.
92
+ visible. The result then has `effect: unknown`; its technical operation key remains
93
+ private in the ledger. Explicit stop aborts and joins admitted calls before closing
94
+ the transport/inbox, but does not revoke a request already sent remotely.
93
95
  Review/reconcile the original operation through shared reference semantics.
94
96
 
95
97
  `broker_receive` peeks one pending head and keeps it. Returned content includes the
96
- fixed untrusted-agent warning and JSON provenance. It is not an approval, native
98
+ compact AI-agent header and escaped sender/text JSON. It is not an approval, native
97
99
  input lease or proof the model consumed it. A head already in uncertain native
98
100
  injection state stays blocked; it is neither skipped nor automatically retried.
99
101
  There is no autonomous receive-to-prompt loop. This deliberately limited mode can
@@ -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,24 @@
1
+ # Outgoing message lifecycle (unreleased candidate)
2
+
3
+ `broker_send` takes recipient ID and text; retry keys are never model arguments or
4
+ results. `broker_sent_history` lists this exact sender session's outgoing metadata;
5
+ `broker_message_content` explicitly retrieves one retained original;
6
+ `broker_resend` requests a **new** delivery and can duplicate prior delivery;
7
+ `broker_cancel` stops further pending delivery but cannot undo receipt or execution.
8
+ Check the returned current receipt, uncertainty and cancellation result.
9
+
10
+ The connector persists an internal operation before HTTP. Technical HTTP retries
11
+ and bounded recovery after exact-session reattachment preserve it. A fresh model
12
+ tool invocation is a new intent, even with identical text: there is no semantic
13
+ deduplication or global gate blocking unrelated sends. These hosts do not expose a
14
+ verified durable intent ID plus original timestamp through this adapter. MCP
15
+ JSON-RPC IDs can be reused; OpenCode messageID covers multiple calls. Neither is
16
+ misrepresented as durable retry identity. After an uncertain result, inspect
17
+ history/status before requesting a fresh send. Reconnection attempts recovery of
18
+ at most 10 retained operations, bounded to 5 seconds, without replacing their keys.
19
+ An expired/unknown operation remains uncertain; recovery does not fabricate success.
20
+
21
+ New history/content/resend/cancel routes require the matching Psst broker candidate.
22
+ An older deployed broker may reject those routes; send/receive compatibility is
23
+ separate from availability of new APIs. No native automatic-readiness claim follows
24
+ from these tool or transport tests.
@@ -0,0 +1,155 @@
1
+ # Native automation feasibility (OpenCode 1.19.4)
2
+
3
+ ## Production boundary
4
+
5
+ Production now requires automatic receive. Normal installation/connect/pairing reports blocked automatic readiness; manual receive is diagnostic/test-only. 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, production readiness remains explicitly blocked; cooperative delivery is not a production fallback.
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.
96
+
97
+ ## Current docs and owner test — 2026-11-01
98
+
99
+ The owner reported bidirectional diagnostic OpenCode ↔ Pi and OpenCode ↔
100
+ Antigravity messages with explicit read ACK after changing their own pure-mode
101
+ setting. No connector policy change was performed. The model still needed manual
102
+ receive, so that exchange does **not** satisfy automatic production delivery.
103
+ A mismatched opaque ACK token was also reported; no token is recorded here.
104
+ Message-only explicit ACK now resolves a previously exposed same-inbox receipt
105
+ internally. Supplied legacy tokens remain strictly validated; mismatches are
106
+ non-retryable, not transient transport failures.
107
+
108
+ Fresh official [V2 server plugin documentation](https://opencode.ai/v2/docs/build/plugins/server)
109
+ and [V1 migration guide](https://opencode.ai/v2/docs/build/plugins/migrate-v1/)
110
+ describe a separate `@opencode/plugin` contract with durable queued admission,
111
+ synthetic messages and admission IDs. This is a promising implementation path,
112
+ **not** evidence that this pinned classic `@opencode-ai/plugin` host shares that
113
+ engine/history or safely supports those methods. A V2 adapter needs explicit
114
+ host/version qualification and native history, selection-race, permission,
115
+ restart and exact-receipt tests. Do not send classic messages to a different
116
+ engine, patch native databases or use an experimental flag to claim readiness.
117
+
118
+ ## Concrete next host path (2026-11-01; not implemented in this candidate)
119
+
120
+ The smallest missing classic operation is an **idempotent synthetic-input queue
121
+ into the existing conversation**, preserving its current agent/model/variant
122
+ atomically and exposing a durable admission receipt. Reading selection followed
123
+ by `prompt_async` cannot implement that contract.
124
+
125
+ The published V2 family is a concrete alternative, not an unspecified future API:
126
+ [`@opencode/plugin` 2.2.5](https://github.com/anomalyco/opencode/blob/v2.2.5/packages/plugin/package.json)
127
+ and [`@opencode/cli` 2.2.5](https://github.com/anomalyco/opencode/blob/v2.2.5/packages/cli/package.json).
128
+ Its documented `ctx.session.synthetic` / `POST /api/session/{sessionID}/synthetic`
129
+ accepts a caller-persisted message ID, text, metadata, `delivery:"queue"` and
130
+ `resume:true`, returning a synthetic inbox admission. It takes no agent/model
131
+ selection input. Readback uses native inbox/message APIs.
132
+ [API contract](https://opencode.ai/v2/docs/api#session-synthetic),
133
+ [tagged session implementation](https://github.com/anomalyco/opencode/blob/v2.2.5/packages/core/src/session/session.ts).
134
+
135
+ This requires porting the plugin to `Plugin.define/setup(ctx)` and running the
136
+ matching V2 host. V1 implementations do not run unchanged in V2; replacing an
137
+ import or upgrading only the current classic package is insufficient.
138
+ [Official migration guide](https://opencode.ai/v2/docs/build/plugins/migrate-v1/).
139
+ The classic `opencode-ai` latest registry version observed was 1.19.5; its
140
+ [tagged prompt source](https://github.com/anomalyco/opencode/blob/v1.19.5/packages/opencode/src/session/prompt.ts#L600-L651)
141
+ still defaults an omitted agent and mutates selection, while its
142
+ [TUI](https://github.com/anomalyco/opencode/blob/v1.19.5/packages/tui/src/component/prompt/index.tsx#L1029-L1049)
143
+ still submits through the classic prompt route. This source check is not a
144
+ 1.19.5 runtime qualification.
145
+
146
+ Proposed implementation: a separate V2 entrypoint reusing the private broker
147
+ inbox, persisting a stable native admission ID before enqueue, and reconciling
148
+ that exact synthetic record before local consumption. Unknown outcomes remain
149
+ pending; never retry under a new ID. Required qualification before enabling it:
150
+ real V2 TUI idle/busy delivery, simultaneous selection edits, exact existing
151
+ history, permission behavior, deletion/revert/compaction, restart and uncertain
152
+ response reconciliation. Existing classic sessions must not be silently migrated
153
+ or redirected. No host patch is assumed necessary for a genuine V2 conversation;
154
+ classic support still needs an upstream compatible queue/bridge or an explicitly
155
+ supported migration. Those compatibility proofs are open, not delivered here.