@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
@@ -0,0 +1,148 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { Store } from './store.js';
3
+ import { ServiceError } from './protocol.js';
4
+
5
+ /**
6
+ * Board reads (operator ruling 2026-10-28, option iii). While it works, the agent may read its own
7
+ * card, its card's runs, and search or open library items, the way an agent with board tools does.
8
+ * Salaam holds no SuperPipeline or SuperLibrary credential: it records the read, the hub sees it in
9
+ * work status, serves it with its own clients, and answers. A read names no card, board or
10
+ * credential; the hub decides scope from the run it is serving. Read-only: nothing here asks for
11
+ * approval, and nothing a read returns authorizes anything.
12
+ */
13
+ export type BoardReadAction = 'card' | 'attempts' | 'library_search' | 'library_get';
14
+ export interface BoardRead {
15
+ id: string;
16
+ action: BoardReadAction;
17
+ args: Record<string, string>;
18
+ state: 'pending' | 'answered';
19
+ ok?: boolean;
20
+ text?: string;
21
+ error?: string;
22
+ }
23
+ const ITEM = /^itm_[0-9a-f]{16}$/;
24
+ /** The most a hub answer may carry back to the agent. */
25
+ export const BOARD_READ_MAX_BYTES = 256 * 1024;
26
+
27
+ const bad = (message: string) => new ServiceError('invalid_board_read', message, 400);
28
+
29
+ /** Closed arguments per action; anything else is refused. */
30
+ export function boardReadArgs(input: Record<string, unknown>): {
31
+ action: BoardReadAction;
32
+ args: Record<string, string>;
33
+ } {
34
+ const { action, ...rest } = input;
35
+ const keys = Object.keys(rest).filter((k) => rest[k] !== undefined);
36
+ if (action === 'card' || action === 'attempts') {
37
+ if (keys.length) throw bad(`${action} takes no arguments: it is always this run's own card`);
38
+ return { action, args: {} };
39
+ }
40
+ if (action === 'library_search') {
41
+ const q = rest.query;
42
+ if (keys.join() !== 'query' || typeof q !== 'string' || !q.trim() || q.length > 500)
43
+ throw bad('library_search takes one query of 1 to 500 characters');
44
+ return { action, args: { query: q.trim() } };
45
+ }
46
+ if (action === 'library_get') {
47
+ const id = rest.item_id;
48
+ if (keys.join() !== 'item_id' || typeof id !== 'string' || !ITEM.test(id))
49
+ throw bad('library_get takes one item_id (itm_ and 16 hex characters)');
50
+ return { action, args: { item_id: id } };
51
+ }
52
+ throw bad('Board reads are card, attempts, library_search or library_get');
53
+ }
54
+
55
+ export class BoardReads {
56
+ constructor(private readonly store: Store) {
57
+ store.db.exec(`CREATE TABLE IF NOT EXISTS board_reads(
58
+ id TEXT PRIMARY KEY, session_id TEXT NOT NULL, tool_call_id TEXT NOT NULL, action TEXT NOT NULL,
59
+ args TEXT NOT NULL, state TEXT NOT NULL, ok INTEGER, text TEXT, error TEXT,
60
+ created_at TEXT NOT NULL, answered_at TEXT, UNIQUE(session_id, tool_call_id))`);
61
+ }
62
+ private view(row: Record<string, unknown>): BoardRead {
63
+ return {
64
+ id: String(row.id),
65
+ action: String(row.action) as BoardReadAction,
66
+ args: JSON.parse(String(row.args)) as Record<string, string>,
67
+ state: String(row.state) as BoardRead['state'],
68
+ ...(row.state === 'answered'
69
+ ? {
70
+ ok: row.ok === 1,
71
+ ...(row.text !== null ? { text: String(row.text) } : {}),
72
+ ...(row.error !== null ? { error: String(row.error) } : {}),
73
+ }
74
+ : {}),
75
+ };
76
+ }
77
+ /** Record a read for this session, once per tool call. */
78
+ request(session: string, toolCallId: string, input: Record<string, unknown>): BoardRead {
79
+ const { action, args } = boardReadArgs(input);
80
+ const existing = this.store.db
81
+ .prepare('SELECT * FROM board_reads WHERE session_id=? AND tool_call_id=?')
82
+ .get(session, toolCallId);
83
+ if (existing) return this.view(existing);
84
+ const id = randomUUID();
85
+ this.store.db
86
+ .prepare(
87
+ "INSERT INTO board_reads(id,session_id,tool_call_id,action,args,state,created_at) VALUES (?,?,?,?,?,'pending',?)",
88
+ )
89
+ .run(id, session, toolCallId, action, JSON.stringify(args), new Date().toISOString());
90
+ return this.get(session, id);
91
+ }
92
+ get(session: string, id: string): BoardRead {
93
+ const row = this.store.db
94
+ .prepare('SELECT * FROM board_reads WHERE id=? AND session_id=?')
95
+ .get(id, session);
96
+ if (!row) throw new ServiceError('board_read_not_found', 'No such board read', 404);
97
+ return this.view(row);
98
+ }
99
+ /** What the hub should serve for this session, oldest first. */
100
+ pending(
101
+ session: string,
102
+ ): { id: string; action: BoardReadAction; args: Record<string, string> }[] {
103
+ return this.store.db
104
+ .prepare(
105
+ "SELECT * FROM board_reads WHERE session_id=? AND state='pending' ORDER BY rowid LIMIT 8",
106
+ )
107
+ .all(session)
108
+ .map((r) => {
109
+ const v = this.view(r);
110
+ return { id: v.id, action: v.action, args: v.args };
111
+ });
112
+ }
113
+ /** The hub's answer: once, for a read of this session, bounded. */
114
+ answer(session: string, value: unknown): BoardRead {
115
+ const a = (value ?? {}) as Record<string, unknown>;
116
+ const keys = Object.keys(a).sort().join();
117
+ const ok = a.ok === true;
118
+ if (
119
+ typeof a.read_id !== 'string' ||
120
+ (ok
121
+ ? keys !== 'ok,read_id,text' || typeof a.text !== 'string'
122
+ : keys !== 'error,ok,read_id' || a.ok !== false || typeof a.error !== 'string')
123
+ )
124
+ throw bad('Closed board read answer required');
125
+ const body = ok ? (a.text as string) : (a.error as string);
126
+ if (Buffer.byteLength(body) > (ok ? BOARD_READ_MAX_BYTES : 2048))
127
+ throw bad('Board read answer too large');
128
+ const done = this.store.db
129
+ .prepare(
130
+ "UPDATE board_reads SET state='answered',ok=?,text=?,error=?,answered_at=? WHERE id=? AND session_id=? AND state='pending'",
131
+ )
132
+ .run(
133
+ ok ? 1 : 0,
134
+ ok ? body : null,
135
+ ok ? null : body,
136
+ new Date().toISOString(),
137
+ a.read_id,
138
+ session,
139
+ );
140
+ if (done.changes !== 1)
141
+ throw new ServiceError(
142
+ 'board_read_settled',
143
+ 'That board read is not pending for this run',
144
+ 409,
145
+ );
146
+ return this.get(session, a.read_id);
147
+ }
148
+ }
package/src/children.ts CHANGED
@@ -7,6 +7,8 @@ import type { RepositorySnapshots } from './repository-snapshots.js';
7
7
  import type { WorkerWorkspaces } from './worker-workspaces.js';
