@matuto-ai/opencode-broker-connector 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruno Orsolon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,90 @@
1
- # Temporary Holding Version
1
+ # Psst connector for OpenCode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
4
+
5
+ ## Install now, before npm publication
6
+
7
+ After this naming PR merges, install 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
+ 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.
24
+
25
+ ```sh
26
+ export PSST_CONFIG_FILE=/absolute/private/path/opencode/connection.json
27
+ opencode
28
+ ```
29
+
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.
31
+
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:
33
+
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
+ ```
44
+
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.
46
+
47
+ ## After npm publication
48
+
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.0` exists on npm, use this native OpenCode configuration instead of the local file:
50
+
51
+ ```json
52
+ {"plugin": ["@matuto-ai/opencode-broker-connector@0.1.0"]}
53
+ ```
54
+
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).
56
+
57
+ ## Use
58
+
59
+ After a user message, the session registers with the broker. Tools also initialize lazily, so resumed sessions work without a separate wrapper:
60
+
61
+ 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
+ 5. `broker_message_status` reports transport state. `broker_update_metadata` updates descriptive metadata.
66
+
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.
68
+
69
+ ## Lifecycle and honest limits
70
+
71
+ 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
+
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.
74
+
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.
76
+
77
+ ## Verification
78
+
79
+ Tested against official OpenCode **1.19.4**, embedded Bun **1.4.4**, and its exact official plugin helper. Node **24.19+ (24.x)** runs development tests; the normal plugin uses OpenCode's runtime. Other OpenCode versions need validation, not an assumed compatibility guarantee.
80
+
81
+ ```sh
82
+ npm ci --ignore-scripts
83
+ npm run check
84
+ npm test
85
+ npm run pack:check
86
+ ```
87
+
88
+ 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
+
90
+ MIT. Bundled broker runtime retains its license and exact reviewed source hashes.
package/bin/launch.mjs ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { launchOwnedRun } from '../src/owned-run.mjs';
3
+ const [configPath, ...prompt] = process.argv.slice(2);
4
+ try {
5
+ if (!configPath || !prompt.length) throw Error('usage');
6
+ const result = await launchOwnedRun({ configPath, prompt: prompt.join(' ') });
7
+ process.exitCode = result.code ?? 1;
8
+ } catch {
9
+ process.stderr.write('Owned OpenCode run unavailable. Use a private config, the pinned Linux x64 1.19.4 binary, exact reference checkout and an explicit prompt. No permission override or automatic retry was applied.\n');
10
+ process.exitCode = 1;
11
+ }
@@ -0,0 +1,101 @@
1
+ # Native host integration contract
2
+
3
+ For a bounded user-launchable mode, see [fresh owned headless run](owned-run.md).
4
+ This document describes the separate lower-level custom-supervisor interface; it
5
+ does not imply that a generic project plugin can attach to an existing TUI.
6
+
7
+ ## Supported native shape, unsupported lifecycle inference
8
+
9
+ Official [custom-tool documentation](https://opencode.ai/docs/custom-tools/) and
10
+ [plugin documentation](https://opencode.ai/docs/plugins/) describe a tool map:
11
+
12
+ ```js
13
+ import { tool } from '@opencode-ai/plugin';
14
+ // Only AFTER a trusted host owner has established the binding below:
15
+ const attached = await attachVerifiedSession({ /* trusted configuration */ });
16
+ // Return/install attached.hooks through the host's supported plugin mechanism.
17
+ // On exact host teardown, abort its lifetime signal and await attached.close().
18
+ ```
19
+
20
+ `tool({ description, args, execute })` takes a Zod property shape and receives
21
+ native context `sessionID`, `messageID`, `directory`, `worktree`, `abort`, etc.
22
+ The adapter uses these documented fields, returns a string tool result, and never
23
+ calls `context.ask`. `@opencode-ai/plugin@1.19.4/dist/tool.d.ts` and `tool.js` were
24
+ inspected and the real helper was executed during tests.
25
+
26
+ The ordinary plugin initializer is project/server scoped. Neither that initializer,
27
+ a server PID, a persisted transcript, session status/idle, nor session ID alone is
28
+ sufficient proof of an attached human TUI lifetime. The generic factory does **not**
29
+ claim a supported existing-TUI lifecycle hook or invent one. A supported owner must
30
+ provide the contract below before this adapter can be enabled. A custom supervisor
31
+ that only runs this factory without owning the actual native host is insufficient.
32
+
33
+ ## Trusted operator configuration
34
+
35
+ Import `createOpenCodeAdapter` and `loadBrokerReference`, or call the
36
+ [example](../examples/owned-session.mjs). Supply:
37
+
38
+ | Value | Requirement |
39
+ | --- | --- |
40
+ | `referenceDirectory` | Absolute path to a reviewed, pinned `ai-agent-msg-broker` checkout. No arbitrary model-supplied module path. |
41
+ | `tool` | Official tool helper from the host's pinned plugin package. |
42
+ | `host.binding.sessionID` | Exact native session identifier, not its title. |
43
+ | `host.binding.directory`, `worktree` | Exact, normalized absolute native paths. No `process.cwd()` guess. |
44
+ | `host.binding.lifetime` | Private `Symbol`, issued for this verified live host incarnation; never serialized or a tool argument. |
45
+ | `host.binding.signal` | Owner's `AbortSignal`; abort immediately when that exact host closes, changes or becomes uncertain. |
46
+ | `host.currentBinding()` | Fresh owner verification returning the same binding only while that exact native host remains live; `null`/throw when unknown. |
47
+ | `inboxDirectory` | New private local directory per new lifetime; owner-only, no symlink/network filesystem. |
48
+ | `broker.url` | Explicit HTTPS origin, no embedded credentials. |
49
+ | `broker.tokenFile` | Existing authorized mounted secret, private `0600`; never a tool argument or command-line token. |
50
+ | `broker.caFile` | Optional explicitly trusted private CA; no TLS bypass. |
51
+ | `metadata` | Truthful name/model/project; unknown model is `{provider:null,id:null,source:'unknown'}`. No full-path project disclosure by default. |
52
+
53
+ The owner installs `{ tool: adapter.tools }` only after `await adapter.start(metadata)`.
54
+ On teardown it aborts `host.binding.signal` and awaits `adapter.stop()`. `stop()` is
55
+ idempotent; the instance cannot restart. Do not stop only when a long-lived server
56
+ finally exits: host close is the relevant boundary.
57
+
58
+ The sample creates fresh broker UUID identity every time. Reconnect of an existing
59
+ reference inbox is available through the lower-level factory only when an owner
60
+ independently proves the exact surviving native lifetime. Same `sessionID` alone
61
+ is explicitly insufficient. Do not clear the inbox merely to make reconnect work.
62
+
63
+ ## Shared modules — no protocol copy
64
+
65
+ `loadBrokerReference()` imports these files from the trusted checkout:
66
+
67
+ - `reference/inbox.mjs`: `Inbox`, `newSessionIdentity`
68
+ - `reference/client.mjs`: `BrokerClient`
69
+ - `reference/runtime.mjs`: `AdapterRuntime`
70
+ - `reference/tools.mjs`: `createTools`, `invokeTool`
71
+ - `reference/mcp-stdio.mjs`: `TOOL_DEFINITIONS` (catalog only)
72
+ - `reference/errors.mjs`: fixed safe `LocalError` and `safeError`
73
+
74
+ Pin shared broker commit `ba2219cd87a1feac23e01b23d2c2b3d3bdd65359`.
75
+ The repository's test snapshot hashes identify the exact local source used for
76
+ verification. The loader exports this expected commit as documentation; it does
77
+ 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
79
+ shapes. Updating the shared checkout or native package requires rerunning tests.
80
+ The adapter does not open an additional HTTP listener or run the stdio server.
81
+
82
+ ## Tool boundary and failure semantics
83
+
84
+ Every call checks exact native session/project context, a real non-aborted native
85
+ `AbortSignal`, private host lifetime and retained inbox identity. Shared client
86
+ operations are rechecked before execution, including after asynchronous metadata
87
+ observation. Concurrent calls carry independent context closures.
88
+
89
+ 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.
93
+ Review/reconcile the original operation through shared reference semantics.
94
+
95
+ `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
97
+ input lease or proof the model consumed it. A head already in uncertain native
98
+ injection state stays blocked; it is neither skipped nor automatically retried.
99
+ There is no autonomous receive-to-prompt loop. This deliberately limited mode can
100
+ be used by a model explicitly calling the native tools in an existing turn; it
101
+ cannot wake an idle session.
@@ -0,0 +1,23 @@
1
+ # Owned-run work in progress (2026-10-23)
2
+
3
+ This feature-branch checkpoint preserves an unfinished, **not yet approved for
4
+ operator use** launcher for one fresh owned `opencode run --format json` process.
5
+ The prior cooperative factory remains the reviewed baseline.
6
+
7
+ - Preliminary isolated source/runtime evidence: official OpenCode 1.19.4,
8
+ bundled Bun 1.4.4, native root `session.created` → `chat.message` → `dispose`,
9
+ and the shared SQLite inbox primitives. The provider was a local deterministic
10
+ fixture; no real model credentials or account was used.
11
+ - The new launcher/plugin is still receiving exact process-incarnation guards,
12
+ real native-tool-loop tests, policy-preservation checks and independent review.
13
+ - Syntax checks at this checkpoint are not native certification. Existing CI
14
+ currently covers the earlier factory tests, not the unfinished owned launcher.
15
+ - No existing TUI attachment, resume, background wakeup, native-consumption ACK,
16
+ permission override, automatic approval, installation or deployment is claimed.
17
+
18
+ See the draft PR for the subsequent reviewed/tested checkpoint. Do not infer that
19
+ `run` process ownership alone proves an unrelated server or TUI session lifetime.
20
+
21
+ This historical WIP note describes commit `9eb4df888ae986e0c0a1589051358ba0dadaf73b`,
22
+ not later reviewed revisions. See [current owned-run documentation](owned-run.md)
23
+ and the draft PR for subsequent exact-process hardening and native test evidence.
@@ -0,0 +1,68 @@
1
+ # Owned fresh OpenCode run (candidate)
2
+
3
+ This mode launches **one new headless `opencode run`**, rather than attaching to an existing TUI, server session, transcript or resumed conversation. It offers the same five cooperative broker tools as the adapter factory. It does not interrupt a task, inject a prompt automatically, turn another agent's text into approval, or mark a native tool result consumed.
4
+
5
+ ## Supported pinned prerequisites
6
+
7
+ - Linux x64 with `/proc` available; launcher Node **24.19.0**.
8
+ - Official `opencode-linux-x64` **1.19.4** executable. Its SHA256 must be `538fcb1b6408c8d4608822acee19f7ff5b3a3eb3383c0633c4f06170857312a7`. Other builds/platforms are deliberately refused, not assumed compatible.
9
+ - `npm ci --ignore-scripts` in this repository installs the locked official `@opencode-ai/plugin` **1.19.4** helper. The external native plugin requires this local dependency; OpenCode's separate config-directory dependency is not a substitute.
10
+ - A local broker reference checkout at **`ba2219cd87a1feac23e01b23d2c2b3d3bdd65359`**. The loader verifies its runtime file hashes before importing it.
11
+ - An already configured, trusted HTTPS broker and private token file. This launcher does not create credentials, expose a server, alter TLS verification or configure a model provider.
12
+
13
+ The pinned OpenCode executable embeds Bun **1.4.4**. Its `node:sqlite` compatibility was exercised with the actual shared Inbox; no separate Bun installation or global Node change is required.
14
+
15
+ For repeatable validation, the repository's `test/official-cli` package lock can install the exact official executable with `npm ci --ignore-scripts`. Verify the digest above before using any binary acquired elsewhere.
16
+
17
+ ## Launch
18
+
19
+ Create an owner-only JSON file outside the repository (mode `0600`), for example:
20
+
21
+ ```json
22
+ {
23
+ "opencodeExecutable": "/absolute/path/to/verified/opencode",
24
+ "referenceDirectory": "/absolute/path/to/ai-agent-msg-broker",
25
+ "stateDirectory": "/absolute/private/path/opencode-broker-runs",
26
+ "broker": {
27
+ "url": "https://broker.example.invalid/",
28
+ "tokenFile": "/absolute/private/path/broker-token",
29
+ "caFile": "/absolute/path/to/trusted-ca.pem"
30
+ }
31
+ }
32
+ ```
33
+
34
+ `caFile` is optional if normal system trust is appropriate. A configured model can be selected with the optional `model` field (`provider/model`); otherwise native OpenCode configuration selects it. The token is never a command argument. State directories must be owner-only (`0700`), owned by the launching user, and not symlinks.
35
+
36
+ From the project whose existing OpenCode policies/plugins should apply:
37
+
38
+ ```sh
39
+ node /absolute/path/to/opencode-review-pair/bin/launch.mjs \
40
+ /absolute/private/path/launch.json \
41
+ 'Complete my task; use broker_list_clients, broker_send and broker_receive when useful.'
42
+ ```
43
+
44
+ The launcher does not change cwd, replace existing native permission rules or add `--auto`. Native configuration and plugins are loaded normally; the overlay appends only this plugin. OpenCode itself may maintain its normal dependency/cache/state files. If `OPENCODE_CONFIG_CONTENT` is already set, it must be an ordinary JSON object; JSONC is refused rather than silently losing policy. No `--session`, `--continue`, `--attach` or resume path is accepted.
45
+
46
+ A fresh broker identity and durable inbox are created for each run. Identity metadata initially reports the native model as unknown rather than guessing from a display name. Pending inbound messages remain in that run's private inbox; this mode intentionally does not reopen old identities or claim native consumption. `broker_receive` peeks and returns model-visible untrusted-agent provenance plus `native_acceptance_recorded:false`.
47
+
48
+ ## Exact lifetime and failure behavior
49
+
50
+ The launcher writes a private record bound to the exact child **PID + Linux process starttime + boot ID**, and its own live process incarnation. The plugin additionally requires a newly observed parentless native `session.created`, then the matching user `chat.message`, and a matching SDK session ID, project directory and creation time. Tool proxies are unavailable before this check. Session reads are abortable and bounded to two seconds. Every binding check revalidates the private marker and both process incarnations, including after the awaited SDK read.
51
+
52
+ A second root session, matching root deletion, native plugin disposal, missing marker, dead/replaced launcher or mismatched native tool context fails closed. Shared runtime shutdown drains registration/reconnect work before closing the final lease. The launcher revokes the marker after child exit and makes a bounded best-effort close of that exact durable identity; it never deletes inbox contents. Abrupt process death can still leave the remote lease visible until expiry.
53
+
54
+ OpenCode may swallow a plugin-loading error and still run the ordinary model task. Therefore the launcher requires a private successful-binding proof before treating native exit code zero as broker availability. If absent, it exits nonzero with a fixed unavailable diagnostic. This is **not** proof the ordinary native prompt did not execute, and no automatic retry is made. Do not repeat a task blindly after an uncertain failure.
55
+
56
+ ## Verification and limits
57
+
58
+ Run the focused suite with the pinned shared checkout and official helper:
59
+
60
+ ```sh
61
+ BROKER_REFERENCE_DIR=/absolute/path/to/ai-agent-msg-broker \
62
+ OPENCODE_TOOL_MODULE="$PWD/node_modules/@opencode-ai/plugin/dist/tool.js" \
63
+ node --test test/owned-run.test.mjs
64
+ ```
65
+
66
+ Set `OPENCODE_BINARY` to the verified executable to enable the optional actual CLI test. That test uses a disposable HOME/project, a deterministic local model module, the real launcher/private process record/native tool contexts/shared runtime/SQLite, and **injected fake broker client methods**. It also checks provider-visible provenance, rejection of a swallowed/missing plugin, and preservation of an existing `bash: deny` policy: native OpenCode reports bash unavailable and the attempted marker command never executes. It does not certify a live broker, real provider authentication, a third-party plugin combination, arbitrary OpenCode versions, or existing TUI attachment. Native executable/header compatibility is a separate boundary from shared broker HTTP tests.
67
+
68
+ The source contract was checked against official OpenCode v1.19.4 `run.ts`, plugin lifecycle and SDK/tool types, plus the [CLI](https://opencode.ai/docs/cli/), [plugins](https://opencode.ai/docs/plugins/) and [custom tools](https://opencode.ai/docs/custom-tools/) documentation. No hidden APIs or native permission shortcuts are used.
@@ -0,0 +1,24 @@
1
+ # Package publication
2
+
3
+ Package name: `@matuto-ai/opencode-broker-connector`. The npm workflow follows the owner's existing release-triggered trusted-publishing pattern, with version verification, full native tests and package audit before publication.
4
+
5
+ 1. Review and merge the feature PR.
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
+ 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 (first `v0.1.0`).
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
+ 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
+
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
+
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.
15
+
16
+ ## Scoped package transition
17
+
18
+ Publication is paused while the `@matuto-ai` naming changes are reviewed.
19
+ The package version remains `0.1.0`; existing unscoped npm packages and old
20
+ GitHub release tags are not renamed, overwritten, or republished by this change.
21
+ After merge, the maintainer must verify npm organization permissions and configure
22
+ the scoped package trusted publisher. A later release must point at the reviewed
23
+ scoped commit and its tag must exactly match the package version; do not reuse
24
+ an old tag pointing to unscoped code. Version/tag planning is a separate step.
@@ -0,0 +1,11 @@
1
+ 93f75786eab29429ace024974e0f61d6ad46d2c5fe065dae387e54fd2373b167 reference/inbox.mjs
2
+ f661b45acba106be03d97dacba3cd90ac18b09e7ff9eb93b995d5036d9d1c785 reference/client.mjs
3
+ 8733076b73a42ab8f12fe20ba9908acdd754d89734ffd335e84b49aee9d737ca reference/runtime.mjs
4
+ 5e6355cef1bec56170dca14b93142a7da8c102f411c5dbf437d84acda7351acf reference/tools.mjs
5
+ 10a93788af426ce898c398e38dd41ac77b6e0129a7697bdc36c8d4f6d6e20138 reference/mcp-stdio.mjs
6
+ e33231d94f514b5a1b59fdd4184f5881dd3712bde725a1cf53352c26293452ac reference/errors.mjs
7
+ f3ff24b4429dddc8a9c10513ab72dbc613c91244591132458c7866a812d6316b src/protocol.mjs
8
+ 575ac7406ed9b8698d97ecdc2f093a99455fdd581f375c4611c844d4618095c8 src/store.mjs
9
+ 69d5130ce90e045d0cd66facdc4380ef03f50895c53997fe9a664814fcca0e7b src/server.mjs
10
+ 8d85ecd3c02142de87de34425b71daa35b7317bbe3cfd19865c43e7bb57e3785 src/auth.mjs
11
+ 2c71a7419c7f21799d66c8e99473870182bc8470b7ac254928d5bda3372f4f5d test/helpers.mjs
@@ -0,0 +1,112 @@
1
+ # Native plugin verification (October 24, 2026)
2
+
3
+ The current default package adds native session-keyed cooperative tools and a self-contained pinned reference runtime. CI runs the full suite with the integrity-verified official OpenCode 1.19.4 Linux x64 fixture (embedded Bun 1.4.4).
4
+
5
+ The new end-to-end test packs the actual npm package, extracts it to a disposable directory, loads that package's production plugin entry in the actual OpenCode CLI, connects to a real local Psst server over certificate-verified HTTPS with a disposable trusted CA, receives and explicitly acknowledges two FIFO messages, and sends a reply. It uses a deterministic local model provider, not a remote provider account. It exercises real native tool contexts, real BrokerClient requests and real SQLite in Bun. Temporary credentials are fixture-only; no live broker or user's sandbox is contacted.
6
+
7
+ Other tests cover exact-session isolation, concurrent initialization, invalid/cancelled contexts, session deletion/disposal, non-blocking chat warm-up when SDK verification stalls, enrollment config permissions/type/URL checks, the explicit model-read acknowledgment ledger, packet contents and reference integrity. npm pack excludes broker daemon/test fixtures, credentials and journals.
8
+
9
+ No test proves live provider authentication, every third-party plugin combination, every OpenCode version/platform, a human TUI's continued presence, or automatic model wakeup. Normal plugin registration is native-session scoped, not TUI ownership. Restarts create a new broker address and retain old journals without replay. Native consumption/action success is never claimed from a cooperative tool return.
10
+
11
+ ## Run all native checks
12
+
13
+ ```sh
14
+ npm ci --ignore-scripts
15
+ npm ci --prefix test/official-cli --ignore-scripts
16
+ OPENCODE_BINARY="$PWD/test/official-cli/node_modules/opencode-linux-x64/bin/opencode" npm test
17
+ npm run check
18
+ npm run pack:check
19
+ ```
20
+
21
+ Without OPENCODE_BINARY, actual-CLI tests are explicitly skipped, not treated as passed. CI supplies the binary. The final PR commit/CI is the authoritative evidence for the current test count.
22
+
23
+ ---
24
+
25
+ ## Historical foundation verification
26
+
27
+ ## Evidence (2026-10-23 UTC)
28
+
29
+ Pinned components:
30
+
31
+ - Node **24.19.0** for launcher and test orchestration.
32
+ - Official `@opencode-ai/plugin` **1.19.4**, Zod **4.1.8**; lockfile integrity pins,
33
+ installed with lifecycle scripts disabled.
34
+ - Official Linux x64 OpenCode **1.19.4**, embedded Bun **1.4.4**; npm tarball
35
+ integrity and executable SHA256 pinned in the fixture/launcher.
36
+ - Shared broker/reference **`ba2219cd87a1feac23e01b23d2c2b3d3bdd65359`**,
37
+ source-byte verified before import.
38
+
39
+ The aggregate suite has **25 tests**. The final exact-SHA CI and independent-review
40
+ checkpoint is recorded in the draft PR. A skipped optional native test is not a
41
+ native pass; CI supplies the pinned binary so it must execute that test.
42
+
43
+ ### Shared transport and factory gate
44
+
45
+ The original 16 tests exercise the official native helper and strict shared
46
+ schemas; exact session/project/worktree/lifetime/cancellation guards; reconnect
47
+ and close-during-registration/final-epoch races; metadata/send-key recovery;
48
+ concurrent native contexts; hostile-text provenance; inbox dedupe/FIFO blocking;
49
+ and pre-import reference tamper/symlink refusal.
50
+
51
+ One test uses an **actual local HTTP broker + BrokerClient + SQLite inbox** and
52
+ native tool-helper execute calls. It verifies durable delivery, cooperative peek,
53
+ reply and same-key retry. Its host lifecycle is simulated. Credentials are random
54
+ fixture values; HTTP is only the explicit numeric-loopback development mode.
55
+
56
+ ### Fresh owned-run gate
57
+
58
+ Eight focused tests cover fixed no-attach/no-resume/no-auto arguments, policy and
59
+ plugin overlay preservation, unbound native proxies, exact fresh root creation,
60
+ second-root/deletion/marker revocation, PID/starttime/boot-ID mismatch, symlink
61
+ records, old or parented native sessions, concurrent disposal and bounded SDK
62
+ reads that never settle.
63
+
64
+ The ninth uses the **actual pinned OpenCode CLI and actual `launchOwnedRun`**:
65
+ private child-incarnation record → native root session/chat hooks → cooperative
66
+ list/send/receive tools → close/dispose → marker revocation. Its deterministic
67
+ local provider receives the fixed untrusted-agent provenance inside the actual
68
+ model-facing tool result, not just terminal output. Existing `bash: deny` policy
69
+ is retained; an attempted fixture command does not execute. A swallowed/no-plugin
70
+ startup followed by native exit zero is rejected for missing binding proof, with
71
+ no automatic retry of the prompt.
72
+
73
+ **Boundary:** that native test substitutes a fixture plugin constructor only to
74
+ inject fake broker client methods. The adapter factory, shared runtime and SQLite
75
+ are real; the default production HTTPS transport is not exercised inside that
76
+ same native process. This gate and the separate real-broker transport gate must
77
+ not be represented as one live end-to-end transport certification.
78
+
79
+ ## Reproduce in isolated local directories
80
+
81
+ The repository-local installs below do not alter a global CLI or package prefix:
82
+
83
+ ```sh
84
+ node --version # v24.19.0
85
+ npm ci --ignore-scripts --no-audit --no-fund
86
+ npm ci --prefix test/official-cli --ignore-scripts --no-audit --no-fund
87
+ echo '538fcb1b6408c8d4608822acee19f7ff5b3a3eb3383c0633c4f06170857312a7 test/official-cli/node_modules/opencode-linux-x64/bin/opencode' | sha256sum -c -
88
+ export BROKER_REFERENCE_DIR=/absolute/path/to/pinned/ai-agent-msg-broker
89
+ export OPENCODE_TOOL_MODULE="$PWD/node_modules/@opencode-ai/plugin/dist/tool.js"
90
+ export OPENCODE_BINARY="$PWD/test/official-cli/node_modules/opencode-linux-x64/bin/opencode"
91
+ npm run check
92
+ npm test
93
+ ```
94
+
95
+ The reference checkout must include its `test/helpers.mjs` for the local HTTP
96
+ fixture. The native test uses disposable HOME/project/config/state and a local
97
+ file-backed model module; no real provider authentication or model call is used.
98
+ No test paths or credentials are exposed as model tool arguments. GitHub Actions
99
+ pins actions, helper/executable packages, the shared reference and Node, and runs
100
+ only isolated tests—not deployment.
101
+
102
+ ## Still not verified or implemented
103
+
104
+ Real provider credentials/native permission UI; default-plugin-to-real-HTTPS
105
+ transport in the actual CLI; arbitrary existing plugin combinations; other
106
+ OpenCode versions/platforms; existing TUI/server-session attachment; native
107
+ consumption receipts; automatic input insertion/wakeup or atomic human/agent
108
+ arbitration; per-CLI Podman containers or two-host LAN operation.
109
+
110
+ `broker_receive` remains a non-consuming FIFO **peek**. A native tool return or
111
+ model read does not advance the inbox. See [owned-run instructions](owned-run.md)
112
+ for operator requirements and failure/unknown-outcome behavior.
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@matuto-ai/opencode-broker-connector",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=24.19.0 <25"
7
+ },
8
+ "scripts": {
9
+ "test": "node scripts/test.mjs",
10
+ "check": "find src bin plugin examples scripts test vendor \\( -name \"*.mjs\" -o -name \"*.ts\" \\) -not -path \"*/node_modules/*\" -print0 | xargs -0 -r -n1 node --check",
11
+ "pack:check": "node scripts/pack-check.mjs"
12
+ },
13
+ "license": "MIT",
14
+ "dependencies": {
15
+ "@opencode-ai/plugin": "1.19.4"
16
+ },
17
+ "description": "Session-scoped Psst messaging plugin for OpenCode",
18
+ "main": "./plugin/index.mjs",
19
+ "exports": "./plugin/index.mjs",
20
+ "files": [
21
+ "plugin",
22
+ "src",
23
+ "bin",
24
+ "vendor/broker/reference",
25
+ "vendor/broker/src/protocol.mjs",
26
+ "vendor/broker/LICENSE",
27
+ "docs",
28
+ "LICENSE",
29
+ "README.md",
30
+ "vendor/broker/ORIGIN.md"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/matuto-ai/opencode-broker-connector.git"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }
@@ -0,0 +1,7 @@
1
+ import { tool } from '@opencode-ai/plugin';
2
+ import { readConfig } from '../src/config.mjs';
3
+ import { loadBrokerReference } from '../src/reference.mjs';
4
+ import { createNativePlugin } from '../src/native-plugin.mjs';
5
+ export default async function PsstPlugin(native) {
6
+ return createNativePlugin({ native, tool, reference: await loadBrokerReference(), config: readConfig(process.env.PSST_CONFIG_FILE) });
7
+ }
@@ -0,0 +1,7 @@
1
+ import { tool } from '@opencode-ai/plugin';
2
+ import { createOwnedHooks } from '../src/owned-run.mjs';
3
+
4
+ export default async function(native) {
5
+ return createOwnedHooks({ native, tool, recordPath: process.env.OPENCODE_REVIEW_RUN_FILE,
6
+ nonce: process.env.OPENCODE_REVIEW_RUN_NONCE });
7
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,42 @@
1
+ import { openSync, closeSync, readFileSync, fstatSync, constants, mkdirSync, lstatSync, realpathSync } from 'node:fs';
2
+ import { isAbsolute, dirname, resolve } from 'node:path';
3
+ const fail = () => { throw new Error('psst_configuration_invalid'); };
4
+ const plain = value => value && typeof value === 'object' && !Array.isArray(value);
5
+ export function readConfig(path) {
6
+ if (typeof path !== 'string' || !isAbsolute(path)) fail();
7
+ let fd;
8
+ try {
9
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
10
+ const stat = fstatSync(fd);
11
+ if (!stat.isFile() || stat.mode & 0o077 || stat.size > 16384 || process.getuid && stat.uid !== process.getuid()) fail();
12
+ let value = JSON.parse(readFileSync(fd, 'utf8'));
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'];
15
+ if (Object.keys(value).some(key => !allowed.includes(key)) || value.protocol_version !== '1'
16
+ || value.file_paths_relative_to !== 'connection_file_directory' || value.authorization_header !== 'Authorization'
17
+ || value.authorization_scheme !== 'Bearer' || value.insecure_development !== false
18
+ || !Array.isArray(value.allowed_client_types) || value.allowed_client_types.length !== 1 || value.allowed_client_types[0] !== 'opencode'
19
+ || value.token_file !== 'token' || typeof value.scope !== 'string') fail();
20
+ const base = dirname(realpathSync(path));
21
+ value = { broker: { url: value.broker_url, tokenFile: resolve(base, value.token_file),
22
+ ...(value.ca_file ? { caFile: resolve(base, value.ca_file) } : {}) }, stateDirectory: resolve(base, 'state') };
23
+ }
24
+ if (!plain(value) || Object.keys(value).some(key => !['broker', 'stateDirectory', 'name'].includes(key))
25
+ || !plain(value.broker) || Object.keys(value.broker).some(key => !['url', 'tokenFile', 'caFile'].includes(key))
26
+ || typeof value.stateDirectory !== 'string' || !isAbsolute(value.stateDirectory)
27
+ || typeof value.broker.tokenFile !== 'string' || !isAbsolute(value.broker.tokenFile)
28
+ || value.broker.caFile !== undefined && (typeof value.broker.caFile !== 'string' || !isAbsolute(value.broker.caFile))
29
+ || value.name !== undefined && (typeof value.name !== 'string' || !value.name.trim() || value.name.length > 100 || /[\x00-\x1f\x7f]/.test(value.name))) fail();
30
+ const url = new URL(value.broker.url);
31
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') fail();
32
+ return value;
33
+ } catch { fail(); }
34
+ finally { if (fd !== undefined) closeSync(fd); }
35
+ }
36
+ export function privateDirectory(path) {
37
+ mkdirSync(path, { recursive: true, mode: 0o700 });
38
+ const stat = lstatSync(path);
39
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.mode & 0o077 || realpathSync(path) !== path
40
+ || process.getuid && stat.uid !== process.getuid()) fail();
41
+ return path;
42
+ }
@@ -0,0 +1,50 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { LocalError } from '../vendor/broker/reference/errors.mjs';
3
+ import { provenance } from '../vendor/broker/reference/runtime.mjs';
4
+ import { validateUuid } from '../vendor/broker/src/protocol.mjs';
5
+
6
+ /** Application-level explicit read ACK. Never a native injection receipt or action success. */
7
+ export function createCooperativeInbox({ inbox, isActive = () => true }) {
8
+ if (!inbox?.db || typeof isActive !== 'function') throw new LocalError('invalid_configuration');
9
+ inbox.transaction(() => inbox.db.exec(`CREATE TABLE IF NOT EXISTS cooperative_receipts(
10
+ message_id TEXT PRIMARY KEY REFERENCES inbox(message_id) ON DELETE CASCADE,
11
+ receipt_token TEXT NOT NULL, exposed_at INTEGER NOT NULL, acknowledged_at INTEGER)`));
12
+ const active = () => { if (isActive() !== true) throw new LocalError('session_mismatch'); inbox.assertUsable(); };
13
+ return {
14
+ async broker_receive(args = {}) {
15
+ if (!args || Array.isArray(args) || typeof args !== 'object' || Object.keys(args).length) throw new LocalError('invalid_configuration');
16
+ active();
17
+ return inbox.transaction(() => {
18
+ const head = inbox.head();
19
+ if (!head) return { message: null, local_mode: 'cooperative_pull' };
20
+ if (head.state !== 'pending') throw new LocalError('injection_uncertain', { effect: 'unknown' });
21
+ let receipt = inbox.db.prepare('SELECT receipt_token FROM cooperative_receipts WHERE message_id=?').get(head.message_id);
22
+ if (!receipt) {
23
+ receipt = { receipt_token: randomUUID() };
24
+ inbox.db.prepare('INSERT INTO cooperative_receipts(message_id,receipt_token,exposed_at) VALUES(?,?,?)').run(head.message_id, receipt.receipt_token, inbox.now());
25
+ }
26
+ return { message_id: head.message_id, message: provenance(head.envelope), receipt_token: receipt.receipt_token,
27
+ local_state: 'pending', local_mode: 'cooperative_pull', native_acceptance_recorded: false,
28
+ acknowledgment_required: true };
29
+ }, { growth: 4096 });
30
+ },
31
+ async broker_acknowledge(args) {
32
+ if (!args || typeof args !== 'object' || Array.isArray(args) || Object.keys(args).sort().join(',') !== 'message_id,receipt_token') throw new LocalError('invalid_configuration');
33
+ try { validateUuid(args.message_id); validateUuid(args.receipt_token); } catch { throw new LocalError('invalid_configuration'); }
34
+ active();
35
+ return inbox.transaction(() => {
36
+ const receipt = inbox.db.prepare('SELECT receipt_token,acknowledged_at FROM cooperative_receipts WHERE message_id=?').get(args.message_id);
37
+ if (!receipt || receipt.receipt_token !== args.receipt_token) throw new LocalError('receipt_required');
38
+ if (receipt.acknowledged_at == null) {
39
+ const head = inbox.head();
40
+ if (!head || head.message_id !== args.message_id || head.state !== 'pending') throw new LocalError('unsafe_boundary');
41
+ inbox.db.prepare('UPDATE cooperative_receipts SET acknowledged_at=? WHERE message_id=?').run(inbox.now(), args.message_id);
42
+ // consumed is local queue bookkeeping. Deliberately do NOT populate native_receipt.
43
+ inbox.db.prepare("UPDATE inbox SET state='consumed',consumed_at=? WHERE message_id=?").run(inbox.now(), args.message_id);
44
+ }
45
+ return { message_id: args.message_id, acknowledged: true, acknowledgment_kind: 'explicit_model_read',
46
+ native_acceptance_recorded: false, action_success: 'unknown' };
47
+ });
48
+ },
49
+ };
50
+ }