@matuto-ai/opencode-broker-connector 0.3.0 → 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 (42) hide show
  1. package/README.md +34 -12
  2. package/bin/doctor.mjs +5 -0
  3. package/docs/candidate-verification.md +25 -0
  4. package/docs/integration.md +8 -6
  5. package/docs/message-lifecycle.md +24 -0
  6. package/docs/native-automation.md +62 -2
  7. package/docs/native-v2.md +121 -0
  8. package/docs/owned-run.md +4 -2
  9. package/docs/publishing.md +2 -2
  10. package/docs/reference-snapshot.sha256 +5 -5
  11. package/docs/sandbox-persistence.md +9 -0
  12. package/docs/verification.md +3 -1
  13. package/package.json +9 -4
  14. package/plugin/tui.mjs +3 -0
  15. package/plugin/v2.mjs +18 -0
  16. package/src/capabilities.mjs +13 -0
  17. package/src/cooperative-inbox.mjs +13 -8
  18. package/src/installation.mjs +2 -1
  19. package/src/message-format.mjs +26 -0
  20. package/src/native-plugin.mjs +8 -6
  21. package/src/opencode-adapter.mjs +31 -15
  22. package/src/reference-pin.mjs +6 -6
  23. package/src/reference.mjs +1 -1
  24. package/src/setup-ui.mjs +1 -1
  25. package/src/tool-catalog.mjs +7 -2
  26. package/src/tool-scope.mjs +26 -0
  27. package/src/v2-delivery.mjs +149 -0
  28. package/src/v2-inbox.mjs +33 -0
  29. package/src/v2-lock.mjs +17 -0
  30. package/src/v2-plugin.mjs +192 -0
  31. package/src/v2-setup.mjs +133 -0
  32. package/v2/LICENSE +21 -0
  33. package/v2/README.md +17 -0
  34. package/v2/index.mjs +2 -0
  35. package/v2/package-lock.json +420 -0
  36. package/v2/package.json +13 -0
  37. package/vendor/broker/ORIGIN.md +11 -4
  38. package/vendor/broker/reference/client.mjs +90 -15
  39. package/vendor/broker/reference/inbox.mjs +55 -10
  40. package/vendor/broker/reference/mcp-stdio.mjs +5 -1
  41. package/vendor/broker/reference/tools.mjs +38 -14
  42. package/vendor/broker/src/protocol.mjs +19 -0
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { LocalError } from '../vendor/broker/reference/errors.mjs';
3
- import { provenance } from '../vendor/broker/reference/runtime.mjs';
3
+ import { nativeMessage } from './message-format.mjs';
4
4
  import { validateUuid } from '../vendor/broker/src/protocol.mjs';
5
5
 
6
6
  /** Application-level explicit read ACK. Never a native injection receipt or action success. */
