@superjackfruitlabs/salaam 0.0.1 → 0.0.2

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 (110) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +25 -17
  3. package/dist/src/a2a-backend.js +225 -0
  4. package/dist/src/a2a-inbound.js +487 -0
  5. package/dist/src/a2a-peer.js +370 -0
  6. package/dist/src/a2a-protocol.js +250 -0
  7. package/dist/src/agent-profiles.js +289 -0
  8. package/dist/src/children.js +132 -26
  9. package/dist/src/claude-provider.js +2 -179
  10. package/dist/src/cli-help.js +107 -6
  11. package/dist/src/cli.js +477 -13
  12. package/dist/src/config.js +26 -3
  13. package/dist/src/credentials.js +7 -2
  14. package/dist/src/external-runtime.js +1 -0
  15. package/dist/src/external-work.js +583 -0
  16. package/dist/src/harness-backend.js +466 -0
  17. package/dist/src/harness-container.js +450 -0
  18. package/dist/src/harness-model-gateway.js +104 -0
  19. package/dist/src/harness-prepare.js +88 -0
  20. package/dist/src/harness-probe.js +111 -0
  21. package/dist/src/harness-protocol.js +325 -0
  22. package/dist/src/harness-readiness.js +50 -0
  23. package/dist/src/harness-rpc.js +206 -0
  24. package/dist/src/harness-tool-identity.js +87 -0
  25. package/dist/src/harness-wire-bridge.js +387 -0
  26. package/dist/src/host.js +6 -1
  27. package/dist/src/house-extension.js +147 -64
  28. package/dist/src/live-provider.js +301 -0
  29. package/dist/src/managed.js +33 -3
  30. package/dist/src/protocol.js +9 -5
  31. package/dist/src/provider-auth.js +300 -0
  32. package/dist/src/provider-config.js +65 -17
  33. package/dist/src/provider-model.js +78 -0
  34. package/dist/src/provider-payload.js +264 -0
  35. package/dist/src/provider-transport.js +36 -0
  36. package/dist/src/provider-usage.js +59 -0
  37. package/dist/src/repository-snapshots.js +219 -0
  38. package/dist/src/service.js +274 -9
  39. package/dist/src/store.js +44 -2
  40. package/dist/src/supervisor.js +246 -35
  41. package/dist/src/task-budget.js +17 -8
  42. package/dist/src/task-context.js +15 -0
  43. package/dist/src/task-driver.js +38 -14
  44. package/dist/src/task-policy.js +20 -1
  45. package/dist/src/task-verification.js +3 -0
  46. package/dist/src/task-work.js +31 -0
  47. package/dist/src/team-work.js +378 -0
  48. package/dist/src/tui-view.js +65 -10
  49. package/dist/src/tui.js +4 -2
  50. package/dist/src/usage.js +18 -0
  51. package/dist/src/worker-boundary.js +298 -0
  52. package/dist/src/worker-workspaces.js +274 -0
  53. package/dist/src/workspaces.js +6 -2
  54. package/harness-runtime/codex-pins.json +10 -0
  55. package/harness-runtime/package-lock.json +560 -0
  56. package/harness-runtime/package.json +9 -0
  57. package/npm-shrinkwrap.json +57 -2
  58. package/package.json +2 -1
  59. package/src/a2a-backend.ts +254 -0
  60. package/src/a2a-inbound.ts +585 -0
  61. package/src/a2a-peer.ts +441 -0
  62. package/src/a2a-protocol.ts +286 -0
  63. package/src/agent-profiles.ts +379 -0
  64. package/src/children.ts +168 -27
  65. package/src/claude-provider.ts +2 -189
  66. package/src/cli-help.ts +107 -6
  67. package/src/cli.ts +602 -12
  68. package/src/config.ts +34 -3
  69. package/src/credentials.ts +9 -2
  70. package/src/external-runtime.ts +54 -0
  71. package/src/external-work.ts +663 -0
  72. package/src/harness-backend.ts +557 -0
  73. package/src/harness-container.ts +543 -0
  74. package/src/harness-model-gateway.ts +138 -0
  75. package/src/harness-prepare.ts +113 -0
  76. package/src/harness-probe.ts +150 -0
  77. package/src/harness-protocol.ts +388 -0
  78. package/src/harness-readiness.ts +71 -0
  79. package/src/harness-rpc.ts +244 -0
  80. package/src/harness-tool-identity.ts +91 -0
  81. package/src/harness-wire-bridge.ts +418 -0
  82. package/src/host.ts +11 -6
  83. package/src/house-extension.ts +165 -66
  84. package/src/live-provider.ts +347 -0
  85. package/src/managed.ts +43 -4
  86. package/src/protocol.ts +36 -8
  87. package/src/provider-auth.ts +336 -0
  88. package/src/provider-config.ts +89 -18
  89. package/src/provider-model.ts +86 -0
  90. package/src/provider-payload.ts +263 -0
  91. package/src/provider-transport.ts +51 -0
  92. package/src/provider-usage.ts +64 -0
  93. package/src/repository-snapshots.ts +259 -0
  94. package/src/service.ts +446 -11
  95. package/src/store.ts +60 -4
  96. package/src/supervisor.ts +294 -41
  97. package/src/task-budget.ts +24 -11
  98. package/src/task-context.ts +17 -0
  99. package/src/task-driver.ts +51 -15
  100. package/src/task-policy.ts +23 -3
  101. package/src/task-verification.ts +2 -0
  102. package/src/task-work.ts +46 -1
  103. package/src/team-work.ts +554 -0
  104. package/src/tui-client.ts +5 -0
  105. package/src/tui-view.ts +164 -12
  106. package/src/tui.ts +5 -2
  107. package/src/usage.ts +17 -0
  108. package/src/worker-boundary.ts +395 -0
  109. package/src/worker-workspaces.ts +390 -0
  110. package/src/workspaces.ts +4 -2
