workbite 0.1.19 → 0.1.21

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/lib/agent.js CHANGED
@@ -1,240 +1,240 @@
1
- 'use strict';
2
- // WorkBite agent interface: binds one already-running, WorkBite-owned Work Stack
3
- // owner and forwards curated agent commands to it through the pinned native
4
- // client's v1 `cli.execute` RPC. The native client is the only mapper to the
5
- // owner's versioned APIs. Nothing here starts, downloads, initializes or adopts
6
- // an owner, falls back to another workspace or tool, or replays a write.
7
- const path = require('node:path');
8
- const { spawn } = require('node:child_process');
9
- const { randomBytes } = require('node:crypto');
10
- const { fail, exists, readSmall, sha256, UUID } = require('./common');
11
- const { cleanEnvironment, stateLayout, workspaceKey, readWorkspaceUid, rememberedWorkspace, engineIdentity } = require('./runtime');
12
-
13
- const OWNER_SCHEMA = 'workbite.owner.v2';
14
- const LOOPBACK = /^http:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/;
15
- const LAUNCH = /^[0-9a-f]{16}$/;
16
- const MAX_PACKET_BYTES = 32768;
17
- const MAX_TRANSFER_BYTES = 1024 * 1024;
18
- const MAX_ARGV = 128;
19
- const MAX_STDOUT = 1024 * 1024 + 8192;
20
- const MAX_STDERR = 65536;
21
- const DEFAULT_DEADLINE = 30000;
22
- // Placeholder authority for the static capability table only; the client never contacts it for that command.
23
- const NO_ENDPOINT = 'http://127.0.0.1:9';
24
- const NO_UID = '00000000-0000-4000-8000-000000000000';
25
-
26
- // Curated command paths; every other native command (raw rpc, legacy, deploy, delete, migration) is refused.
27
- const AGENT_COMMANDS = Object.freeze(['capabilities', 'status', 'tasks list', 'tasks create', 'tasks update', 'context',
28
- 'checkpoint', 'operation-status', 'export', 'import', 'import-status', 'import-rollback']);
29
- const WRITE_COMMANDS = Object.freeze(['tasks create', 'tasks update', 'checkpoint', 'import', 'import-rollback']);
30
- const FORBIDDEN_FLAGS = new Set(['--data-dir', '--endpoint', '--token-env', '--token', '--workspace', '--read-retries',
31
- '--timeout-ms', '--request-id', '--generation', '--packet-file']);
32
-
33
- const EXPLICIT = 'Name it with --workspace ABS --workspace-uid UUID, or open it once with: workbite open --workspace ABS';
34
- const bindings = new WeakMap();
35
-
36
- // Only the bound --workspace-uid (once, exactly that UID) may precede the command words.
37
- function commandOf(argv, uid) {
38
- if (!Array.isArray(argv) || argv[0] !== 'agent') return null;
39
- let start = 1;
40
- if (typeof uid === 'string' && uid) {
41
- if (argv[1] === '--workspace-uid' && argv[2] === uid) start = 3;
42
- else if (argv[1] === `--workspace-uid=${uid}`) start = 2;
43
- }
44
- const words = [];
45
- for (const item of argv.slice(start)) { if (item.startsWith('-')) break; words.push(item); }
46
- const name = words.join(' ');
47
- return AGENT_COMMANDS.includes(name) ? name : null;
48
- }
49
- function checkedArgv(argv, uid) {
50
- if (!Array.isArray(argv) || !argv.length || argv.length > MAX_ARGV
51
- || argv.some(item => typeof item !== 'string' || item.includes('\0'))
52
- || argv.reduce((total, item) => total + Buffer.byteLength(item), 0) > MAX_PACKET_BYTES) fail('Invalid WorkBite agent arguments');
53
- const command = commandOf(argv, uid);
54
- if (!command) fail('Unsupported WorkBite agent command; supported: ' + AGENT_COMMANDS.join(', '));
55
- argv.forEach((item, index) => {
56
- const key = item.split('=')[0];
57
- if (FORBIDDEN_FLAGS.has(key)) fail(`WorkBite agent commands do not accept ${key}; authority comes from the binding`);
58
- if (key === '--workspace-uid') {
59
- const value = item.includes('=') ? item.slice(item.indexOf('=') + 1) : argv[index + 1];
60
- if (value !== uid) fail('--workspace-uid does not match the bound WorkBite workspace');
61
- }
62
- });
63
- return command;
64
- }
65
- function checkedStdin(stdin, command) {
66
- if (stdin === undefined || stdin === null) return null;
67
- const maximum = command === 'import' ? MAX_TRANSFER_BYTES : MAX_PACKET_BYTES;
68
- if (typeof stdin !== 'string' || Buffer.byteLength(stdin) > maximum || stdin.includes('\0') || stdin.isWellFormed?.() === false) {
69
- fail(`WorkBite agent input must be UTF-8 text of at most ${maximum} bytes`);
70
- }
71
- return stdin;
72
- }
73
- // Capture recovery identity before launch; never reconstruct it from native error text.
74
- function checkedIntent(argv, command, write) {
75
- const flags = argv.filter(item => item.split('=')[0] === '--intent-id');
76
- if (!flags.length && !write) return null;
77
- if (flags.length !== 1 || (!WRITE_COMMANDS.includes(command)
78
- && !['operation-status', 'import-status'].includes(command)) || argv.includes('--')) {
79
- fail('Invalid or repeated --intent-id');
80
- }
81
- const flag = flags[0];
82
- const value = flag.includes('=') ? flag.slice(flag.indexOf('=') + 1) : argv[argv.indexOf(flag) + 1];
83
- if (typeof value !== 'string' || (!flag.includes('=') && value.startsWith('--'))
84
- || value.length < 8 || value.length > 128 || /[^A-Za-z0-9._:-]/.test(value)) fail('Invalid --intent-id');
85
- return value;
86
- }
87
- function clientCommand(installed, commands) {
88
- const command = commands?.workstack_client || [installed?.executables?.workstack_client];
89
- if (!Array.isArray(command) || typeof command[0] !== 'string' || !path.isAbsolute(command[0])) {
90
- fail('The verified WorkBite native client is not available; run workbite check.');
91
- }
92
- return command;
93
- }
94
-
95
- function explicitWorkspace(workspace, workspaceUid) {
96
- if ((workspace === undefined) !== (workspaceUid === undefined)) fail('--workspace and --workspace-uid must be given together');
97
- if (typeof workspace !== 'string' || !path.isAbsolute(workspace) || /[\x00-\x1f]/.test(workspace)
98
- || (process.platform === 'win32' && /^[\\/](?![\\/])/.test(workspace))) fail('--workspace must be an absolute path');
99
- if (typeof workspaceUid !== 'string' || !UUID.test(workspaceUid)) fail('--workspace-uid must be a canonical workspace UUID');
100
- return { workspace: path.resolve(workspace), uid: workspaceUid };
101
- }
102
- // Only the saved selection (written after a fully proven open) names a default; never a guess.
103
- async function selectedWorkspace(layout) {
104
- if (!await exists(layout.selection)) fail(`No WorkBite workspace is selected. ${EXPLICIT}`);
105
- return rememberedWorkspace(layout);
106
- }
107
-
108
- async function ownerRecord(layout, workspace, uid) {
109
- const file = path.join(layout.owners, `${workspaceKey(workspace)}.json`);
110
- let bytes, record;
111
- try { bytes = await readSmall(file, 65536); record = JSON.parse(bytes.toString('utf8')); } catch {
112
- fail(`No ready WorkBite-owned Work Stack owner is recorded for ${workspace}. Start it with: workbite open --workspace ABS`);
113
- }
114
- const same = typeof record?.workspace === 'string' && path.isAbsolute(record.workspace)
115
- && workspaceKey(path.resolve(record.workspace)) === workspaceKey(workspace);
116
- if (record === null || typeof record !== 'object' || Array.isArray(record) || record.schema !== OWNER_SCHEMA
117
- || record.state !== 'ready' || !same || record.workspace_uid !== uid || typeof record.launch !== 'string'
118
- || !LAUNCH.test(record.launch) || typeof record.token !== 'string' || !/^[0-9a-f]{64}$/.test(record.token)
119
- || typeof record.endpoint !== 'string' || !LOOPBACK.test(record.endpoint)) {
120
- fail(`The Work Stack owner record for ${workspace} is not a ready WorkBite owner for that workspace UID. Open it with: workbite open --workspace ABS`);
121
- }
122
- return { file, digest: sha256(bytes), record };
123
- }
124
- async function proveOwner(layout, workspace, uid) {
125
- if (await readWorkspaceUid(workspace) !== uid) fail(`The workspace.json id of ${workspace} does not match --workspace-uid`);
126
- const owner = await ownerRecord(layout, workspace, uid);
127
- if (!await engineIdentity(owner.record.endpoint, owner.record.token, uid)) {
128
- fail(`The WorkBite Work Stack owner for ${workspace} could not be authenticated (stopped or replaced). Start it with: workbite open --workspace ABS`);
129
- }
130
- return owner;
131
- }
132
-
133
- async function bindAgent({ installed, stateRoot, environment = process.env, workspace, workspaceUid, commands, timeouts = {} } = {}) {
134
- if (typeof stateRoot !== 'string' || !path.isAbsolute(stateRoot)) fail('WorkBite state directory must be an absolute path');
135
- const layout = stateLayout(path.resolve(stateRoot));
136
- const command = clientCommand(installed, commands);
137
- const chosen = workspace === undefined && workspaceUid === undefined
138
- ? await selectedWorkspace(layout) : explicitWorkspace(workspace, workspaceUid);
139
- const owner = await proveOwner(layout, chosen.workspace, chosen.uid);
140
- const binding = Object.freeze({ workspace: chosen.workspace, workspaceUid: chosen.uid, endpoint: owner.record.endpoint,
141
- stateRoot: layout.root });
142
- bindings.set(binding, Object.freeze({ layout, command, environment: { ...environment }, digest: owner.digest,
143
- launch: owner.record.launch, deadline: timeouts.agent || DEFAULT_DEADLINE }));
144
- return binding;
145
- }
146
-
147
- // The bound owner must be the very same proven generation for every call.
148
- async function recheck(binding, state) {
149
- const owner = await proveOwner(state.layout, binding.workspace, binding.workspaceUid);
150
- if (owner.digest !== state.digest || owner.record.launch !== state.launch || owner.record.endpoint !== binding.endpoint) {
151
- fail('The bound WorkBite Work Stack owner changed since binding; bind again (nothing was sent).');
152
- }
153
- }
154
-
155
- // JSON quoting retains the exact path without letting terminal controls become diagnostics.
156
- const diagnosticJson = value => JSON.stringify(value).replace(/[\x7f-\x9f\u2028-\u202e\u2066-\u2069]/g,
157
- character => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`);
158
- // One request, one response, no retry. `write` decides how an uncertain outcome is reported.
159
- function rpc(command, { endpoint, uid, workspace, environment, argv, stdin, deadline, write, intent, name }) {
160
- const requestId = `workbite-agent-${randomBytes(8).toString('hex')}`;
161
- const request = JSON.stringify({ version: 1, request_id: requestId, workspace_uid: uid, generation: 0,
162
- method: 'cli.execute', params: { argv, stdin } }) + '\n';
163
- const nativeTimeout = Math.max(1000, Math.min(300000, deadline));
164
- const [file, ...prefix] = command;
165
- // cli.execute wraps escaped stdout in JSON; a bounded archive may expand at both layers.
166
- const transfer = ['export', 'import', 'import-status', 'import-rollback'].includes(name);
167
- const stdoutLimit = transfer ? 8 * MAX_TRANSFER_BYTES : MAX_STDOUT;
168
- const lookup = transfer ? 'import-status' : 'operation-status';
169
- const intentFlag = intent?.startsWith('--') ? `--intent-id=${intent}` : `--intent-id ${intent}`;
170
- const uncertain = what => ({ code: 1, stdout: '', stderr: write
171
- ? `error: commit unknown (${what}); WorkBite did not replay it. Check: workbite agent ${lookup} ${intentFlag}\n`
172
- + `Use the same bound workspace: ${diagnosticJson({ workspace, workspace_uid: uid })}\n`
173
- : `error: ${what}; nothing was retried\n`,
174
- ...(write ? { meta: { command: `agent.${name.replace(/ /g, '.')}`, intent_id: intent, commit_state: 'unknown' },
175
- workbite: { schema: 'workbite.agent.binding.v1', workspace, workspace_uid: uid } } : {}) });
176
- return new Promise(resolve => {
177
- let child;
178
- try {
179
- child = spawn(file, [...prefix, '--endpoint', endpoint, '--workspace-uid', uid, '--timeout-ms', String(nativeTimeout),
180
- '--read-retries', '0', 'rpc'], { env: cleanEnvironment(environment), shell: false, windowsHide: true,
181
- stdio: ['pipe', 'pipe', 'pipe'] });
182
- } catch { resolve({ code: 1, stdout: '', stderr: 'error: cannot start the verified Work Stack native client\n' }); return; }
183
- const out = [], err = [];
184
- let outSize = 0, errSize = 0, done = false, overflow = false, started = false;
185
- const finish = result => { if (!done) { done = true; clearTimeout(timer); resolve(result); } };
186
- const timer = setTimeout(() => { child.kill(); finish(uncertain('native client deadline exceeded')); }, nativeTimeout + 5000);
187
- child.stdout.on('data', chunk => {
188
- outSize += chunk.length;
189
- if (outSize > stdoutLimit) { overflow = true; child.kill(); } else out.push(chunk);
190
- });
191
- child.stderr.on('data', chunk => { errSize += chunk.length; if (errSize <= MAX_STDERR) err.push(chunk); });
192
- child.stdin.on('error', () => {});
193
- child.once('spawn', () => { started = true; });
194
- child.once('error', () => finish(started ? uncertain('native client failed')
195
- : { code: 1, stdout: '', stderr: 'error: cannot start the verified Work Stack native client\n' }));
196
- child.once('close', () => {
197
- if (overflow) { finish(uncertain('native response exceeded its bound')); return; }
198
- const lines = Buffer.concat(out).toString('utf8').split('\n').filter(line => line.trim());
199
- let response = null;
200
- try { response = lines.length === 1 ? JSON.parse(lines[0]) : null; } catch { response = null; }
201
- if (response === null || typeof response !== 'object' || response.version !== 1 || response.request_id !== requestId) {
202
- finish(uncertain('no valid native response')); return;
203
- }
204
- if (response.error) {
205
- const code = ['InvalidRequest', 'UnsupportedVersion', 'Unavailable', 'Timeout', 'Conflict', 'Unauthorized', 'Io', 'UnknownCommit']
206
- .includes(response.error.code) ? response.error.code : 'Error';
207
- if (code === 'UnknownCommit') { finish(uncertain('owner response was lost')); return; }
208
- finish({ code: 1, stdout: '', stderr: `error: ${code}: native request failed\n` });
209
- return;
210
- }
211
- const result = response.result;
212
- if (result === null || typeof result !== 'object' || !Number.isInteger(result.exit_code)
213
- || typeof result.stdout !== 'string' || typeof result.stderr !== 'string') { finish(uncertain('invalid native result')); return; }
214
- finish({ code: result.exit_code, stdout: result.stdout, stderr: result.stderr });
215
- });
216
- child.stdin.end(request);
217
- });
218
- }
219
-
220
- async function executeAgent(binding, argv, { stdin } = {}) {
221
- const state = bindings.get(binding);
222
- if (!state) fail('Not a WorkBite agent binding; use bindAgent');
223
- const command = checkedArgv(argv, binding.workspaceUid);
224
- const args = [...argv];
225
- const input = checkedStdin(stdin, command);
226
- const write = WRITE_COMMANDS.includes(command) && !(command === 'import' && args.includes('--dry-run'));
227
- const intent = checkedIntent(args, command, write);
228
- await recheck(binding, state);
229
- return rpc(state.command, { endpoint: binding.endpoint, uid: binding.workspaceUid, workspace: binding.workspace,
230
- environment: state.environment, argv: args, stdin: input, deadline: state.deadline, write, intent, name: command });
231
- }
232
-
233
- // The static capability table needs no workspace authority; it never reaches an owner.
234
- async function agentCapabilities({ installed, commands, environment = process.env, argv = ['agent', 'capabilities'], timeouts = {} } = {}) {
235
- if (checkedArgv(argv, NO_UID) !== 'capabilities') fail('Only agent capabilities is authority-free');
236
- return rpc(clientCommand(installed, commands), { endpoint: NO_ENDPOINT, uid: NO_UID, environment, argv: [...argv], stdin: null,
237
- deadline: timeouts.agent || DEFAULT_DEADLINE, write: false, name: 'capabilities' });
238
- }
239
-
240
- module.exports = { bindAgent, executeAgent, agentCapabilities, AGENT_COMMANDS, WRITE_COMMANDS, MAX_PACKET_BYTES, MAX_TRANSFER_BYTES, commandOf };
1
+ 'use strict';
2
+ // WorkBite agent interface: binds one already-running, WorkBite-owned Work Stack
3
+ // owner and forwards curated agent commands to it through the pinned native
4
+ // client's v1 `cli.execute` RPC. The native client is the only mapper to the
5
+ // owner's versioned APIs. Nothing here starts, downloads, initializes or adopts
6
+ // an owner, falls back to another workspace or tool, or replays a write.
7
+ const path = require('node:path');
8
+ const { spawn } = require('node:child_process');
9
+ const { randomBytes } = require('node:crypto');
10
+ const { fail, exists, readSmall, sha256, UUID } = require('./common');
11
+ const { cleanEnvironment, stateLayout, workspaceKey, readWorkspaceUid, rememberedWorkspace, engineIdentity } = require('./runtime');
12
+
13
+ const OWNER_SCHEMA = 'workbite.owner.v2';
14
+ const LOOPBACK = /^http:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/;
15
+ const LAUNCH = /^[0-9a-f]{16}$/;
16
+ const MAX_PACKET_BYTES = 32768;
17
+ const MAX_TRANSFER_BYTES = 1024 * 1024;
18
+ const MAX_ARGV = 128;
19
+ const MAX_STDOUT = 1024 * 1024 + 8192;
20
+ const MAX_STDERR = 65536;
21
+ const DEFAULT_DEADLINE = 30000;
22
+ // Placeholder authority for the static capability table only; the client never contacts it for that command.
23
+ const NO_ENDPOINT = 'http://127.0.0.1:9';
24
+ const NO_UID = '00000000-0000-4000-8000-000000000000';
25
+
26
+ // Curated command paths; every other native command (raw rpc, legacy, deploy, delete, migration) is refused.
27
+ const AGENT_COMMANDS = Object.freeze(['capabilities', 'status', 'tasks list', 'tasks create', 'tasks update', 'context',
28
+ 'checkpoint', 'operation-status', 'export', 'import', 'import-status', 'import-rollback']);
29
+ const WRITE_COMMANDS = Object.freeze(['tasks create', 'tasks update', 'checkpoint', 'import', 'import-rollback']);
30
+ const FORBIDDEN_FLAGS = new Set(['--data-dir', '--endpoint', '--token-env', '--token', '--workspace', '--read-retries',
31
+ '--timeout-ms', '--request-id', '--generation', '--packet-file']);
32
+
33
+ const EXPLICIT = 'Name it with --workspace ABS --workspace-uid UUID, or open it once with: workbite open --workspace ABS';
34
+ const bindings = new WeakMap();
35
+
36
+ // Only the bound --workspace-uid (once, exactly that UID) may precede the command words.
37
+ function commandOf(argv, uid) {
38
+ if (!Array.isArray(argv) || argv[0] !== 'agent') return null;
39
+ let start = 1;
40
+ if (typeof uid === 'string' && uid) {
41
+ if (argv[1] === '--workspace-uid' && argv[2] === uid) start = 3;
42
+ else if (argv[1] === `--workspace-uid=${uid}`) start = 2;
43
+ }
44
+ const words = [];
45
+ for (const item of argv.slice(start)) { if (item.startsWith('-')) break; words.push(item); }
46
+ const name = words.join(' ');
47
+ return AGENT_COMMANDS.includes(name) ? name : null;
48
+ }
49
+ function checkedArgv(argv, uid) {
50
+ if (!Array.isArray(argv) || !argv.length || argv.length > MAX_ARGV
51
+ || argv.some(item => typeof item !== 'string' || item.includes('\0'))
52
+ || argv.reduce((total, item) => total + Buffer.byteLength(item), 0) > MAX_PACKET_BYTES) fail('Invalid WorkBite agent arguments');
53
+ const command = commandOf(argv, uid);
54
+ if (!command) fail('Unsupported WorkBite agent command; supported: ' + AGENT_COMMANDS.join(', '));
55
+ argv.forEach((item, index) => {
56
+ const key = item.split('=')[0];
57
+ if (FORBIDDEN_FLAGS.has(key)) fail(`WorkBite agent commands do not accept ${key}; authority comes from the binding`);
58
+ if (key === '--workspace-uid') {
59
+ const value = item.includes('=') ? item.slice(item.indexOf('=') + 1) : argv[index + 1];
60
+ if (value !== uid) fail('--workspace-uid does not match the bound WorkBite workspace');
61
+ }
62
+ });
63
+ return command;
64
+ }
65
+ function checkedStdin(stdin, command) {
66
+ if (stdin === undefined || stdin === null) return null;
67
+ const maximum = command === 'import' ? MAX_TRANSFER_BYTES : MAX_PACKET_BYTES;
68
+ if (typeof stdin !== 'string' || Buffer.byteLength(stdin) > maximum || stdin.includes('\0') || stdin.isWellFormed?.() === false) {
69
+ fail(`WorkBite agent input must be UTF-8 text of at most ${maximum} bytes`);
70
+ }
71
+ return stdin;
72
+ }
73
+ // Capture recovery identity before launch; never reconstruct it from native error text.
74
+ function checkedIntent(argv, command, write) {
75
+ const flags = argv.filter(item => item.split('=')[0] === '--intent-id');
76
+ if (!flags.length && !write) return null;
77
+ if (flags.length !== 1 || (!WRITE_COMMANDS.includes(command)
78
+ && !['operation-status', 'import-status'].includes(command)) || argv.includes('--')) {
79
+ fail('Invalid or repeated --intent-id');
80
+ }
81
+ const flag = flags[0];
82
+ const value = flag.includes('=') ? flag.slice(flag.indexOf('=') + 1) : argv[argv.indexOf(flag) + 1];
83
+ if (typeof value !== 'string' || (!flag.includes('=') && value.startsWith('--'))
84
+ || value.length < 8 || value.length > 128 || /[^A-Za-z0-9._:-]/.test(value)) fail('Invalid --intent-id');
85
+ return value;
86
+ }
87
+ function clientCommand(installed, commands) {
88
+ const command = commands?.workstack_client || [installed?.executables?.workstack_client];
89
+ if (!Array.isArray(command) || typeof command[0] !== 'string' || !path.isAbsolute(command[0])) {
90
+ fail('The verified WorkBite native client is not available; run workbite check.');
91
+ }
92
+ return command;
93
+ }
94
+
95
+ function explicitWorkspace(workspace, workspaceUid) {
96
+ if ((workspace === undefined) !== (workspaceUid === undefined)) fail('--workspace and --workspace-uid must be given together');
97
+ if (typeof workspace !== 'string' || !path.isAbsolute(workspace) || /[\x00-\x1f]/.test(workspace)
98
+ || (process.platform === 'win32' && /^[\\/](?![\\/])/.test(workspace))) fail('--workspace must be an absolute path');
99
+ if (typeof workspaceUid !== 'string' || !UUID.test(workspaceUid)) fail('--workspace-uid must be a canonical workspace UUID');
100
+ return { workspace: path.resolve(workspace), uid: workspaceUid };
101
+ }
102
+ // Only the saved selection (written after a fully proven open) names a default; never a guess.
103
+ async function selectedWorkspace(layout) {
104
+ if (!await exists(layout.selection)) fail(`No WorkBite workspace is selected. ${EXPLICIT}`);
105
+ return rememberedWorkspace(layout);
106
+ }
107
+
108
+ async function ownerRecord(layout, workspace, uid) {
109
+ const file = path.join(layout.owners, `${workspaceKey(workspace)}.json`);
110
+ let bytes, record;
111
+ try { bytes = await readSmall(file, 65536); record = JSON.parse(bytes.toString('utf8')); } catch {
112
+ fail(`No ready WorkBite-owned Work Stack owner is recorded for ${workspace}. Start it with: workbite open --workspace ABS`);
113
+ }
114
+ const same = typeof record?.workspace === 'string' && path.isAbsolute(record.workspace)
115
+ && workspaceKey(path.resolve(record.workspace)) === workspaceKey(workspace);
116
+ if (record === null || typeof record !== 'object' || Array.isArray(record) || record.schema !== OWNER_SCHEMA
117
+ || record.state !== 'ready' || !same || record.workspace_uid !== uid || typeof record.launch !== 'string'
118
+ || !LAUNCH.test(record.launch) || typeof record.token !== 'string' || !/^[0-9a-f]{64}$/.test(record.token)
119
+ || typeof record.endpoint !== 'string' || !LOOPBACK.test(record.endpoint)) {
120
+ fail(`The Work Stack owner record for ${workspace} is not a ready WorkBite owner for that workspace UID. Open it with: workbite open --workspace ABS`);
121
+ }
122
+ return { file, digest: sha256(bytes), record };
123
+ }
124
+ async function proveOwner(layout, workspace, uid) {
125
+ if (await readWorkspaceUid(workspace) !== uid) fail(`The workspace.json id of ${workspace} does not match --workspace-uid`);
126
+ const owner = await ownerRecord(layout, workspace, uid);
127
+ if (!await engineIdentity(owner.record.endpoint, owner.record.token, uid)) {
128
+ fail(`The WorkBite Work Stack owner for ${workspace} could not be authenticated (stopped or replaced). Start it with: workbite open --workspace ABS`);
129
+ }
130
+ return owner;
131
+ }
132
+
133
+ async function bindAgent({ installed, stateRoot, environment = process.env, workspace, workspaceUid, commands, timeouts = {} } = {}) {
134
+ if (typeof stateRoot !== 'string' || !path.isAbsolute(stateRoot)) fail('WorkBite state directory must be an absolute path');
135
+ const layout = stateLayout(path.resolve(stateRoot));
136
+ const command = clientCommand(installed, commands);
137
+ const chosen = workspace === undefined && workspaceUid === undefined
138
+ ? await selectedWorkspace(layout) : explicitWorkspace(workspace, workspaceUid);
139
+ const owner = await proveOwner(layout, chosen.workspace, chosen.uid);
140
+ const binding = Object.freeze({ workspace: chosen.workspace, workspaceUid: chosen.uid, endpoint: owner.record.endpoint,
141
+ stateRoot: layout.root });
142
+ bindings.set(binding, Object.freeze({ layout, command, environment: { ...environment }, digest: owner.digest,
143
+ launch: owner.record.launch, deadline: timeouts.agent || DEFAULT_DEADLINE }));
144
+ return binding;
145
+ }
146
+
147
+ // The bound owner must be the very same proven generation for every call.
148
+ async function recheck(binding, state) {
149
+ const owner = await proveOwner(state.layout, binding.workspace, binding.workspaceUid);
150
+ if (owner.digest !== state.digest || owner.record.launch !== state.launch || owner.record.endpoint !== binding.endpoint) {
151
+ fail('The bound WorkBite Work Stack owner changed since binding; bind again (nothing was sent).');
152
+ }
153
+ }
154
+
155
+ // JSON quoting retains the exact path without letting terminal controls become diagnostics.
156
+ const diagnosticJson = value => JSON.stringify(value).replace(/[\x7f-\x9f\u2028-\u202e\u2066-\u2069]/g,
157
+ character => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`);
158
+ // One request, one response, no retry. `write` decides how an uncertain outcome is reported.
159
+ function rpc(command, { endpoint, uid, workspace, environment, argv, stdin, deadline, write, intent, name }) {
160
+ const requestId = `workbite-agent-${randomBytes(8).toString('hex')}`;
161
+ const request = JSON.stringify({ version: 1, request_id: requestId, workspace_uid: uid, generation: 0,
162
+ method: 'cli.execute', params: { argv, stdin } }) + '\n';
163
+ const nativeTimeout = Math.max(1000, Math.min(300000, deadline));
164
+ const [file, ...prefix] = command;
165
+ // cli.execute wraps escaped stdout in JSON; a bounded archive may expand at both layers.
166
+ const transfer = ['export', 'import', 'import-status', 'import-rollback'].includes(name);
167
+ const stdoutLimit = transfer ? 8 * MAX_TRANSFER_BYTES : MAX_STDOUT;
168
+ const lookup = transfer ? 'import-status' : 'operation-status';
169
+ const intentFlag = intent?.startsWith('--') ? `--intent-id=${intent}` : `--intent-id ${intent}`;
170
+ const uncertain = what => ({ code: 1, stdout: '', stderr: write
171
+ ? `error: commit unknown (${what}); WorkBite did not replay it. Check: workbite agent ${lookup} ${intentFlag}\n`
172
+ + `Use the same bound workspace: ${diagnosticJson({ workspace, workspace_uid: uid })}\n`
173
+ : `error: ${what}; nothing was retried\n`,
174
+ ...(write ? { meta: { command: `agent.${name.replace(/ /g, '.')}`, intent_id: intent, commit_state: 'unknown' },
175
+ workbite: { schema: 'workbite.agent.binding.v1', workspace, workspace_uid: uid } } : {}) });
176
+ return new Promise(resolve => {
177
+ let child;
178
+ try {
179
+ child = spawn(file, [...prefix, '--endpoint', endpoint, '--workspace-uid', uid, '--timeout-ms', String(nativeTimeout),
180
+ '--read-retries', '0', 'rpc'], { env: cleanEnvironment(environment), shell: false, windowsHide: true,
181
+ stdio: ['pipe', 'pipe', 'pipe'] });
182
+ } catch { resolve({ code: 1, stdout: '', stderr: 'error: cannot start the verified Work Stack native client\n' }); return; }
183
+ const out = [], err = [];
184
+ let outSize = 0, errSize = 0, done = false, overflow = false, started = false;
185
+ const finish = result => { if (!done) { done = true; clearTimeout(timer); resolve(result); } };
186
+ const timer = setTimeout(() => { child.kill(); finish(uncertain('native client deadline exceeded')); }, nativeTimeout + 5000);
187
+ child.stdout.on('data', chunk => {
188
+ outSize += chunk.length;
189
+ if (outSize > stdoutLimit) { overflow = true; child.kill(); } else out.push(chunk);
190
+ });
191
+ child.stderr.on('data', chunk => { errSize += chunk.length; if (errSize <= MAX_STDERR) err.push(chunk); });
192
+ child.stdin.on('error', () => {});
193
+ child.once('spawn', () => { started = true; });
194
+ child.once('error', () => finish(started ? uncertain('native client failed')
195
+ : { code: 1, stdout: '', stderr: 'error: cannot start the verified Work Stack native client\n' }));
196
+ child.once('close', () => {
197
+ if (overflow) { finish(uncertain('native response exceeded its bound')); return; }
198
+ const lines = Buffer.concat(out).toString('utf8').split('\n').filter(line => line.trim());
199
+ let response = null;
200
+ try { response = lines.length === 1 ? JSON.parse(lines[0]) : null; } catch { response = null; }
201
+ if (response === null || typeof response !== 'object' || response.version !== 1 || response.request_id !== requestId) {
202
+ finish(uncertain('no valid native response')); return;
203
+ }
204
+ if (response.error) {
205
+ const code = ['InvalidRequest', 'UnsupportedVersion', 'Unavailable', 'Timeout', 'Conflict', 'Unauthorized', 'Io', 'UnknownCommit']
206
+ .includes(response.error.code) ? response.error.code : 'Error';
207
+ if (code === 'UnknownCommit') { finish(uncertain('owner response was lost')); return; }
208
+ finish({ code: 1, stdout: '', stderr: `error: ${code}: native request failed\n` });
209
+ return;
210
+ }
211
+ const result = response.result;
212
+ if (result === null || typeof result !== 'object' || !Number.isInteger(result.exit_code)
213
+ || typeof result.stdout !== 'string' || typeof result.stderr !== 'string') { finish(uncertain('invalid native result')); return; }
214
+ finish({ code: result.exit_code, stdout: result.stdout, stderr: result.stderr });
215
+ });
216
+ child.stdin.end(request);
217
+ });
218
+ }
219
+
220
+ async function executeAgent(binding, argv, { stdin } = {}) {
221
+ const state = bindings.get(binding);
222
+ if (!state) fail('Not a WorkBite agent binding; use bindAgent');
223
+ const command = checkedArgv(argv, binding.workspaceUid);
224
+ const args = [...argv];
225
+ const input = checkedStdin(stdin, command);
226
+ const write = WRITE_COMMANDS.includes(command) && !(command === 'import' && args.includes('--dry-run'));
227
+ const intent = checkedIntent(args, command, write);
228
+ await recheck(binding, state);
229
+ return rpc(state.command, { endpoint: binding.endpoint, uid: binding.workspaceUid, workspace: binding.workspace,
230
+ environment: state.environment, argv: args, stdin: input, deadline: state.deadline, write, intent, name: command });
231
+ }
232
+
233
+ // The static capability table needs no workspace authority; it never reaches an owner.
234
+ async function agentCapabilities({ installed, commands, environment = process.env, argv = ['agent', 'capabilities'], timeouts = {} } = {}) {
235
+ if (checkedArgv(argv, NO_UID) !== 'capabilities') fail('Only agent capabilities is authority-free');
236
+ return rpc(clientCommand(installed, commands), { endpoint: NO_ENDPOINT, uid: NO_UID, environment, argv: [...argv], stdin: null,
237
+ deadline: timeouts.agent || DEFAULT_DEADLINE, write: false, name: 'capabilities' });
238
+ }
239
+
240
+ module.exports = { bindAgent, executeAgent, agentCapabilities, AGENT_COMMANDS, WRITE_COMMANDS, MAX_PACKET_BYTES, MAX_TRANSFER_BYTES, commandOf };