@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.
- package/README.md +34 -12
- package/bin/doctor.mjs +5 -0
- package/docs/candidate-verification.md +25 -0
- package/docs/integration.md +8 -6
- package/docs/message-lifecycle.md +24 -0
- package/docs/native-automation.md +62 -2
- package/docs/native-v2.md +121 -0
- package/docs/owned-run.md +4 -2
- package/docs/publishing.md +2 -2
- package/docs/reference-snapshot.sha256 +5 -5
- package/docs/sandbox-persistence.md +9 -0
- package/docs/verification.md +3 -1
- package/package.json +9 -4
- package/plugin/tui.mjs +3 -0
- package/plugin/v2.mjs +18 -0
- package/src/capabilities.mjs +13 -0
- package/src/cooperative-inbox.mjs +13 -8
- package/src/installation.mjs +2 -1
- package/src/message-format.mjs +26 -0
- package/src/native-plugin.mjs +8 -6
- package/src/opencode-adapter.mjs +31 -15
- package/src/reference-pin.mjs +6 -6
- package/src/reference.mjs +1 -1
- package/src/setup-ui.mjs +1 -1
- package/src/tool-catalog.mjs +7 -2
- package/src/tool-scope.mjs +26 -0
- package/src/v2-delivery.mjs +149 -0
- package/src/v2-inbox.mjs +33 -0
- package/src/v2-lock.mjs +17 -0
- package/src/v2-plugin.mjs +192 -0
- package/src/v2-setup.mjs +133 -0
- package/v2/LICENSE +21 -0
- package/v2/README.md +17 -0
- package/v2/index.mjs +2 -0
- package/v2/package-lock.json +420 -0
- package/v2/package.json +13 -0
- package/vendor/broker/ORIGIN.md +11 -4
- package/vendor/broker/reference/client.mjs +90 -15
- package/vendor/broker/reference/inbox.mjs +55 -10
- package/vendor/broker/reference/mcp-stdio.mjs +5 -1
- package/vendor/broker/reference/tools.mjs +38 -14
- 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 {
|
|
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).
|
|
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: '
|
|
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:
|
|
27
|
-
local_state: 'pending', local_mode: '
|
|
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(',')
|
|
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)
|
|
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');
|
package/src/installation.mjs
CHANGED
|
@@ -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
|
+
}
|
package/src/native-plugin.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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,
|
|
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?.
|
|
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 };
|
package/src/opencode-adapter.mjs
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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: '
|
|
204
|
+
return Object.freeze({ tools, start, stop, localMode: 'diagnostic_pull' });
|
|
189
205
|
}
|
package/src/reference-pin.mjs
CHANGED
|
@@ -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 = '
|
|
4
|
+
export const REFERENCE_COMMIT = '2de9cc3758b9fc593f5afca4fc501fd013d416ec';
|
|
5
5
|
const HASHES = Object.freeze({
|
|
6
|
-
"reference/inbox.mjs": "
|
|
7
|
-
"reference/client.mjs": "
|
|
6
|
+
"reference/inbox.mjs": "fbf0fa6096996cb9bc3f928e55f3eca496420880a03460e993dda3417b32ad88",
|
|
7
|
+
"reference/client.mjs": "c4c313e937d1003f28928a672072b926022aa0e9fd65825bc5db1078eb90afa9",
|
|
8
8
|
"reference/runtime.mjs": "8733076b73a42ab8f12fe20ba9908acdd754d89734ffd335e84b49aee9d737ca",
|
|
9
|
-
"reference/tools.mjs": "
|
|
10
|
-
"reference/mcp-stdio.mjs": "
|
|
9
|
+
"reference/tools.mjs": "5d3b16212a048912395d74b95a8b590d278b8b0915bb0bd7a0c3d80daf353839",
|
|
10
|
+
"reference/mcp-stdio.mjs": "f34b0cdcda9c2d607815e4ef99f1ba80fbd8936ef8c2893a858b6d408bc0ff09",
|
|
11
11
|
"reference/errors.mjs": "e33231d94f514b5a1b59fdd4184f5881dd3712bde725a1cf53352c26293452ac",
|
|
12
|
-
"src/protocol.mjs": "
|
|
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 = '
|
|
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.
|
|
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?',
|
package/src/tool-catalog.mjs
CHANGED
|
@@ -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'
|
|
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
|
+
}
|
package/src/v2-inbox.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/v2-lock.mjs
ADDED
|
@@ -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
|
+
}
|