package/src/store.ts CHANGED
@@ -3,6 +3,7 @@ import { chmodSync } from 'node:fs';
3
3
  import { randomUUID, randomBytes } from 'node:crypto';
4
4
  import { EventEmitter } from 'node:events';
5
5
  import { UsageLedger } from './usage.js';
6
+ import { WorkerBoundary } from './worker-boundary.js';
6
7
  import { OFFLINE_ROUTE, type ProviderRoute } from './provider-config.js';
7
8
  import { canonical } from './protocol.js';
8
9
  import {
@@ -26,9 +27,11 @@ export class Store {
26
27
  readonly db: DatabaseSync;
27
28
  readonly changes = new EventEmitter();
28
29
  readonly usage = new UsageLedger(this);
30
+ readonly workerBoundary = new WorkerBoundary(this);
29
31
  reconcileCompactions?: (id: string) => void;
30
32
  finishChild?: (attempt: Attempt) => void;
31
33
  projectTaskAttempt?: (attempt: Attempt) => void;
34
+ teamSnapshot?: (session: string) => unknown;
32
35
  guardTaskSubmission?: (session: string, step?: string) => void;
33
36
  cancelChildren?: (attempt: string) => void;
34
37
  recordTaskMutation?: (
@@ -59,7 +62,7 @@ export class Store {
59
62
  );
60
63
  const version = (this.db.prepare('PRAGMA user_version').get() as { user_version: number })
61
64
  .user_version;
62
- if (version > 11) {
65
+ if (version > 17) {
63
66
  this.db.close();
64
67
  throw new ServiceError('schema_too_new', 'Database schema is newer than this service');
65
68
  }
@@ -202,6 +205,50 @@ export class Store {
202
205
  CREATE TABLE recurring_occurrences(id TEXT PRIMARY KEY,job_id TEXT NOT NULL REFERENCES recurring_jobs(id),occurrence_key TEXT NOT NULL,session_id TEXT NOT NULL UNIQUE REFERENCES sessions(id),job_run_id TEXT UNIQUE REFERENCES jobs(id),schedule_revision INTEGER NOT NULL,due_at INTEGER,state TEXT NOT NULL,error TEXT,created_at INTEGER NOT NULL,UNIQUE(job_id,occurrence_key));
203
206
  CREATE TABLE operator_inbox(id TEXT PRIMARY KEY,session_id TEXT NOT NULL REFERENCES sessions(id),source_key TEXT NOT NULL UNIQUE,kind TEXT NOT NULL,detail TEXT NOT NULL,revision INTEGER NOT NULL,state TEXT NOT NULL,ack_revision INTEGER NOT NULL DEFAULT 0,updated_at INTEGER NOT NULL);
204
207
  PRAGMA user_version=11; COMMIT;`);
208
+ if (
209
+ (this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 12
210
+ ) {
211
+ this.db.exec('BEGIN IMMEDIATE');
212
+ this.workerBoundary.migrate();
213
+ this.db.exec('PRAGMA user_version=12; COMMIT;');
214
+ }
215
+ if (
216
+ (this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 13
217
+ )
218
+ this.db.exec(`BEGIN IMMEDIATE;
219
+ CREATE TABLE agent_grants(id TEXT PRIMARY KEY,owner_session TEXT NOT NULL REFERENCES sessions(id),profile_id TEXT NOT NULL,document TEXT NOT NULL,digest TEXT NOT NULL,state TEXT NOT NULL);
220
+ CREATE TABLE specialist_reports(worker_id TEXT PRIMARY KEY,input_digest TEXT NOT NULL,artifact_id TEXT NOT NULL REFERENCES artifacts(id));
221
+ PRAGMA user_version=13; COMMIT;`);
222
+ if (
223
+ (this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 14
224
+ )
225
+ this.db.exec(`BEGIN IMMEDIATE;
226
+ CREATE TABLE team_assignments(id TEXT PRIMARY KEY,task_id TEXT NOT NULL REFERENCES durable_tasks(id),step_id TEXT NOT NULL REFERENCES task_steps(id),plan_revision INTEGER NOT NULL,profile_id TEXT NOT NULL,grant_id TEXT NOT NULL REFERENCES agent_grants(id),budget INTEGER NOT NULL,generation INTEGER NOT NULL DEFAULT 0,child_id TEXT REFERENCES child_tasks(id),superseded INTEGER NOT NULL DEFAULT 0);
227
+ CREATE UNIQUE INDEX team_active_step ON team_assignments(step_id) WHERE superseded=0;
228
+ CREATE TABLE team_messages(id TEXT PRIMARY KEY,task_id TEXT NOT NULL REFERENCES durable_tasks(id),sender TEXT NOT NULL,recipient TEXT NOT NULL REFERENCES team_assignments(id),text TEXT NOT NULL,input_digest TEXT NOT NULL,created_at INTEGER NOT NULL,delivered_at INTEGER,acknowledged_at INTEGER);
229
+ PRAGMA user_version=14; COMMIT;`);
230
+ if (
231
+ (this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 15
232
+ )
233
+ this.db.exec(`BEGIN IMMEDIATE;
234
+ CREATE TABLE worker_integrations(id TEXT PRIMARY KEY,parent_session TEXT NOT NULL REFERENCES sessions(id),child_id TEXT NOT NULL UNIQUE REFERENCES child_tasks(id),worker_id TEXT NOT NULL,patch_digest TEXT NOT NULL,before_revision INTEGER NOT NULL,before_digest TEXT NOT NULL,after_artifact TEXT NOT NULL REFERENCES artifacts(id),state TEXT NOT NULL,mutation_id TEXT REFERENCES workspace_mutations(id),mutation_key TEXT NOT NULL UNIQUE,generation INTEGER NOT NULL DEFAULT 1);
235
+ CREATE TABLE team_rejections(step_id TEXT NOT NULL REFERENCES task_steps(id),attempt_id TEXT NOT NULL REFERENCES attempts(id),request_id TEXT NOT NULL,reason TEXT NOT NULL,PRIMARY KEY(step_id,attempt_id));
236
+ PRAGMA user_version=15; COMMIT;`);
237
+ if (
238
+ (this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 16
239
+ )
240
+ this.db.exec(`BEGIN IMMEDIATE;
241
+ CREATE TABLE external_assignments(assignment_id TEXT PRIMARY KEY REFERENCES team_assignments(id),runtime TEXT NOT NULL,digest TEXT NOT NULL);
242
+ CREATE TABLE external_runs(child_id TEXT PRIMARY KEY REFERENCES child_tasks(id),run_id TEXT NOT NULL UNIQUE,state TEXT NOT NULL,epoch INTEGER NOT NULL,checkpoint TEXT,receipt TEXT,stopped INTEGER NOT NULL DEFAULT 0,error TEXT,result TEXT,import_artifact TEXT REFERENCES artifacts(id));
243
+ CREATE TABLE task_invocations(attempt_id TEXT PRIMARY KEY REFERENCES attempts(id),task_id TEXT NOT NULL REFERENCES durable_tasks(id),runtime_id TEXT NOT NULL,input_digest TEXT NOT NULL,created_at INTEGER NOT NULL);
244
+ CREATE TABLE external_model_requests(id TEXT PRIMARY KEY,child_id TEXT NOT NULL REFERENCES external_runs(child_id),input_bytes INTEGER NOT NULL,output_bound INTEGER NOT NULL);
245
+ PRAGMA user_version=16; COMMIT;`);
246
+ if (
247
+ (this.db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version < 17
248
+ )
249
+ this.db.exec(`BEGIN IMMEDIATE;
250
+ CREATE TABLE a2a_inbound(id TEXT PRIMARY KEY,peer TEXT NOT NULL,tenant TEXT NOT NULL,message_id TEXT NOT NULL,input_digest TEXT NOT NULL,assignment_id TEXT NOT NULL UNIQUE REFERENCES team_assignments(id),grant_digest TEXT NOT NULL,context_id TEXT NOT NULL UNIQUE,child_id TEXT NOT NULL UNIQUE REFERENCES child_tasks(id),created_at INTEGER NOT NULL,skill TEXT NOT NULL,cancel_requested INTEGER NOT NULL DEFAULT 0,UNIQUE(peer,tenant,message_id));
251
+ PRAGMA user_version=17; COMMIT;`);
205
252
  } catch (error) {
206
253
  if (this.db.isTransaction) this.db.exec('ROLLBACK');
207
254
  this.db.close();
@@ -785,9 +832,18 @@ export class Store {
785
832
  history_truncated: cursor !== null,
786
833
  approvals: this.approvals(id),
787
834
  operations: this.operations(id),
788
- children: this.db
789
- .prepare('SELECT * FROM child_tasks WHERE parent_session=? ORDER BY rowid DESC LIMIT 100')
790
- .all(id) as unknown as SessionSnapshot['children'],
835
+ children: (
836
+ this.db
837
+ .prepare('SELECT * FROM child_tasks WHERE parent_session=? ORDER BY rowid DESC LIMIT 100')
838
+ .all(id) as unknown as SessionSnapshot['children']
839
+ ).map((child) => ({
840
+ ...child,
841
+ worker: this.workerBoundary.view(child.id),
842
+ })),
843
+ ...(this.workerBoundary.profiles
844
+ ? { agents: this.workerBoundary.profiles.snapshot(id) }
845
+ : {}),
846
+ ...(this.teamSnapshot ? { team: this.teamSnapshot(id) } : {}),
791
847
  };
792
848
  }
793
849
  cancelled(id: string): boolean {
package/src/supervisor.ts CHANGED
@@ -25,13 +25,19 @@ import type { ChildWork } from './children.js';
25
25
  import {
26
26
  OFFLINE_ROUTE,
27
27
  providerRoute,
28
- validateClaudePayload,
28
+ providerHasOutputCap,
29
29
  type ProviderRoute,
30
30
  } from './provider-config.js';
31
31
  import type { TaskContext, Compaction } from './task-context.js';
32
- import { loadApiKey, validateApiKey } from './credentials.js';
32
+ import { validateApiKey } from './credentials.js';
33
+ import { resolveProviderAuth } from './provider-auth.js';
34
+ import { bindProviderConfiguration } from './provider-model.js';
35
+ import { validateProviderPayload } from './provider-payload.js';
33
36
  import { digest } from './protocol.js';
34
37
  import type { CodingWork } from './coding.js';
38
+ import type { TaskLimits } from './task-policy.js';
39
+ import { repositoryPath, type RepositorySnapshots } from './repository-snapshots.js';
40
+ import type { TeamWork } from './team-work.js';
35
41
 
36
42
  export interface Launch {
37
43
  command: string;
@@ -49,8 +55,12 @@ export interface SupervisorOptions {
49
55
  effectsEnabled?: boolean;
50
56
  providerRoute?: ProviderRoute;
51
57
  anthropicApiKey?: string;
58
+ providerCredentials?: import('@earendil-works/pi-ai').CredentialStore;
59
+ providerEnvironment?: NodeJS.ProcessEnv;
52
60
  }
53
61
  interface Worker {
62
+ route: ProviderRoute;
63
+ providerOutputCap?: boolean;
54
64
  id: string;
55
65
  epoch: number;
56
66
  token: string;
@@ -100,18 +110,79 @@ export class Supervisor extends EventEmitter {
100
110
  managed?: ManagedWork;
101
111
  children?: ChildWork;
102
112
  coding?: CodingWork;
113
+ repositories?: RepositorySnapshots;
114
+ team?: TeamWork;
115
+ routeFor(id: string): ProviderRoute {
116
+ const child = this.children?.owned(id);
117
+ return child
118
+ ? (this.store.workerBoundary.view(child.id).binding.agent?.route ?? this.route)
119
+ : this.route;
120
+ }
121
+ async prepareProviderRoute(input: ProviderRoute) {
122
+ const route = structuredClone(input);
123
+ if (route.provider === 'salaam-proof') return { route, output_cap: true, auth: undefined };
124
+ const auth =
125
+ route.provider === 'anthropic' && this.apiKey
126
+ ? { auth: { apiKey: this.apiKey }, env: {} }
127
+ : await resolveProviderAuth(
128
+ route,
129
+ this.options.providerCredentials,
130
+ this.options.providerEnvironment,
131
+ );
132
+ const bound = bindProviderConfiguration(route, auth);
133
+ return { route: bound, output_cap: providerHasOutputCap(bound, auth), auth };
134
+ }
103
135
  private codingBinding(id: string): CodingBinding | undefined {
104
- return this.children?.owned(id) ? undefined : this.coding?.binding(id);
136
+ const child = this.children?.owned(id),
137
+ coding = this.coding?.binding(id);
138
+ if (!child) return coding;
139
+ const binding = this.store.workerBoundary.view(child.id).binding;
140
+ return binding.isolation && coding
141
+ ? {
142
+ ...coding,
143
+ files: binding.tools.includes('salaam_files'),
144
+ commands: coding.commands && binding.tools.includes('salaam_command'),
145
+ }
146
+ : undefined;
105
147
  }
106
148
  private tools(id: string): string[] {
149
+ const child = this.children?.owned(id);
150
+ if (child) return JSON.parse(child.authority) as string[];
107
151
  return workerTools(
108
152
  !!this.options.effectsEnabled,
109
153
  !!this.children?.owned(id),
110
154
  this.codingBinding(id),
111
155
  );
112
156
  }
157
+ private childBoundary(id: string, epoch: number) {
158
+ const child = this.children?.owned(id);
159
+ return child ? this.store.workerBoundary.assert(child, epoch, this.routeFor(id)) : undefined;
160
+ }
113
161
  readonly route: ProviderRoute;
114
162
  private apiKey?: string;
163
+ private outputCap = true;
164
+ private authInitialized = false;
165
+ providerCapabilities() {
166
+ return {
167
+ output_token_cap: this.outputCap,
168
+ durable_tasks: true,
169
+ warning: this.outputCap
170
+ ? null
171
+ : 'No provider-enforced output cap; tasks must explicitly disable token budgeting',
172
+ };
173
+ }
174
+ assertTaskProviderSupported(limits: TaskLimits, outputCap = this.outputCap) {
175
+ if (!outputCap && (limits.token_units !== null || limits.output_token_bound !== null))
176
+ throw new ServiceError(
177
+ 'provider_capability',
178
+ 'This provider authentication has no output cap; set limits.token_units to null and omit output_token_bound',
179
+ );
180
+ }
181
+ assertSessionProviderSupported(id: string, outputCap = this.outputCap) {
182
+ const parent = this.children?.owned(id)?.parent_session ?? id;
183
+ const task = this.tasks.work?.forSession(parent);
184
+ if (task) this.assertTaskProviderSupported(task.limits, outputCap);
185
+ }
115
186
  readonly bootId = randomUUID();
116
187
  private workers = new Map<string, Worker>();
117
188
  private monitor: NodeJS.Timeout;
@@ -121,27 +192,54 @@ export class Supervisor extends EventEmitter {
121
192
  readonly options: SupervisorOptions,
122
193
  ) {
123
194
  super();
124
- this.route = options.providerRoute ?? { ...OFFLINE_ROUTE };
195
+ this.route = structuredClone(options.providerRoute ?? OFFLINE_ROUTE);
125
196
  // Validate public configuration without trusting programmatic callers.
126
197
  const checked = providerRoute(
127
198
  this.route.provider === 'salaam-proof' ? 'offline' : this.route.provider,
128
- this.route.provider === 'anthropic' ? this.route.model : undefined,
129
- this.route.provider === 'anthropic' ? this.route.max_tokens : undefined,
199
+ this.route.provider !== 'salaam-proof' ? this.route.model : undefined,
200
+ this.route.provider !== 'salaam-proof' ? this.route.max_tokens : undefined,
130
201
  this.route.workspace_id,
202
+ this.route.connection,
131
203
  );
132
204
  if (canonical(checked) !== canonical(this.route)) throw new Error('Invalid provider route');
133
- if (this.route.provider === 'anthropic')
134
- this.apiKey = options.anthropicApiKey
135
- ? validateApiKey(options.anthropicApiKey)
136
- : loadApiKey();
205
+ if (options.anthropicApiKey) this.apiKey = validateApiKey(options.anthropicApiKey);
137
206
  this.bundle = bundleDigest();
138
207
  this.monitor = setInterval(() => {
139
208
  for (const worker of this.workers.values())
140
- if (worker.ready && Date.now() - worker.lastBeat > (options.heartbeatTimeout ?? 2500))
141
- this.invalidate(worker, 'Worker heartbeat expired');
209
+ if (worker.ready) {
210
+ if (Date.now() - worker.lastBeat > (options.heartbeatTimeout ?? 2500))
211
+ this.invalidate(worker, 'Worker heartbeat expired');
212
+ const child = this.children?.owned(worker.id);
213
+ if (child?.state === 'accepted') {
214
+ try {
215
+ this.childBoundary(worker.id, worker.epoch);
216
+ } catch (error) {
217
+ this.invalidate(worker, 'Worker boundary revoked: ' + (error as Error).message);
218
+ }
219
+ }
220
+ }
142
221
  }, 250);
143
222
  this.monitor.unref();
144
223
  }
224
+ async providerAuth() {
225
+ if (this.route.provider === 'salaam-proof') return undefined;
226
+ if (this.route.provider === 'anthropic' && this.apiKey)
227
+ return { auth: { apiKey: this.apiKey }, env: {} };
228
+ const auth = await resolveProviderAuth(
229
+ this.route,
230
+ this.options.providerCredentials,
231
+ this.options.providerEnvironment,
232
+ );
233
+ Object.assign(this.route, bindProviderConfiguration(this.route, auth));
234
+ const outputCap = providerHasOutputCap(this.route, auth);
235
+ if (this.authInitialized && this.outputCap !== outputCap)
236
+ throw new Error(
237
+ 'Provider output-cap capability changed; restart the service before using the new authentication',
238
+ );
239
+ this.outputCap = outputCap;
240
+ this.authInitialized = true;
241
+ return auth;
242
+ }
145
243
  async recover(): Promise<void> {
146
244
  const rows = this.store.db.prepare("SELECT * FROM workers WHERE state!='exited'").all() as {
147
245
  session_id: string;
@@ -177,6 +275,13 @@ export class Supervisor extends EventEmitter {
177
275
  }
178
276
  }
179
277
  async start(id: string): Promise<void> {
278
+ const child = this.children?.owned(id);
279
+ if (child && this.store.workerBoundary.view(child.id).binding.runtime !== 'native-pi')
280
+ throw new ServiceError(
281
+ 'external_worker_owned',
282
+ 'Use the external worker lifecycle; Pi cannot claim this session',
283
+ );
284
+ const route = this.routeFor(id);
180
285
  const prior = this.workers.get(id);
181
286
  if (prior && !prior.stopping)
182
287
  throw new ServiceError('worker_exists', 'Worker already supervised');
@@ -193,7 +298,7 @@ export class Supervisor extends EventEmitter {
193
298
  'reconciliation_required',
194
299
  previous.error ?? 'Session requires reconciliation',
195
300
  );
196
- this.store.bindRoute(id, this.route);
301
+ this.store.bindRoute(id, route);
197
302
  const epoch = this.store.allocate(id, this.bootId);
198
303
  let accept!: () => void;
199
304
  let reject!: (e: Error) => void;
@@ -204,6 +309,7 @@ export class Supervisor extends EventEmitter {
204
309
  const binding = randomBytes(16).toString('hex');
205
310
  const frame = this.tasks.frame(id, binding);
206
311
  const worker: Worker = {
312
+ route: structuredClone(route),
207
313
  id,
208
314
  epoch,
209
315
  token: randomBytes(32).toString('hex'),
@@ -224,6 +330,8 @@ export class Supervisor extends EventEmitter {
224
330
  void handshake.catch(() => {});
225
331
  let timer: NodeJS.Timeout | undefined;
226
332
  try {
333
+ const child = this.children?.owned(id);
334
+ if (child) this.store.workerBoundary.claim(child, epoch, route);
227
335
  const workerDir = join(this.options.stateDir, 'workers', id);
228
336
  mkdirSync(join(workerDir, 'agent'), { recursive: true, mode: 0o700 });
229
337
  writeFileSync(
@@ -245,7 +353,8 @@ export class Supervisor extends EventEmitter {
245
353
  frame,
246
354
  effects: !!this.options.effectsEnabled && !this.children?.owned(id),
247
355
  child: !!this.children?.owned(id),
248
- route: this.route,
356
+ worker_binding: this.childBoundary(id, epoch),
357
+ route: route,
249
358
  coding: this.codingBinding(id),
250
359
  };
251
360
  const args = [
@@ -271,9 +380,9 @@ export class Supervisor extends EventEmitter {
271
380
  '--tools',
272
381
  this.tools(id).join(','),
273
382
  '--provider',
274
- this.route.provider,
383
+ route.provider,
275
384
  '--model',
276
- this.route.model,
385
+ route.model,
277
386
  ];
278
387
  if (previous.pi_session_file && existsSync(previous.pi_session_file)) {
279
388
  const index = args.indexOf('--session-id');
@@ -293,7 +402,7 @@ export class Supervisor extends EventEmitter {
293
402
  LANG: process.env.LANG ?? 'en_US.UTF-8',
294
403
  PI_CODING_AGENT_DIR: join(workerDir, 'agent'),
295
404
  SALAAM_WORKER_CONFIG: JSON.stringify(config),
296
- ...(this.apiKey ? { ANTHROPIC_API_KEY: this.apiKey } : {}),
405
+ ...(route.connection?.configuration ?? {}),
297
406
  };
298
407
  worker.pi = (this.options.launch ?? ((x) => new PiProcess(x.command, x.args, x.cwd, x.env)))({
299
408
  command: process.execPath,
@@ -387,16 +496,21 @@ export class Supervisor extends EventEmitter {
387
496
  const file = typeof state.sessionFile === 'string' ? resolve(state.sessionFile) : null;
388
497
  if (!file || !file.startsWith(resolve(sessionDir) + '/'))
389
498
  throw new Error('Pi session is outside its managed directory');
390
- if (
391
- object(state.model).provider !== this.route.provider ||
392
- object(state.model).id !== this.route.model
393
- )
499
+ if (object(state.model).provider !== route.provider || object(state.model).id !== route.model)
394
500
  throw new Error('Pi selected an unapproved provider route');
395
501
  if (worker.invalid) throw new Error(worker.invalid);
396
502
  // Pi buffers setup-only entries until there is a conversation. Do not
397
503
  // persist a cursor into that ephemeral tree before its file exists.
398
504
  if (existsSync(file)) await this.syncEntries(worker);
399
- this.store.state(id, epoch, 'ready', null, workerCapabilities(this.codingBinding(id)), file);
505
+ if (child) this.store.workerBoundary.accepted(child, epoch, route);
506
+ this.store.state(
507
+ id,
508
+ epoch,
509
+ 'ready',
510
+ null,
511
+ workerCapabilities(this.codingBinding(id), this.tools(id)),
512
+ file,
513
+ );
400
514
  worker.ready = true;
401
515
  worker.lastBeat = Date.now();
402
516
  } catch (error) {
@@ -455,6 +569,7 @@ export class Supervisor extends EventEmitter {
455
569
  body: Record<string, unknown>,
456
570
  ): Promise<unknown> {
457
571
  const w = this.authenticate(id, token, body);
572
+ const route = w.route;
458
573
  if (method === 'failure') {
459
574
  if (!w.stopping) this.invalidate(w, 'Required extension failed');
460
575
  return { ok: true };
@@ -464,6 +579,7 @@ export class Supervisor extends EventEmitter {
464
579
  if (method === 'handshake') {
465
580
  try {
466
581
  if (w.manifest) throw new Error('Duplicate handshake');
582
+ const boundary = this.childBoundary(id, w.epoch);
467
583
  const expected = {
468
584
  protocol_version: PROTOCOL_VERSION,
469
585
  bundle_digest: w.bundle,
@@ -471,13 +587,14 @@ export class Supervisor extends EventEmitter {
471
587
  session_id: id,
472
588
  pi_session_id: id,
473
589
  epoch: w.epoch,
474
- capabilities: workerCapabilities(this.codingBinding(id)),
590
+ capabilities: workerCapabilities(this.codingBinding(id), this.tools(id)),
475
591
  tools: this.tools(id),
476
592
  active_tools: this.tools(id),
477
593
  ...(this.codingBinding(id) ? { coding: this.codingBinding(id) } : {}),
478
- provider: this.route.provider,
479
- route: this.route,
594
+ provider: route.provider,
595
+ route: route,
480
596
  context_digest: textDigest(w.frame),
597
+ ...(boundary ? { worker_binding: boundary } : {}),
481
598
  };
482
599
  for (const [key, value] of Object.entries(expected))
483
600
  if (canonical(body[key]) !== canonical(value))
@@ -499,12 +616,98 @@ export class Supervisor extends EventEmitter {
499
616
  if (w.attempt && this.store.cancelled(w.attempt))
500
617
  throw new ServiceError('attempt_cancelled', 'Attempt cancellation revoked further grants');
501
618
  try {
619
+ this.childBoundary(id, w.epoch);
620
+ if (method.startsWith('team.')) {
621
+ const child = this.children?.owned(id);
622
+ if (!child || !this.team || !w.attempt) throw new Error('No owned team assignment');
623
+ const binding = this.store.workerBoundary.view(child.id).binding;
624
+ if (!binding.assignment || !binding.tools.includes('salaam_team'))
625
+ throw new Error('No team message authority');
626
+ const authorize = () => {
627
+ this.assertActive(id, w.epoch, w.attempt!);
628
+ this.team!.assert(binding);
629
+ this.store.taskGrantCheck?.(id);
630
+ };
631
+ authorize();
632
+ const request = string(body.request_id, 'request_id', 128),
633
+ assignment = binding.assignment.id;
634
+ if (method === 'team.send')
635
+ return this.team.send(
636
+ binding.owner_task!,
637
+ assignment,
638
+ string(body.recipient, 'recipient', 128),
639
+ string(body.text, 'text', 4096),
640
+ request,
641
+ authorize,
642
+ );
643
+ if (method === 'team.receive') return this.team.receive(assignment, request, authorize);
644
+ if (method === 'team.ack')
645
+ return this.team.acknowledge(
646
+ assignment,
647
+ string(body.message_id, 'message_id', 128),
648
+ request,
649
+ authorize,
650
+ );
651
+ throw new ServiceError('invalid_field', 'Unknown team method', 400);
652
+ }
653
+ if (method === 'specialist.read' || method === 'specialist.report') {
654
+ const child = this.children?.owned(id);
655
+ if (!child || !this.repositories || !w.attempt)
656
+ throw new Error('No active specialist assignment');
657
+ this.assertActive(id, w.epoch, w.attempt);
658
+ const binding = this.store.workerBoundary.view(child.id).binding;
659
+ const tool = method === 'specialist.read' ? 'salaam_repository' : 'salaam_findings';
660
+ if (!binding.tools.includes(tool)) throw new Error('Specialist authority is unavailable');
661
+ this.store.taskGrantCheck?.(id);
662
+ if (method === 'specialist.report') return this.repositories.report(binding, body.findings);
663
+ const args = object(body.args);
664
+ return this.store.request(
665
+ `specialist-read:${id}:${w.epoch}`,
666
+ string(body.tool_call_id, 'tool_call_id', 512),
667
+ args,
668
+ () => {
669
+ const result = this.repositories!.read(binding, args);
670
+ this.store.event(id, 'specialist.read', {
671
+ attempt_id: w.attempt,
672
+ snapshot_digest: binding.repository?.digest,
673
+ argument_digest: digest(args),
674
+ result_digest: digest(result),
675
+ });
676
+ return result;
677
+ },
678
+ );
679
+ }
680
+ if (method === 'provider.auth') {
681
+ if (!this.children?.owned(id)) {
682
+ const auth = await this.providerAuth();
683
+ if (canonical(this.route) !== canonical(route)) throw new Error('Provider route changed');
684
+ w.providerOutputCap = this.outputCap;
685
+ this.assertSessionProviderSupported(id, this.outputCap);
686
+ return auth;
687
+ }
688
+ const prepared = await this.prepareProviderRoute(route);
689
+ const expectedCap = this.children?.owned(id)
690
+ ? this.store.workerBoundary.view(this.children.owned(id)!.id).binding.agent?.output_cap
691
+ : this.outputCap;
692
+ if (
693
+ canonical(prepared.route) !== canonical(route) ||
694
+ (expectedCap ?? this.outputCap) !== prepared.output_cap
695
+ )
696
+ throw new Error('Provider capability or route changed; restart or renew admission');
697
+ w.providerOutputCap = prepared.output_cap;
698
+ this.assertSessionProviderSupported(id, prepared.output_cap);
699
+ const auth = prepared.auth;
700
+ return auth;
701
+ }
502
702
  if (method === 'coding.files' || method === 'coding.command') {
503
- if (this.children?.owned(id) || !this.coding || !this.codingBinding(id) || !w.attempt)
703
+ if (!this.coding || !this.codingBinding(id) || !w.attempt)
504
704
  throw new Error('Coding authority is unavailable');
505
705
  this.assertActive(id, w.epoch, w.attempt);
706
+ this.store.taskGrantCheck?.(id);
506
707
  const args = object(body.args);
507
- if (method === 'coding.files')
708
+ if (method === 'coding.files') {
709
+ if (!this.codingBinding(id)?.files) throw new Error('File authority is unavailable');
710
+ if (this.children?.owned(id) && args.path !== undefined) repositoryPath(args.path);
508
711
  return this.coding.file(
509
712
  id,
510
713
  w.epoch,
@@ -512,6 +715,7 @@ export class Supervisor extends EventEmitter {
512
715
  string(body.tool_call_id, 'tool_call_id', 512),
513
716
  args,
514
717
  );
718
+ }
515
719
  if (!this.codingBinding(id)?.commands) throw new Error('Command execution is unavailable');
516
720
  if (args.action === 'start')
517
721
  return this.coding.start(id, w.attempt, args, () =>
@@ -557,8 +761,16 @@ export class Supervisor extends EventEmitter {
557
761
  }
558
762
  if (method.startsWith('child.')) {
559
763
  if (!this.children || !w.attempt) throw new Error('No active parent attempt');
560
- this.assertActive(id, w.epoch, w.attempt);
561
- if (method === 'child.spawn')
764
+ // spawn() resolves a saved step before testing a fresh invocation/grant.
765
+ if (method !== 'child.spawn') this.assertActive(id, w.epoch, w.attempt);
766
+ if (method === 'child.spawn') {
767
+ if (
768
+ Object.keys(body).some(
769
+ (key) =>
770
+ !['epoch', 'step_key', 'prompt', 'budget', 'authority', 'profile'].includes(key),
771
+ )
772
+ )
773
+ throw new ServiceError('invalid_child', 'Unsupported child admission field');
562
774
  return this.children.spawn(
563
775
  id,
564
776
  w.epoch,
@@ -568,8 +780,12 @@ export class Supervisor extends EventEmitter {
568
780
  Number(body.budget),
569
781
  Array.isArray(body.authority)
570
782
  ? body.authority.map((x) => string(x, 'authority', 128))
571
- : ['salaam_probe'],
783
+ : body.profile
784
+ ? []
785
+ : ['salaam_probe'],
786
+ body.profile === undefined ? undefined : string(body.profile, 'profile', 128),
572
787
  );
788
+ }
573
789
  if (method === 'child.list') return this.children.list(id);
574
790
  const child = string(body.child_id, 'child_id', 128);
575
791
  if (method === 'child.inspect') return this.children.inspect(id, child);
@@ -614,10 +830,10 @@ export class Supervisor extends EventEmitter {
614
830
  this.store.usage.observe(
615
831
  id,
616
832
  string(body.source_key, 'source_key', 256),
617
- this.route.provider,
618
- this.route.model,
833
+ route.provider,
834
+ route.model,
619
835
  body.usage,
620
- this.route.provider === 'anthropic' ? 'estimated' : 'known',
836
+ route.provider !== 'salaam-proof' ? 'estimated' : 'known',
621
837
  ),
622
838
  );
623
839
  return { ok: true };
@@ -626,22 +842,26 @@ export class Supervisor extends EventEmitter {
626
842
  this.coding?.assertSafe(id);
627
843
  this.store.taskGrantCheck?.(id);
628
844
  if (
629
- body.provider !== this.route.provider ||
630
- body.model !== this.route.model ||
845
+ body.provider !== route.provider ||
846
+ body.model !== route.model ||
631
847
  body.context_digest !== textDigest(this.tasks.frame(id, w.binding))
632
848
  )
633
849
  throw new Error('Provider or task context is invalid');
634
- if (this.route.provider === 'anthropic') {
635
- validateClaudePayload(
850
+ if (route.provider !== 'salaam-proof') {
851
+ if (w.providerOutputCap === undefined)
852
+ throw new Error('Provider authentication was not admitted');
853
+ this.assertSessionProviderSupported(id, w.providerOutputCap);
854
+ validateProviderPayload(
636
855
  body.payload,
637
- this.route,
856
+ route,
638
857
  this.tasks.frame(id, w.binding),
639
858
  this.tools(id),
859
+ !w.providerOutputCap,
640
860
  );
641
861
  if (body.payload_digest !== digest(body.payload))
642
- throw new Error('Claude payload digest changed');
862
+ throw new Error('Provider payload digest changed');
643
863
  }
644
- if (this.route.provider === 'salaam-proof') {
864
+ if (route.provider === 'salaam-proof') {
645
865
  const payload = object(body.payload);
646
866
  if (!Array.isArray(payload.messages)) throw new Error('Proof payload missing');
647
867
  const last = object(payload.messages.at(-1));
@@ -666,7 +886,7 @@ export class Supervisor extends EventEmitter {
666
886
  string(body.source_key, 'source_key', 256),
667
887
  String(body.provider),
668
888
  String(body.model),
669
- { input: Buffer.byteLength(JSON.stringify(body.payload)), output: this.route.max_tokens },
889
+ { input: Buffer.byteLength(JSON.stringify(body.payload)), output: route.max_tokens },
670
890
  );
671
891
  this.store.transaction(() => {
672
892
  this.store.event(id, 'model.admitted', {
@@ -692,6 +912,19 @@ export class Supervisor extends EventEmitter {
692
912
  return { ok: true };
693
913
  } catch (error) {
694
914
  this.store.taskBudgetDenied?.(id, error);
915
+ if (
916
+ method.startsWith('specialist.') &&
917
+ error instanceof ServiceError &&
918
+ [
919
+ 'not_found',
920
+ 'binary_file',
921
+ 'line_limit',
922
+ 'invalid_field',
923
+ 'unsafe_path',
924
+ 'request_conflict',
925
+ ].includes(error.code)
926
+ )
927
+ throw error;
695
928
  if (
696
929
  method.startsWith('coding.') &&
697
930
  error instanceof ServiceError &&
@@ -740,9 +973,26 @@ export class Supervisor extends EventEmitter {
740
973
  }
741
974
  }
742
975
  inspect(id: string): Session {
976
+ const externalChild = this.children?.owned(id);
977
+ if (externalChild && this.store.workerBoundary.view(externalChild.id).binding.external)
978
+ return this.store.session(id);
979
+ const child = this.children?.owned(id);
980
+ const cap = child
981
+ ? this.store.workerBoundary.view(child.id).binding.agent?.output_cap
982
+ : undefined;
743
983
  const state = {
744
984
  ...this.store.session(id),
745
985
  provider_route: this.store.providerRoute(id) ?? undefined,
986
+ provider_capabilities:
987
+ cap === undefined
988
+ ? this.providerCapabilities()
989
+ : {
990
+ output_token_cap: cap,
991
+ durable_tasks: true,
992
+ warning: cap
993
+ ? null
994
+ : 'No provider-enforced output cap; tasks must explicitly disable token budgeting',
995
+ },
746
996
  };
747
997
  const w = this.workers.get(id);
748
998
  if (w?.invalid) return { ...state, state: 'unhealthy', capabilities: [], error: w.invalid };
@@ -942,6 +1192,7 @@ export class Supervisor extends EventEmitter {
942
1192
  if (w.invalid || end.aborted) throw new Error(w.invalid ?? 'Attempt aborted');
943
1193
  const entries = await this.readEntries(w);
944
1194
  const result = object((await w.pi.command('get_last_assistant_text')).data);
1195
+ this.childBoundary(id, w.epoch);
945
1196
  this.store.finish(
946
1197
  work.attempt.id,
947
1198
  outcomeError ? 'failed' : 'completed',
@@ -957,6 +1208,8 @@ export class Supervisor extends EventEmitter {
957
1208
  w.pi.off('record', record);
958
1209
  w.busy = false;
959
1210
  delete w.attempt;
1211
+ const child = this.children?.owned(id);
1212
+ if (child && child.state !== 'accepted') await this.stopWorker(w);
960
1213
  this.emit('idle', id);
961
1214
  }
962
1215
  return true;