@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
@@ -6,9 +6,13 @@ import { join, resolve } from 'node:path';
6
6
  import { PROTOCOL_VERSION, canonical, object, ServiceError, textDigest, workerTools, workerCapabilities, string, } from './protocol.js';
7
7
  import { PI_CLI, HOUSE_EXTENSION, POLICY_DIGEST, bundleDigest } from './config.js';
8
8
  import { PiProcess, PiCommandError } from './pi.js';
9
- import { OFFLINE_ROUTE, providerRoute, validateClaudePayload, } from './provider-config.js';
10
- import { loadApiKey, validateApiKey } from './credentials.js';
9
+ import { OFFLINE_ROUTE, providerRoute, providerHasOutputCap, } from './provider-config.js';
10
+ import { validateApiKey } from './credentials.js';
11
+ import { resolveProviderAuth } from './provider-auth.js';
12
+ import { bindProviderConfiguration } from './provider-model.js';
13
+ import { validateProviderPayload } from './provider-payload.js';
11
14
  import { digest } from './protocol.js';
15
+ import { repositoryPath } from './repository-snapshots.js';
12
16
  export function processIdentity(pid) {
13
17
  try {
14
18
  return (execFileSync('/bin/ps', ['-ww', '-p', String(pid), '-o', 'lstart=', '-o', 'command='], {
@@ -42,14 +46,70 @@ export class Supervisor extends EventEmitter {
42
46
  managed;
43
47
  children;
44
48
  coding;
49
+ repositories;
50
+ team;
51
+ routeFor(id) {
52
+ const child = this.children?.owned(id);
53
+ return child
54
+ ? (this.store.workerBoundary.view(child.id).binding.agent?.route ?? this.route)
55
+ : this.route;
56
+ }
57
+ async prepareProviderRoute(input) {
58
+ const route = structuredClone(input);
59
+ if (route.provider === 'salaam-proof')
60
+ return { route, output_cap: true, auth: undefined };
61
+ const auth = route.provider === 'anthropic' && this.apiKey
62
+ ? { auth: { apiKey: this.apiKey }, env: {} }
63
+ : await resolveProviderAuth(route, this.options.providerCredentials, this.options.providerEnvironment);
64
+ const bound = bindProviderConfiguration(route, auth);
65
+ return { route: bound, output_cap: providerHasOutputCap(bound, auth), auth };
66
+ }
45
67
  codingBinding(id) {
46
- return this.children?.owned(id) ? undefined : this.coding?.binding(id);
68
+ const child = this.children?.owned(id), coding = this.coding?.binding(id);
69
+ if (!child)
70
+ return coding;
71
+ const binding = this.store.workerBoundary.view(child.id).binding;
72
+ return binding.isolation && coding
73
+ ? {
74
+ ...coding,
75
+ files: binding.tools.includes('salaam_files'),
76
+ commands: coding.commands && binding.tools.includes('salaam_command'),
77
+ }
78
+ : undefined;
47
79
  }
48
80
  tools(id) {
81
+ const child = this.children?.owned(id);
82
+ if (child)
83
+ return JSON.parse(child.authority);
49
84
  return workerTools(!!this.options.effectsEnabled, !!this.children?.owned(id), this.codingBinding(id));
50
85
  }
86
+ childBoundary(id, epoch) {
87
+ const child = this.children?.owned(id);
88
+ return child ? this.store.workerBoundary.assert(child, epoch, this.routeFor(id)) : undefined;
89
+ }
51
90
  route;
52
91
  apiKey;
92
+ outputCap = true;
93
+ authInitialized = false;
94
+ providerCapabilities() {
95
+ return {
96
+ output_token_cap: this.outputCap,
97
+ durable_tasks: true,
98
+ warning: this.outputCap
99
+ ? null
100
+ : 'No provider-enforced output cap; tasks must explicitly disable token budgeting',
101
+ };
102
+ }
103
+ assertTaskProviderSupported(limits, outputCap = this.outputCap) {
104
+ if (!outputCap && (limits.token_units !== null || limits.output_token_bound !== null))
105
+ throw new ServiceError('provider_capability', 'This provider authentication has no output cap; set limits.token_units to null and omit output_token_bound');
106
+ }
107
+ assertSessionProviderSupported(id, outputCap = this.outputCap) {
108
+ const parent = this.children?.owned(id)?.parent_session ?? id;
109
+ const task = this.tasks.work?.forSession(parent);
110
+ if (task)
111
+ this.assertTaskProviderSupported(task.limits, outputCap);
112
+ }
53
113
  bootId = randomUUID();
54
114
  workers = new Map();
55
115
  monitor;
@@ -58,23 +118,46 @@ export class Supervisor extends EventEmitter {
58
118
  super();
59
119
  this.store = store;
60
120
  this.options = options;
61
- this.route = options.providerRoute ?? { ...OFFLINE_ROUTE };
121
+ this.route = structuredClone(options.providerRoute ?? OFFLINE_ROUTE);
62
122
  // Validate public configuration without trusting programmatic callers.
63
- const checked = providerRoute(this.route.provider === 'salaam-proof' ? 'offline' : this.route.provider, this.route.provider === 'anthropic' ? this.route.model : undefined, this.route.provider === 'anthropic' ? this.route.max_tokens : undefined, this.route.workspace_id);
123
+ const checked = providerRoute(this.route.provider === 'salaam-proof' ? 'offline' : this.route.provider, this.route.provider !== 'salaam-proof' ? this.route.model : undefined, this.route.provider !== 'salaam-proof' ? this.route.max_tokens : undefined, this.route.workspace_id, this.route.connection);
64
124
  if (canonical(checked) !== canonical(this.route))
65
125
  throw new Error('Invalid provider route');
66
- if (this.route.provider === 'anthropic')
67
- this.apiKey = options.anthropicApiKey
68
- ? validateApiKey(options.anthropicApiKey)
69
- : loadApiKey();
126
+ if (options.anthropicApiKey)
127
+ this.apiKey = validateApiKey(options.anthropicApiKey);
70
128
  this.bundle = bundleDigest();
71
129
  this.monitor = setInterval(() => {
72
130
  for (const worker of this.workers.values())
73
- if (worker.ready && Date.now() - worker.lastBeat > (options.heartbeatTimeout ?? 2500))
74
- this.invalidate(worker, 'Worker heartbeat expired');
131
+ if (worker.ready) {
132
+ if (Date.now() - worker.lastBeat > (options.heartbeatTimeout ?? 2500))
133
+ this.invalidate(worker, 'Worker heartbeat expired');
134
+ const child = this.children?.owned(worker.id);
135
+ if (child?.state === 'accepted') {
136
+ try {
137
+ this.childBoundary(worker.id, worker.epoch);
138
+ }
139
+ catch (error) {
140
+ this.invalidate(worker, 'Worker boundary revoked: ' + error.message);
141
+ }
142
+ }
143
+ }
75
144
  }, 250);
76
145
  this.monitor.unref();
77
146
  }
147
+ async providerAuth() {
148
+ if (this.route.provider === 'salaam-proof')
149
+ return undefined;
150
+ if (this.route.provider === 'anthropic' && this.apiKey)
151
+ return { auth: { apiKey: this.apiKey }, env: {} };
152
+ const auth = await resolveProviderAuth(this.route, this.options.providerCredentials, this.options.providerEnvironment);
153
+ Object.assign(this.route, bindProviderConfiguration(this.route, auth));
154
+ const outputCap = providerHasOutputCap(this.route, auth);
155
+ if (this.authInitialized && this.outputCap !== outputCap)
156
+ throw new Error('Provider output-cap capability changed; restart the service before using the new authentication');
157
+ this.outputCap = outputCap;
158
+ this.authInitialized = true;
159
+ return auth;
160
+ }
78
161
  async recover() {
79
162
  const rows = this.store.db.prepare("SELECT * FROM workers WHERE state!='exited'").all();
80
163
  for (const row of rows) {
@@ -102,6 +185,10 @@ export class Supervisor extends EventEmitter {
102
185
  }
103
186
  }
104
187
  async start(id) {
188
+ const child = this.children?.owned(id);
189
+ if (child && this.store.workerBoundary.view(child.id).binding.runtime !== 'native-pi')
190
+ throw new ServiceError('external_worker_owned', 'Use the external worker lifecycle; Pi cannot claim this session');
191
+ const route = this.routeFor(id);
105
192
  const prior = this.workers.get(id);
106
193
  if (prior && !prior.stopping)
107
194
  throw new ServiceError('worker_exists', 'Worker already supervised');
@@ -113,7 +200,7 @@ export class Supervisor extends EventEmitter {
113
200
  .prepare("SELECT 1 FROM workers WHERE session_id=? AND state!='exited' LIMIT 1")
114
201
  .get(id))
115
202
  throw new ServiceError('reconciliation_required', previous.error ?? 'Session requires reconciliation');
116
- this.store.bindRoute(id, this.route);
203
+ this.store.bindRoute(id, route);
117
204
  const epoch = this.store.allocate(id, this.bootId);
118
205
  let accept;
119
206
  let reject;
@@ -124,6 +211,7 @@ export class Supervisor extends EventEmitter {
124
211
  const binding = randomBytes(16).toString('hex');
125
212
  const frame = this.tasks.frame(id, binding);
126
213
  const worker = {
214
+ route: structuredClone(route),
127
215
  id,
128
216
  epoch,
129
217
  token: randomBytes(32).toString('hex'),
@@ -144,6 +232,9 @@ export class Supervisor extends EventEmitter {
144
232
  void handshake.catch(() => { });
145
233
  let timer;
146
234
  try {
235
+ const child = this.children?.owned(id);
236
+ if (child)
237
+ this.store.workerBoundary.claim(child, epoch, route);
147
238
  const workerDir = join(this.options.stateDir, 'workers', id);
148
239
  mkdirSync(join(workerDir, 'agent'), { recursive: true, mode: 0o700 });
149
240
  writeFileSync(join(workerDir, 'agent', 'settings.json'), JSON.stringify({
@@ -161,7 +252,8 @@ export class Supervisor extends EventEmitter {
161
252
  frame,
162
253
  effects: !!this.options.effectsEnabled && !this.children?.owned(id),
163
254
  child: !!this.children?.owned(id),
164
- route: this.route,
255
+ worker_binding: this.childBoundary(id, epoch),
256
+ route: route,
165
257
  coding: this.codingBinding(id),
166
258
  };
167
259
  const args = [
@@ -187,9 +279,9 @@ export class Supervisor extends EventEmitter {
187
279
  '--tools',
188
280
  this.tools(id).join(','),
189
281
  '--provider',
190
- this.route.provider,
282
+ route.provider,
191
283
  '--model',
192
- this.route.model,
284
+ route.model,
193
285
  ];
194
286
  if (previous.pi_session_file && existsSync(previous.pi_session_file)) {
195
287
  const index = args.indexOf('--session-id');
@@ -208,7 +300,7 @@ export class Supervisor extends EventEmitter {
208
300
  LANG: process.env.LANG ?? 'en_US.UTF-8',
209
301
  PI_CODING_AGENT_DIR: join(workerDir, 'agent'),
210
302
  SALAAM_WORKER_CONFIG: JSON.stringify(config),
211
- ...(this.apiKey ? { ANTHROPIC_API_KEY: this.apiKey } : {}),
303
+ ...(route.connection?.configuration ?? {}),
212
304
  };
213
305
  worker.pi = (this.options.launch ?? ((x) => new PiProcess(x.command, x.args, x.cwd, x.env)))({
214
306
  command: process.execPath,
@@ -298,8 +390,7 @@ export class Supervisor extends EventEmitter {
298
390
  const file = typeof state.sessionFile === 'string' ? resolve(state.sessionFile) : null;
299
391
  if (!file || !file.startsWith(resolve(sessionDir) + '/'))
300
392
  throw new Error('Pi session is outside its managed directory');
301
- if (object(state.model).provider !== this.route.provider ||
302
- object(state.model).id !== this.route.model)
393
+ if (object(state.model).provider !== route.provider || object(state.model).id !== route.model)
303
394
  throw new Error('Pi selected an unapproved provider route');
304
395
  if (worker.invalid)
305
396
  throw new Error(worker.invalid);
@@ -307,7 +398,9 @@ export class Supervisor extends EventEmitter {
307
398
  // persist a cursor into that ephemeral tree before its file exists.
308
399
  if (existsSync(file))
309
400
  await this.syncEntries(worker);
310
- this.store.state(id, epoch, 'ready', null, workerCapabilities(this.codingBinding(id)), file);
401
+ if (child)
402
+ this.store.workerBoundary.accepted(child, epoch, route);
403
+ this.store.state(id, epoch, 'ready', null, workerCapabilities(this.codingBinding(id), this.tools(id)), file);
311
404
  worker.ready = true;
312
405
  worker.lastBeat = Date.now();
313
406
  }
@@ -362,6 +455,7 @@ export class Supervisor extends EventEmitter {
362
455
  }
363
456
  async bridge(id, method, token, body) {
364
457
  const w = this.authenticate(id, token, body);
458
+ const route = w.route;
365
459
  if (method === 'failure') {
366
460
  if (!w.stopping)
367
461
  this.invalidate(w, 'Required extension failed');
@@ -373,6 +467,7 @@ export class Supervisor extends EventEmitter {
373
467
  try {
374
468
  if (w.manifest)
375
469
  throw new Error('Duplicate handshake');
470
+ const boundary = this.childBoundary(id, w.epoch);
376
471
  const expected = {
377
472
  protocol_version: PROTOCOL_VERSION,
378
473
  bundle_digest: w.bundle,
@@ -380,13 +475,14 @@ export class Supervisor extends EventEmitter {
380
475
  session_id: id,
381
476
  pi_session_id: id,
382
477
  epoch: w.epoch,
383
- capabilities: workerCapabilities(this.codingBinding(id)),
478
+ capabilities: workerCapabilities(this.codingBinding(id), this.tools(id)),
384
479
  tools: this.tools(id),
385
480
  active_tools: this.tools(id),
386
481
  ...(this.codingBinding(id) ? { coding: this.codingBinding(id) } : {}),
387
- provider: this.route.provider,
388
- route: this.route,
482
+ provider: route.provider,
483
+ route: route,
389
484
  context_digest: textDigest(w.frame),
485
+ ...(boundary ? { worker_binding: boundary } : {}),
390
486
  };
391
487
  for (const [key, value] of Object.entries(expected))
392
488
  if (canonical(body[key]) !== canonical(value))
@@ -409,13 +505,87 @@ export class Supervisor extends EventEmitter {
409
505
  if (w.attempt && this.store.cancelled(w.attempt))
410
506
  throw new ServiceError('attempt_cancelled', 'Attempt cancellation revoked further grants');
411
507
  try {
508
+ this.childBoundary(id, w.epoch);
509
+ if (method.startsWith('team.')) {
510
+ const child = this.children?.owned(id);
511
+ if (!child || !this.team || !w.attempt)
512
+ throw new Error('No owned team assignment');
513
+ const binding = this.store.workerBoundary.view(child.id).binding;
514
+ if (!binding.assignment || !binding.tools.includes('salaam_team'))
515
+ throw new Error('No team message authority');
516
+ const authorize = () => {
517
+ this.assertActive(id, w.epoch, w.attempt);
518
+ this.team.assert(binding);
519
+ this.store.taskGrantCheck?.(id);
520
+ };
521
+ authorize();
522
+ const request = string(body.request_id, 'request_id', 128), assignment = binding.assignment.id;
523
+ if (method === 'team.send')
524
+ return this.team.send(binding.owner_task, assignment, string(body.recipient, 'recipient', 128), string(body.text, 'text', 4096), request, authorize);
525
+ if (method === 'team.receive')
526
+ return this.team.receive(assignment, request, authorize);
527
+ if (method === 'team.ack')
528
+ return this.team.acknowledge(assignment, string(body.message_id, 'message_id', 128), request, authorize);
529
+ throw new ServiceError('invalid_field', 'Unknown team method', 400);
530
+ }
531
+ if (method === 'specialist.read' || method === 'specialist.report') {
532
+ const child = this.children?.owned(id);
533
+ if (!child || !this.repositories || !w.attempt)
534
+ throw new Error('No active specialist assignment');
535
+ this.assertActive(id, w.epoch, w.attempt);
536
+ const binding = this.store.workerBoundary.view(child.id).binding;
537
+ const tool = method === 'specialist.read' ? 'salaam_repository' : 'salaam_findings';
538
+ if (!binding.tools.includes(tool))
539
+ throw new Error('Specialist authority is unavailable');
540
+ this.store.taskGrantCheck?.(id);
541
+ if (method === 'specialist.report')
542
+ return this.repositories.report(binding, body.findings);
543
+ const args = object(body.args);
544
+ return this.store.request(`specialist-read:${id}:${w.epoch}`, string(body.tool_call_id, 'tool_call_id', 512), args, () => {
545
+ const result = this.repositories.read(binding, args);
546
+ this.store.event(id, 'specialist.read', {
547
+ attempt_id: w.attempt,
548
+ snapshot_digest: binding.repository?.digest,
549
+ argument_digest: digest(args),
550
+ result_digest: digest(result),
551
+ });
552
+ return result;
553
+ });
554
+ }
555
+ if (method === 'provider.auth') {
556
+ if (!this.children?.owned(id)) {
557
+ const auth = await this.providerAuth();
558
+ if (canonical(this.route) !== canonical(route))
559
+ throw new Error('Provider route changed');
560
+ w.providerOutputCap = this.outputCap;
561
+ this.assertSessionProviderSupported(id, this.outputCap);
562
+ return auth;
563
+ }
564
+ const prepared = await this.prepareProviderRoute(route);
565
+ const expectedCap = this.children?.owned(id)
566
+ ? this.store.workerBoundary.view(this.children.owned(id).id).binding.agent?.output_cap
567
+ : this.outputCap;
568
+ if (canonical(prepared.route) !== canonical(route) ||
569
+ (expectedCap ?? this.outputCap) !== prepared.output_cap)
570
+ throw new Error('Provider capability or route changed; restart or renew admission');
571
+ w.providerOutputCap = prepared.output_cap;
572
+ this.assertSessionProviderSupported(id, prepared.output_cap);
573
+ const auth = prepared.auth;
574
+ return auth;
575
+ }
412
576
  if (method === 'coding.files' || method === 'coding.command') {
413
- if (this.children?.owned(id) || !this.coding || !this.codingBinding(id) || !w.attempt)
577
+ if (!this.coding || !this.codingBinding(id) || !w.attempt)
414
578
  throw new Error('Coding authority is unavailable');
415
579
  this.assertActive(id, w.epoch, w.attempt);
580
+ this.store.taskGrantCheck?.(id);
416
581
  const args = object(body.args);
417
- if (method === 'coding.files')
582
+ if (method === 'coding.files') {
583
+ if (!this.codingBinding(id)?.files)
584
+ throw new Error('File authority is unavailable');
585
+ if (this.children?.owned(id) && args.path !== undefined)
586
+ repositoryPath(args.path);
418
587
  return this.coding.file(id, w.epoch, w.attempt, string(body.tool_call_id, 'tool_call_id', 512), args);
588
+ }
419
589
  if (!this.codingBinding(id)?.commands)
420
590
  throw new Error('Command execution is unavailable');
421
591
  if (args.action === 'start')
@@ -452,11 +622,18 @@ export class Supervisor extends EventEmitter {
452
622
  if (method.startsWith('child.')) {
453
623
  if (!this.children || !w.attempt)
454
624
  throw new Error('No active parent attempt');
455
- this.assertActive(id, w.epoch, w.attempt);
456
- if (method === 'child.spawn')
625
+ // spawn() resolves a saved step before testing a fresh invocation/grant.
626
+ if (method !== 'child.spawn')
627
+ this.assertActive(id, w.epoch, w.attempt);
628
+ if (method === 'child.spawn') {
629
+ if (Object.keys(body).some((key) => !['epoch', 'step_key', 'prompt', 'budget', 'authority', 'profile'].includes(key)))
630
+ throw new ServiceError('invalid_child', 'Unsupported child admission field');
457
631
  return this.children.spawn(id, w.epoch, w.attempt, string(body.step_key, 'step_key', 128), string(body.prompt, 'prompt', 32768), Number(body.budget), Array.isArray(body.authority)
458
632
  ? body.authority.map((x) => string(x, 'authority', 128))
459
- : ['salaam_probe']);
633
+ : body.profile
634
+ ? []
635
+ : ['salaam_probe'], body.profile === undefined ? undefined : string(body.profile, 'profile', 128));
636
+ }
460
637
  if (method === 'child.list')
461
638
  return this.children.list(id);
462
639
  const child = string(body.child_id, 'child_id', 128);
@@ -494,22 +671,25 @@ export class Supervisor extends EventEmitter {
494
671
  .get(String(body.source_key));
495
672
  if (!source || source.session_id !== id || source.attempt_id !== (w.attempt ?? null))
496
673
  throw new Error('Usage does not belong to this request');
497
- this.store.transaction(() => this.store.usage.observe(id, string(body.source_key, 'source_key', 256), this.route.provider, this.route.model, body.usage, this.route.provider === 'anthropic' ? 'estimated' : 'known'));
674
+ this.store.transaction(() => this.store.usage.observe(id, string(body.source_key, 'source_key', 256), route.provider, route.model, body.usage, route.provider !== 'salaam-proof' ? 'estimated' : 'known'));
498
675
  return { ok: true };
499
676
  }
500
677
  if (method === 'model') {
501
678
  this.coding?.assertSafe(id);
502
679
  this.store.taskGrantCheck?.(id);
503
- if (body.provider !== this.route.provider ||
504
- body.model !== this.route.model ||
680
+ if (body.provider !== route.provider ||
681
+ body.model !== route.model ||
505
682
  body.context_digest !== textDigest(this.tasks.frame(id, w.binding)))
506
683
  throw new Error('Provider or task context is invalid');
507
- if (this.route.provider === 'anthropic') {
508
- validateClaudePayload(body.payload, this.route, this.tasks.frame(id, w.binding), this.tools(id));
684
+ if (route.provider !== 'salaam-proof') {
685
+ if (w.providerOutputCap === undefined)
686
+ throw new Error('Provider authentication was not admitted');
687
+ this.assertSessionProviderSupported(id, w.providerOutputCap);
688
+ validateProviderPayload(body.payload, route, this.tasks.frame(id, w.binding), this.tools(id), !w.providerOutputCap);
509
689
  if (body.payload_digest !== digest(body.payload))
510
- throw new Error('Claude payload digest changed');
690
+ throw new Error('Provider payload digest changed');
511
691
  }
512
- if (this.route.provider === 'salaam-proof') {
692
+ if (route.provider === 'salaam-proof') {
513
693
  const payload = object(body.payload);
514
694
  if (!Array.isArray(payload.messages))
515
695
  throw new Error('Proof payload missing');
@@ -526,7 +706,7 @@ export class Supervisor extends EventEmitter {
526
706
  body.payload_digest !== digest(payload))
527
707
  throw new Error('Proof task payload is stale or invalid');
528
708
  }
529
- this.store.usage.admit(id, w.attempt, string(body.source_key, 'source_key', 256), String(body.provider), String(body.model), { input: Buffer.byteLength(JSON.stringify(body.payload)), output: this.route.max_tokens });
709
+ this.store.usage.admit(id, w.attempt, string(body.source_key, 'source_key', 256), String(body.provider), String(body.model), { input: Buffer.byteLength(JSON.stringify(body.payload)), output: route.max_tokens });
530
710
  this.store.transaction(() => {
531
711
  this.store.event(id, 'model.admitted', {
532
712
  attempt_id: w.attempt ?? null,
@@ -556,6 +736,17 @@ export class Supervisor extends EventEmitter {
556
736
  }
557
737
  catch (error) {
558
738
  this.store.taskBudgetDenied?.(id, error);
739
+ if (method.startsWith('specialist.') &&
740
+ error instanceof ServiceError &&
741
+ [
742
+ 'not_found',
743
+ 'binary_file',
744
+ 'line_limit',
745
+ 'invalid_field',
746
+ 'unsafe_path',
747
+ 'request_conflict',
748
+ ].includes(error.code))
749
+ throw error;
559
750
  if (method.startsWith('coding.') &&
560
751
  error instanceof ServiceError &&
561
752
  [
@@ -599,9 +790,25 @@ export class Supervisor extends EventEmitter {
599
790
  }
600
791
  }
601
792
  inspect(id) {
793
+ const externalChild = this.children?.owned(id);
794
+ if (externalChild && this.store.workerBoundary.view(externalChild.id).binding.external)
795
+ return this.store.session(id);
796
+ const child = this.children?.owned(id);
797
+ const cap = child
798
+ ? this.store.workerBoundary.view(child.id).binding.agent?.output_cap
799
+ : undefined;
602
800
  const state = {
603
801
  ...this.store.session(id),
604
802
  provider_route: this.store.providerRoute(id) ?? undefined,
803
+ provider_capabilities: cap === undefined
804
+ ? this.providerCapabilities()
805
+ : {
806
+ output_token_cap: cap,
807
+ durable_tasks: true,
808
+ warning: cap
809
+ ? null
810
+ : 'No provider-enforced output cap; tasks must explicitly disable token budgeting',
811
+ },
605
812
  };
606
813
  const w = this.workers.get(id);
607
814
  if (w?.invalid)
@@ -805,6 +1012,7 @@ export class Supervisor extends EventEmitter {
805
1012
  throw new Error(w.invalid ?? 'Attempt aborted');
806
1013
  const entries = await this.readEntries(w);
807
1014
  const result = object((await w.pi.command('get_last_assistant_text')).data);
1015
+ this.childBoundary(id, w.epoch);
808
1016
  this.store.finish(work.attempt.id, outcomeError ? 'failed' : 'completed', String(result.text ?? ''), outcomeError, { epoch: w.epoch, ...entries });
809
1017
  }
810
1018
  catch (error) {
@@ -816,6 +1024,9 @@ export class Supervisor extends EventEmitter {
816
1024
  w.pi.off('record', record);
817
1025
  w.busy = false;
818
1026
  delete w.attempt;
1027
+ const child = this.children?.owned(id);
1028
+ if (child && child.state !== 'accepted')
1029
+ await this.stopWorker(w);
819
1030
  this.emit('idle', id);
820
1031
  }
821
1032
  return true;
@@ -34,14 +34,18 @@ export class TaskBudget {
34
34
  return child ? this.tasks.forSession(child.parent_session) : null;
35
35
  }
36
36
  state(id) {
37
- return this.store.db.prepare('SELECT * FROM task_runtime WHERE task_id=?').get(id);
37
+ const runtime = this.store.db.prepare('SELECT * FROM task_runtime WHERE task_id=?').get(id);
38
+ const n = Number(this.store.db.prepare('SELECT count(*) AS n FROM task_invocations WHERE task_id=?').get(id)
39
+ .n);
40
+ return { ...runtime, invocations_spent: n, external_model_usage: n ? 'unknown' : 'none' };
38
41
  }
39
42
  active(task) {
40
43
  const row = this.store.db
41
44
  .prepare(`WITH tree(id) AS (SELECT ? UNION ALL SELECT child_session FROM child_tasks WHERE parent_session=?) SELECT
42
45
  (SELECT count(*) FROM attempts WHERE session_id IN (SELECT id FROM tree) AND state IN ('accepted','dispatching','running')) +
43
- (SELECT count(*) FROM command_runs WHERE session_id IN (SELECT id FROM tree) AND state NOT IN ('succeeded','failed','cancelled','timed_out','output_limited','interrupted')) AS n`)
44
- .get(task.session_id, task.session_id);
46
+ (SELECT count(*) FROM command_runs WHERE session_id IN (SELECT id FROM tree) AND state NOT IN ('succeeded','failed','cancelled','timed_out','output_limited','interrupted')) +
47
+ (SELECT count(*) FROM external_runs e JOIN child_tasks c ON c.id=e.child_id JOIN attempts a ON a.id=c.child_attempt WHERE c.parent_session=? AND e.stopped=0 AND a.state NOT IN ('accepted','dispatching','running')) AS n`)
48
+ .get(task.session_id, task.session_id, task.session_id);
45
49
  return Number(row.n);
46
50
  }
47
51
  deny(session, message) {
@@ -74,8 +78,9 @@ export class TaskBudget {
74
78
  this.check(session);
75
79
  if (!attempt)
76
80
  this.deny(task.session_id, 'Task model work must belong to a durable attempt');
77
- const l = task.limits, r = this.state(task.id), charge = l.input_token_bound + l.output_token_bound;
78
- if (input > l.input_token_bound || output > l.output_token_bound)
81
+ const l = task.limits, r = this.state(task.id), charge = l.token_units === null ? 0 : l.input_token_bound + l.output_token_bound;
82
+ if (input > l.input_token_bound ||
83
+ (l.output_token_bound !== null && output > l.output_token_bound))
79
84
  this.deny(task.session_id, 'Provider payload exceeds declared per-request token bounds');
80
85
  const reservation = this.store.db
81
86
  .prepare('SELECT * FROM task_reservations WHERE attempt_id=? AND task_id=?')
@@ -93,7 +98,8 @@ export class TaskBudget {
93
98
  else {
94
99
  if (Number(r.requests_spent) + Number(r.reserved_requests) >= l.requests)
95
100
  this.deny(task.session_id, 'Task request limit reached');
96
- if (Number(r.token_units) + Number(r.reserved_tokens) + charge > l.token_units)
101
+ if (l.token_units !== null &&
102
+ Number(r.token_units) + Number(r.reserved_tokens) + charge > l.token_units)
97
103
  this.deny(task.session_id, 'Task declared token budget reached');
98
104
  this.store.db
99
105
  .prepare('UPDATE task_runtime SET requests_spent=requests_spent+1,token_units=token_units+? WHERE task_id=?')
@@ -105,9 +111,12 @@ export class TaskBudget {
105
111
  if (!task)
106
112
  return;
107
113
  this.check(session);
108
- const r = this.state(task.id), tokens = count * (task.limits.input_token_bound + task.limits.output_token_bound);
114
+ const r = this.state(task.id), tokens = task.limits.token_units === null
115
+ ? 0
116
+ : count * (task.limits.input_token_bound + task.limits.output_token_bound);
109
117
  if (Number(r.requests_spent) + Number(r.reserved_requests) + count > task.limits.requests ||
110
- Number(r.token_units) + Number(r.reserved_tokens) + tokens > task.limits.token_units)
118
+ (task.limits.token_units !== null &&
119
+ Number(r.token_units) + Number(r.reserved_tokens) + tokens > task.limits.token_units))
111
120
  this.deny(task.session_id, 'Task cannot reserve the child request/token bounds');
112
121
  this.store.db
113
122
  .prepare('INSERT INTO task_reservations VALUES (?,?,?,?)')
@@ -36,6 +36,20 @@ export class TaskContext {
36
36
  }
37
37
  frame(id, binding) {
38
38
  const state = this.state(id);
39
+ const admitted = [
40
+ ...new Map((this.store.workerBoundary.profiles?.snapshot(id) ?? [])
41
+ .filter((p) => p.state === 'active')
42
+ .map((p) => [
43
+ p.profile_id,
44
+ {
45
+ profile_id: p.profile_id,
46
+ profile_digest: p.profile_digest,
47
+ tools: p.tools,
48
+ route: p.route,
49
+ max_depth: 1,
50
+ },
51
+ ])).values(),
52
+ ].sort((a, b) => a.profile_id.localeCompare(b.profile_id));
39
53
  // The compaction cursor is a projection, not a task fact/version.
40
54
  const frame = 'SALAAM_TASK_STATE ' +
41
55
  canonical({
@@ -44,6 +58,7 @@ export class TaskContext {
44
58
  version: state.version,
45
59
  instructions: 'Use active facts. Superseded facts are historical. Do not repeat completed steps. Retrieve evidence with salaam_history.',
46
60
  facts: state.facts,
61
+ ...(admitted.length ? { admitted_agents: admitted } : {}),
47
62
  ...(this.work?.forSession(id) ? { work_task: this.work.frame(id) } : {}),
48
63
  });
49
64
  if (Buffer.byteLength(frame) > TASK_FRAME_BYTES)