@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/src/host.ts
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 {
|
|
4
6
|
readFileSync,
|
|
@@ -30,6 +32,14 @@ import { acquireStateLock } from './state-lock.js';
|
|
|
30
32
|
import { call } from './transport.js';
|
|
31
33
|
import { readStationConfig } from './station-config.js';
|
|
32
34
|
import { readToolkitConfig } from './toolkit-config.js';
|
|
35
|
+
import { OperatorMcp } from './operator-mcp.js';
|
|
36
|
+
import {
|
|
37
|
+
chooseLocalSandbox,
|
|
38
|
+
resolveReadPaths,
|
|
39
|
+
sandboxLabel,
|
|
40
|
+
type SandboxKind,
|
|
41
|
+
} from './command-sandbox.js';
|
|
42
|
+
import { loadPersona } from './persona.js';
|
|
33
43
|
interface HostIdentity {
|
|
34
44
|
node: string;
|
|
35
45
|
node_version: string;
|
|
@@ -53,6 +63,7 @@ interface AgentPodHostIdentity extends HostIdentity {
|
|
|
53
63
|
pins_digest: string;
|
|
54
64
|
workspace: string;
|
|
55
65
|
cli_digest: string;
|
|
66
|
+
local_sandbox?: HostLocalSandbox;
|
|
56
67
|
}
|
|
57
68
|
export interface AgentPodHostManifest extends AgentPodHostIdentity {
|
|
58
69
|
schema: 2;
|
|
@@ -78,6 +89,135 @@ export interface AgentPodStrictWorkHostManifest extends Omit<
|
|
|
78
89
|
> {
|
|
79
90
|
schema: 6;
|
|
80
91
|
strict_work: { version: 1 };
|
|
92
|
+
/** Launcher profile: a root host whose operator socket one unprivileged group may use. */
|
|
93
|
+
operator_access?: OperatorAccess;
|
|
94
|
+
/** Operator-declared MCP servers (docs/mcp.md), pinned by declaration digest. */
|
|
95
|
+
operator_mcp?: { config: string; digest: string };
|
|
96
|
+
/** Item 9: the operator's persona file, pinned by content digest. */
|
|
97
|
+
persona?: { config: string; digest: string };
|
|
98
|
+
/** How long a strict approval offer stays answerable; the daemon default when absent. */
|
|
99
|
+
approval_ttl_ms?: number;
|
|
100
|
+
}
|
|
101
|
+
/** Item 10: the command sandbox this host gives local commands, as reviewed at host plan. With no
|
|
102
|
+
* usable sandbox the host refuses local commands (label "refused") unless the operator set
|
|
103
|
+
* `unsandboxed_commands: "allow"`, which the manifest pins (operator ruling 2026-10-30, option A). */
|
|
104
|
+
export interface HostLocalSandbox {
|
|
105
|
+
kind: SandboxKind;
|
|
106
|
+
label: string;
|
|
107
|
+
unsandboxed_commands?: 'allow';
|
|
108
|
+
/** Network allowlist (operator rulings 2026-10-30): local commands reach the network only
|
|
109
|
+
* through stk's local-commands socket, whose egress.hosts the host operator sets. */
|
|
110
|
+
command_network?: { mode: 'broker'; socket: string; label: string };
|
|
111
|
+
/** Phase 1 (2026-10-31): operator-allowed read-only paths outside the workspace, resolved at host
|
|
112
|
+
* plan (`toolchain` expanded) so the reviewed manifest names every one. */
|
|
113
|
+
read_paths?: string[];
|
|
114
|
+
/** Phase 2b: whether an interactive session on this host may choose full access (no OS sandbox).
|
|
115
|
+
* Hosts allow it only when the operator sets `allow_full_access_sessions: true`. */
|
|
116
|
+
full_access_sessions?: 'allow';
|
|
117
|
+
/** Operator ruling 2026-10-31: whether a desk session on this host may choose YOLO (no sandbox, no
|
|
118
|
+
* approvals). Hosts allow it only when the operator sets `allow_yolo_sessions: true`. */
|
|
119
|
+
yolo_sessions?: 'allow';
|
|
120
|
+
/** Operator 2026-10-31: the command reviewer on this host. Forbidden unless the operator names a
|
|
121
|
+
* reviewed config (its digest pinned here), optionally pinning one of its options. */
|
|
122
|
+
danger_classifier?:
|
|
123
|
+
{ mode: 'forbid' } | { mode: 'config'; config: string; digest: string; pin?: string };
|
|
124
|
+
}
|
|
125
|
+
export function hostLocalSandbox(
|
|
126
|
+
platform: NodeJS.Platform = process.platform,
|
|
127
|
+
find?: Parameters<typeof chooseLocalSandbox>[2],
|
|
128
|
+
findSb?: Parameters<typeof chooseLocalSandbox>[3],
|
|
129
|
+
allowUnsandboxed = false,
|
|
130
|
+
brokerSocket?: string,
|
|
131
|
+
readPaths: readonly string[] = [],
|
|
132
|
+
allowFullAccessSessions = false,
|
|
133
|
+
allowYoloSessions = false,
|
|
134
|
+
dangerClassifier?: HostLocalSandbox['danger_classifier'],
|
|
135
|
+
): HostLocalSandbox {
|
|
136
|
+
const { kind } = chooseLocalSandbox('workspace-write', platform, find, findSb);
|
|
137
|
+
return {
|
|
138
|
+
kind,
|
|
139
|
+
label: kind === 'none' && !allowUnsandboxed ? 'refused' : sandboxLabel(kind),
|
|
140
|
+
...(allowUnsandboxed ? { unsandboxed_commands: 'allow' as const } : {}),
|
|
141
|
+
...(brokerSocket
|
|
142
|
+
? {
|
|
143
|
+
command_network: {
|
|
144
|
+
mode: 'broker' as const,
|
|
145
|
+
socket: brokerSocket,
|
|
146
|
+
label: 'broker (hosts set by the stk policy)',
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
150
|
+
...(readPaths.length ? { read_paths: [...readPaths] } : {}),
|
|
151
|
+
...(allowFullAccessSessions ? { full_access_sessions: 'allow' as const } : {}),
|
|
152
|
+
...(allowYoloSessions ? { yolo_sessions: 'allow' as const } : {}),
|
|
153
|
+
...(dangerClassifier ? { danger_classifier: dangerClassifier } : {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** The read-only paths outside the workspace the reviewed manifest lets local commands read. */
|
|
157
|
+
export function hostReadPaths(m: HostManifest): string[] {
|
|
158
|
+
const r = (m as { local_sandbox?: HostLocalSandbox }).local_sandbox?.read_paths;
|
|
159
|
+
return Array.isArray(r) ? r.filter((p): p is string => typeof p === 'string') : [];
|
|
160
|
+
}
|
|
161
|
+
/** Whether the reviewed manifest lets an interactive session choose full access (Phase 2b). */
|
|
162
|
+
export function hostAllowsFullAccessSessions(m: HostManifest): boolean {
|
|
163
|
+
return (
|
|
164
|
+
(m as { local_sandbox?: HostLocalSandbox }).local_sandbox?.full_access_sessions === 'allow'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
/** Whether the reviewed manifest lets a desk session choose YOLO. */
|
|
168
|
+
export function hostAllowsYoloSessions(m: HostManifest): boolean {
|
|
169
|
+
return (m as { local_sandbox?: HostLocalSandbox }).local_sandbox?.yolo_sessions === 'allow';
|
|
170
|
+
}
|
|
171
|
+
/** The command reviewer a host service may run: a reviewed config and optional pin, or none
|
|
172
|
+
* ({ pin: 'off' }). Absent means forbidden: hosts get no reviewer without the operator. */
|
|
173
|
+
export function hostDangerClassifier(m: HostManifest): { config?: string; pin?: string } {
|
|
174
|
+
const d = (m as { local_sandbox?: HostLocalSandbox }).local_sandbox?.danger_classifier;
|
|
175
|
+
if (!d || d.mode !== 'config') return { pin: 'off' };
|
|
176
|
+
return { config: d.config, ...(d.pin ? { pin: d.pin } : {}) };
|
|
177
|
+
}
|
|
178
|
+
/** The broker socket local commands reach the network through, or null for no network. */
|
|
179
|
+
export function hostCommandNetwork(m: HostManifest): { socket: string } | null {
|
|
180
|
+
const n = (m as { local_sandbox?: HostLocalSandbox }).local_sandbox?.command_network;
|
|
181
|
+
return n?.mode === 'broker' && typeof n.socket === 'string' ? { socket: n.socket } : null;
|
|
182
|
+
}
|
|
183
|
+
/** Whether the reviewed manifest lets local commands run unsandboxed on a host without a sandbox. */
|
|
184
|
+
export function hostAllowsUnsandboxed(m: HostManifest): boolean {
|
|
185
|
+
return (
|
|
186
|
+
(m as { local_sandbox?: HostLocalSandbox }).local_sandbox?.unsandboxed_commands === 'allow'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
export interface OperatorAccess {
|
|
190
|
+
group: string;
|
|
191
|
+
gid: number;
|
|
192
|
+
}
|
|
193
|
+
const groupName = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
194
|
+
/** Resolve a system group by name. The launcher profile never accepts a numeric id alone. */
|
|
195
|
+
export function resolveOperatorGroup(group: unknown): OperatorAccess {
|
|
196
|
+
if (typeof group !== 'string' || !groupName.test(group))
|
|
197
|
+
throw Error('operator_group must be a system group name');
|
|
198
|
+
let line: string;
|
|
199
|
+
try {
|
|
200
|
+
line = execFileSync('/usr/bin/getent', ['group', group], { encoding: 'utf8' }).trim();
|
|
201
|
+
} catch {
|
|
202
|
+
throw Error('operator_group does not exist');
|
|
203
|
+
}
|
|
204
|
+
const [name, , id] = line.split(':');
|
|
205
|
+
const gid = Number(id);
|
|
206
|
+
if (name !== group || !Number.isInteger(gid) || gid <= 0)
|
|
207
|
+
throw Error('operator_group must name a non-root group');
|
|
208
|
+
return { group, gid };
|
|
209
|
+
}
|
|
210
|
+
/** One minute to one day: long enough for a person to answer, short enough to free the station. */
|
|
211
|
+
function approvalTtl(value: unknown): number {
|
|
212
|
+
if (!Number.isInteger(value) || (value as number) < 60000 || (value as number) > 86400000)
|
|
213
|
+
throw Error('approval_ttl_ms must be an integer from 60000 to 86400000');
|
|
214
|
+
return value as number;
|
|
215
|
+
}
|
|
216
|
+
export function hostApprovalTtlMs(m: HostManifest): number | undefined {
|
|
217
|
+
return m.schema === 6 ? m.approval_ttl_ms : undefined;
|
|
218
|
+
}
|
|
219
|
+
export function hostOperatorAccess(m: HostManifest): OperatorAccess | undefined {
|
|
220
|
+
return m.schema === 6 ? m.operator_access : undefined;
|
|
81
221
|
}
|
|
82
222
|
export type HostManifest =
|
|
83
223
|
| CodingHostManifest
|
|
@@ -109,7 +249,7 @@ function privateDocument(path: string): Record<string, unknown> {
|
|
|
109
249
|
closeSync(fd);
|
|
110
250
|
}
|
|
111
251
|
}
|
|
112
|
-
function privateState(path: string): string {
|
|
252
|
+
function privateState(path: string, gid?: number): string {
|
|
113
253
|
if (!isAbsolute(path) || resolve(path) !== path)
|
|
114
254
|
throw Error('Use canonical absolute private state paths');
|
|
115
255
|
// Existing ancestors must resolve to their literal identity; never follow a workspace symlink.
|
|
@@ -119,7 +259,9 @@ function privateState(path: string): string {
|
|
|
119
259
|
throw Error('Private state path must not contain symlinks');
|
|
120
260
|
if (existsSync(path)) {
|
|
121
261
|
const st = lstatSync(path);
|
|
122
|
-
|
|
262
|
+
// The launcher profile lets exactly one group traverse (x) to the operator socket.
|
|
263
|
+
const shared = gid !== undefined && (st.mode & 0o077) === 0o010 && st.gid === gid;
|
|
264
|
+
if (!st.isDirectory() || st.uid !== process.getuid?.() || (st.mode & 0o077 && !shared))
|
|
123
265
|
throw Error('Host state must be a private owner-held directory');
|
|
124
266
|
}
|
|
125
267
|
return path;
|
|
@@ -145,6 +287,17 @@ async function planAgentPod(
|
|
|
145
287
|
'toolkit_config',
|
|
146
288
|
'station_config',
|
|
147
289
|
'strict_work_version',
|
|
290
|
+
'operator_group',
|
|
291
|
+
'operator_mcp_config',
|
|
292
|
+
'persona_config',
|
|
293
|
+
'approval_ttl_ms',
|
|
294
|
+
'unsandboxed_commands',
|
|
295
|
+
'command_network',
|
|
296
|
+
'command_network_socket',
|
|
297
|
+
'local_sandbox_read_paths',
|
|
298
|
+
'allow_full_access_sessions',
|
|
299
|
+
'allow_yolo_sessions',
|
|
300
|
+
'danger_classifier',
|
|
148
301
|
];
|
|
149
302
|
if (Object.keys(c).some((k) => !fields.includes(k)))
|
|
150
303
|
throw Error('Unexpected AgentPod host config field');
|
|
@@ -153,7 +306,71 @@ async function planAgentPod(
|
|
|
153
306
|
(c.strict_work_version !== 1 || c.station_config === undefined)
|
|
154
307
|
)
|
|
155
308
|
throw Error('Strict work version1 requires reviewed protected station config');
|
|
156
|
-
|
|
309
|
+
let access: OperatorAccess | undefined;
|
|
310
|
+
if (c.operator_group !== undefined) {
|
|
311
|
+
if (c.strict_work_version !== 1)
|
|
312
|
+
throw Error('operator_group is only for the strict work launcher profile');
|
|
313
|
+
if (process.platform !== 'linux' || process.getuid?.() !== 0)
|
|
314
|
+
throw Error('The launcher profile is planned by root on Linux');
|
|
315
|
+
access = resolveOperatorGroup(c.operator_group);
|
|
316
|
+
}
|
|
317
|
+
let operatorMcp: { config: string; digest: string } | undefined;
|
|
318
|
+
if (c.operator_mcp_config !== undefined) {
|
|
319
|
+
if (c.strict_work_version !== 1)
|
|
320
|
+
throw Error('operator_mcp_config is only for the strict work host profile');
|
|
321
|
+
const config = string(c.operator_mcp_config, 'operator_mcp_config');
|
|
322
|
+
if (!isAbsolute(config) || realpathSync(config) !== config)
|
|
323
|
+
throw Error('Use a canonical absolute operator MCP config path');
|
|
324
|
+
operatorMcp = { config, digest: OperatorMcp.load(config).digest };
|
|
325
|
+
}
|
|
326
|
+
let persona: { config: string; digest: string } | undefined;
|
|
327
|
+
if (c.persona_config !== undefined) {
|
|
328
|
+
if (c.strict_work_version !== 1)
|
|
329
|
+
throw Error('persona_config is only for the strict work host profile');
|
|
330
|
+
const config = string(c.persona_config, 'persona_config');
|
|
331
|
+
persona = { config, digest: loadPersona(config).digest };
|
|
332
|
+
}
|
|
333
|
+
let ttl: number | undefined;
|
|
334
|
+
if (c.approval_ttl_ms !== undefined) {
|
|
335
|
+
if (c.strict_work_version !== 1)
|
|
336
|
+
throw Error('approval_ttl_ms is only for the strict work host profile');
|
|
337
|
+
ttl = approvalTtl(c.approval_ttl_ms);
|
|
338
|
+
}
|
|
339
|
+
if (c.unsandboxed_commands !== undefined && c.unsandboxed_commands !== 'allow')
|
|
340
|
+
throw Error('unsandboxed_commands is "allow" or absent');
|
|
341
|
+
const allowUnsandboxed = c.unsandboxed_commands === 'allow';
|
|
342
|
+
if (c.allow_full_access_sessions !== undefined && c.allow_full_access_sessions !== true)
|
|
343
|
+
throw Error('allow_full_access_sessions is true or absent');
|
|
344
|
+
const allowFullAccessSessions = c.allow_full_access_sessions === true;
|
|
345
|
+
if (c.allow_yolo_sessions !== undefined && c.allow_yolo_sessions !== true)
|
|
346
|
+
throw Error('allow_yolo_sessions is true or absent');
|
|
347
|
+
const allowYoloSessions = c.allow_yolo_sessions === true;
|
|
348
|
+
if (c.command_network !== undefined && c.command_network !== 'broker')
|
|
349
|
+
throw Error('command_network is "broker" or absent');
|
|
350
|
+
if (c.command_network_socket !== undefined) {
|
|
351
|
+
if (c.command_network !== 'broker')
|
|
352
|
+
throw Error('command_network_socket is only for command_network "broker"');
|
|
353
|
+
const sock = c.command_network_socket;
|
|
354
|
+
if (typeof sock !== 'string' || !isAbsolute(sock) || sock.includes('\0'))
|
|
355
|
+
throw Error('command_network_socket must be an absolute path');
|
|
356
|
+
}
|
|
357
|
+
const dangerClassifier = parseHostDangerClassifier(c.danger_classifier);
|
|
358
|
+
let readPaths: string[] = [];
|
|
359
|
+
if (c.local_sandbox_read_paths !== undefined) {
|
|
360
|
+
const raw = c.local_sandbox_read_paths;
|
|
361
|
+
if (!Array.isArray(raw) || raw.some((p) => typeof p !== 'string'))
|
|
362
|
+
throw Error('local_sandbox_read_paths is a list of absolute paths or "toolchain"');
|
|
363
|
+
try {
|
|
364
|
+
readPaths = resolveReadPaths(raw as string[]);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
throw Error((error as Error).message);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const brokerSocket =
|
|
370
|
+
c.command_network === 'broker'
|
|
371
|
+
? String(c.command_network_socket ?? DEFAULT_COMMAND_NETWORK_SOCKET)
|
|
372
|
+
: undefined;
|
|
373
|
+
const state = privateState(string(c.state, 'state'), access?.gid);
|
|
157
374
|
const config = string(c.agentpod_config, 'agentpod_config');
|
|
158
375
|
if (!isAbsolute(config) || realpathSync(config) !== config)
|
|
159
376
|
throw Error('Use a canonical absolute pins path');
|
|
@@ -205,7 +422,14 @@ async function planAgentPod(
|
|
|
205
422
|
...(station
|
|
206
423
|
? {
|
|
207
424
|
...(c.strict_work_version === 1
|
|
208
|
-
? {
|
|
425
|
+
? {
|
|
426
|
+
schema: 6 as const,
|
|
427
|
+
strict_work: { version: 1 as const },
|
|
428
|
+
...(access ? { operator_access: access } : {}),
|
|
429
|
+
...(operatorMcp ? { operator_mcp: operatorMcp } : {}),
|
|
430
|
+
...(persona ? { persona } : {}),
|
|
431
|
+
...(ttl !== undefined ? { approval_ttl_ms: ttl } : {}),
|
|
432
|
+
}
|
|
209
433
|
: { schema: 5 as const }),
|
|
210
434
|
station_execution: station,
|
|
211
435
|
...(toolkit ? { managed_toolkit: toolkit } : {}),
|
|
@@ -231,6 +455,54 @@ async function planAgentPod(
|
|
|
231
455
|
pins_digest: digest(pins),
|
|
232
456
|
workspace: pins.workspace,
|
|
233
457
|
cli_digest: digest(readFileSync(join(ROOT, 'bin/salaam.mjs'), 'utf8')),
|
|
458
|
+
local_sandbox: hostLocalSandbox(
|
|
459
|
+
undefined,
|
|
460
|
+
undefined,
|
|
461
|
+
undefined,
|
|
462
|
+
allowUnsandboxed,
|
|
463
|
+
brokerSocket,
|
|
464
|
+
readPaths,
|
|
465
|
+
allowFullAccessSessions,
|
|
466
|
+
allowYoloSessions,
|
|
467
|
+
dangerClassifier,
|
|
468
|
+
),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function reviewerDigest(path: string): string | null {
|
|
472
|
+
try {
|
|
473
|
+
return digest(readFileSync(path, 'utf8'));
|
|
474
|
+
} catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function parseHostDangerClassifier(raw: unknown): HostLocalSandbox['danger_classifier'] {
|
|
479
|
+
if (raw === undefined) return undefined;
|
|
480
|
+
if (raw === 'forbid') return { mode: 'forbid' };
|
|
481
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
482
|
+
throw Error('danger_classifier is "forbid" or { config, pin? }');
|
|
483
|
+
const c = raw as Record<string, unknown>;
|
|
484
|
+
if (Object.keys(c).some((k) => k !== 'config' && k !== 'pin'))
|
|
485
|
+
throw Error('danger_classifier takes only config and pin');
|
|
486
|
+
if (typeof c.config !== 'string' || !isAbsolute(c.config) || c.config.includes('\0'))
|
|
487
|
+
throw Error('danger_classifier.config must be an absolute path');
|
|
488
|
+
let names: string[];
|
|
489
|
+
try {
|
|
490
|
+
// Keys are named by environment variable and checked when the service starts, not here.
|
|
491
|
+
const anyKey = new Proxy({}, { get: () => 'checked-at-start' }) as Record<string, string>;
|
|
492
|
+
names = loadDangerClassifierOptions(
|
|
493
|
+
JSON.parse(readFileSync(c.config, 'utf8')),
|
|
494
|
+
anyKey,
|
|
495
|
+
).options.map((o) => o.name);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
throw Error('danger_classifier.config: ' + (error as Error).message);
|
|
498
|
+
}
|
|
499
|
+
if (c.pin !== undefined && (typeof c.pin !== 'string' || !names.includes(c.pin)))
|
|
500
|
+
throw Error("danger_classifier.pin must name one of the config's options");
|
|
501
|
+
return {
|
|
502
|
+
mode: 'config',
|
|
503
|
+
config: c.config,
|
|
504
|
+
digest: reviewerDigest(c.config)!,
|
|
505
|
+
...(typeof c.pin === 'string' ? { pin: c.pin } : {}),
|
|
234
506
|
};
|
|
235
507
|
}
|
|
236
508
|
export function hostDigests() {
|
|
@@ -335,6 +607,11 @@ export function readHost(path: string): HostManifest {
|
|
|
335
607
|
: []),
|
|
336
608
|
...([5, 6].includes(m.schema) ? ['station_execution'] : []),
|
|
337
609
|
...(m.schema === 6 ? ['strict_work'] : []),
|
|
610
|
+
...(m.schema === 6 && Object.hasOwn(m, 'operator_access') ? ['operator_access'] : []),
|
|
611
|
+
...(m.schema === 6 && Object.hasOwn(m, 'operator_mcp') ? ['operator_mcp'] : []),
|
|
612
|
+
...(m.schema === 6 && Object.hasOwn(m, 'persona') ? ['persona'] : []),
|
|
613
|
+
...(m.schema === 6 && Object.hasOwn(m, 'approval_ttl_ms') ? ['approval_ttl_ms'] : []),
|
|
614
|
+
...(Object.hasOwn(m, 'local_sandbox') ? ['local_sandbox'] : []),
|
|
338
615
|
];
|
|
339
616
|
if (m.mode !== 'agentpod' || Object.keys(m).sort().join(',') !== fields.sort().join(','))
|
|
340
617
|
throw Error('Unsupported AgentPod host manifest fields');
|
|
@@ -343,9 +620,64 @@ export function readHost(path: string): HostManifest {
|
|
|
343
620
|
(Object.keys(object(m.strict_work)).join(',') !== 'version' || m.strict_work.version !== 1)
|
|
344
621
|
)
|
|
345
622
|
throw Error('Unsupported strict work host contract');
|
|
346
|
-
|
|
623
|
+
const access = hostOperatorAccess(m);
|
|
624
|
+
if (access !== undefined) {
|
|
625
|
+
const a = object(access);
|
|
626
|
+
if (
|
|
627
|
+
Object.keys(a).sort().join(',') !== 'gid,group' ||
|
|
628
|
+
canonical(resolveOperatorGroup(a.group)) !== canonical({ group: a.group, gid: a.gid })
|
|
629
|
+
)
|
|
630
|
+
throw Error('Host operator group changed');
|
|
631
|
+
}
|
|
632
|
+
privateState(m.state, access?.gid);
|
|
633
|
+
if (m.schema === 6 && Object.hasOwn(m, 'approval_ttl_ms')) approvalTtl(m.approval_ttl_ms);
|
|
634
|
+
if (m.schema === 6 && Object.hasOwn(m, 'operator_mcp')) {
|
|
635
|
+
const o = object(m.operator_mcp);
|
|
636
|
+
if (
|
|
637
|
+
Object.keys(o).sort().join(',') !== 'config,digest' ||
|
|
638
|
+
typeof o.config !== 'string' ||
|
|
639
|
+
!isAbsolute(o.config) ||
|
|
640
|
+
realpathSync(o.config) !== o.config ||
|
|
641
|
+
OperatorMcp.load(o.config).digest !== o.digest
|
|
642
|
+
)
|
|
643
|
+
throw Error('Operator MCP declarations changed');
|
|
644
|
+
}
|
|
645
|
+
if (m.schema === 6 && Object.hasOwn(m, 'persona')) {
|
|
646
|
+
const p = object(m.persona);
|
|
647
|
+
let current: string | null = null;
|
|
648
|
+
try {
|
|
649
|
+
current = loadPersona(String(p.config)).digest;
|
|
650
|
+
} catch {
|
|
651
|
+
current = null;
|
|
652
|
+
}
|
|
653
|
+
if (Object.keys(p).sort().join(',') !== 'config,digest' || current !== p.digest)
|
|
654
|
+
throw Error('Host persona changed; run host plan again');
|
|
655
|
+
}
|
|
347
656
|
if (m.cli_digest !== digest(readFileSync(join(ROOT, 'bin/salaam.mjs'), 'utf8')))
|
|
348
657
|
throw Error('Host CLI changed');
|
|
658
|
+
if (
|
|
659
|
+
Object.hasOwn(m, 'local_sandbox') &&
|
|
660
|
+
canonical(m.local_sandbox) !==
|
|
661
|
+
canonical(
|
|
662
|
+
hostLocalSandbox(
|
|
663
|
+
undefined,
|
|
664
|
+
undefined,
|
|
665
|
+
undefined,
|
|
666
|
+
hostAllowsUnsandboxed(m),
|
|
667
|
+
hostCommandNetwork(m)?.socket,
|
|
668
|
+
hostReadPaths(m),
|
|
669
|
+
hostAllowsFullAccessSessions(m),
|
|
670
|
+
hostAllowsYoloSessions(m),
|
|
671
|
+
(m as { local_sandbox?: HostLocalSandbox }).local_sandbox?.danger_classifier,
|
|
672
|
+
),
|
|
673
|
+
)
|
|
674
|
+
)
|
|
675
|
+
throw Error('Host command sandbox changed; run host plan again');
|
|
676
|
+
{
|
|
677
|
+
const d = (m as { local_sandbox?: HostLocalSandbox }).local_sandbox?.danger_classifier;
|
|
678
|
+
if (d?.mode === 'config' && reviewerDigest(d.config) !== d.digest)
|
|
679
|
+
throw Error('Host command reviewer config changed; run host plan again');
|
|
680
|
+
}
|
|
349
681
|
const pins = readAgentPodPins(m.agentpod_config, m.state);
|
|
350
682
|
const managed = hostMcp(m);
|
|
351
683
|
if (m.schema === 3 || ([4, 5, 6].includes(m.schema) && Object.hasOwn(m, 'managed_mcp'))) {
|
|
@@ -396,6 +728,33 @@ export function readHost(path: string): HostManifest {
|
|
|
396
728
|
}
|
|
397
729
|
return m;
|
|
398
730
|
}
|
|
731
|
+
/** What the daemon reports about itself so that a caller which cannot read the
|
|
732
|
+
* manifest (the launcher profile) receives the same receipt as host start. */
|
|
733
|
+
export function hostReceipt(m: HostManifest) {
|
|
734
|
+
if (m.schema === 1) return undefined;
|
|
735
|
+
const pins = readAgentPodPins(m.agentpod_config, m.state);
|
|
736
|
+
return {
|
|
737
|
+
version: 1,
|
|
738
|
+
ready: true,
|
|
739
|
+
manifest_digest: digest(m),
|
|
740
|
+
node: m.node,
|
|
741
|
+
cli: join(m.repo, 'bin/salaam.mjs'),
|
|
742
|
+
state: m.state,
|
|
743
|
+
workspace: m.workspace,
|
|
744
|
+
node_id: pins.node_id,
|
|
745
|
+
pins_digest: m.pins_digest,
|
|
746
|
+
strict_work: m.schema === 6 ? m.strict_work : null,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
/** The tools user a reviewed station config names, for host serve's --tools-user. */
|
|
750
|
+
export function hostToolsUser(m: HostManifest): string | undefined {
|
|
751
|
+
if (m.schema !== 5 && m.schema !== 6) return undefined;
|
|
752
|
+
return readStationConfig(
|
|
753
|
+
m.station_execution.config,
|
|
754
|
+
m.workspace,
|
|
755
|
+
m.station_execution.configuration_digest,
|
|
756
|
+
).tools_user;
|
|
757
|
+
}
|
|
399
758
|
export function hostUnit(path: string, platform: string): string {
|
|
400
759
|
const m = readHost(path),
|
|
401
760
|
manifest = realpathSync(path),
|