@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/host.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DEFAULT_COMMAND_NETWORK_SOCKET } from './command-network.js';
|
|
2
|
+
import { loadDangerClassifierOptions } from './danger-classifier.js';
|
|
3
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
2
4
|
import { once } from 'node:events';
|
|
3
5
|
import { readFileSync, readdirSync, mkdirSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync, existsSync, realpathSync, constants, fstatSync, lstatSync, } from 'node:fs';
|
|
4
6
|
import { join, resolve, isAbsolute, sep } from 'node:path';
|
|
@@ -15,6 +17,90 @@ import { acquireStateLock } from './state-lock.js';
|
|
|
15
17
|
import { call } from './transport.js';
|
|
16
18
|
import { readStationConfig } from './station-config.js';
|
|
17
19
|
import { readToolkitConfig } from './toolkit-config.js';
|
|
20
|
+
import { OperatorMcp } from './operator-mcp.js';
|
|
21
|
+
import { chooseLocalSandbox, resolveReadPaths, sandboxLabel, } from './command-sandbox.js';
|
|
22
|
+
import { loadPersona } from './persona.js';
|
|
23
|
+
export function hostLocalSandbox(platform = process.platform, find, findSb, allowUnsandboxed = false, brokerSocket, readPaths = [], allowFullAccessSessions = false, allowYoloSessions = false, dangerClassifier) {
|
|
24
|
+
const { kind } = chooseLocalSandbox('workspace-write', platform, find, findSb);
|
|
25
|
+
return {
|
|
26
|
+
kind,
|
|
27
|
+
label: kind === 'none' && !allowUnsandboxed ? 'refused' : sandboxLabel(kind),
|
|
28
|
+
...(allowUnsandboxed ? { unsandboxed_commands: 'allow' } : {}),
|
|
29
|
+
...(brokerSocket
|
|
30
|
+
? {
|
|
31
|
+
command_network: {
|
|
32
|
+
mode: 'broker',
|
|
33
|
+
socket: brokerSocket,
|
|
34
|
+
label: 'broker (hosts set by the stk policy)',
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
: {}),
|
|
38
|
+
...(readPaths.length ? { read_paths: [...readPaths] } : {}),
|
|
39
|
+
...(allowFullAccessSessions ? { full_access_sessions: 'allow' } : {}),
|
|
40
|
+
...(allowYoloSessions ? { yolo_sessions: 'allow' } : {}),
|
|
41
|
+
...(dangerClassifier ? { danger_classifier: dangerClassifier } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** The read-only paths outside the workspace the reviewed manifest lets local commands read. */
|
|
45
|
+
export function hostReadPaths(m) {
|
|
46
|
+
const r = m.local_sandbox?.read_paths;
|
|
47
|
+
return Array.isArray(r) ? r.filter((p) => typeof p === 'string') : [];
|
|
48
|
+
}
|
|
49
|
+
/** Whether the reviewed manifest lets an interactive session choose full access (Phase 2b). */
|
|
50
|
+
export function hostAllowsFullAccessSessions(m) {
|
|
51
|
+
return (m.local_sandbox?.full_access_sessions === 'allow');
|
|
52
|
+
}
|
|
53
|
+
/** Whether the reviewed manifest lets a desk session choose YOLO. */
|
|
54
|
+
export function hostAllowsYoloSessions(m) {
|
|
55
|
+
return m.local_sandbox?.yolo_sessions === 'allow';
|
|
56
|
+
}
|
|
57
|
+
/** The command reviewer a host service may run: a reviewed config and optional pin, or none
|
|
58
|
+
* ({ pin: 'off' }). Absent means forbidden: hosts get no reviewer without the operator. */
|
|
59
|
+
export function hostDangerClassifier(m) {
|
|
60
|
+
const d = m.local_sandbox?.danger_classifier;
|
|
61
|
+
if (!d || d.mode !== 'config')
|
|
62
|
+
return { pin: 'off' };
|
|
63
|
+
return { config: d.config, ...(d.pin ? { pin: d.pin } : {}) };
|
|
64
|
+
}
|
|
65
|
+
/** The broker socket local commands reach the network through, or null for no network. */
|
|
66
|
+
export function hostCommandNetwork(m) {
|
|
67
|
+
const n = m.local_sandbox?.command_network;
|
|
68
|
+
return n?.mode === 'broker' && typeof n.socket === 'string' ? { socket: n.socket } : null;
|
|
69
|
+
}
|
|
70
|
+
/** Whether the reviewed manifest lets local commands run unsandboxed on a host without a sandbox. */
|
|
71
|
+
export function hostAllowsUnsandboxed(m) {
|
|
72
|
+
return (m.local_sandbox?.unsandboxed_commands === 'allow');
|
|
73
|
+
}
|
|
74
|
+
const groupName = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
75
|
+
/** Resolve a system group by name. The launcher profile never accepts a numeric id alone. */
|
|
76
|
+
export function resolveOperatorGroup(group) {
|
|
77
|
+
if (typeof group !== 'string' || !groupName.test(group))
|
|
78
|
+
throw Error('operator_group must be a system group name');
|
|
79
|
+
let line;
|
|
80
|
+
try {
|
|
81
|
+
line = execFileSync('/usr/bin/getent', ['group', group], { encoding: 'utf8' }).trim();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw Error('operator_group does not exist');
|
|
85
|
+
}
|
|
86
|
+
const [name, , id] = line.split(':');
|
|
87
|
+
const gid = Number(id);
|
|
88
|
+
if (name !== group || !Number.isInteger(gid) || gid <= 0)
|
|
89
|
+
throw Error('operator_group must name a non-root group');
|
|
90
|
+
return { group, gid };
|
|
91
|
+
}
|
|
92
|
+
/** One minute to one day: long enough for a person to answer, short enough to free the station. */
|
|
93
|
+
function approvalTtl(value) {
|
|
94
|
+
if (!Number.isInteger(value) || value < 60000 || value > 86400000)
|
|
95
|
+
throw Error('approval_ttl_ms must be an integer from 60000 to 86400000');
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
export function hostApprovalTtlMs(m) {
|
|
99
|
+
return m.schema === 6 ? m.approval_ttl_ms : undefined;
|
|
100
|
+
}
|
|
101
|
+
export function hostOperatorAccess(m) {
|
|
102
|
+
return m.schema === 6 ? m.operator_access : undefined;
|
|
103
|
+
}
|
|
18
104
|
export function hostMcp(m) {
|
|
19
105
|
return m.schema === 3 || m.schema === 4 || m.schema === 5 || m.schema === 6
|
|
20
106
|
? m.managed_mcp
|
|
@@ -35,7 +121,7 @@ function privateDocument(path) {
|
|
|
35
121
|
closeSync(fd);
|
|
36
122
|
}
|
|
37
123
|
}
|
|
38
|
-
function privateState(path) {
|
|
124
|
+
function privateState(path, gid) {
|
|
39
125
|
if (!isAbsolute(path) || resolve(path) !== path)
|
|
40
126
|
throw Error('Use canonical absolute private state paths');
|
|
41
127
|
// Existing ancestors must resolve to their literal identity; never follow a workspace symlink.
|
|
@@ -46,7 +132,9 @@ function privateState(path) {
|
|
|
46
132
|
throw Error('Private state path must not contain symlinks');
|
|
47
133
|
if (existsSync(path)) {
|
|
48
134
|
const st = lstatSync(path);
|
|
49
|
-
|
|
135
|
+
// The launcher profile lets exactly one group traverse (x) to the operator socket.
|
|
136
|
+
const shared = gid !== undefined && (st.mode & 0o077) === 0o010 && st.gid === gid;
|
|
137
|
+
if (!st.isDirectory() || st.uid !== process.getuid?.() || (st.mode & 0o077 && !shared))
|
|
50
138
|
throw Error('Host state must be a private owner-held directory');
|
|
51
139
|
}
|
|
52
140
|
return path;
|
|
@@ -64,13 +152,88 @@ async function planAgentPod(c) {
|
|
|
64
152
|
'toolkit_config',
|
|
65
153
|
'station_config',
|
|
66
154
|
'strict_work_version',
|
|
155
|
+
'operator_group',
|
|
156
|
+
'operator_mcp_config',
|
|
157
|
+
'persona_config',
|
|
158
|
+
'approval_ttl_ms',
|
|
159
|
+
'unsandboxed_commands',
|
|
160
|
+
'command_network',
|
|
161
|
+
'command_network_socket',
|
|
162
|
+
'local_sandbox_read_paths',
|
|
163
|
+
'allow_full_access_sessions',
|
|
164
|
+
'allow_yolo_sessions',
|
|
165
|
+
'danger_classifier',
|
|
67
166
|
];
|
|
68
167
|
if (Object.keys(c).some((k) => !fields.includes(k)))
|
|
69
168
|
throw Error('Unexpected AgentPod host config field');
|
|
70
169
|
if (c.strict_work_version !== undefined &&
|
|
71
170
|
(c.strict_work_version !== 1 || c.station_config === undefined))
|
|
72
171
|
throw Error('Strict work version1 requires reviewed protected station config');
|
|
73
|
-
|
|
172
|
+
let access;
|
|
173
|
+
if (c.operator_group !== undefined) {
|
|
174
|
+
if (c.strict_work_version !== 1)
|
|
175
|
+
throw Error('operator_group is only for the strict work launcher profile');
|
|
176
|
+
if (process.platform !== 'linux' || process.getuid?.() !== 0)
|
|
177
|
+
throw Error('The launcher profile is planned by root on Linux');
|
|
178
|
+
access = resolveOperatorGroup(c.operator_group);
|
|
179
|
+
}
|
|
180
|
+
let operatorMcp;
|
|
181
|
+
if (c.operator_mcp_config !== undefined) {
|
|
182
|
+
if (c.strict_work_version !== 1)
|
|
183
|
+
throw Error('operator_mcp_config is only for the strict work host profile');
|
|
184
|
+
const config = string(c.operator_mcp_config, 'operator_mcp_config');
|
|
185
|
+
if (!isAbsolute(config) || realpathSync(config) !== config)
|
|
186
|
+
throw Error('Use a canonical absolute operator MCP config path');
|
|
187
|
+
operatorMcp = { config, digest: OperatorMcp.load(config).digest };
|
|
188
|
+
}
|
|
189
|
+
let persona;
|
|
190
|
+
if (c.persona_config !== undefined) {
|
|
191
|
+
if (c.strict_work_version !== 1)
|
|
192
|
+
throw Error('persona_config is only for the strict work host profile');
|
|
193
|
+
const config = string(c.persona_config, 'persona_config');
|
|
194
|
+
persona = { config, digest: loadPersona(config).digest };
|
|
195
|
+
}
|
|
196
|
+
let ttl;
|
|
197
|
+
if (c.approval_ttl_ms !== undefined) {
|
|
198
|
+
if (c.strict_work_version !== 1)
|
|
199
|
+
throw Error('approval_ttl_ms is only for the strict work host profile');
|
|
200
|
+
ttl = approvalTtl(c.approval_ttl_ms);
|
|
201
|
+
}
|
|
202
|
+
if (c.unsandboxed_commands !== undefined && c.unsandboxed_commands !== 'allow')
|
|
203
|
+
throw Error('unsandboxed_commands is "allow" or absent');
|
|
204
|
+
const allowUnsandboxed = c.unsandboxed_commands === 'allow';
|
|
205
|
+
if (c.allow_full_access_sessions !== undefined && c.allow_full_access_sessions !== true)
|
|
206
|
+
throw Error('allow_full_access_sessions is true or absent');
|
|
207
|
+
const allowFullAccessSessions = c.allow_full_access_sessions === true;
|
|
208
|
+
if (c.allow_yolo_sessions !== undefined && c.allow_yolo_sessions !== true)
|
|
209
|
+
throw Error('allow_yolo_sessions is true or absent');
|
|
210
|
+
const allowYoloSessions = c.allow_yolo_sessions === true;
|
|
211
|
+
if (c.command_network !== undefined && c.command_network !== 'broker')
|
|
212
|
+
throw Error('command_network is "broker" or absent');
|
|
213
|
+
if (c.command_network_socket !== undefined) {
|
|
214
|
+
if (c.command_network !== 'broker')
|
|
215
|
+
throw Error('command_network_socket is only for command_network "broker"');
|
|
216
|
+
const sock = c.command_network_socket;
|
|
217
|
+
if (typeof sock !== 'string' || !isAbsolute(sock) || sock.includes('\0'))
|
|
218
|
+
throw Error('command_network_socket must be an absolute path');
|
|
219
|
+
}
|
|
220
|
+
const dangerClassifier = parseHostDangerClassifier(c.danger_classifier);
|
|
221
|
+
let readPaths = [];
|
|
222
|
+
if (c.local_sandbox_read_paths !== undefined) {
|
|
223
|
+
const raw = c.local_sandbox_read_paths;
|
|
224
|
+
if (!Array.isArray(raw) || raw.some((p) => typeof p !== 'string'))
|
|
225
|
+
throw Error('local_sandbox_read_paths is a list of absolute paths or "toolchain"');
|
|
226
|
+
try {
|
|
227
|
+
readPaths = resolveReadPaths(raw);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
throw Error(error.message);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const brokerSocket = c.command_network === 'broker'
|
|
234
|
+
? String(c.command_network_socket ?? DEFAULT_COMMAND_NETWORK_SOCKET)
|
|
235
|
+
: undefined;
|
|
236
|
+
const state = privateState(string(c.state, 'state'), access?.gid);
|
|
74
237
|
const config = string(c.agentpod_config, 'agentpod_config');
|
|
75
238
|
if (!isAbsolute(config) || realpathSync(config) !== config)
|
|
76
239
|
throw Error('Use a canonical absolute pins path');
|
|
@@ -106,7 +269,14 @@ async function planAgentPod(c) {
|
|
|
106
269
|
...(station
|
|
107
270
|
? {
|
|
108
271
|
...(c.strict_work_version === 1
|
|
109
|
-
? {
|
|
272
|
+
? {
|
|
273
|
+
schema: 6,
|
|
274
|
+
strict_work: { version: 1 },
|
|
275
|
+
...(access ? { operator_access: access } : {}),
|
|
276
|
+
...(operatorMcp ? { operator_mcp: operatorMcp } : {}),
|
|
277
|
+
...(persona ? { persona } : {}),
|
|
278
|
+
...(ttl !== undefined ? { approval_ttl_ms: ttl } : {}),
|
|
279
|
+
}
|
|
110
280
|
: { schema: 5 }),
|
|
111
281
|
station_execution: station,
|
|
112
282
|
...(toolkit ? { managed_toolkit: toolkit } : {}),
|
|
@@ -132,6 +302,45 @@ async function planAgentPod(c) {
|
|
|
132
302
|
pins_digest: digest(pins),
|
|
133
303
|
workspace: pins.workspace,
|
|
134
304
|
cli_digest: digest(readFileSync(join(ROOT, 'bin/salaam.mjs'), 'utf8')),
|
|
305
|
+
local_sandbox: hostLocalSandbox(undefined, undefined, undefined, allowUnsandboxed, brokerSocket, readPaths, allowFullAccessSessions, allowYoloSessions, dangerClassifier),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function reviewerDigest(path) {
|
|
309
|
+
try {
|
|
310
|
+
return digest(readFileSync(path, 'utf8'));
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function parseHostDangerClassifier(raw) {
|
|
317
|
+
if (raw === undefined)
|
|
318
|
+
return undefined;
|
|
319
|
+
if (raw === 'forbid')
|
|
320
|
+
return { mode: 'forbid' };
|
|
321
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
322
|
+
throw Error('danger_classifier is "forbid" or { config, pin? }');
|
|
323
|
+
const c = raw;
|
|
324
|
+
if (Object.keys(c).some((k) => k !== 'config' && k !== 'pin'))
|
|
325
|
+
throw Error('danger_classifier takes only config and pin');
|
|
326
|
+
if (typeof c.config !== 'string' || !isAbsolute(c.config) || c.config.includes('\0'))
|
|
327
|
+
throw Error('danger_classifier.config must be an absolute path');
|
|
328
|
+
let names;
|
|
329
|
+
try {
|
|
330
|
+
// Keys are named by environment variable and checked when the service starts, not here.
|
|
331
|
+
const anyKey = new Proxy({}, { get: () => 'checked-at-start' });
|
|
332
|
+
names = loadDangerClassifierOptions(JSON.parse(readFileSync(c.config, 'utf8')), anyKey).options.map((o) => o.name);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
throw Error('danger_classifier.config: ' + error.message);
|
|
336
|
+
}
|
|
337
|
+
if (c.pin !== undefined && (typeof c.pin !== 'string' || !names.includes(c.pin)))
|
|
338
|
+
throw Error("danger_classifier.pin must name one of the config's options");
|
|
339
|
+
return {
|
|
340
|
+
mode: 'config',
|
|
341
|
+
config: c.config,
|
|
342
|
+
digest: reviewerDigest(c.config),
|
|
343
|
+
...(typeof c.pin === 'string' ? { pin: c.pin } : {}),
|
|
135
344
|
};
|
|
136
345
|
}
|
|
137
346
|
export function hostDigests() {
|
|
@@ -221,15 +430,59 @@ export function readHost(path) {
|
|
|
221
430
|
: []),
|
|
222
431
|
...([5, 6].includes(m.schema) ? ['station_execution'] : []),
|
|
223
432
|
...(m.schema === 6 ? ['strict_work'] : []),
|
|
433
|
+
...(m.schema === 6 && Object.hasOwn(m, 'operator_access') ? ['operator_access'] : []),
|
|
434
|
+
...(m.schema === 6 && Object.hasOwn(m, 'operator_mcp') ? ['operator_mcp'] : []),
|
|
435
|
+
...(m.schema === 6 && Object.hasOwn(m, 'persona') ? ['persona'] : []),
|
|
436
|
+
...(m.schema === 6 && Object.hasOwn(m, 'approval_ttl_ms') ? ['approval_ttl_ms'] : []),
|
|
437
|
+
...(Object.hasOwn(m, 'local_sandbox') ? ['local_sandbox'] : []),
|
|
224
438
|
];
|
|
225
439
|
if (m.mode !== 'agentpod' || Object.keys(m).sort().join(',') !== fields.sort().join(','))
|
|
226
440
|
throw Error('Unsupported AgentPod host manifest fields');
|
|
227
441
|
if (m.schema === 6 &&
|
|
228
442
|
(Object.keys(object(m.strict_work)).join(',') !== 'version' || m.strict_work.version !== 1))
|
|
229
443
|
throw Error('Unsupported strict work host contract');
|
|
230
|
-
|
|
444
|
+
const access = hostOperatorAccess(m);
|
|
445
|
+
if (access !== undefined) {
|
|
446
|
+
const a = object(access);
|
|
447
|
+
if (Object.keys(a).sort().join(',') !== 'gid,group' ||
|
|
448
|
+
canonical(resolveOperatorGroup(a.group)) !== canonical({ group: a.group, gid: a.gid }))
|
|
449
|
+
throw Error('Host operator group changed');
|
|
450
|
+
}
|
|
451
|
+
privateState(m.state, access?.gid);
|
|
452
|
+
if (m.schema === 6 && Object.hasOwn(m, 'approval_ttl_ms'))
|
|
453
|
+
approvalTtl(m.approval_ttl_ms);
|
|
454
|
+
if (m.schema === 6 && Object.hasOwn(m, 'operator_mcp')) {
|
|
455
|
+
const o = object(m.operator_mcp);
|
|
456
|
+
if (Object.keys(o).sort().join(',') !== 'config,digest' ||
|
|
457
|
+
typeof o.config !== 'string' ||
|
|
458
|
+
!isAbsolute(o.config) ||
|
|
459
|
+
realpathSync(o.config) !== o.config ||
|
|
460
|
+
OperatorMcp.load(o.config).digest !== o.digest)
|
|
461
|
+
throw Error('Operator MCP declarations changed');
|
|
462
|
+
}
|
|
463
|
+
if (m.schema === 6 && Object.hasOwn(m, 'persona')) {
|
|
464
|
+
const p = object(m.persona);
|
|
465
|
+
let current = null;
|
|
466
|
+
try {
|
|
467
|
+
current = loadPersona(String(p.config)).digest;
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
current = null;
|
|
471
|
+
}
|
|
472
|
+
if (Object.keys(p).sort().join(',') !== 'config,digest' || current !== p.digest)
|
|
473
|
+
throw Error('Host persona changed; run host plan again');
|
|
474
|
+
}
|
|
231
475
|
if (m.cli_digest !== digest(readFileSync(join(ROOT, 'bin/salaam.mjs'), 'utf8')))
|
|
232
476
|
throw Error('Host CLI changed');
|
|
477
|
+
if (Object.hasOwn(m, 'local_sandbox') &&
|
|
478
|
+
canonical(m.local_sandbox) !==
|
|
479
|
+
canonical(hostLocalSandbox(undefined, undefined, undefined, hostAllowsUnsandboxed(m), hostCommandNetwork(m)?.socket, hostReadPaths(m), hostAllowsFullAccessSessions(m), hostAllowsYoloSessions(m), m.local_sandbox?.danger_classifier)))
|
|
480
|
+
throw Error('Host command sandbox changed; run host plan again');
|
|
481
|
+
{
|
|
482
|
+
const d = m.local_sandbox?.danger_classifier;
|
|
483
|
+
if (d?.mode === 'config' && reviewerDigest(d.config) !== d.digest)
|
|
484
|
+
throw Error('Host command reviewer config changed; run host plan again');
|
|
485
|
+
}
|
|
233
486
|
const pins = readAgentPodPins(m.agentpod_config, m.state);
|
|
234
487
|
const managed = hostMcp(m);
|
|
235
488
|
if (m.schema === 3 || ([4, 5, 6].includes(m.schema) && Object.hasOwn(m, 'managed_mcp'))) {
|
|
@@ -268,6 +521,31 @@ export function readHost(path) {
|
|
|
268
521
|
}
|
|
269
522
|
return m;
|
|
270
523
|
}
|
|
524
|
+
/** What the daemon reports about itself so that a caller which cannot read the
|
|
525
|
+
* manifest (the launcher profile) receives the same receipt as host start. */
|
|
526
|
+
export function hostReceipt(m) {
|
|
527
|
+
if (m.schema === 1)
|
|
528
|
+
return undefined;
|
|
529
|
+
const pins = readAgentPodPins(m.agentpod_config, m.state);
|
|
530
|
+
return {
|
|
531
|
+
version: 1,
|
|
532
|
+
ready: true,
|
|
533
|
+
manifest_digest: digest(m),
|
|
534
|
+
node: m.node,
|
|
535
|
+
cli: join(m.repo, 'bin/salaam.mjs'),
|
|
536
|
+
state: m.state,
|
|
537
|
+
workspace: m.workspace,
|
|
538
|
+
node_id: pins.node_id,
|
|
539
|
+
pins_digest: m.pins_digest,
|
|
540
|
+
strict_work: m.schema === 6 ? m.strict_work : null,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
/** The tools user a reviewed station config names, for host serve's --tools-user. */
|
|
544
|
+
export function hostToolsUser(m) {
|
|
545
|
+
if (m.schema !== 5 && m.schema !== 6)
|
|
546
|
+
return undefined;
|
|
547
|
+
return readStationConfig(m.station_execution.config, m.workspace, m.station_execution.configuration_digest).tools_user;
|
|
548
|
+
}
|
|
271
549
|
export function hostUnit(path, platform) {
|
|
272
550
|
const m = readHost(path), manifest = realpathSync(path), args = [m.node, join(m.repo, 'bin/salaam.mjs'), 'host', 'run', '--manifest', manifest];
|
|
273
551
|
if (platform === 'systemd') {
|