@@ -12,29 +12,34 @@ export function createCooperativeInbox({ inbox, isActive = () => true }) {
12
12
  const active = () => { if (isActive() !== true) throw new LocalError('session_mismatch'); inbox.assertUsable(); };
13
13
  return {
14
14
  async broker_receive(args = {}) {
15
- if (!args || Array.isArray(args) || typeof args !== 'object' || Object.keys(args).length) throw new LocalError('invalid_configuration');
15
+ if (!args || Array.isArray(args) || typeof args !== 'object' || Object.keys(args).some(key => key !== 'diagnostic')) throw new LocalError('invalid_configuration');
16
+ if (args.diagnostic !== true) { const error = new LocalError('unsafe_boundary', {field:'automatic_delivery_required'}); error.message = 'Automatic delivery is required. Manual receive is available only for an explicitly requested diagnostic test with diagnostic:true; nothing was consumed.'; throw error; }
16
17
  active();
17
18
  return inbox.transaction(() => {
18
19
  const head = inbox.head();
19
- if (!head) return { message: null, local_mode: 'cooperative_pull' };
20
+ if (!head) return { message: null, local_mode: 'diagnostic_pull' };
20
21
  if (head.state !== 'pending') throw new LocalError('injection_uncertain', { effect: 'unknown' });
21
22
  let receipt = inbox.db.prepare('SELECT receipt_token FROM cooperative_receipts WHERE message_id=?').get(head.message_id);
22
23
  if (!receipt) {
23
24
  receipt = { receipt_token: randomUUID() };
24
25
  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
  }
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,
27
+ return { message_id: head.message_id, message: nativeMessage(head.envelope),
28
+ local_state: 'pending', local_mode: 'diagnostic_pull', native_acceptance_recorded: false,
28
29
  acknowledgment_required: true };
29
30
  }, { growth: 4096 });
30
31
  },
31
32
  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'); }
33
+ if (!args || typeof args !== 'object' || Array.isArray(args) || !['message_id','message_id,receipt_token'].includes(Object.keys(args).sort().join(','))) throw new LocalError('invalid_configuration');
34
+ try { validateUuid(args.message_id); if (args.receipt_token !== undefined) validateUuid(args.receipt_token); } catch { throw new LocalError('invalid_configuration'); }
34
35
  active();
35
36
  return inbox.transaction(() => {
36
37
  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 || args.receipt_token !== undefined && receipt.receipt_token !== args.receipt_token) {
39
+ const error = new LocalError('receipt_required', {field: receipt ? 'receipt_token' : 'message_id'});
40
+ error.message = receipt ? 'The supplied legacy read receipt does not match. Nothing was acknowledged; this is not a transient network error. Omit the token and use the exact previously read message_id.' : 'Read this exact message in this inbox before acknowledging it. Nothing was acknowledged; this is not a transient network error.';
41
+ throw error;
42
+ }
38
43
  if (receipt.acknowledged_at == null) {
39
44
  const head = inbox.head();
40
45
  if (!head || head.message_id !== args.message_id || head.state !== 'pending') throw new LocalError('unsafe_boundary');
@@ -37,8 +37,9 @@ export async function readInstallation(directory = installationDirectory()) {
37
37
  return selected ? readExistingConnection(selected.connectionFile) : undefined;
38
38
  }
39
39
  /** No network. Compare-and-swap fences concurrent UI flows; never deletes old bundles/inboxes. */
40
- export async function selectInstallation({ directory = installationDirectory(), connectionFile, expectedRevision, signal, pendingFlowId }) {
40
+ export async function selectInstallation({ directory = installationDirectory(), connectionFile, expectedRevision, signal, pendingFlowId, verify }) {
41
41
  await readExistingConnection(connectionFile); // exact broker approval receipt, type, URL and token binding
42
+ if (verify !== undefined && (typeof verify !== 'function' || !await verify())) throw new Error('psst_setup_binding_changed');
42
43
  return withSelectionLock(directory, () => {
43
44
  let temp;
44
45
  try {
@@ -0,0 +1,26 @@
1
+ import { canonical, validateEnvelope } from '../vendor/broker/src/protocol.mjs';
2
+ import { provenance as legacyProvenance } from '../vendor/broker/reference/runtime.mjs';
3
+ import { LocalError } from '../vendor/broker/reference/errors.mjs';
4
+
5
+ export const NATIVE_FORMAT = 'compact-v1';
6
+
7
+ // Versioned presentation only. Wire validation/authenticated origin and native
8
+ // custom-message role are unchanged. Do not change compact-v1 after publication:
9
+ // persisted handoffs must reconcile against the exact format originally sent.
10
+ export function nativeMessage(rawEnvelope, format = NATIVE_FORMAT) {
11
+ if (format === null || format === 'legacy-v1') return legacyProvenance(rawEnvelope);
12
+ if (format !== NATIVE_FORMAT) throw new LocalError('invalid_configuration');
13
+ const envelope = validateEnvelope(rawEnvelope);
14
+ const sender = { name: envelope.sender.name, client_type: envelope.sender.client_type };
15
+ if (envelope.sender.project?.trim()) sender.project = envelope.sender.project;
16
+ const model = {};
17
+ for (const key of ['provider', 'id']) if (envelope.sender.model[key]?.trim()) model[key] = envelope.sender.model[key];
18
+ if (Object.keys(model).length) sender.model = { ...model, source: envelope.sender.model.source };
19
+ const data = { message_id: envelope.message_id, sender, text: envelope.content.text };
20
+ if (envelope.correlation_id) data.correlation_id = envelope.correlation_id;
21
+ if (envelope.reply_to_message_id) data.reply_to_message_id = envelope.reply_to_message_id;
22
+ // ASCII controls are JSON escaped; escape terminal/bidi controls as well.
23
+ const escaped = canonical(data).replace(/[\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/gu,
24
+ char => `\\u${char.codePointAt(0).toString(16).padStart(4, '0')}`);
25
+ return `Broker message from AI agent ${envelope.sender.client_id}\n${escaped}`;
26
+ }
@@ -1,4 +1,5 @@
1
- import { ACK_DEFINITION } from './tool-catalog.mjs';
1
+ import { deliveryCapability } from './capabilities.mjs';
2
+ import { ACK_DEFINITION, DIAGNOSTIC_RECEIVE_DEFINITION } from './tool-catalog.mjs';
2
3
  import { mkdtempSync, realpathSync } from 'node:fs';
3
4
  import { join } from 'node:path';
4
5
  import { createHash } from 'node:crypto';
@@ -10,7 +11,7 @@ import { contextMarker } from './native-context.mjs';
10
11
  const validId = value => typeof value === 'string' && /^[a-zA-Z0-9_.:-]{1,256}$/.test(value);
11
12
  /** Native, session-keyed cooperative tools. Does not attach to or control a TUI. */
12
13
  export async function createNativePlugin({ native, tool, reference, config, clientFactory, experimentalNativeDelivery = false, connectFile = async path => readConfig(path),
13
- loadInstallation = async () => undefined, contextReceiver, onDispose, adapterFactory = createOpenCodeAdapter }) {
14
+ loadInstallation = async () => undefined, contextReceiver, onDispose, adapterFactory = createOpenCodeAdapter, env = process.env }) {
14
15
  const directory = realpathSync(native.directory), worktree = realpathSync(native.worktree);
15
16
  const sessions = new Map();
16
17
  const connections = new Map(), disconnected = new Set(), connectionErrors = new Map();
@@ -124,7 +125,7 @@ export async function createNativePlugin({ native, tool, reference, config, clie
124
125
  try { return await state.starting; }
125
126
  catch (error) { if (sessions.get(id) === state) sessions.delete(id); throw error; }
126
127
  }
127
- const catalog = [...reference.TOOL_DEFINITIONS, ACK_DEFINITION];
128
+ const catalog = [...reference.TOOL_DEFINITIONS.map(def => def.name === 'broker_receive' ? DIAGNOSTIC_RECEIVE_DEFINITION : def), ACK_DEFINITION];
128
129
  const proxies = Object.fromEntries(catalog.map(definition => [definition.name, tool({
129
130
  description: definition.description,
130
131
  args: nativeShape(tool.schema, definition.inputSchema),
@@ -150,7 +151,7 @@ export async function createNativePlugin({ native, tool, reference, config, clie
150
151
  for (const [name, description] of Object.entries({
151
152
  'psst-connect': 'Connect this session using an absolute private pairing/connection file path (never a secret).',
152
153
  'psst-disconnect': 'Stop Psst messaging in this session.',
153
- 'psst-status': 'Show this session’s Psst connection status.',
154
+ 'psst-status': 'Show transport, exact client address and automatic-delivery readiness separately.',
154
155
  'psst-peers': 'List currently addressable Psst peers.',
155
156
  })) {
156
157
  if (value.command[name]) throw new Error('psst_command_conflict');
@@ -181,14 +182,15 @@ export async function createNativePlugin({ native, tool, reference, config, clie
181
182
  disconnected.delete(input.sessionID);
182
183
  try { await getSession(input.sessionID); }
183
184
  catch (error) { connections.delete(input.sessionID); throw error; }
184
- result = { connected: true, delivery: experimentalNativeDelivery ? 'experimental_fixed_snapshot' : 'cooperative_pull',
185
+ result = { connected: true, ...deliveryCapability(env, {experimental: experimentalNativeDelivery}), client_id: sessions.get(input.sessionID)?.client?.inbox?.identity?.client_id ?? null,
185
186
  ...(experimentalNativeDelivery ? { fixed_native_selection: selected, warning: 'Experimental snapshot only; no atomic native selection lease. Changing agent/model disconnects messaging.' } : {}) };
186
187
  } else if (input.command === 'psst-disconnect') {
187
188
  disconnected.add(input.sessionID); connections.delete(input.sessionID); await close(input.sessionID);
188
189
  result = { connected: false };
189
190
  } else if (input.command === 'psst-status') {
190
191
  const state = sessions.get(input.sessionID);
191
- result = { connected: !!state?.adapter, delivery: state?.delivery ? 'experimental_fixed_snapshot' : 'cooperative_pull',
192
+ result = { connected: !!state?.client, ...deliveryCapability(env, {experimental: !!state?.delivery}),
193
+ client_id: state?.client?.inbox?.identity?.client_id ?? null, identity_scope: 'native_session',
192
194
  last_delivery_status: state?.deliveryStatus ?? null,
193
195
  credential_id: state?.enrollment?.credentialId ?? null, expires_at: state?.enrollment?.expiresAt ?? null,
194
196
  connection_error: connectionErrors.get(input.sessionID) ?? null };
@@ -1,8 +1,9 @@
1
+ import { createToolScope } from './tool-scope.mjs';
1
2
  import { createCooperativeInbox } from './cooperative-inbox.mjs';
2
- import { ACK_DEFINITION } from './tool-catalog.mjs';
3
+ import { ACK_DEFINITION, DIAGNOSTIC_RECEIVE_DEFINITION } from './tool-catalog.mjs';
3
4
  import { isAbsolute, normalize } from 'node:path';
4
5
 
5
- const TOOL_NAMES = ['broker_list_clients', 'broker_send', 'broker_update_metadata', 'broker_message_status', 'broker_receive'];
6
+ const TOOL_NAMES = ['broker_list_clients', 'broker_send', 'broker_update_metadata', 'broker_message_status', 'broker_receive', 'broker_sent_history', 'broker_message_content', 'broker_resend', 'broker_cancel'];
6
7
  const localId = value => typeof value === 'string' && /^[a-zA-Z0-9_.:-]{1,256}$/.test(value);
7
8
  const absolute = value => typeof value === 'string' && isAbsolute(value) && normalize(value) === value;
8
9
  const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -62,13 +63,15 @@ function displayJSON(value) {
62
63
  * are insufficient lifetime proof. No automatic plugin attachment is implied.
63
64
  */
64
65
  export function createOpenCodeAdapter({ reference, tool, binding, currentBinding,
65
- client, inbox = client?.inbox, observeMetadata = null, onDiagnostic = () => {}, cooperativeAck = false }) {
66
+ client, inbox = client?.inbox, observeMetadata = null, onDiagnostic = () => {}, cooperativeAck = false, operationContext = null, closeSessionOnStop = true }) {
66
67
  if (!reference || typeof reference.AdapterRuntime !== 'function' || typeof reference.createTools !== 'function'
67
68
  || typeof reference.invokeTool !== 'function' || typeof reference.safeError !== 'function'
68
69
  || typeof reference.LocalError !== 'function' || !Array.isArray(reference.TOOL_DEFINITIONS)
69
70
  || typeof tool !== 'function' || !tool.schema || !client || !inbox
70
71
  || !binding || !localId(binding.sessionID) || !absolute(binding.directory) || !absolute(binding.worktree)
71
72
  || typeof binding.lifetime !== 'symbol' || !(binding.signal instanceof AbortSignal)
73
+ || typeof closeSessionOnStop !== 'boolean'
74
+ || operationContext !== null && typeof operationContext !== 'function'
72
75
  || typeof currentBinding !== 'function' || observeMetadata !== null && typeof observeMetadata !== 'function'
73
76
  || inbox.identity?.client_type !== 'opencode' || client.inbox !== inbox) {
74
77
  throw new Error('Invalid adapter configuration: provide a verified exact host binding and shared broker reference objects.');
@@ -78,13 +81,17 @@ export function createOpenCodeAdapter({ reference, tool, binding, currentBinding
78
81
  || new Set(definitions.map(definition => definition.name)).size !== TOOL_NAMES.length) {
79
82
  throw new Error('Shared tool catalog changed; review it before enabling this adapter.');
80
83
  }
81
- const catalog = [...definitions, ...(cooperativeAck ? [ACK_DEFINITION] : [])].map(definition => {
84
+ const catalog = [...definitions, ...(cooperativeAck ? [ACK_DEFINITION] : [])].map(original => {
85
+ const definition = cooperativeAck && original.name === 'broker_receive' ? DIAGNOSTIC_RECEIVE_DEFINITION : original;
82
86
  const args = nativeShape(tool.schema, definition.inputSchema);
83
87
  return { definition, args, validator: tool.schema.object(args).strict() };
84
88
  });
85
89
  const expected = Object.freeze({ sessionID: binding.sessionID, directory: binding.directory,
86
90
  worktree: binding.worktree, lifetime: binding.lifetime, signal: binding.signal });
87
91
  const identity = Object.freeze({ ...inbox.identity });
92
+ const toolScope = createToolScope({ closedError: () => new reference.LocalError('session_mismatch', { effect: 'unknown' }), abortedError: () => new reference.LocalError('request_aborted') });
93
+ const recoveryAbort = new AbortController();
94
+ let recoveryPromise;
88
95
  let started = false, closed = false, startPromise, runtimeStartPromise, stopPromise;
89
96
  const diagnostic = code => { try { onDiagnostic({ code }); } catch { /* No raw exception data. */ } };
90
97
  function sameIdentity(candidate) {
@@ -104,16 +111,19 @@ export function createOpenCodeAdapter({ reference, tool, binding, currentBinding
104
111
  onDiagnostic: entry => diagnostic(entry.code) });
105
112
  async function stop() {
106
113
  if (stopPromise) return stopPromise;
107
- closed = true; started = false;
114
+ closed = true; started = false; recoveryAbort.abort();
115
+ const toolsClosed = toolScope.close();
108
116
  expected.signal.removeEventListener('abort', onClose);
117
+ // Fence transport lifetime synchronously, before any deferred tool drain.
118
+ const runtimeStopped = runtime.stop({ closeSession: false });
109
119
  stopPromise = Promise.resolve().then(async () => {
110
120
  try {
111
- await runtime.stop({ closeSession: false }); // Cancel, but do not close a stale pre-reconnect epoch.
121
+ await runtimeStopped; // Cancel, but do not close a stale pre-reconnect epoch.
112
122
  await runtimeStartPromise?.catch(() => {});
113
123
  // Registration may have committed after cancellation. Keep
114
124
  // its journal open until a second close fences that exact late lease.
115
- await runtime.stop();
116
- } finally { inbox.close(); }
125
+ await runtime.stop({ closeSession: closeSessionOnStop });
126
+ } finally { await toolsClosed; await recoveryPromise?.catch(() => {}); inbox.close(); }
117
127
  });
118
128
  return stopPromise;
119
129
  }
@@ -141,6 +151,11 @@ export function createOpenCodeAdapter({ reference, tool, binding, currentBinding
141
151
  runtimeStartPromise = runtime.start(metadata);
142
152
  const result = await runtimeStartPromise;
143
153
  await assertBound(undefined, 'unknown');
154
+ // No durable tool-call ID exists in this pinned ToolContext. Reconcile
155
+ // private operations after exact-session reattachment, never guess from text/messageID.
156
+ recoveryPromise = client.recoverOutgoing?.({ limit: 10, signal: AbortSignal.any([recoveryAbort.signal, expected.signal, AbortSignal.timeout(5000)]) });
157
+ await recoveryPromise?.catch(() => diagnostic('outgoing_recovery_incomplete'));
158
+ await assertBound(undefined, 'unknown');
144
159
  started = true;
145
160
  return result;
146
161
  } catch (error) {
@@ -153,6 +168,7 @@ export function createOpenCodeAdapter({ reference, tool, binding, currentBinding
153
168
  const tools = Object.fromEntries(catalog.map(({ definition, args, validator }) => {
154
169
  return [definition.name, tool({ description: definition.description, args,
155
170
  async execute(arguments_, context) {
171
+ return toolScope.run(async operationSignal => {
156
172
  try {
157
173
  if (!started || !context) throw new reference.LocalError('session_mismatch');
158
174
  await assertBound(context);
@@ -160,8 +176,9 @@ export function createOpenCodeAdapter({ reference, tool, binding, currentBinding
160
176
  if (!parsed.success) throw new reference.LocalError('invalid_configuration', { field: 'arguments' });
161
177
  // Per-invocation closure, never a mutable global current tool context.
162
178
  const scopedClient = { inbox, get attachment() { return client.attachment; },
163
- prepareSend: (...args_) => client.prepareSend(...args_) };
164
- for (const method of ['listClients', 'sendPrepared', 'updateMetadata', 'messageStatus', 'refreshOwnedMetadata']) {
179
+ prepareSend: (...args_) => client.prepareSend(...args_),
180
+ prepareResend: (...args_) => client.prepareResend(...args_) };
181
+ for (const method of ['listClients', 'sendPrepared', 'updateMetadata', 'messageStatus', 'refreshOwnedMetadata', 'resendPrepared', 'sentHistory', 'messageContent', 'cancelMessage']) {
165
182
  scopedClient[method] = async (...args_) => {
166
183
  await assertBound(context);
167
184
  return client[method](...args_);
@@ -170,20 +187,19 @@ export function createOpenCodeAdapter({ reference, tool, binding, currentBinding
170
187
  const facade = reference.createTools({ client: scopedClient, inbox, observeMetadata,
171
188
  cooperativeBoundary: async read => { await assertBound(context); return read(); } });
172
189
  if (cooperativeInbox) Object.assign(facade, cooperativeInbox);
173
- const result = await reference.invokeTool(facade, definition.name, parsed.data);
190
+ const result = await reference.invokeTool(facade, definition.name, parsed.data, { ...(operationContext ? operationContext(context) : {}), signal: AbortSignal.any([operationSignal, expected.signal]) });
174
191
  // Unknown/cancelled host results never get repackaged as acceptance.
175
- // A write may already have committed; retain its stable send key.
192
+ // The private outgoing ledger retains any uncertain operation and its key.
176
193
  try { await assertBound(context, 'unknown'); }
177
194
  catch (error) {
178
195
  const failure = reference.safeError(error);
179
- const sendKey = result?.send_key ?? parsed.data.send_key;
180
- if (definition.name === 'broker_send' && typeof sendKey === 'string') failure.send_key = sendKey;
181
196
  return displayJSON(failure);
182
197
  }
183
198
  return displayJSON(result);
184
199
  } catch (error) { return displayJSON(reference.safeError(error)); }
200
+ }, context?.abort).catch(error => displayJSON(reference.safeError(error)));
185
201
  },
186
202
  })];
187
203
  }));
188
- return Object.freeze({ tools, start, stop, localMode: 'cooperative_pull' });
204
+ return Object.freeze({ tools, start, stop, localMode: 'diagnostic_pull' });
189
205
  }
@@ -1,15 +1,15 @@
1
1
  import { readFileSync, lstatSync } from 'node:fs';
2
2
  import { join, isAbsolute } from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
- export const REFERENCE_COMMIT = 'ba2219cd87a1feac23e01b23d2c2b3d3bdd65359';
4
+ export const REFERENCE_COMMIT = '2de9cc3758b9fc593f5afca4fc501fd013d416ec';
5
5
  const HASHES = Object.freeze({
6
- "reference/inbox.mjs": "93f75786eab29429ace024974e0f61d6ad46d2c5fe065dae387e54fd2373b167",
7
- "reference/client.mjs": "f661b45acba106be03d97dacba3cd90ac18b09e7ff9eb93b995d5036d9d1c785",
6
+ "reference/inbox.mjs": "fbf0fa6096996cb9bc3f928e55f3eca496420880a03460e993dda3417b32ad88",
7
+ "reference/client.mjs": "c4c313e937d1003f28928a672072b926022aa0e9fd65825bc5db1078eb90afa9",
8
8
  "reference/runtime.mjs": "8733076b73a42ab8f12fe20ba9908acdd754d89734ffd335e84b49aee9d737ca",
9
- "reference/tools.mjs": "5e6355cef1bec56170dca14b93142a7da8c102f411c5dbf437d84acda7351acf",
10
- "reference/mcp-stdio.mjs": "10a93788af426ce898c398e38dd41ac77b6e0129a7697bdc36c8d4f6d6e20138",
9
+ "reference/tools.mjs": "5d3b16212a048912395d74b95a8b590d278b8b0915bb0bd7a0c3d80daf353839",
10
+ "reference/mcp-stdio.mjs": "f34b0cdcda9c2d607815e4ef99f1ba80fbd8936ef8c2893a858b6d408bc0ff09",
11
11
  "reference/errors.mjs": "e33231d94f514b5a1b59fdd4184f5881dd3712bde725a1cf53352c26293452ac",
12
- "src/protocol.mjs": "f3ff24b4429dddc8a9c10513ab72dbc613c91244591132458c7866a812d6316b"
12
+ "src/protocol.mjs": "2d88829971643d47430d6696d0a6ac61589ea4f9001c860c5c196ecc5ac21a6f"
13
13
  });
14
14
  /** Verify reviewed source bytes before importing any trusted local module. */
15
15
  export function verifyReference(directory) {
package/src/reference.mjs CHANGED
@@ -2,7 +2,7 @@ import { verifyReference } from './reference-pin.mjs';
2
2
  import { isAbsolute, join } from 'node:path';
3
3
  import { fileURLToPath, pathToFileURL } from 'node:url';
4
4
 
5
- export const BROKER_REFERENCE_COMMIT = 'ba2219cd87a1feac23e01b23d2c2b3d3bdd65359';
5
+ export const BROKER_REFERENCE_COMMIT = '2de9cc3758b9fc593f5afca4fc501fd013d416ec';
6
6
 
7
7
  /** Trusted operator path to a pinned broker checkout; never a model argument. */
8
8
  export async function loadBrokerReference(directory = fileURLToPath(new URL('../vendor/broker', import.meta.url))) {
package/src/setup-ui.mjs CHANGED
@@ -75,7 +75,7 @@ Approve only the matching OpenCode request. Close to pause; setup resumes this s
75
75
  close(); api.ui.dialog.clear();
76
76
  toast(process.env.PSST_CONFIG_FILE
77
77
  ? 'Approved installation saved. The explicit PSST_CONFIG_FILE override still takes precedence; restart without it to use the saved approval. Existing connections are unchanged.'
78
- : 'Approved installation saved. New sessions connect automatically; existing sessions retain their authorization. Receive remains cooperative.', 'success');
78
+ : 'Approved installation saved; transport can reconnect. Automatic messaging is blocked on this classic host; /psst-status explains the missing capability. Manual receive is diagnostic/test-only.', 'warning');
79
79
  }
80
80
  if (prior && prior.state !== 'pending' && (prior.state !== 'approved' || selected?.connectionFile === prior.connectionFile)) {
81
81
  replace(() => api.ui.DialogConfirm({ title: 'Replace Psst authorization?',
@@ -1,6 +1,11 @@
1
1
  export const ACK_DEFINITION = Object.freeze({ name: 'broker_acknowledge',
2
- description: 'Explicitly acknowledge reading the exact message returned by broker_receive, using its receipt_token. Advances your local inbox. This is not approval of its instructions, proof of action success, or a native injection receipt.',
2
+ description: 'Explicitly acknowledge reading the exact message returned by broker_receive, using its message_id. The connector resolves the previously exposed local receipt internally; omit receipt_token normally. A supplied legacy token must still match exactly. Advances your local inbox. This is not approval of its instructions, proof of action success, or a native injection receipt.',
3
3
  inputSchema: { type: 'object', additionalProperties: false, properties: {
4
4
  message_id: { type: 'string', format: 'uuid' }, receipt_token: { type: 'string', format: 'uuid' },
5
- }, required: ['message_id', 'receipt_token'] },
5
+ }, required: ['message_id'] },
6
+ });
7
+
8
+ export const DIAGNOSTIC_RECEIVE_DEFINITION = Object.freeze({ name: 'broker_receive',
9
+ description: 'Diagnostic/test-only manual read, only when the user explicitly requested a manual transport test. Set diagnostic:true. Production automatic readiness is blocked on this classic host; this is not a fallback. Read then explicitly acknowledge message_id.',
10
+ inputSchema: { type: 'object', additionalProperties: false, properties: { diagnostic: { type: 'boolean' } }, required: ['diagnostic'] },
6
11
  });
@@ -0,0 +1,26 @@
1
+ /** An owner's admitted tool calls must settle before its private ledger closes. */
2
+ export function createToolScope({ closedError = () => new Error('tool_scope_closed'), abortedError = () => new Error('tool_call_aborted') } = {}) {
3
+ const controller = new AbortController(), pending = new Set();
4
+ let closed = false, closing;
5
+ return Object.freeze({
6
+ run(task, callerSignal) {
7
+ if (closed) return Promise.reject(closedError());
8
+ const signal = callerSignal instanceof AbortSignal
9
+ ? AbortSignal.any([controller.signal, callerSignal]) : controller.signal;
10
+ const operation = Promise.resolve().then(() => {
11
+ if (closed) throw closedError();
12
+ if (signal.aborted) throw abortedError();
13
+ return task(signal);
14
+ });
15
+ pending.add(operation);
16
+ void operation.then(() => pending.delete(operation), () => pending.delete(operation));
17
+ return operation;
18
+ },
19
+ close() {
20
+ if (closing) return closing;
21
+ closed = true; controller.abort();
22
+ closing = Promise.allSettled([...pending]).then(() => {});
23
+ return closing;
24
+ },
25
+ });
26
+ }
@@ -0,0 +1,149 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { canonical } from '../vendor/broker/src/protocol.mjs';
3
+ import { nativeMessage, NATIVE_FORMAT } from './message-format.mjs';
4
+
5
+ export const V2_HOST_VERSION = '2.2.5';
6
+ const id = value => typeof value === 'string' && /^[A-Za-z0-9_.:-]{1,256}$/.test(value);
7
+ export function v2SessionBinding(info, hostID) {
8
+ if (!id(info?.id) || !id(hostID) || !id(info?.projectID)
9
+ || !Number.isSafeInteger(info?.time?.created) || info.time.created <= 0
10
+ || typeof info?.location?.directory !== 'string' || !info.location.directory.startsWith('/')) throw new Error('v2_session_binding_unavailable');
11
+ return { hostID, sessionID: info.id, created: info.time.created, projectID: info.projectID,
12
+ location: { directory: info.location.directory, ...(info.location.workspaceID ? { workspaceID: info.location.workspaceID } : {}) } };
13
+ }
14
+
15
+ /** Native V2 queue admission, never classic promptAsync or a human-role prompt.
16
+ * A successful receipt means durable native admission, NOT model consumption.
17
+ * No mutation retry: native cancellation removes its idempotency lookup row.
18
+ */
19
+ export function createV2Delivery({ session, inbox, binding, signal, verify, interval = 250,
20
+ requestTimeout = 2000, onDiagnostic = () => {} }) {
21
+ if (!session || ['synthetic', 'context'].some(key => typeof session[key] !== 'function')
22
+ || !(signal instanceof AbortSignal) || typeof verify !== 'function' || !id(binding?.sessionID)
23
+ || !Number.isSafeInteger(interval) || interval < 10 || interval > 60000
24
+ || !Number.isSafeInteger(requestTimeout) || requestTimeout < 10 || requestTimeout > 30000) throw new Error('v2_capability_unavailable');
25
+ inbox.transaction(() => inbox.db.exec('CREATE TABLE IF NOT EXISTS v2_state(key TEXT PRIMARY KEY, value TEXT NOT NULL)'));
26
+ const getState = key => { inbox.assertUsable(); const row = inbox.db.prepare('SELECT value FROM v2_state WHERE key=?').get(key); return row ? JSON.parse(row.value) : null; };
27
+ const setState = (key, value) => inbox.transaction(() => inbox.db.prepare('INSERT INTO v2_state(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value').run(key, canonical(value)));
28
+ const persisted = getState('v2_binding');
29
+ if (persisted && canonical(persisted) !== canonical(binding)) throw new Error('v2_session_mismatch');
30
+ if (!persisted) setState('v2_binding', binding);
31
+ inbox.transaction(() => inbox.db.exec(`CREATE TABLE IF NOT EXISTS v2_submissions (
32
+ broker_message_id TEXT PRIMARY KEY REFERENCES inbox(message_id) ON DELETE CASCADE,
33
+ native_id TEXT NOT NULL UNIQUE, injection_id TEXT NOT NULL, payload TEXT NOT NULL,
34
+ promoted INTEGER NOT NULL DEFAULT 0)`));
35
+ let stopped = false, running, timer, epoch = 0;
36
+ const active = () => !stopped && !signal.aborted;
37
+ const diagnostic = code => { try { onDiagnostic({ code }); } catch {} };
38
+ async function call(method, input, permitted = active) {
39
+ const bounded = AbortSignal.any([signal, AbortSignal.timeout(requestTimeout)]);
40
+ let listener;
41
+ try {
42
+ return await Promise.race([
43
+ Promise.resolve().then(() => { if (bounded.aborted || !permitted()) throw new Error('v2_request_aborted'); return method(input, { signal: bounded }); }),
44
+ new Promise((_, reject) => { listener = () => reject(new Error('v2_request_incomplete'));
45
+ if (bounded.aborted) listener(); else bounded.addEventListener('abort', listener, { once: true }); }),
46
+ ]);
47
+ } finally { bounded.removeEventListener('abort', listener); }
48
+ }
49
+ async function current() {
50
+ if (!active()) return null;
51
+ const info = await verify();
52
+ if (!active() || !info || canonical(v2SessionBinding(info, binding.hostID)) !== canonical(binding)
53
+ || info.revert || info.time?.archived) return null;
54
+ return info;
55
+ }
56
+ function exactPayload(candidate, target) {
57
+ return candidate && canonical(candidate) === target.payload;
58
+ }
59
+ function exactAdmission(receipt, target) {
60
+ return receipt?.id === target.native_id && receipt.sessionID === binding.sessionID
61
+ && receipt.type === 'synthetic' && receipt.delivery === 'queue' && exactPayload(receipt.payload, target);
62
+ }
63
+ function exactContext(message, target) {
64
+ return message?.id === target.native_id && message.type === 'synthetic'
65
+ && exactPayload({ text: message.text, description: message.description, metadata: message.metadata }, target);
66
+ }
67
+ async function contextProof(target) {
68
+ const messages = await call(session.context.bind(session), { sessionID: binding.sessionID });
69
+ if (!Array.isArray(messages)) return false;
70
+ return messages.some(message => exactContext(message, target));
71
+ }
72
+ function accept(head, target) {
73
+ inbox.recordNativeAcceptance(head.message_id, target.injection_id, {
74
+ accepted: true, receipt_id: `${binding.sessionID}/${target.native_id}`,
75
+ });
76
+ }
77
+ async function deliver() {
78
+ const observedEpoch = epoch;
79
+ const info = await current();
80
+ if (!info) return;
81
+ // Interrupt/failure is durable across reload. Never wake a cancelled or
82
+ // failed session just because the broker has another pending envelope.
83
+ const idle = info.time.idle ?? 0, busy = info.time.busy ?? 0;
84
+ if (['interrupted', 'failed'].includes(info.outcome) && idle >= busy || getState('v2_paused')) return;
85
+ const head = inbox.head();
86
+ if (!head) return;
87
+ if (head.state !== 'pending') {
88
+ const target = inbox.db.prepare('SELECT * FROM v2_submissions WHERE broker_message_id=?').get(head.message_id);
89
+ if (!target || target.injection_id !== head.injection_id) return;
90
+ if (await contextProof(target) && active() && observedEpoch === epoch && await current()) {
91
+ accept(head, target);
92
+ inbox.transaction(() => inbox.db.prepare('UPDATE v2_submissions SET promoted=1 WHERE broker_message_id=?').run(head.message_id));
93
+ diagnostic('native_admission_reconciled');
94
+ }
95
+ return;
96
+ }
97
+ // Bound native backlog to one outstanding item. A lost delivered event is
98
+ // recovered by exact read-only context inspection, never another submit.
99
+ const outstanding = inbox.db.prepare('SELECT * FROM v2_submissions WHERE promoted=0 ORDER BY rowid LIMIT 1').get();
100
+ if (outstanding) {
101
+ if (!await contextProof(outstanding)) return;
102
+ if (!active() || observedEpoch !== epoch || !await current()) return;
103
+ inbox.transaction(() => inbox.db.prepare('UPDATE v2_submissions SET promoted=1 WHERE broker_message_id=?').run(outstanding.broker_message_id));
104
+ }
105
+ if (!active() || observedEpoch !== epoch || !await current()) return;
106
+ const payload = { text: nativeMessage(head.envelope), description: 'Psst agent message', metadata: {
107
+ source: 'psst', broker_message_id: head.message_id, sender_client_id: head.envelope.sender.client_id, format: NATIVE_FORMAT,
108
+ } };
109
+ const target = { native_id: 'msg_psst_' + randomUUID().replaceAll('-', ''), injection_id: randomUUID(), payload: canonical(payload) };
110
+ // State and exact immutable payload are committed atomically before effect.
111
+ inbox.transaction(() => {
112
+ const now = inbox.head();
113
+ if (now?.message_id !== head.message_id || now.state !== 'pending') throw new Error('v2_inbox_changed');
114
+ inbox.db.prepare('INSERT INTO v2_submissions(broker_message_id,native_id,injection_id,payload) VALUES(?,?,?,?)')
115
+ .run(head.message_id, target.native_id, target.injection_id, target.payload);
116
+ inbox.db.prepare("UPDATE inbox SET state='injecting',injection_id=? WHERE message_id=?").run(target.injection_id, head.message_id);
117
+ }, { growth: Buffer.byteLength(target.payload) * 2 + 8192 });
118
+ // Even an aborted request may have reached the server. Leave its journal
119
+ // pinned, including on timeout, exception, cancellation or payload mismatch.
120
+ if (!active() || observedEpoch !== epoch) return;
121
+ try {
122
+ const receipt = await call(session.synthetic.bind(session), { sessionID: binding.sessionID,
123
+ id: target.native_id, ...payload, delivery: 'queue', resume: true }, () => active() && observedEpoch === epoch);
124
+ if (!active() || observedEpoch !== epoch || !await current()) return;
125
+ if (!exactAdmission(receipt, target)) { diagnostic('native_receipt_mismatch'); return; }
126
+ accept(head, target);
127
+ diagnostic('native_admission_confirmed');
128
+ } catch { diagnostic('native_admission_uncertain'); }
129
+ }
130
+ function tick() {
131
+ if (!active() || running) return running;
132
+ running = Promise.resolve().then(deliver).catch(() => diagnostic('native_delivery_paused')).finally(() => { running = null; });
133
+ return running;
134
+ }
135
+ return {
136
+ tick,
137
+ start() { if (!timer && active()) { timer = setInterval(tick, interval); timer.unref?.(); } },
138
+ event(event) {
139
+ if (!active() || event?.data?.sessionID !== binding.sessionID) return;
140
+ if (['session.execution.interrupted', 'session.execution.failed', 'session.inbox.cancelled', 'session.revert.staged'].includes(event.type)) {
141
+ epoch++; setState('v2_paused', event.type); diagnostic('native_delivery_paused');
142
+ }
143
+ // Only host-started work, not an idle poll, can reopen cancellation.
144
+ if (event.type === 'session.execution.started') setState('v2_paused', null);
145
+ if (['session.deleted', 'session.moved', 'session.archived'].includes(event.type)) { epoch++; stopped = true; clearInterval(timer); }
146
+ },
147
+ async stop() { stopped = true; epoch++; clearInterval(timer); await running; },
148
+ };
149
+ }
@@ -0,0 +1,33 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { openSync, closeSync, readFileSync, writeFileSync, fsyncSync, fstatSync, constants } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { canonical } from '../vendor/broker/src/protocol.mjs';
5
+ import { privateDirectory } from './config.mjs';
6
+
7
+ /** Reopen only this installation + native server store + exact native session.
8
+ * Exclusive Inbox ownership protects concurrent owners. Never reset a partial,
9
+ * foreign or unreadable manifest: its broker/native effects may be unknown.
10
+ */
11
+ export function openV2Inbox({ reference, settings, binding }) {
12
+ const key = createHash('sha256').update(canonical({ binding, broker: settings.broker.url })).digest('hex');
13
+ const directory = privateDirectory(join(privateDirectory(settings.stateDirectory), 'opencode-v2-' + key));
14
+ const path = join(directory, 'binding.json');
15
+ let fd;
16
+ try {
17
+ fd = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
18
+ writeFileSync(fd, canonical({ version: 1, binding, broker: settings.broker.url, identity: reference.newSessionIdentity('opencode') }) + '\n');
19
+ fsyncSync(fd); closeSync(fd); fd = undefined;
20
+ const parent = openSync(directory, constants.O_RDONLY); try { fsyncSync(parent); } finally { closeSync(parent); }
21
+ } catch (error) { if (error.code !== 'EEXIST') throw error; }
22
+ finally { if (fd !== undefined) closeSync(fd); }
23
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
24
+ let manifest;
25
+ try {
26
+ const stat = fstatSync(fd);
27
+ if (!stat.isFile() || stat.mode & 0o077 || stat.size > 8192 || process.getuid && stat.uid !== process.getuid()) throw new Error('v2_binding_unavailable');
28
+ manifest = JSON.parse(readFileSync(fd, 'utf8'));
29
+ } finally { closeSync(fd); }
30
+ if (manifest.version !== 1 || manifest.broker !== settings.broker.url || canonical(manifest.binding) !== canonical(binding)
31
+ || Object.keys(manifest).sort().join(',') !== 'binding,broker,identity,version') throw new Error('v2_binding_mismatch');
32
+ return new reference.Inbox({ directory, identity: manifest.identity });
33
+ }
@@ -0,0 +1,17 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { openSync, closeSync, lstatSync, constants } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { privateDirectory } from './config.mjs';
5
+
6
+ /** Cross-location/process ownership, released by the OS on crash. No stale reset. */
7
+ export async function withV2Lock(directory, name, action) {
8
+ if (!['host-anchor','setup'].includes(name)) throw new Error('v2_lock_invalid');
9
+ const path=join(privateDirectory(directory),`v2-${name}-owner.sqlite`);
10
+ try { const fd=openSync(path,constants.O_CREAT|constants.O_EXCL|constants.O_WRONLY|constants.O_NOFOLLOW,0o600);closeSync(fd); }
11
+ catch(error){if(error.code!=='EEXIST')throw error;}
12
+ const stat=lstatSync(path);
13
+ if(!stat.isFile()||stat.isSymbolicLink()||stat.mode&0o077||process.getuid&&stat.uid!==process.getuid())throw new Error('v2_lock_invalid');
14
+ let db;
15
+ try{db=new DatabaseSync(path);db.exec('PRAGMA busy_timeout=0; BEGIN EXCLUSIVE; CREATE TABLE IF NOT EXISTS owner(id INTEGER)');return await action();}
16
+ finally{db?.close();}
17
+ }