workbite 0.1.22 → 0.1.24
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 +28 -30
- package/bin/workbite.js +13 -13
- package/lib/agent.js +240 -240
- package/lib/archive.js +334 -334
- package/lib/boot-provenance.js +54 -54
- package/lib/cli.js +334 -334
- package/lib/common.js +68 -68
- package/lib/diagnostics.js +238 -238
- package/lib/download.js +53 -53
- package/lib/install.js +274 -274
- package/lib/mcp-task-context.js +188 -188
- package/lib/mcp.js +671 -671
- package/lib/private-state.js +116 -116
- package/lib/runtime.js +775 -775
- package/lib/startup-lock.js +71 -71
- package/lib/transfer.js +113 -113
- package/lib/update.js +86 -86
- package/package.json +35 -35
- package/release.json +13 -13
- package/skills/workbite-native/SKILL.md +126 -126
- package/skills/workbite-native/agents/openai.yaml +4 -4
- package/skills/workbite-native/references/commands.md +213 -213
- package/skills/workbite-native/references/task-fields.md +208 -208
- package/skills/workbite-native/references/transfer.md +204 -204
package/README.md
CHANGED
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
WorkBite installs and opens **Work Stack** (organizes the work) and
|
|
4
4
|
**StackBite** (gets it moving) as one native suite.
|
|
5
5
|
|
|
6
|
-
Launcher 0.1.
|
|
7
|
-
TypeScript TUI
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
success is claimed and the existing CI publication helper is unchanged.
|
|
6
|
+
Launcher 0.1.24 pins qualified Windows x64 and Linux x64 native archives.
|
|
7
|
+
TypeScript TUI remains 1.0.38 and the embedded StackBite is 0.5.39.
|
|
8
|
+
Both archives use build source `ba44d63cb89c9f979c236a2df6a7dcf73228a129`,
|
|
9
|
+
tree `15c07e0d5de33440422210675855c73c8de8a0b9`. Their actual archive hashes,
|
|
10
|
+
embedded bundle digests and sizes are sealed in `release.json` for `v0.1.24`
|
|
11
|
+
in `Shinick-Han/WorkBite-releases`. Publication, registry delivery and installed
|
|
12
|
+
runtime handover are separately verified maintainer operations. The maintainer
|
|
13
|
+
uses the selected manual publication path because hosted Actions quota is
|
|
14
|
+
exhausted; no hosted CI/OIDC success is claimed.
|
|
16
15
|
It opens the familiar TypeScript Work Stack UI by default, which
|
|
17
16
|
has one Tasks workbench: switch Focus/Table/Board without losing your selected
|
|
18
17
|
Task, open its persistent Room's plain terminal, and control it beside or below
|
|
@@ -30,23 +29,22 @@ an explicit alternative per open: `workbite open --frontend rust` (or bare
|
|
|
30
29
|
StackBite backend; the choice is not saved. Leaving the view or closing the UI
|
|
31
30
|
keeps the agent running, and Task completion stays an explicit action.
|
|
32
31
|
|
|
33
|
-
The 0.1.
|
|
32
|
+
The 0.1.24 pair passed independent archive/runtime review: all 17 Windows probes,
|
|
34
33
|
all 12 Linux ELF version-needs audits at GLIBC <= 2.28, and 29 AlmaLinux 8.10
|
|
35
|
-
userspace probes.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
or clear startup records automatically.
|
|
34
|
+
userspace probes. Fresh bounded lifecycle source checks passed on Windows and
|
|
35
|
+
Linux; Windows-only checks skipped on Linux remain explicit. These checks do
|
|
36
|
+
not claim a real provider turn, interactive-screen acceptance, booted AlmaLinux,
|
|
37
|
+
baseline CPU, SSH/VM, NFS or physical Phone behavior. Linux Phone is unsupported.
|
|
38
|
+
See the [0.1.24 release notes](../../docs/RELEASE-0.1.24.md) and
|
|
39
|
+
[qualification record](../../docs/releases/0.1.24/README.md) for pins and limits.
|
|
40
|
+
|
|
41
|
+
The Rust frontend highlights the active Room and Seat with a background.
|
|
42
|
+
Optional Claude/Codex lifecycle hooks add observed work status for CLIs started
|
|
43
|
+
manually inside shell Seats. Stop means idle, never verified completion. Hook
|
|
44
|
+
installation and an existing CLI's normal restart/resume may be required; Codex
|
|
45
|
+
retains its normal hook trust review. Current owners and Seats remain on their
|
|
46
|
+
existing runtime until a safe operator handover; updates do not stop them or
|
|
47
|
+
clear unknown startup records automatically.
|
|
50
48
|
|
|
51
49
|
The candidate keeps a stable Seat across explicit shell restarts, with a fresh
|
|
52
50
|
generation and no inferred previous CLI/provider resume. Stop retains the Seat;
|
|
@@ -89,7 +87,7 @@ new one. A failure can leave the launcher installed without its suite: inspect
|
|
|
89
87
|
`workbite check`. Never replay an unknown npm install without inspecting it.
|
|
90
88
|
|
|
91
89
|
```sh
|
|
92
|
-
npm install -g workbite@0.1.
|
|
90
|
+
npm install -g workbite@0.1.24 # only after registry publication and public-asset verification
|
|
93
91
|
workbite install
|
|
94
92
|
workbite open --workspace /abs/path/new-folder --init # first time, once
|
|
95
93
|
workbite # afterwards (TypeScript UI)
|
|
@@ -175,12 +173,12 @@ assignment refuses discovery. Standalone/unbound terminals retain the explicit
|
|
|
175
173
|
workspace/context path. WorkBite does not enable MCP or configure providers
|
|
176
174
|
automatically; source fixture checks do not prove an actual provider turn.
|
|
177
175
|
|
|
178
|
-
The
|
|
176
|
+
The candidate retains the repair for initial Task Room capability discovery and
|
|
179
177
|
recovery notices after overlapping reads. Task Room inventory is filtered to
|
|
180
178
|
the requested Task's Rooms before runtime inventory is read. Selecting a Task
|
|
181
179
|
continues to create no Room, Seat or provider.
|
|
182
180
|
|
|
183
|
-
The
|
|
181
|
+
The candidate retains **Connect agent** in Task Execution (`a`),
|
|
184
182
|
the global 9 Execution monitor and launch picker (`+`), clickable controls and
|
|
185
183
|
scoped command palettes (`:`). Claude Code, Codex, Cursor and Grok Build use
|
|
186
184
|
their official CLI login and credential storage. Missing CLIs offer install
|
|
@@ -202,7 +200,7 @@ Source checks used disposable fixtures, with no live account login, browser
|
|
|
202
200
|
completion, account-entitlement proof or paid provider turn acceptance. See the
|
|
203
201
|
[source acceptance record](../../docs/WORKBITE-AGENT-CONNECTION-ACCEPTANCE-2026-10-16.md).
|
|
204
202
|
|
|
205
|
-
The Windows
|
|
203
|
+
The Windows candidate retains the read-only phone companion pilot from 0.1.14.
|
|
206
204
|
Linux Phone support remains unsupported; paired releases do not imply identical
|
|
207
205
|
platform capabilities.
|
|
208
206
|
Phone actions preserve their selected target during status reads, show pending
|
package/bin/workbite.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
const { main } = require('../lib/cli');
|
|
4
|
-
main().then(({ code, signal }) => {
|
|
5
|
-
if (signal && process.platform !== 'win32') {
|
|
6
|
-
process.kill(process.pid, signal);
|
|
7
|
-
} else {
|
|
8
|
-
process.exitCode = code === null ? 1 : code;
|
|
9
|
-
}
|
|
10
|
-
}).catch(error => {
|
|
11
|
-
console.error(`WorkBite: ${error.message}`);
|
|
12
|
-
process.exitCode = 1;
|
|
13
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const { main } = require('../lib/cli');
|
|
4
|
+
main().then(({ code, signal }) => {
|
|
5
|
+
if (signal && process.platform !== 'win32') {
|
|
6
|
+
process.kill(process.pid, signal);
|
|
7
|
+
} else {
|
|
8
|
+
process.exitCode = code === null ? 1 : code;
|
|
9
|
+
}
|
|
10
|
+
}).catch(error => {
|
|
11
|
+
console.error(`WorkBite: ${error.message}`);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
});
|
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 };
|