8
8
  import type { ExternalRuntime } from './external-runtime.js';
9
9
  import type { ExternalWork } from './external-work.js';
10
+ import type { AgentGrant } from './agent-profiles.js';
11
+ import type { WorkspaceTree } from './workspaces.js';
10
12
 
11
13
  export class ChildWork {
12
14
  external?: ExternalWork;
@@ -222,6 +224,116 @@ export class ChildWork {
222
224
  this.start(child);
223
225
  return child;
224
226
  }
227
+ /**
228
+ * The task critic (item 8): a reviewer child over an immutable snapshot of the verified
229
+ * deliverables. No parent attempt is active at completion, so it is linked to the task's last
230
+ * step attempt and charged to its own bounded budget, never to an attempt's reservation. The
231
+ * grant must be the built-in reviewer; the child can read the snapshot and report findings only.
232
+ */
233
+ spawnCritic(
234
+ parent: string,
235
+ parentAttempt: string,
236
+ step: string,
237
+ prompt: string,
238
+ budget: number,
239
+ grant: AgentGrant,
240
+ snapshot: WorkspaceTree,
241
+ ): ChildTask {
242
+ if (!Number.isSafeInteger(budget) || budget < 1 || budget > 8)
243
+ throw new ServiceError('invalid_budget', 'Critic request budget must be 1–8');
244
+ if (grant.profile.id !== 'salaam/reviewer' || grant.owner_session !== parent)
245
+ throw new ServiceError('authority_denied', 'The critic must be the built-in reviewer');
246
+ if (!Object.keys(snapshot).length)
247
+ throw new ServiceError(
248
+ 'invalid_child',
249
+ 'A critic needs a snapshot of the verified deliverables',
250
+ );
251
+ if (!prompt.trim() || prompt.length > 32768)
252
+ throw new ServiceError('invalid_child', 'Invalid critic prompt');
253
+ const authority = [...grant.profile.tools].sort();
254
+ const input = digest({
255
+ prompt,
256
+ budget,
257
+ critic: grant.profile_digest,
258
+ snapshot: digest(snapshot),
259
+ });
260
+ const prepare = () => {
261
+ const old = this.store.db
262
+ .prepare('SELECT * FROM child_tasks WHERE parent_session=? AND step_key=?')
263
+ .get(parent, step) as unknown as ChildTask | undefined;
264
+ if (old) {
265
+ if (old.input_digest !== input)
266
+ throw new ServiceError('spawn_conflict', 'Critic step already binds a different review');
267
+ return { child: old, fresh: false };
268
+ }
269
+ if (this.paused) throw new ServiceError('stopping', 'Child dispatch is stopping');
270
+ this.store.execution.assert(parent);
271
+ if (this.activeCount() >= 8)
272
+ throw new ServiceError('child_limit', 'Daemon worker admission limit reached');
273
+ const effectivePrompt = `${grant.profile.prompt}\n\nGoal:\n${prompt}`;
274
+ const id = randomUUID(),
275
+ session = randomUUID(),
276
+ submission = randomUUID(),
277
+ childAttempt = randomUUID();
278
+ this.store.db
279
+ .prepare('INSERT INTO sessions(id,cwd,pi_session_id) VALUES (?,?,?)')
280
+ .run(session, this.store.session(parent).cwd, session);
281
+ this.store.db
282
+ .prepare('INSERT INTO submissions(id,session_id,text,attempt_id) VALUES (?,?,?,?)')
283
+ .run(submission, session, effectivePrompt, childAttempt);
284
+ this.store.db
285
+ .prepare(
286
+ "INSERT INTO attempts(id,session_id,submission_id,state) VALUES (?,?,?,'accepted')",
287
+ )
288
+ .run(childAttempt, session, submission);
289
+ this.store.db
290
+ .prepare(
291
+ "INSERT INTO child_tasks(id,parent_session,parent_attempt,step_key,input_digest,child_session,child_attempt,authority,budget,state) VALUES (?,?,?,?,?,?,?,?,?,'accepted')",
292
+ )
293
+ .run(
294
+ id,
295
+ parent,
296
+ parentAttempt,
297
+ step,
298
+ input,
299
+ session,
300
+ childAttempt,
301
+ JSON.stringify(authority),
302
+ budget,
303
+ );
304
+ this.store.taskChildReserve?.(parent, childAttempt, budget);
305
+ this.store.usage.budget(childAttempt, budget);
306
+ const artifact = this.artifacts.prepare(session, effectivePrompt);
307
+ artifact.commit();
308
+ // The reviewed snapshot is the verified bytes; its base is empty, so its diff is all of it.
309
+ const current = this.artifacts.prepare(session, JSON.stringify(snapshot)),
310
+ base = this.artifacts.prepare(session, JSON.stringify({}));
311
+ current.commit();
312
+ base.commit();
313
+ this.store.workerBoundary.freeze(this.owned(session)!, artifact.id, false, grant, {
314
+ artifact_id: current.id,
315
+ base_artifact: base.id,
316
+ digest: digest(snapshot),
317
+ });
318
+ this.store.event(session, 'session.created', { parent_session: parent, child_id: id });
319
+ this.store.event(session, 'submission.accepted', {
320
+ attempt_id: childAttempt,
321
+ submission_id: submission,
322
+ });
323
+ this.store.event(parent, 'child.spawned', {
324
+ child_id: id,
325
+ child_session: session,
326
+ step_key: step,
327
+ budget,
328
+ authority,
329
+ critic: true,
330
+ });
331
+ return { child: this.inspect(parent, id), fresh: true };
332
+ };
333
+ const { child, fresh } = this.store.transaction(prepare);
334
+ if (fresh) this.start(child);
335
+ return child;
336
+ }
225
337
  activeCount(parent?: string): number {
226
338
  return Number(
227
339
  this.store.db
package/src/cli-help.ts CHANGED
@@ -53,12 +53,22 @@ salaam agents revoke GRANT_ID --session SESSION_ID
53
53
  Profiles are data in .salaam/agents/*.md with JSON front matter and a prompt body.
54
54
  Inspect the effective tools and digest before admission. Project definitions cannot
55
55
  grant themselves authority. Mutations accept --request-id and --takeover.`,
56
+ code: `salaam code [--continue | --new | --resume] [--cwd DIR] [--takeover]
57
+ salaam here (the same)
58
+ salaam -c (short for salaam code --continue)
59
+
60
+ Work in this folder with one command: start the service if it is not running, then continue
61
+ the folder's latest session (the default), start a new local coding session (--new), or pick
62
+ any session from a list (--resume), this folder's sessions first. No session id needed.
63
+ SALAAM_TERMINAL_STATUS=off stops the window-title and Herdr status reports.`,
56
64
  tui: `salaam tui [--session SESSION_ID] [--observe|--takeover] [--theme night|paper]
57
65
  [--cwd DIR] [--coding-root PRIVATE_DIR] [--coding-socket DOCKER_SOCKET]
58
66
 
59
67
  Bare sal opens the same Salaam terminal interface in a TTY.
60
- Ctrl+K actions · Ctrl+P sessions · Ctrl+N new · Ctrl+O approvals
61
- Ctrl+G changes · Ctrl+T commands · Ctrl+E external editor · Ctrl+C detach.
68
+ Ctrl+K (empty composer) or F2 actions · Ctrl+P sessions · Ctrl+N new · Ctrl+O approvals
69
+ Alt+G changes · Ctrl+T commands · Ctrl+G external editor · Ctrl+R history search
70
+ F5 refresh · !command shell · Ctrl+C stops/clears, twice to detach.
71
+ ? on an empty composer (or F1) shows the keys for where you are.
62
72
  Work continues in the service when the terminal closes.`,
63
73
  remote: `salaam remote SSH_HOST [--remote-bin ABSOLUTE_PATH] [--state REMOTE_DIR]
64
74
  [--session SESSION_ID] [--observe|--takeover] [--theme night|paper]
@@ -88,9 +98,11 @@ Run the M9.1 boundary proof on a pre-provisioned local Linux Docker engine.
88
98
  Only disposable fixtures are mounted. The pinned image must already be present.
89
99
  No coding tools are enabled. A failed/unavailable backend exits nonzero.
90
100
  Cleanup reconciles the saved proof run without replaying commands.`,
91
- acp: `salaam acp [--state DIR] [--coding]
101
+ acp: `salaam acp [--state DIR] [--local | --coding]
92
102
 
93
103
  Run the ACP v1 stdio client adapter against an already running Salaam service.
104
+ --local creates your desk session in the editor's project folder, the same as
105
+ salaam in a terminal there: modes, approvals with diffs, rules and /rewind.
94
106
  --coding creates owned coding sessions; the service must have --coding-root.
95
107
  Loading an existing session preserves its original tool authority.`,
96
108
  artifacts: `salaam artifacts put SESSION_ID --text TEXT [--request-id ID] [--takeover]
@@ -103,6 +115,10 @@ salaam host inspect --manifest host-manifest.json
103
115
  salaam host unit --manifest host-manifest.json --platform systemd|launchd
104
116
  salaam host run --manifest host-manifest.json
105
117
  salaam host start --manifest host-manifest.json
118
+ salaam host unit-start --unit salaam-host@STATION.service --state DIR
119
+ salaam host bind --organization ORG --node NODE --station-key KEY --workspace DIR \
120
+ --tools-user USER --files HELPER --runtime-root DIR --runtime-image-digest sha256:… \
121
+ [--operator-group GROUP] [--provider P --model M] [--mcp-config FILE] [--persona FILE] [--managed-mcp-config FILE] [--dir DIR] [--state DIR]
106
122
 
107
123
  Config requires absolute state, coding_root and coding_socket paths; optional
108
124
  provider, model, max_output_tokens and workspace_id. Planning probes the existing
@@ -116,7 +132,18 @@ For a bound AgentPod host, use mode="agentpod", state and agentpod_config instea
116
132
  of coding_root/coding_socket. This profile enables no station tools. The private
117
133
  manifest pins Node, CLI, source/build, Pi, provider and authority pins. Invoke the
118
134
  canonical Node executable explicitly. host start waits for matching live health
119
- and safely coalesces concurrent callers; it never installs or upgrades packages.`,
135
+ and safely coalesces concurrent callers; it never installs or upgrades packages.
136
+
137
+ Launcher profile (Linux): root plans a strict work host with operator_group set.
138
+ A root-owned unit runs host run from that root-held manifest. The daemon lets that
139
+ one group traverse its state and use operator.sock. An unprivileged member then
140
+ runs host unit-start, which asks systemd to start the named salaam-host@ unit and
141
+ prints the receipt the running daemon reports. It cannot choose what root runs.
142
+
143
+ host bind (root, Linux) writes everything a launcher-profile station needs into
144
+ --dir (default /etc/salaam/stations/<key>): agentpod-pins.json, station-config.json
145
+ (with tools_user), host-config.json, and host.json from host plan. It is
146
+ idempotent: the same inputs leave the files and the manifest digest unchanged.`,
120
147
  schedules: `salaam schedules create TASK_ID --name NAME --revision N --max-steps N [--schedule FILE] [--compact]
121
148
  salaam schedules list [--offset N]
122
149
  salaam schedules inspect JOB_ID
@@ -208,10 +235,32 @@ Status prints metadata only. Existing Anthropic keys remain compatible.`,
208
235
  [--coding-root PRIVATE_DIR] [--coding-socket DOCKER_SOCKET]
209
236
  [--harness-config OWNER_ONLY_FILE] [--a2a-config OWNER_ONLY_FILE]
210
237
  [--a2a-listen-config OWNER_ONLY_FILE]
211
- [--agentpod-config OWNER_ONLY_FILE]
238
+ [--agentpod-config OWNER_ONLY_FILE] [--mcp-config FILE]
239
+ [--tools-user USER]
240
+ [--local-sandbox read-only|workspace-write|full-access]
241
+ [--local-sandbox-read PATH,…|toolchain] [--unsandboxed-commands allow]
212
242
  salaam service status
213
243
  salaam service stop
214
244
 
245
+ --tools-user USER (or SALAAM_TOOLS_USER) runs local agent commands and edits as
246
+ USER through sudo -n -u USER on Linux. Without it, tools share the provider
247
+ credential's OS user and start prints a warning.
248
+
249
+ Local commands (session create --local-coding) run under the host OS sandbox:
250
+ bubblewrap on Linux, Seatbelt on macOS. --local-sandbox (or SALAAM_LOCAL_SANDBOX)
251
+ picks the level: workspace-write (default) writes only inside the workspace,
252
+ read-only writes nothing, full-access runs commands unsandboxed with network. A
253
+ sandboxed command has no network and cannot read outside the workspace and the
254
+ system; --local-sandbox-read (or SALAAM_LOCAL_SANDBOX_READ) lists read-only paths
255
+ it may also read, comma-separated, and "toolchain" adds this host's node prefix,
256
+ ~/.npm, ~/.nvm, ~/.cargo, ~/.rustup, ~/go and ~/.gitconfig where present. Home
257
+ itself and credential directories (~/.ssh, ~/.aws, keychains …) are refused. On
258
+ macOS commands get the real git from the active developer dir. A host with no
259
+ usable sandbox refuses commands unless --unsandboxed-commands allow.
260
+
261
+ Without --agentpod-config, start binds to ~/.config/salaam/agentpod-pins.json
262
+ ($XDG_CONFIG_HOME/salaam) when that file exists.
263
+
215
264
  The default provider is offline. Anthropic defaults to claude-sonnet-5-5.
216
265
  Other providers require --model; use salaam providers list and salaam models list PROVIDER.
217
266
  Start with --foreground for process-supervisor use; otherwise the service detaches.
@@ -219,7 +268,8 @@ An AgentPod daemon requires node-bound admissions and a fresh authority lease.
219
268
  Keep its private state outside the pinned workspace. Coding, external runtimes,
220
269
  effect connectors and managed MCP are unavailable in this profile.`,
221
270
  session: `salaam session create [--cwd DIR] [--request-id ID] [--coding | --local | --local-coding]
222
- --local-coding allows reviewed edits and native commands (no OS sandbox)
271
+ --local-coding allows reviewed edits and native commands, each under the host OS sandbox
272
+ (see salaam help service for levels and read paths)
223
273
  --local allows bounded local file inspection; optionally select --provider ID --model ID
224
274
  salaam session list
225
275
  salaam session inspect SESSION_ID
@@ -272,6 +322,7 @@ const globalOptions = `Global options:
272
322
  -h, --help Show help without starting a service or model request.
273
323
  -V, --version Show the installed version.`;
274
324
  export function cliHelp(group?: string): string | undefined {
325
+ if (group === 'here') group = 'code';
275
326
  if (group) return commands[group] ? `${commands[group]}\n\n${globalOptions}` : undefined;
276
327
  return `Salaam — persistent coding sessions
277
328
 
@@ -279,6 +330,7 @@ Usage: salaam <command> [options]
279
330
  sal <command> [options]
280
331
 
281
332
  Commands:
333
+ code Work in this folder: continue its session or start one (also: here, -c)
282
334
  tui Open the Salaam terminal interface (also bare sal)
283
335
  remote Open the same session over SSH
284
336
  providers List pinned Pi providers and capabilities
@@ -286,6 +338,7 @@ Commands:
286
338
  auth Configure provider credentials or OAuth
287
339
  service Start, inspect or stop the session service
288
340
  session Create, inspect, submit, resume or cancel work
341
+ mcp Declare operator MCP servers (add, list, rm)
289
342
  attach Open an interactive terminal on a session
290
343
  acp Connect an editor over ACP v1 stdio
291
344
  approvals Inspect, approve or deny managed operations
@@ -356,6 +409,7 @@ const valueOptions = new Set([
356
409
  '--a2a-config',
357
410
  '--agentpod-config',
358
411
  '--a2a-listen-config',
412
+ '--tools-user',
359
413
  '--assignment',
360
414
  '--peer',
361
415
  '--peer-client',