@superjackfruitlabs/salaam 0.0.3 → 0.0.5
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/CHANGELOG.md +86 -0
- package/README.md +12 -11
- package/dist/src/acp-desk.js +140 -0
- package/dist/src/acp-outcome.js +182 -18
- package/dist/src/acp.js +296 -15
- package/dist/src/agent-profiles.js +45 -0
- package/dist/src/agentpod-authority.js +169 -11
- package/dist/src/agentpod-default-config.js +12 -0
- package/dist/src/agentpod-work-decision-protocol.js +40 -6
- package/dist/src/agentpod-work-decisions.js +108 -4
- package/dist/src/agentpod-work.js +1214 -127
- package/dist/src/approval-rules.js +287 -0
- package/dist/src/attach.js +3 -1
- package/dist/src/autonomy.js +80 -9
- package/dist/src/board-reads.js +106 -0
- package/dist/src/children.js +83 -0
- package/dist/src/cli-help.js +61 -6
- package/dist/src/cli.js +140 -4
- package/dist/src/command-danger.js +797 -0
- package/dist/src/command-network.js +200 -0
- package/dist/src/command-sandbox.js +327 -0
- package/dist/src/custom-commands.js +159 -0
- package/dist/src/danger-classifier.js +286 -0
- package/dist/src/execution-evidence.js +33 -1
- package/dist/src/git-helpers.js +160 -0
- package/dist/src/hooks.js +351 -0
- package/dist/src/host-bind.js +111 -0
- package/dist/src/host-breaker.js +88 -0
- package/dist/src/host-start.js +41 -1
- package/dist/src/host.js +284 -6
- package/dist/src/house-extension.js +292 -61
- package/dist/src/local-coding.js +726 -35
- package/dist/src/local-edit-helper.js +25 -0
- package/dist/src/local-edits.js +168 -6
- package/dist/src/local-undo.js +25 -16
- package/dist/src/managed.js +1 -1
- package/dist/src/mentions.js +277 -0
- package/dist/src/operator-mcp-cli.js +107 -0
- package/dist/src/operator-mcp.js +342 -0
- package/dist/src/persona.js +31 -0
- package/dist/src/pi-sessions.js +5 -1
- package/dist/src/pi.js +22 -1
- package/dist/src/project-instructions.js +195 -0
- package/dist/src/protocol.js +9 -3
- package/dist/src/rate-limit-retries.js +62 -0
- package/dist/src/recurring-work.js +1 -0
- package/dist/src/repository-snapshots.js +5 -2
- package/dist/src/run-grants.js +62 -0
- package/dist/src/service.js +482 -10
- package/dist/src/session-pick.js +77 -0
- package/dist/src/shell-passthrough.js +39 -0
- package/dist/src/station-checkpoints.js +139 -0
- package/dist/src/station-config.js +8 -1
- package/dist/src/station-files.js +20 -1
- package/dist/src/station-task-verification.js +6 -3
- package/dist/src/station-work.js +315 -22
- package/dist/src/store.js +70 -4
- package/dist/src/supervisor.js +408 -25
- package/dist/src/task-budget.js +39 -3
- package/dist/src/task-context.js +8 -2
- package/dist/src/task-critic.js +115 -0
- package/dist/src/task-driver.js +46 -2
- package/dist/src/task-policy.js +44 -12
- package/dist/src/task-verification.js +42 -9
- package/dist/src/task-work.js +47 -0
- package/dist/src/terminal-status.js +88 -0
- package/dist/src/tool-identity.js +69 -0
- package/dist/src/tui-activity.js +99 -0
- package/dist/src/tui-classifier.js +25 -0
- package/dist/src/tui-client.js +186 -5
- package/dist/src/tui-commands.js +23 -5
- package/dist/src/tui-composer.js +113 -0
- package/dist/src/tui-keymap.js +116 -0
- package/dist/src/tui-mentions.js +38 -0
- package/dist/src/tui-modals.js +157 -34
- package/dist/src/tui-notify.js +62 -0
- package/dist/src/tui-review.js +55 -0
- package/dist/src/tui-turn-keys.js +52 -0
- package/dist/src/tui-view.js +243 -69
- package/dist/src/tui.js +875 -118
- package/dist/src/turn-checkpoints.js +221 -0
- package/dist/src/turn-rewind.js +168 -0
- package/dist/src/usage.js +1 -1
- package/dist/src/web-fetch.js +114 -0
- package/dist/src/web-guard.js +163 -0
- package/dist/src/web-search.js +302 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/acp-desk.ts +155 -0
- package/src/acp-outcome.ts +174 -20
- package/src/acp.ts +386 -30
- package/src/agent-profiles.ts +48 -0
- package/src/agentpod-authority.ts +234 -15
- package/src/agentpod-default-config.ts +16 -0
- package/src/agentpod-work-decision-protocol.ts +69 -8
- package/src/agentpod-work-decisions.ts +123 -5
- package/src/agentpod-work.ts +1455 -152
- package/src/approval-rules.ts +360 -0
- package/src/attach.ts +4 -1
- package/src/autonomy.ts +111 -10
- package/src/board-reads.ts +148 -0
- package/src/children.ts +112 -0
- package/src/cli-help.ts +60 -6
- package/src/cli.ts +174 -4
- package/src/command-danger.ts +886 -0
- package/src/command-network.ts +215 -0
- package/src/command-sandbox.ts +393 -0
- package/src/custom-commands.ts +175 -0
- package/src/danger-classifier.ts +321 -0
- package/src/execution-evidence.ts +31 -1
- package/src/git-helpers.ts +177 -0
- package/src/hooks.ts +425 -0
- package/src/host-bind.ts +156 -0
- package/src/host-breaker.ts +89 -0
- package/src/host-start.ts +51 -1
- package/src/host.ts +365 -6
- package/src/house-extension.ts +313 -60
- package/src/local-coding.ts +894 -43
- package/src/local-edit-helper.ts +26 -0
- package/src/local-edits.ts +191 -8
- package/src/local-undo.ts +29 -14
- package/src/managed.ts +2 -2
- package/src/mentions.ts +304 -0
- package/src/operator-mcp-cli.ts +108 -0
- package/src/operator-mcp.ts +420 -0
- package/src/persona.ts +37 -0
- package/src/pi-sessions.ts +9 -1
- package/src/pi.ts +23 -1
- package/src/project-instructions.ts +229 -0
- package/src/protocol.ts +25 -3
- package/src/rate-limit-retries.ts +69 -0
- package/src/recurring-work.ts +1 -0
- package/src/repository-snapshots.ts +5 -2
- package/src/run-grants.ts +107 -0
- package/src/service.ts +709 -10
- package/src/session-pick.ts +102 -0
- package/src/shell-passthrough.ts +38 -0
- package/src/station-checkpoints.ts +183 -0
- package/src/station-config.ts +10 -1
- package/src/station-files.ts +20 -1
- package/src/station-task-verification.ts +6 -3
- package/src/station-work.ts +369 -20
- package/src/store.ts +70 -3
- package/src/supervisor.ts +463 -26
- package/src/task-budget.ts +48 -3
- package/src/task-context.ts +9 -2
- package/src/task-critic.ts +171 -0
- package/src/task-driver.ts +49 -2
- package/src/task-policy.ts +65 -13
- package/src/task-verification.ts +53 -9
- package/src/task-work.ts +75 -0
- package/src/terminal-status.ts +103 -0
- package/src/tool-identity.ts +110 -0
- package/src/tui-activity.ts +133 -0
- package/src/tui-classifier.ts +38 -0
- package/src/tui-client.ts +239 -8
- package/src/tui-commands.ts +49 -10
- package/src/tui-composer.ts +124 -0
- package/src/tui-keymap.ts +152 -0
- package/src/tui-mentions.ts +43 -0
- package/src/tui-modals.ts +173 -43
- package/src/tui-notify.ts +77 -0
- package/src/tui-review.ts +76 -0
- package/src/tui-turn-keys.ts +77 -0
- package/src/tui-view.ts +341 -88
- package/src/tui.ts +1047 -137
- package/src/turn-checkpoints.ts +253 -0
- package/src/turn-rewind.ts +258 -0
- package/src/usage.ts +1 -1
- package/src/web-fetch.ts +140 -0
- package/src/web-guard.ts +195 -0
- package/src/web-search.ts +402 -0
package/dist/src/supervisor.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { BoardReads } from './board-reads.js';
|
|
2
|
+
import { takeImages } from './mentions.js';
|
|
3
|
+
import { HostBreaker, callThroughBreaker } from './host-breaker.js';
|
|
1
4
|
import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
5
|
import { execFileSync } from 'node:child_process';
|
|
3
6
|
import { EventEmitter } from 'node:events';
|
|
4
|
-
import { mkdirSync, existsSync, writeFileSync, readFileSync, readlinkSync } from 'node:fs';
|
|
7
|
+
import { mkdirSync, existsSync, writeFileSync, readFileSync, readlinkSync, rmSync } from 'node:fs';
|
|
5
8
|
import { join, resolve } from 'node:path';
|
|
6
9
|
import { PROTOCOL_VERSION, canonical, object, ServiceError, textDigest, workerTools, workerCapabilities, string, } from './protocol.js';
|
|
7
10
|
import { PI_CLI, HOUSE_EXTENSION, POLICY_DIGEST, bundleDigest, VERSION, PI_VERSION, } from './config.js';
|
|
8
11
|
import { readinessFingerprint } from './readiness-fingerprint.js';
|
|
9
12
|
import { PiProcess, PiCommandError } from './pi.js';
|
|
13
|
+
import { mergeMcpProvenance } from './operator-mcp.js';
|
|
10
14
|
import { OFFLINE_ROUTE, providerRoute, providerHasOutputCap, } from './provider-config.js';
|
|
11
15
|
import { validateApiKey } from './credentials.js';
|
|
12
16
|
import { resolveProviderAuth } from './provider-auth.js';
|
|
@@ -16,6 +20,28 @@ import { digest } from './protocol.js';
|
|
|
16
20
|
import { readLocalWorkspace } from './local-workspace.js';
|
|
17
21
|
import { repositoryPath } from './repository-snapshots.js';
|
|
18
22
|
import { acpFailure, credentialStrings, parseAcpFailure, providerOutcome, redactFailureText, toolMetadata, } from './acp-outcome.js';
|
|
23
|
+
/** Errors a local tool call returns to the model as feedback; any other one fails the worker.
|
|
24
|
+
* Every code src/local-edits.ts can throw belongs here (a failed replacement is not a crash). */
|
|
25
|
+
export const LOCAL_TOOL_ERRORS = [
|
|
26
|
+
'local_busy',
|
|
27
|
+
'workspace_read_failed',
|
|
28
|
+
'unsafe_path',
|
|
29
|
+
'edit_mismatch',
|
|
30
|
+
'workspace_changed',
|
|
31
|
+
'workspace_busy',
|
|
32
|
+
'workspace_limit',
|
|
33
|
+
'workspace_conflict',
|
|
34
|
+
'not_found',
|
|
35
|
+
'binary_file',
|
|
36
|
+
'line_limit',
|
|
37
|
+
'invalid_field',
|
|
38
|
+
'request_conflict',
|
|
39
|
+
'command_conflict',
|
|
40
|
+
'command_limit',
|
|
41
|
+
'invalid_cursor',
|
|
42
|
+
'shell_binding',
|
|
43
|
+
'listen_unreachable',
|
|
44
|
+
];
|
|
19
45
|
export function processIdentity(pid) {
|
|
20
46
|
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
21
47
|
return null;
|
|
@@ -37,7 +63,18 @@ export function processIdentity(pid) {
|
|
|
37
63
|
throw Error('Invalid process start identity');
|
|
38
64
|
return { start: fields[19], group: fields[2], session: fields[3] };
|
|
39
65
|
};
|
|
40
|
-
const before = stat(), boot = readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim(),
|
|
66
|
+
const before = stat(), boot = readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim(),
|
|
67
|
+
// A tools-user launcher (sudo) is root-owned; its exe link is unreadable to the service.
|
|
68
|
+
executable = (() => {
|
|
69
|
+
try {
|
|
70
|
+
return readlinkSync(path + '/exe');
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.code === 'EACCES')
|
|
74
|
+
return 'unreadable';
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
})(), argv = readFileSync(path + '/cmdline');
|
|
41
78
|
if (!/^[a-f0-9-]{36}$/.test(boot) ||
|
|
42
79
|
!argv.length ||
|
|
43
80
|
argv.length > 16384 ||
|
|
@@ -58,6 +95,35 @@ export function processIdentity(pid) {
|
|
|
58
95
|
return null;
|
|
59
96
|
}
|
|
60
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* When a process started, keyed to this boot. Unlike processIdentity it survives exec, so it
|
|
100
|
+
* names a background task whose gated shell has since become the user's command.
|
|
101
|
+
*/
|
|
102
|
+
export function processStart(pid) {
|
|
103
|
+
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
if (process.platform === 'linux') {
|
|
107
|
+
const value = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
108
|
+
const fields = value
|
|
109
|
+
.slice(value.lastIndexOf(') ') + 2)
|
|
110
|
+
.trim()
|
|
111
|
+
.split(/\s+/);
|
|
112
|
+
const boot = readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
|
|
113
|
+
return /^\d+$/.test(fields[19] ?? '') ? `linux-start-v1:${pid}:${boot}:${fields[19]}` : null;
|
|
114
|
+
}
|
|
115
|
+
const started = execFileSync('/bin/ps', ['-p', String(pid), '-o', 'lstart='], {
|
|
116
|
+
encoding: 'utf8',
|
|
117
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
118
|
+
timeout: 2000,
|
|
119
|
+
killSignal: 'SIGKILL',
|
|
120
|
+
}).trim();
|
|
121
|
+
return started ? `ps-start-v1:${pid}:${started}` : null;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
61
127
|
function groupExists(pid) {
|
|
62
128
|
try {
|
|
63
129
|
process.kill(-pid, 0);
|
|
@@ -73,10 +139,18 @@ export class Supervisor extends EventEmitter {
|
|
|
73
139
|
store;
|
|
74
140
|
options;
|
|
75
141
|
localCoding;
|
|
142
|
+
/** Built-in web search and fetch for interactive coding (operator ruling 2026-10-31). */
|
|
143
|
+
web;
|
|
144
|
+
/** Desk-session hooks (Phase 4b); absent means none run. */
|
|
145
|
+
hooks;
|
|
76
146
|
tasks;
|
|
77
147
|
compactionJobs = new Set();
|
|
148
|
+
fetchBreaker = new HostBreaker();
|
|
78
149
|
managed;
|
|
79
150
|
mcp;
|
|
151
|
+
operatorMcp;
|
|
152
|
+
/** Item 9: the host's pinned persona, given to every worker on this host. */
|
|
153
|
+
persona;
|
|
80
154
|
toolkit;
|
|
81
155
|
station;
|
|
82
156
|
children;
|
|
@@ -118,7 +192,26 @@ export class Supervisor extends EventEmitter {
|
|
|
118
192
|
const child = this.children?.owned(id);
|
|
119
193
|
if (child)
|
|
120
194
|
return JSON.parse(child.authority);
|
|
121
|
-
return workerTools(!!this.options.effectsEnabled, !!this.children?.owned(id), this.codingBinding(id), undefined, !!this.mcp, !!this.toolkit, !!this.station, this.store.localWorkspace(id), this.store.localCoding(id));
|
|
195
|
+
return workerTools(!!this.options.effectsEnabled, !!this.children?.owned(id), this.codingBinding(id), undefined, !!this.mcp, !!this.toolkit, !!this.station, this.store.localWorkspace(id), this.store.localCoding(id), this.declaredMcp(id));
|
|
196
|
+
}
|
|
197
|
+
/** Operator-declared MCP reaches root sessions only; children get no MCP authority. */
|
|
198
|
+
declaredMcp(id) {
|
|
199
|
+
return !!this.operatorMcp && !this.operatorMcp.empty && !this.children?.owned(id);
|
|
200
|
+
}
|
|
201
|
+
/** Every MCP server the session got, from both sources; persisted beside the worker. */
|
|
202
|
+
mcpServers(id) {
|
|
203
|
+
const live = this.workers.get(id)?.mcpServers;
|
|
204
|
+
if (live)
|
|
205
|
+
return structuredClone(live);
|
|
206
|
+
try {
|
|
207
|
+
const path = join(this.options.stateDir, 'workers', id, 'mcp-servers.json');
|
|
208
|
+
return existsSync(path)
|
|
209
|
+
? JSON.parse(readFileSync(path, 'utf8'))
|
|
210
|
+
: undefined;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
122
215
|
}
|
|
123
216
|
childBoundary(id, epoch) {
|
|
124
217
|
const child = this.children?.owned(id);
|
|
@@ -149,6 +242,10 @@ export class Supervisor extends EventEmitter {
|
|
|
149
242
|
}
|
|
150
243
|
bootId = randomUUID();
|
|
151
244
|
workers = new Map();
|
|
245
|
+
boardReadsStore;
|
|
246
|
+
get boardReads() {
|
|
247
|
+
return (this.boardReadsStore ??= new BoardReads(this.store));
|
|
248
|
+
}
|
|
152
249
|
monitor;
|
|
153
250
|
bundle;
|
|
154
251
|
constructor(store, options) {
|
|
@@ -276,6 +373,18 @@ export class Supervisor extends EventEmitter {
|
|
|
276
373
|
worker.mcp = await this.mcp.ready(id);
|
|
277
374
|
if (this.toolkit && !child)
|
|
278
375
|
worker.toolkit = await this.toolkit.ready(id);
|
|
376
|
+
if (this.declaredMcp(id))
|
|
377
|
+
worker.operatorMcp = this.operatorMcp.provenance();
|
|
378
|
+
if (worker.operatorMcp || worker.mcp) {
|
|
379
|
+
// Collisions are refused at admission; nothing is silently shadowed.
|
|
380
|
+
worker.mcpServers = mergeMcpProvenance(worker.operatorMcp ?? [], (worker.mcp?.servers ?? []).map((server) => ({
|
|
381
|
+
name: server.name,
|
|
382
|
+
source: 'agentpod',
|
|
383
|
+
transport: 'http',
|
|
384
|
+
endpoint_digest: server.endpoint_digest,
|
|
385
|
+
declaration_digest: digest(server),
|
|
386
|
+
})));
|
|
387
|
+
}
|
|
279
388
|
if (this.station && !child)
|
|
280
389
|
worker.station = this.station.ready(id);
|
|
281
390
|
this.store.execution.assert(id);
|
|
@@ -290,6 +399,11 @@ export class Supervisor extends EventEmitter {
|
|
|
290
399
|
}), { mode: 0o600 });
|
|
291
400
|
const sessionDir = join(workerDir, 'sessions');
|
|
292
401
|
mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
|
|
402
|
+
const provenancePath = join(workerDir, 'mcp-servers.json');
|
|
403
|
+
if (worker.mcpServers)
|
|
404
|
+
writeFileSync(provenancePath, JSON.stringify(worker.mcpServers, null, 2), { mode: 0o600 });
|
|
405
|
+
else
|
|
406
|
+
rmSync(provenancePath, { force: true });
|
|
293
407
|
const config = {
|
|
294
408
|
socket: this.options.bridgeSocket,
|
|
295
409
|
token: worker.token,
|
|
@@ -306,8 +420,11 @@ export class Supervisor extends EventEmitter {
|
|
|
306
420
|
local: this.store.localWorkspace(id),
|
|
307
421
|
localCoding: this.store.localCoding(id),
|
|
308
422
|
...(worker.mcp ? { mcp: worker.mcp } : {}),
|
|
423
|
+
...(worker.operatorMcp ? { operator_mcp: worker.operatorMcp } : {}),
|
|
424
|
+
...(worker.mcpServers ? { mcp_servers: worker.mcpServers } : {}),
|
|
309
425
|
...(worker.toolkit ? { toolkit: worker.toolkit } : {}),
|
|
310
426
|
...(worker.station ? { station: worker.station } : {}),
|
|
427
|
+
...(this.persona ? { persona: this.persona } : {}),
|
|
311
428
|
};
|
|
312
429
|
const args = [
|
|
313
430
|
PI_CLI,
|
|
@@ -368,6 +485,16 @@ export class Supervisor extends EventEmitter {
|
|
|
368
485
|
worker.pi.on('activity', (observation) => {
|
|
369
486
|
if (!worker.attempt || worker.stopping)
|
|
370
487
|
return;
|
|
488
|
+
// PostToolUse runs after the tool and cannot change it; it is recorded like any hook.
|
|
489
|
+
if (observation.phase === 'finished' && this.hooks) {
|
|
490
|
+
const input = parsedInput(observation.input);
|
|
491
|
+
void this.hook(id, 'PostToolUse', {
|
|
492
|
+
tool_name: observation.tool,
|
|
493
|
+
tool_input: input,
|
|
494
|
+
tool_use_id: observation.tool_call_id,
|
|
495
|
+
tool_response: { output: observation.output ?? '', failed: !!observation.failed },
|
|
496
|
+
}, [observation.tool, ...hookAliases(observation.tool, input)]).catch(() => { });
|
|
497
|
+
}
|
|
371
498
|
try {
|
|
372
499
|
this.store.request(`tool-activity:${id}:${epoch}:${worker.attempt}`, `${observation.phase}:${observation.tool_call_id}`, observation, () => {
|
|
373
500
|
this.store.event(id, `tool.${observation.phase}`, {
|
|
@@ -543,6 +670,58 @@ export class Supervisor extends EventEmitter {
|
|
|
543
670
|
}
|
|
544
671
|
return w;
|
|
545
672
|
}
|
|
673
|
+
/** Is this attempt a task step that has acceptance checks to decide it? */
|
|
674
|
+
/** A person's own local coding session: not a delegated child and not AgentPod-admitted work,
|
|
675
|
+
* whose strict semantics stay as they are. */
|
|
676
|
+
isInteractiveCoding(id) {
|
|
677
|
+
return this.interactiveCoding(id);
|
|
678
|
+
}
|
|
679
|
+
/** Run this desk session's hooks for `event`; other sessions (board, station, children) run none. */
|
|
680
|
+
async hook(id, event, payload, names = []) {
|
|
681
|
+
if (!this.hooks || !this.interactiveCoding(id))
|
|
682
|
+
return null;
|
|
683
|
+
return this.hooks.fire(id, event, payload, names);
|
|
684
|
+
}
|
|
685
|
+
/** A PreToolUse hook that blocks turns the tool call into an error the model reads. */
|
|
686
|
+
async preToolUse(id, tool, input, toolCallId) {
|
|
687
|
+
const out = await this.hook(id, 'PreToolUse', { tool_name: tool, tool_input: input, tool_use_id: toolCallId }, [tool, ...hookAliases(tool, input)]);
|
|
688
|
+
if (out?.blocked)
|
|
689
|
+
throw new ServiceError('hook_blocked', `Blocked by a PreToolUse hook: ${out.blocked}`);
|
|
690
|
+
}
|
|
691
|
+
interactiveCoding(id) {
|
|
692
|
+
return (this.store.localCoding(id) &&
|
|
693
|
+
!this.children?.owned(id) &&
|
|
694
|
+
!this.store.db.prepare('SELECT 1 FROM agentpod_admissions WHERE session_id=?').get(id));
|
|
695
|
+
}
|
|
696
|
+
checkedStepAttempt(attempt) {
|
|
697
|
+
const row = this.store.db
|
|
698
|
+
.prepare('SELECT checks FROM task_steps WHERE attempt_id=?')
|
|
699
|
+
.get(attempt);
|
|
700
|
+
try {
|
|
701
|
+
const checks = row?.checks ? JSON.parse(String(row.checks)) : [];
|
|
702
|
+
return Array.isArray(checks) && checks.length > 0;
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/** A station call refused before it runs leaves no station_calls row, so record its
|
|
709
|
+
* method, requested action and refusal code: never its path, arguments or content. */
|
|
710
|
+
recordStationRefusal(id, method, body, error) {
|
|
711
|
+
const args = body.args && typeof body.args === 'object' ? body.args : {};
|
|
712
|
+
const ident = (v) => typeof v === 'string' && /^[A-Za-z0-9_.:-]{1,64}$/.test(v) ? v : null;
|
|
713
|
+
try {
|
|
714
|
+
this.store.transaction(() => this.store.event(id, 'station.call_refused', {
|
|
715
|
+
attempt_id: this.workers.get(id)?.attempt ?? null,
|
|
716
|
+
method: ident(method),
|
|
717
|
+
action: ident(args.action),
|
|
718
|
+
code: error instanceof ServiceError ? ident(error.code) : 'error',
|
|
719
|
+
}));
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
// Recording a refusal never changes the refusal itself.
|
|
723
|
+
}
|
|
724
|
+
}
|
|
546
725
|
async bridge(id, method, token, body) {
|
|
547
726
|
const w = this.authenticate(id, token, body);
|
|
548
727
|
const route = w.route;
|
|
@@ -587,8 +766,14 @@ export class Supervisor extends EventEmitter {
|
|
|
587
766
|
context_digest: textDigest(w.frame),
|
|
588
767
|
...(boundary ? { worker_binding: boundary } : {}),
|
|
589
768
|
...(w.mcp ? { mcp: w.mcp } : {}),
|
|
769
|
+
...(w.operatorMcp ? { operator_mcp: w.operatorMcp } : {}),
|
|
770
|
+
...(w.mcpServers
|
|
771
|
+
? { mcp_servers: w.mcpServers, mcp_servers_digest: digest(w.mcpServers) }
|
|
772
|
+
: {}),
|
|
590
773
|
...(w.toolkit ? { toolkit: w.toolkit } : {}),
|
|
591
774
|
...(w.station ? { station: w.station } : {}),
|
|
775
|
+
// Item 9: the worker proves it runs with the host's pinned persona.
|
|
776
|
+
...(this.persona ? { persona_digest: this.persona.digest } : {}),
|
|
592
777
|
};
|
|
593
778
|
for (const [key, value] of Object.entries(expected))
|
|
594
779
|
if (canonical(body[key]) !== canonical(value))
|
|
@@ -680,6 +865,45 @@ export class Supervisor extends EventEmitter {
|
|
|
680
865
|
w.secrets = [...new Set([...(w.secrets ?? []), ...credentialStrings(auth)])];
|
|
681
866
|
return auth;
|
|
682
867
|
}
|
|
868
|
+
if (method.startsWith('operator_mcp.')) {
|
|
869
|
+
if (!this.declaredMcp(id) || !w.operatorMcp || !w.attempt)
|
|
870
|
+
throw new ServiceError('mcp_disabled', 'Worker has no declared MCP servers', 403);
|
|
871
|
+
this.assertActive(id, w.epoch, w.attempt);
|
|
872
|
+
if (method === 'operator_mcp.catalog')
|
|
873
|
+
return await this.operatorMcp.catalog(id, body.server, body.tool);
|
|
874
|
+
if (method !== 'operator_mcp.call')
|
|
875
|
+
throw new ServiceError('invalid_field', 'Unknown declared MCP method', 400);
|
|
876
|
+
const attempt = w.attempt;
|
|
877
|
+
const args = body.arguments ?? {};
|
|
878
|
+
const server = String(body.server ?? ''), tool = String(body.tool ?? '');
|
|
879
|
+
// A host failing again and again is paused briefly (never called, so no operator MCP call
|
|
880
|
+
// is recorded); the agent gets a tool error and the run carries on.
|
|
881
|
+
return await callThroughBreaker(this.fetchBreaker, id, args, (paused) => this.store.transaction(() => this.store.event(id, 'fetch.host_paused', { attempt_id: attempt, ...paused })), async () => {
|
|
882
|
+
try {
|
|
883
|
+
const result = await this.operatorMcp.call(id, body.server, body.tool, args);
|
|
884
|
+
this.store.transaction(() => this.store.event(id, 'operator_mcp.call', {
|
|
885
|
+
attempt_id: attempt,
|
|
886
|
+
server,
|
|
887
|
+
tool,
|
|
888
|
+
argument_digest: digest(args),
|
|
889
|
+
outcome: result.is_error ? 'tool_error' : 'returned',
|
|
890
|
+
result_digest: digest(result),
|
|
891
|
+
}));
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
this.store.transaction(() => this.store.event(id, 'operator_mcp.call', {
|
|
896
|
+
attempt_id: attempt,
|
|
897
|
+
server,
|
|
898
|
+
tool,
|
|
899
|
+
argument_digest: digest(args),
|
|
900
|
+
outcome: 'failed',
|
|
901
|
+
error: error.code ?? 'mcp_server_failed',
|
|
902
|
+
}));
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
}
|
|
683
907
|
if (method.startsWith('toolkit.')) {
|
|
684
908
|
if (!this.toolkit || !w.toolkit || !w.attempt || this.children?.owned(id))
|
|
685
909
|
throw new ServiceError('toolkit_disabled', 'Worker has no native Toolkit authority', 403);
|
|
@@ -732,6 +956,15 @@ export class Supervisor extends EventEmitter {
|
|
|
732
956
|
: {}),
|
|
733
957
|
};
|
|
734
958
|
}
|
|
959
|
+
if (method === 'board.read' || method === 'board.status') {
|
|
960
|
+
// Read-only board reads, served by the hub (operator ruling 2026-10-28, option iii).
|
|
961
|
+
if (!this.station || !w.station || !w.attempt || this.children?.owned(id))
|
|
962
|
+
throw new ServiceError('station_disabled', 'Board reads need protected station work');
|
|
963
|
+
this.assertActive(id, w.epoch, w.attempt);
|
|
964
|
+
return method === 'board.read'
|
|
965
|
+
? this.boardReads.request(id, string(body.tool_call_id, 'tool_call_id', 512), object(body.args))
|
|
966
|
+
: this.boardReads.get(id, string(body.read_id, 'read_id', 64));
|
|
967
|
+
}
|
|
735
968
|
if (method.startsWith('station.')) {
|
|
736
969
|
if (!this.station || !w.station || !w.attempt || this.children?.owned(id))
|
|
737
970
|
throw new ServiceError('station_disabled', 'Station execution is unavailable');
|
|
@@ -750,6 +983,77 @@ export class Supervisor extends EventEmitter {
|
|
|
750
983
|
return this.station.view(c.id);
|
|
751
984
|
throw new ServiceError('invalid_field', 'Unsupported station method');
|
|
752
985
|
}
|
|
986
|
+
if (method === 'web.search' || method === 'web.fetch') {
|
|
987
|
+
if (!this.web || !this.store.localCoding(id) || this.children?.owned(id) || !w.attempt)
|
|
988
|
+
throw new ServiceError('web_denied', 'Web search is not admitted for this session', 403);
|
|
989
|
+
this.assertActive(id, w.epoch, w.attempt);
|
|
990
|
+
const args = object(body.args);
|
|
991
|
+
const toolCallId = string(body.tool_call_id, 'tool_call_id', 512);
|
|
992
|
+
await this.preToolUse(id, method === 'web.search' ? 'salaam_web_search' : 'salaam_web_fetch', args, toolCallId);
|
|
993
|
+
if (method === 'web.search') {
|
|
994
|
+
const out = await this.web.search({
|
|
995
|
+
query: string(args.query, 'query', 300),
|
|
996
|
+
...(typeof args.kind === 'string'
|
|
997
|
+
? { kind: args.kind }
|
|
998
|
+
: {}),
|
|
999
|
+
...(typeof args.limit === 'number' ? { limit: args.limit } : {}),
|
|
1000
|
+
});
|
|
1001
|
+
// Recorded: kind, backends tried, which answered and how long. Never a key or the query.
|
|
1002
|
+
this.store.transaction(() => this.store.event(id, 'web.search', {
|
|
1003
|
+
attempt_id: w.attempt,
|
|
1004
|
+
tool_call_id: toolCallId,
|
|
1005
|
+
kind: out.kind,
|
|
1006
|
+
cached: !!out.cached,
|
|
1007
|
+
results: out.results.length,
|
|
1008
|
+
tried: out.tried,
|
|
1009
|
+
}));
|
|
1010
|
+
return out;
|
|
1011
|
+
}
|
|
1012
|
+
const url = string(args.url, 'url', 2000);
|
|
1013
|
+
let host = '';
|
|
1014
|
+
try {
|
|
1015
|
+
host = new URL(url).hostname;
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
/* fetchPage refuses it */
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
const page = await (await import('./web-fetch.js')).fetchPage(url);
|
|
1022
|
+
this.store.transaction(() => this.store.event(id, 'web.fetch', {
|
|
1023
|
+
attempt_id: w.attempt,
|
|
1024
|
+
tool_call_id: toolCallId,
|
|
1025
|
+
host,
|
|
1026
|
+
status: page.status,
|
|
1027
|
+
chars: page.text.length,
|
|
1028
|
+
truncated: page.truncated,
|
|
1029
|
+
}));
|
|
1030
|
+
const { renderFetched } = await import('./web-fetch.js');
|
|
1031
|
+
return { ok: true, url: page.url, text: renderFetched(page) };
|
|
1032
|
+
}
|
|
1033
|
+
catch (error) {
|
|
1034
|
+
const message = String(error.message ?? error).slice(0, 300);
|
|
1035
|
+
this.store.transaction(() => this.store.event(id, 'web.fetch', {
|
|
1036
|
+
attempt_id: w.attempt,
|
|
1037
|
+
tool_call_id: toolCallId,
|
|
1038
|
+
host,
|
|
1039
|
+
error: message,
|
|
1040
|
+
}));
|
|
1041
|
+
return { ok: false, url, error: message };
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (method.startsWith('bgtask.')) {
|
|
1045
|
+
if (!this.localCoding || !this.interactiveCoding(id) || !w.attempt)
|
|
1046
|
+
throw new ServiceError('background_denied', 'Background commands are not admitted', 403);
|
|
1047
|
+
this.assertActive(id, w.epoch, w.attempt);
|
|
1048
|
+
if (method === 'bgtask.list')
|
|
1049
|
+
return { tasks: this.localCoding.tasks(id) };
|
|
1050
|
+
const task = string(body.task_id, 'task_id', 64);
|
|
1051
|
+
if (method === 'bgtask.output')
|
|
1052
|
+
return this.localCoding.taskOutput(id, task);
|
|
1053
|
+
if (method === 'bgtask.stop')
|
|
1054
|
+
return this.localCoding.stopTask(id, task);
|
|
1055
|
+
throw new ServiceError('invalid_field', 'Unsupported background task method');
|
|
1056
|
+
}
|
|
753
1057
|
if (method.startsWith('local.') && method !== 'local.read') {
|
|
754
1058
|
if (!this.localCoding ||
|
|
755
1059
|
!this.store.localCoding(id) ||
|
|
@@ -757,8 +1061,21 @@ export class Supervisor extends EventEmitter {
|
|
|
757
1061
|
!w.attempt)
|
|
758
1062
|
throw new ServiceError('local_denied', 'Local coding is not admitted', 403);
|
|
759
1063
|
this.assertActive(id, w.epoch, w.attempt);
|
|
760
|
-
if (method === 'local.prepare')
|
|
761
|
-
|
|
1064
|
+
if (method === 'local.prepare') {
|
|
1065
|
+
const toolCall = string(body.tool_call_id, 'tool_call_id', 512);
|
|
1066
|
+
const args = object(body.args);
|
|
1067
|
+
// A retried prepare returns the recorded call; its hooks already ran.
|
|
1068
|
+
const fresh = !this.localCoding.hasCall(id, w.epoch, toolCall);
|
|
1069
|
+
if (fresh)
|
|
1070
|
+
await this.preToolUse(id, 'salaam_local', args, toolCall);
|
|
1071
|
+
const prepared = await this.localCoding.prepareReviewed(id, w.epoch, w.attempt, toolCall, args);
|
|
1072
|
+
if (fresh && prepared.approval?.state === 'pending')
|
|
1073
|
+
void this.hook(id, 'Notification', {
|
|
1074
|
+
notification_type: 'permission_prompt',
|
|
1075
|
+
message: `Salaam needs your approval for a ${String(args.action ?? 'local')} request`,
|
|
1076
|
+
}).catch(() => { });
|
|
1077
|
+
return prepared;
|
|
1078
|
+
}
|
|
762
1079
|
const c = this.localCoding.get(string(body.call_id, 'call_id'));
|
|
763
1080
|
if (c.session_id !== id || c.epoch !== w.epoch || c.attempt_id !== w.attempt)
|
|
764
1081
|
throw new ServiceError('local_scope', 'Operation belongs to another invocation', 403);
|
|
@@ -774,6 +1091,7 @@ export class Supervisor extends EventEmitter {
|
|
|
774
1091
|
this.assertActive(id, w.epoch, w.attempt);
|
|
775
1092
|
this.store.taskGrantCheck?.(id);
|
|
776
1093
|
const args = object(body.args);
|
|
1094
|
+
await this.preToolUse(id, 'salaam_workspace', args, string(body.tool_call_id, 'tool_call_id', 512));
|
|
777
1095
|
const result = readLocalWorkspace(this.store.session(id).cwd, args);
|
|
778
1096
|
this.store.transaction(() => this.store.event(id, 'workspace.read', {
|
|
779
1097
|
attempt_id: w.attempt,
|
|
@@ -948,6 +1266,8 @@ export class Supervisor extends EventEmitter {
|
|
|
948
1266
|
}
|
|
949
1267
|
catch (error) {
|
|
950
1268
|
this.store.taskBudgetDenied?.(id, error);
|
|
1269
|
+
if (method.startsWith('station.'))
|
|
1270
|
+
this.recordStationRefusal(id, method, body, error);
|
|
951
1271
|
if (method.startsWith('station.') &&
|
|
952
1272
|
error instanceof ServiceError &&
|
|
953
1273
|
[
|
|
@@ -960,6 +1280,16 @@ export class Supervisor extends EventEmitter {
|
|
|
960
1280
|
'not_found',
|
|
961
1281
|
].includes(error.code))
|
|
962
1282
|
throw error;
|
|
1283
|
+
if (method.startsWith('operator_mcp.') &&
|
|
1284
|
+
error instanceof ServiceError &&
|
|
1285
|
+
[
|
|
1286
|
+
'invalid_field',
|
|
1287
|
+
'invalid_mcp_server',
|
|
1288
|
+
'invalid_mcp_tool',
|
|
1289
|
+
'invalid_mcp_arguments',
|
|
1290
|
+
'mcp_server_failed',
|
|
1291
|
+
].includes(error.code))
|
|
1292
|
+
throw error;
|
|
963
1293
|
if ((method.startsWith('mcp.') || method.startsWith('toolkit.')) &&
|
|
964
1294
|
error instanceof ServiceError &&
|
|
965
1295
|
[
|
|
@@ -989,25 +1319,13 @@ export class Supervisor extends EventEmitter {
|
|
|
989
1319
|
'request_conflict',
|
|
990
1320
|
].includes(error.code))
|
|
991
1321
|
throw error;
|
|
1322
|
+
// A hook that blocked a tool call is the person's policy speaking, not a broken control: the
|
|
1323
|
+
// model reads the reason as the tool's result (Phase 4b).
|
|
1324
|
+
if (error instanceof ServiceError && error.code === 'hook_blocked')
|
|
1325
|
+
throw error;
|
|
992
1326
|
if ((method.startsWith('coding.') || method.startsWith('local.')) &&
|
|
993
1327
|
error instanceof ServiceError &&
|
|
994
|
-
|
|
995
|
-
'local_busy',
|
|
996
|
-
'workspace_read_failed',
|
|
997
|
-
'unsafe_path',
|
|
998
|
-
'workspace_changed',
|
|
999
|
-
'workspace_busy',
|
|
1000
|
-
'workspace_limit',
|
|
1001
|
-
'workspace_conflict',
|
|
1002
|
-
'not_found',
|
|
1003
|
-
'binary_file',
|
|
1004
|
-
'line_limit',
|
|
1005
|
-
'invalid_field',
|
|
1006
|
-
'request_conflict',
|
|
1007
|
-
'command_conflict',
|
|
1008
|
-
'command_limit',
|
|
1009
|
-
'invalid_cursor',
|
|
1010
|
-
].includes(error.code))
|
|
1328
|
+
LOCAL_TOOL_ERRORS.includes(error.code))
|
|
1011
1329
|
throw error;
|
|
1012
1330
|
if (method === 'history' &&
|
|
1013
1331
|
error instanceof ServiceError &&
|
|
@@ -1055,6 +1373,7 @@ export class Supervisor extends EventEmitter {
|
|
|
1055
1373
|
const state = {
|
|
1056
1374
|
...this.store.session(id),
|
|
1057
1375
|
provider_route: this.store.providerRoute(id) ?? undefined,
|
|
1376
|
+
project_instructions: this.tasks.instructions?.summary(id) ?? undefined,
|
|
1058
1377
|
provider_capabilities: cap === undefined
|
|
1059
1378
|
? this.providerCapabilities()
|
|
1060
1379
|
: {
|
|
@@ -1064,6 +1383,7 @@ export class Supervisor extends EventEmitter {
|
|
|
1064
1383
|
? null
|
|
1065
1384
|
: 'No provider-enforced output cap; tasks must explicitly disable token budgeting',
|
|
1066
1385
|
},
|
|
1386
|
+
...(this.mcpServers(id) ? { mcp_servers: this.mcpServers(id) } : {}),
|
|
1067
1387
|
};
|
|
1068
1388
|
const w = this.workers.get(id);
|
|
1069
1389
|
if (w?.invalid)
|
|
@@ -1317,16 +1637,49 @@ export class Supervisor extends EventEmitter {
|
|
|
1317
1637
|
outcome = candidate;
|
|
1318
1638
|
}
|
|
1319
1639
|
}
|
|
1320
|
-
|
|
1640
|
+
// A checked task step is decided by its checks, not by a tool error the model went on
|
|
1641
|
+
// to recover from (operator decision, 2026-10-23). The error stays in the journal
|
|
1642
|
+
// (tool.finished failed=true). A delegated child turn, whose denied probes are its
|
|
1643
|
+
// boundary proof, and any attempt outside a checked step still fail on it, except an
|
|
1644
|
+
// interactive coding turn: at a desk a failed command is feedback, and the turn is
|
|
1645
|
+
// decided by how it ends (step 0, 2026-10-31).
|
|
1646
|
+
if (value.type === 'tool_execution_end' &&
|
|
1647
|
+
value.isError === true &&
|
|
1648
|
+
(this.children?.owned(id) ||
|
|
1649
|
+
(!this.checkedStepAttempt(work.attempt.id) && !this.interactiveCoding(id))))
|
|
1321
1650
|
outcome ??= {
|
|
1322
1651
|
version: 1,
|
|
1323
1652
|
stopReason: 'error',
|
|
1324
|
-
failure: acpFailure(new ServiceError('tool_failed',
|
|
1653
|
+
failure: acpFailure(new ServiceError('tool_failed', toolFailureMessage(value.toolName))),
|
|
1325
1654
|
};
|
|
1326
1655
|
};
|
|
1327
1656
|
w.pi.on('record', record);
|
|
1328
1657
|
try {
|
|
1329
|
-
|
|
1658
|
+
// Interactive sessions pick up AGENTS.md edits at the start of each turn (recorded as a new
|
|
1659
|
+
// instructions.loaded revision). A discovery failure never blocks the turn.
|
|
1660
|
+
try {
|
|
1661
|
+
this.tasks.instructions?.refresh(id);
|
|
1662
|
+
}
|
|
1663
|
+
catch {
|
|
1664
|
+
/* the previously loaded guidance stays in force */
|
|
1665
|
+
}
|
|
1666
|
+
// The folder as it was before this turn, so /rewind can return to it.
|
|
1667
|
+
if (this.interactiveCoding(id))
|
|
1668
|
+
this.tasks.checkpoints?.capture(id, work.attempt.id);
|
|
1669
|
+
// UserPromptSubmit hooks may stop the prompt or add context to it (Phase 4b).
|
|
1670
|
+
let message = work.submission.text;
|
|
1671
|
+
const gate = await this.hook(id, 'UserPromptSubmit', { prompt: message });
|
|
1672
|
+
if (gate?.blocked) {
|
|
1673
|
+
this.store.finish(work.attempt.id, 'failed', null, 'Blocked by a UserPromptSubmit hook: ' + gate.blocked);
|
|
1674
|
+
return true;
|
|
1675
|
+
}
|
|
1676
|
+
if (gate?.context)
|
|
1677
|
+
message += '\n\n<hook-context>\n' + gate.context + '\n</hook-context>';
|
|
1678
|
+
const images = takeImages(this.store.db, work.submission.id);
|
|
1679
|
+
const response = await w.pi.command('prompt', {
|
|
1680
|
+
message,
|
|
1681
|
+
...(images.length ? { images } : {}),
|
|
1682
|
+
});
|
|
1330
1683
|
const disposition = object(response.data ?? {}).disposition;
|
|
1331
1684
|
if (disposition !== 'started')
|
|
1332
1685
|
throw new Error('Pi did not start the dispatched prompt');
|
|
@@ -1338,6 +1691,7 @@ export class Supervisor extends EventEmitter {
|
|
|
1338
1691
|
const result = object((await w.pi.command('get_last_assistant_text')).data);
|
|
1339
1692
|
this.childBoundary(id, w.epoch);
|
|
1340
1693
|
this.store.finish(work.attempt.id, outcome ? 'failed' : 'completed', String(result.text ?? ''), outcome?.failure?.message ?? null, { epoch: w.epoch, ...entries }, outcome);
|
|
1694
|
+
void this.hook(id, 'Stop', { outcome: outcome ? 'failed' : 'completed' }).catch(() => { });
|
|
1341
1695
|
}
|
|
1342
1696
|
catch (error) {
|
|
1343
1697
|
const message = redactFailureText(error.message, w.secrets);
|
|
@@ -1386,6 +1740,7 @@ export class Supervisor extends EventEmitter {
|
|
|
1386
1740
|
try {
|
|
1387
1741
|
this.managed?.invalidateWorker(w.id, w.epoch);
|
|
1388
1742
|
this.cancelMcpWorker(w);
|
|
1743
|
+
void this.operatorMcp?.stopSession(w.id).catch(() => { });
|
|
1389
1744
|
this.children?.workerLost(w.id, 'Worker stopped before child dispatch');
|
|
1390
1745
|
}
|
|
1391
1746
|
catch {
|
|
@@ -1414,3 +1769,31 @@ export class Supervisor extends EventEmitter {
|
|
|
1414
1769
|
throw new AggregateError(errors, 'Worker shutdown could not persist all final states');
|
|
1415
1770
|
}
|
|
1416
1771
|
}
|
|
1772
|
+
/** Names the failing tool (never its arguments or output), so a failed step says which call
|
|
1773
|
+
* failed it. Only a plain tool identifier is echoed. */
|
|
1774
|
+
export function toolFailureMessage(toolName) {
|
|
1775
|
+
return typeof toolName === 'string' && /^[A-Za-z0-9_.:-]{1,64}$/.test(toolName)
|
|
1776
|
+
? `Tool execution failed (tool ${toolName})`
|
|
1777
|
+
: 'Tool execution failed';
|
|
1778
|
+
}
|
|
1779
|
+
/** Claude Code names for Salaam's tools, so a `Bash` or `Edit|Write` matcher works unchanged. */
|
|
1780
|
+
export function hookAliases(tool, input) {
|
|
1781
|
+
if (tool === 'salaam_local')
|
|
1782
|
+
return input.action === 'command' ? ['Bash'] : input.action === 'edit' ? ['Edit', 'Write'] : [];
|
|
1783
|
+
if (tool === 'salaam_web_search')
|
|
1784
|
+
return ['WebSearch'];
|
|
1785
|
+
if (tool === 'salaam_web_fetch')
|
|
1786
|
+
return ['WebFetch'];
|
|
1787
|
+
if (tool === 'salaam_workspace')
|
|
1788
|
+
return ['Read'];
|
|
1789
|
+
return [];
|
|
1790
|
+
}
|
|
1791
|
+
function parsedInput(input) {
|
|
1792
|
+
try {
|
|
1793
|
+
const value = JSON.parse(input ?? '{}');
|
|
1794
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
1795
|
+
}
|
|
1796
|
+
catch {
|
|
1797
|
+
return {};
|
|
1798
|
+
}
|
|
1799
|
+
}
|