@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.
Files changed (172) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +12 -11
  3. package/dist/src/acp-desk.js +140 -0
  4. package/dist/src/acp-outcome.js +182 -18
  5. package/dist/src/acp.js +296 -15
  6. package/dist/src/agent-profiles.js +45 -0
  7. package/dist/src/agentpod-authority.js +169 -11
  8. package/dist/src/agentpod-default-config.js +12 -0
  9. package/dist/src/agentpod-work-decision-protocol.js +40 -6
  10. package/dist/src/agentpod-work-decisions.js +108 -4
  11. package/dist/src/agentpod-work.js +1214 -127
  12. package/dist/src/approval-rules.js +287 -0
  13. package/dist/src/attach.js +3 -1
  14. package/dist/src/autonomy.js +80 -9
  15. package/dist/src/board-reads.js +106 -0
  16. package/dist/src/children.js +83 -0
  17. package/dist/src/cli-help.js +61 -6
  18. package/dist/src/cli.js +140 -4
  19. package/dist/src/command-danger.js +797 -0
  20. package/dist/src/command-network.js +200 -0
  21. package/dist/src/command-sandbox.js +327 -0
  22. package/dist/src/custom-commands.js +159 -0
  23. package/dist/src/danger-classifier.js +286 -0
  24. package/dist/src/execution-evidence.js +33 -1
  25. package/dist/src/git-helpers.js +160 -0
  26. package/dist/src/hooks.js +351 -0
  27. package/dist/src/host-bind.js +111 -0
  28. package/dist/src/host-breaker.js +88 -0
  29. package/dist/src/host-start.js +41 -1
  30. package/dist/src/host.js +284 -6
  31. package/dist/src/house-extension.js +292 -61
  32. package/dist/src/local-coding.js +726 -35
  33. package/dist/src/local-edit-helper.js +25 -0
  34. package/dist/src/local-edits.js +168 -6
  35. package/dist/src/local-undo.js +25 -16
  36. package/dist/src/managed.js +1 -1
  37. package/dist/src/mentions.js +277 -0
  38. package/dist/src/operator-mcp-cli.js +107 -0
  39. package/dist/src/operator-mcp.js +342 -0
  40. package/dist/src/persona.js +31 -0
  41. package/dist/src/pi-sessions.js +5 -1
  42. package/dist/src/pi.js +22 -1
  43. package/dist/src/project-instructions.js +195 -0
  44. package/dist/src/protocol.js +9 -3
  45. package/dist/src/rate-limit-retries.js +62 -0
  46. package/dist/src/recurring-work.js +1 -0
  47. package/dist/src/repository-snapshots.js +5 -2
  48. package/dist/src/run-grants.js +62 -0
  49. package/dist/src/service.js +482 -10
  50. package/dist/src/session-pick.js +77 -0
  51. package/dist/src/shell-passthrough.js +39 -0
  52. package/dist/src/station-checkpoints.js +139 -0
  53. package/dist/src/station-config.js +8 -1
  54. package/dist/src/station-files.js +20 -1
  55. package/dist/src/station-task-verification.js +6 -3
  56. package/dist/src/station-work.js +315 -22
  57. package/dist/src/store.js +70 -4
  58. package/dist/src/supervisor.js +408 -25
  59. package/dist/src/task-budget.js +39 -3
  60. package/dist/src/task-context.js +8 -2
  61. package/dist/src/task-critic.js +115 -0
  62. package/dist/src/task-driver.js +46 -2
  63. package/dist/src/task-policy.js +44 -12
  64. package/dist/src/task-verification.js +42 -9
  65. package/dist/src/task-work.js +47 -0
  66. package/dist/src/terminal-status.js +88 -0
  67. package/dist/src/tool-identity.js +69 -0
  68. package/dist/src/tui-activity.js +99 -0
  69. package/dist/src/tui-classifier.js +25 -0
  70. package/dist/src/tui-client.js +186 -5
  71. package/dist/src/tui-commands.js +23 -5
  72. package/dist/src/tui-composer.js +113 -0
  73. package/dist/src/tui-keymap.js +116 -0
  74. package/dist/src/tui-mentions.js +38 -0
  75. package/dist/src/tui-modals.js +157 -34
  76. package/dist/src/tui-notify.js +62 -0
  77. package/dist/src/tui-review.js +55 -0
  78. package/dist/src/tui-turn-keys.js +52 -0
  79. package/dist/src/tui-view.js +243 -69
  80. package/dist/src/tui.js +875 -118
  81. package/dist/src/turn-checkpoints.js +221 -0
  82. package/dist/src/turn-rewind.js +168 -0
  83. package/dist/src/usage.js +1 -1
  84. package/dist/src/web-fetch.js +114 -0
  85. package/dist/src/web-guard.js +163 -0
  86. package/dist/src/web-search.js +302 -0
  87. package/npm-shrinkwrap.json +2 -2
  88. package/package.json +1 -1
  89. package/src/acp-desk.ts +155 -0
  90. package/src/acp-outcome.ts +174 -20
  91. package/src/acp.ts +386 -30
  92. package/src/agent-profiles.ts +48 -0
  93. package/src/agentpod-authority.ts +234 -15
  94. package/src/agentpod-default-config.ts +16 -0
  95. package/src/agentpod-work-decision-protocol.ts +69 -8
  96. package/src/agentpod-work-decisions.ts +123 -5
  97. package/src/agentpod-work.ts +1455 -152
  98. package/src/approval-rules.ts +360 -0
  99. package/src/attach.ts +4 -1
  100. package/src/autonomy.ts +111 -10
  101. package/src/board-reads.ts +148 -0
  102. package/src/children.ts +112 -0
  103. package/src/cli-help.ts +60 -6
  104. package/src/cli.ts +174 -4
  105. package/src/command-danger.ts +886 -0
  106. package/src/command-network.ts +215 -0
  107. package/src/command-sandbox.ts +393 -0
  108. package/src/custom-commands.ts +175 -0
  109. package/src/danger-classifier.ts +321 -0
  110. package/src/execution-evidence.ts +31 -1
  111. package/src/git-helpers.ts +177 -0
  112. package/src/hooks.ts +425 -0
  113. package/src/host-bind.ts +156 -0
  114. package/src/host-breaker.ts +89 -0
  115. package/src/host-start.ts +51 -1
  116. package/src/host.ts +365 -6
  117. package/src/house-extension.ts +313 -60
  118. package/src/local-coding.ts +894 -43
  119. package/src/local-edit-helper.ts +26 -0
  120. package/src/local-edits.ts +191 -8
  121. package/src/local-undo.ts +29 -14
  122. package/src/managed.ts +2 -2
  123. package/src/mentions.ts +304 -0
  124. package/src/operator-mcp-cli.ts +108 -0
  125. package/src/operator-mcp.ts +420 -0
  126. package/src/persona.ts +37 -0
  127. package/src/pi-sessions.ts +9 -1
  128. package/src/pi.ts +23 -1
  129. package/src/project-instructions.ts +229 -0
  130. package/src/protocol.ts +25 -3
  131. package/src/rate-limit-retries.ts +69 -0
  132. package/src/recurring-work.ts +1 -0
  133. package/src/repository-snapshots.ts +5 -2
  134. package/src/run-grants.ts +107 -0
  135. package/src/service.ts +709 -10
  136. package/src/session-pick.ts +102 -0
  137. package/src/shell-passthrough.ts +38 -0
  138. package/src/station-checkpoints.ts +183 -0
  139. package/src/station-config.ts +10 -1
  140. package/src/station-files.ts +20 -1
  141. package/src/station-task-verification.ts +6 -3
  142. package/src/station-work.ts +369 -20
  143. package/src/store.ts +70 -3
  144. package/src/supervisor.ts +463 -26
  145. package/src/task-budget.ts +48 -3
  146. package/src/task-context.ts +9 -2
  147. package/src/task-critic.ts +171 -0
  148. package/src/task-driver.ts +49 -2
  149. package/src/task-policy.ts +65 -13
  150. package/src/task-verification.ts +53 -9
  151. package/src/task-work.ts +75 -0
  152. package/src/terminal-status.ts +103 -0
  153. package/src/tool-identity.ts +110 -0
  154. package/src/tui-activity.ts +133 -0
  155. package/src/tui-classifier.ts +38 -0
  156. package/src/tui-client.ts +239 -8
  157. package/src/tui-commands.ts +49 -10
  158. package/src/tui-composer.ts +124 -0
  159. package/src/tui-keymap.ts +152 -0
  160. package/src/tui-mentions.ts +43 -0
  161. package/src/tui-modals.ts +173 -43
  162. package/src/tui-notify.ts +77 -0
  163. package/src/tui-review.ts +76 -0
  164. package/src/tui-turn-keys.ts +77 -0
  165. package/src/tui-view.ts +341 -88
  166. package/src/tui.ts +1047 -137
  167. package/src/turn-checkpoints.ts +253 -0
  168. package/src/turn-rewind.ts +258 -0
  169. package/src/usage.ts +1 -1
  170. package/src/web-fetch.ts +140 -0
  171. package/src/web-guard.ts +195 -0
  172. package/src/web-search.ts +402 -0
package/dist/src/host.js CHANGED
@@ -1,4 +1,6 @@
1
- import { spawn } from 'node:child_process';
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
- if (!st.isDirectory() || st.uid !== process.getuid?.() || st.mode & 0o077)
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
- const state = privateState(string(c.state, 'state'));
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
- ? { schema: 6, strict_work: { version: 1 } }
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
- privateState(m.state);
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') {