@matuto-ai/opencode-broker-connector 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +89 -2
- package/bin/launch.mjs +11 -0
- package/docs/integration.md +101 -0
- package/docs/owned-run-checkpoint.md +23 -0
- package/docs/owned-run.md +68 -0
- package/docs/publishing.md +25 -0
- package/docs/reference-snapshot.sha256 +11 -0
- package/docs/verification.md +112 -0
- package/package.json +37 -4
- package/plugin/index.mjs +7 -0
- package/plugin/owned-run.ts +7 -0
- package/src/config.mjs +42 -0
- package/src/cooperative-inbox.mjs +50 -0
- package/src/native-plugin.mjs +105 -0
- package/src/opencode-adapter.mjs +189 -0
- package/src/owned-run.mjs +193 -0
- package/src/reference-pin.mjs +21 -0
- package/src/reference.mjs +20 -0
- package/src/tool-catalog.mjs +6 -0
- package/vendor/broker/LICENSE +21 -0
- package/vendor/broker/ORIGIN.md +5 -0
- package/vendor/broker/reference/client.mjs +318 -0
- package/vendor/broker/reference/errors.mjs +42 -0
- package/vendor/broker/reference/inbox.mjs +289 -0
- package/vendor/broker/reference/mcp-stdio.mjs +153 -0
- package/vendor/broker/reference/runtime.mjs +306 -0
- package/vendor/broker/reference/tools.mjs +59 -0
- package/vendor/broker/src/protocol.mjs +299 -0
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
|
-
#
|
|
1
|
+
# Psst connector for OpenCode
|
|
2
2
|
|
|
3
|
-
|
|
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.1` 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.1"]}
|
|
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,25 @@
|
|
|
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 (next `v0.1.1`).
|
|
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
|
+
## Patch release
|
|
17
|
+
|
|
18
|
+
This PR prepares `0.1.1`; the intended GitHub release tag is `v0.1.1`.
|
|
19
|
+
After review and merge, the maintainer can publish that release from the merged
|
|
20
|
+
commit to exercise npm Trusted Publishing. This code change does not itself
|
|
21
|
+
create a tag, release, or npm publication. Verify the workflow succeeds and the
|
|
22
|
+
scoped npm version exists before recommending it for installation.
|
|
23
|
+
|
|
24
|
+
Existing `0.1.0` npm packages and old GitHub tags remain untouched. Do not reuse
|
|
25
|
+
or move an old release tag; the new tag must exactly match the package version.
|
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
}
|
package/plugin/index.mjs
ADDED
|
@@ -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
|
+
}
|