@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
@@ -9,7 +9,8 @@ export function modelLabel(client) {
9
9
  return 'Connect your workspace';
10
10
  if (['offline', 'salaam-proof'].includes(route.provider))
11
11
  return 'Offline demo';
12
- return route.model.replace('claude-sonnet-', 'Claude Sonnet ').replace(/(\d)-(\d)$/, '$1.$2');
12
+ const warning = client.health?.provider_capabilities?.output_token_cap === false ? ' · no output cap' : '';
13
+ return (route.model.replace('claude-sonnet-', 'Claude Sonnet ').replace(/(\d)-(\d)$/, '$1.$2') + warning);
13
14
  }
14
15
  export class Dynamic {
15
16
  content;
@@ -305,6 +306,42 @@ export class DocumentView {
305
306
  ? 'Occurrence ' + job.occurrence.id + ' · ' + job.occurrence.occurrence_key
306
307
  : 'No occurrence admitted', width), ...this.text(job.reason, width));
307
308
  }
309
+ const team = c.snapshot.team;
310
+ if (team?.assignments.length) {
311
+ lines.push(...this.title('TEAM BOARD', width));
312
+ for (const a of team.assignments) {
313
+ lines.push(...this.text(`${shortId(a.id)} · ${a.profile_id} · claim ${a.generation} · ${a.step_state}${a.stale ? ' · stale plan' : ''}`, width));
314
+ if (a.runtime)
315
+ lines.push(...this.text(a.runtime.id + ' · invocation budget · model usage unknown', width));
316
+ if (a.execution?.peer)
317
+ lines.push(...this.text('Peer ' +
318
+ a.execution.peer.peer_id +
319
+ ' · task ' +
320
+ (a.execution.peer.task_id ?? 'acceptance unknown'), width), ...this.text((a.execution.peer.remote_state ?? 'remote state unknown') +
321
+ ' · ' +
322
+ (a.execution.peer.cancel_state ?? 'no cancel requested'), width));
323
+ if (a.runtime?.provider_route)
324
+ lines.push(...this.text('Backend ' +
325
+ a.runtime.provider_route.provider +
326
+ '/' +
327
+ a.runtime.provider_route.model, width));
328
+ if (a.execution)
329
+ lines.push(...this.text(a.execution.state +
330
+ ' · ' +
331
+ (a.execution.stop_scope === 'never-dispatched'
332
+ ? 'not dispatched'
333
+ : a.execution.stop_scope === 'remote-terminal-receipt'
334
+ ? a.execution.stopped
335
+ ? 'remote terminal receipt'
336
+ : 'remote stop unconfirmed'
337
+ : a.execution.stopped
338
+ ? 'stop confirmed'
339
+ : 'stop unconfirmed'), width));
340
+ }
341
+ lines.push(...this.text('Messages are passive. Starting or continuing work requires a separate grant.', width));
342
+ for (const m of team.messages)
343
+ lines.push(...this.text(`${shortId(m.id)} · ${m.sender === 'operator' ? 'operator' : shortId(m.sender)} → ${shortId(m.recipient)} · ${m.acknowledged_at ? 'acknowledged' : m.delivered_at ? 'delivered' : 'stored'}`, width));
344
+ }
308
345
  const task = c.snapshot.work_task;
309
346
  if (task) {
310
347
  lines.push(...this.title('DURABLE TASK · ' + task.state.toUpperCase(), width), ...this.text(task.id + ' · revision ' + task.revision + ' · plan ' + task.plan_revision, width), ...this.text(task.objective, width), ...this.text(task.reason, width));
@@ -314,18 +351,30 @@ export class DocumentView {
314
351
  ' spent + ' +
315
352
  task.runtime.reserved_requests +
316
353
  ' reserved / ' +
317
- task.limits.requests, width), ...this.text('Declared token units ' +
318
- task.runtime.token_units +
319
- ' + ' +
320
- task.runtime.reserved_tokens +
321
- ' reserved / ' +
322
- task.limits.token_units, width), ...this.text('Bounded progression ' +
354
+ task.limits.requests, width), ...this.text(task.limits.token_units === null
355
+ ? 'Token budget disabled · ' +
356
+ (task.limits.output_token_bound === null
357
+ ? 'no task output-token bound'
358
+ : 'output bound ' + task.limits.output_token_bound)
359
+ : 'Declared token units ' +
360
+ task.runtime.token_units +
361
+ ' + ' +
362
+ task.runtime.reserved_tokens +
363
+ ' reserved / ' +
364
+ task.limits.token_units, width), ...this.text('Bounded progression ' +
323
365
  (task.runtime.auto_enabled
324
366
  ? 'enabled · ' + task.runtime.remaining_steps + ' new steps remain'
325
367
  : 'paused') +
326
368
  (task.runtime.auto_error ? ' · ' + task.runtime.auto_error : ''), width));
327
- if (task.verification)
328
- lines.push(...this.title('VERIFICATION', width), ...this.text(JSON.stringify(task.verification, null, 2), width));
369
+ if (task.verification) {
370
+ const verification = task.verification;
371
+ lines.push(...this.title('VERIFICATION', width));
372
+ for (const check of verification.checks ?? [])
373
+ lines.push(...this.text(`${check.check_key} · ${check.state} · ${check.fresh ? 'current' : 'historical'} · r${check.workspace_revision} · ${shortId(check.id)}`, width));
374
+ if (verification.completion)
375
+ lines.push(...this.text(`Task ${verification.completion.state} · ${verification.completion.fresh ? 'current verification' : 'verification needs refresh'}`, width));
376
+ lines.push(...this.text('Ctrl+K → Inspect task for complete check receipts.', width));
377
+ }
329
378
  for (const constraint of task.constraints)
330
379
  lines.push(...this.text('Constraint: ' + constraint, width));
331
380
  for (const step of task.steps)
@@ -339,6 +388,8 @@ export class DocumentView {
339
388
  for (const fact of facts)
340
389
  lines.push(...this.title(fact.kind.toUpperCase(), width, 'muted'), ...this.prose(fact.id, fact.text, width - 4).map((line) => t.line(line, width)), t.fill('', width));
341
390
  lines.push(...this.title('CHILD WORK', width));
391
+ for (const agent of c.snapshot.agents ?? [])
392
+ lines.push(...this.text(`${agent.profile_id} · revision ${agent.revision} · ${agent.state} · ${agent.route.provider}/${agent.route.model} · ${agent.tools.join(', ')}`, width));
342
393
  if (!c.snapshot.children.length)
343
394
  lines.push(...this.text('No child work in this session.', width));
344
395
  for (const child of c.snapshot.children)
@@ -347,7 +398,11 @@ export class DocumentView {
347
398
  ' · ' +
348
399
  child.state +
349
400
  (child.result_id ? ' · result ' + shortId(child.result_id) : '') +
350
- (child.acknowledged ? ' · acknowledged' : ''), width));
401
+ (child.acknowledged ? ' · acknowledged' : ''), width), ...(child.worker
402
+ ? this.text(`${child.worker.binding.profile.id} · ${child.worker.binding.runtime} · generation ${child.worker.generation} · ${child.worker.dispatch_state} · ${child.worker.binding.bounds.scope === 'model-requests' ? child.worker.binding.bounds.requests + ' model requests' : child.worker.binding.bounds.invocations + ' invocation · internal model usage unknown'}`, width)
403
+ : []), ...(child.worker?.binding.isolation
404
+ ? this.text(`Private draft ${shortId(child.worker.binding.isolation.workspace_id)} · execution ${child.worker.binding.isolation.execution.evidence} · root integration requires a reviewed proposal digest`, width)
405
+ : []));
351
406
  lines.push(t.fill('', width), ...this.title('USAGE', width));
352
407
  if (c.usage)
353
408
  lines.push(...this.text(`${c.usage.observations} provider observations · ${c.usage.unknown_observations} unknown`, width), ...this.text(`Recorded tokens: ${c.usage.known_input} input · ${c.usage.known_output} output`, width), ...this.text(`Recorded cost $${c.usage.known_cost.toFixed(4)} · estimated $${c.usage.estimated_cost.toFixed(4)}`, width));
package/dist/src/tui.js CHANGED
@@ -337,7 +337,9 @@ export class SalaamApp {
337
337
  await this.client.list();
338
338
  if (this.client.connection === 'connected')
339
339
  return;
340
- const provider = existsSync(credentialPath()) ? 'anthropic' : 'offline';
340
+ const provider = existsSync(credentialPath()) || existsSync(credentialPath() + '.json')
341
+ ? 'anthropic'
342
+ : 'offline';
341
343
  await promisify(execFile)(process.execPath, [
342
344
  join(ROOT, 'bin/salaam.mjs'),
343
345
  'service',
@@ -627,7 +629,7 @@ export class SalaamApp {
627
629
  else if (action === 'auto')
628
630
  this.taskInput('Bounded progression', String(task.steps.filter((s) => s.state === 'pending').length), 'Maximum new steps (1–32)', 'Steps advance only after their configured checks pass. Human approvals remain separate.', (value) => {
629
631
  const max_steps = Number(value);
630
- this.confirm('Run this bounded plan', `Task ${shortId(task.id)} · plan revision ${task.plan_revision}\nAt most ${max_steps} new steps; each step needs fresh passing checks.\nLimits: ${task.limits.requests} requests · ${task.limits.runtime_ms} ms · ${task.limits.concurrency} concurrent executions.\n${task.limits.token_units} declared token units · ${task.limits.verification_runs} verification runs.\nChecks: ${task.acceptance.map((c) => c.key).join(', ') || 'none'} · Deliverables: ${task.deliverables.length}.\nDeterministic compaction at idle boundaries. Human approvals remain separate.`, 'Run bounded task', () => this.client.taskAction('auto', { max_steps, compact: true }, reviewed));
632
+ this.confirm('Run this bounded plan', `Task ${shortId(task.id)} · plan revision ${task.plan_revision}\nAt most ${max_steps} new steps; each step needs fresh passing checks.\nLimits: ${task.limits.requests} requests · ${task.limits.runtime_ms} ms · ${task.limits.concurrency} concurrent executions.\n${task.limits.token_units === null ? 'Token budget disabled' : task.limits.token_units + ' declared token units'} · ${task.limits.verification_runs} verification runs.\nChecks: ${task.acceptance.map((c) => c.key).join(', ') || 'none'} · Deliverables: ${task.deliverables.length}.\nDeterministic compaction at idle boundaries. Human approvals remain separate.`, 'Run bounded task', () => this.client.taskAction('auto', { max_steps, compact: true }, reviewed));
631
633
  });
632
634
  else if (action === 'pause')
633
635
  void this.execute(() => this.client.taskAction('auto', { max_steps: 0, compact: false }, reviewed));
package/dist/src/usage.js CHANGED
@@ -80,6 +80,24 @@ export class UsageLedger {
80
80
  const key = typeof m?.responseId === 'string' && m.responseId.startsWith('salaam-request:')
81
81
  ? m.responseId
82
82
  : `pi:${session}:${entry.id}`;
83
+ // Managed live streams report receipts through the authenticated bridge.
84
+ // Pi normalizes a missing receipt to zeros; importing that transcript must
85
+ // never upgrade a conservatively unknown observation to a zero charge.
86
+ if (key.startsWith('salaam-request:')) {
87
+ const managed = this.store.db
88
+ .prepare('SELECT session_id,provider FROM usage_observations WHERE source_key=?')
89
+ .get(key);
90
+ if (managed && managed.provider !== 'salaam-proof') {
91
+ if (managed.session_id !== session)
92
+ throw new ServiceError('invalid_usage', 'Usage does not belong to this session');
93
+ continue;
94
+ }
95
+ // A rejected live request can still produce a Pi error transcript. Its
96
+ // absence from admission is definitive: no request was dispatched.
97
+ const route = this.store.providerRoute(session);
98
+ if (!managed && route && route.provider !== 'salaam-proof')
99
+ continue;
100
+ }
83
101
  // An aborted provider message may carry placeholder zeros. They are not evidence of zero spend.
84
102
  const complete = m && !['error', 'aborted'].includes(String(m.stopReason));
85
103
  if ((complete && m.usage) || entry.usage)
@@ -0,0 +1,298 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { canonical, digest, textDigest, ServiceError } from './protocol.js';
3
+ import { POLICY_DIGEST } from './config.js';
4
+ /** Local contract v1. These are enforced capabilities, not upstream feature claims. */
5
+ export const NATIVE_CAPABILITIES = {
6
+ schema_version: 1,
7
+ runtime: 'native-pi',
8
+ model_requests: true,
9
+ output_cap: 'authentication-dependent',
10
+ usage_receipts: true,
11
+ resume_dispatch: false,
12
+ confirmed_stop: false,
13
+ files: false,
14
+ commands: false,
15
+ isolation: 'managed-process-no-coding-authority',
16
+ retention: 'durable-local',
17
+ };
18
+ export const DIAGNOSTIC_PROFILE = {
19
+ schema_version: 1,
20
+ id: 'salaam/diagnostic',
21
+ revision: 1,
22
+ tools: ['salaam_probe'],
23
+ max_depth: 1,
24
+ };
25
+ export function nativeCapabilities(profile, coordination = false, isolation) {
26
+ const caps = profile.role
27
+ ? {
28
+ ...NATIVE_CAPABILITIES,
29
+ repository_read: profile.tools.includes('salaam_repository'),
30
+ findings: profile.tools.includes('salaam_findings'),
31
+ }
32
+ : NATIVE_CAPABILITIES;
33
+ return {
34
+ ...caps,
35
+ ...(coordination ? { team_messages: true } : {}),
36
+ ...(isolation
37
+ ? {
38
+ files: profile.tools.includes('salaam_files'),
39
+ commands: profile.tools.includes('salaam_command'),
40
+ isolation: isolation.execution.evidence === 'fixture'
41
+ ? 'simulated-owned-copy'
42
+ : 'owned-copy-and-proven-executor',
43
+ execution_profile: isolation.execution.profile_digest,
44
+ execution_engine: isolation.execution.engine_id,
45
+ }
46
+ : {}),
47
+ };
48
+ }
49
+ export function externalCapabilities(runtime, tools) {
50
+ return {
51
+ schema_version: 1,
52
+ runtime: runtime.kind,
53
+ model_requests: false,
54
+ output_cap: runtime.output_cap,
55
+ files: runtime.files && tools.includes('salaam_files'),
56
+ commands: runtime.commands && tools.includes('salaam_command'),
57
+ model_usage: 'unknown',
58
+ isolation: runtime.kind === 'a2a'
59
+ ? 'remote-peer-no-local-tool-authority'
60
+ : runtime.evidence === 'fixture'
61
+ ? 'simulated-whole-runtime'
62
+ : 'proven-whole-runtime',
63
+ };
64
+ }
65
+ /** Never equate one opaque invocation with one model request. Invocation admission is separate. */
66
+ export function assertModelRequestBoundary(capabilities) {
67
+ if (capabilities.schema_version !== 1)
68
+ throw new ServiceError('worker_capability_version', 'Unsupported worker capability version');
69
+ if (capabilities.model_requests !== true)
70
+ throw new ServiceError('worker_budget_scope', 'Model-request bounds require enforced admission; opaque workers need explicit invocation bounds');
71
+ }
72
+ export class WorkerBoundary {
73
+ store;
74
+ profiles;
75
+ assertAssignment;
76
+ assertIsolation;
77
+ constructor(store) {
78
+ this.store = store;
79
+ }
80
+ /** Called within the schema migration transaction. Cached responses and child rows stay intact. */
81
+ migrate() {
82
+ this.store.db.exec(`
83
+ CREATE TABLE worker_profiles(id TEXT NOT NULL,revision INTEGER NOT NULL,digest TEXT NOT NULL,document TEXT NOT NULL,revoked INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(id,revision));
84
+ CREATE TABLE child_workers(child_id TEXT PRIMARY KEY REFERENCES child_tasks(id),binding TEXT NOT NULL,binding_digest TEXT NOT NULL,generation INTEGER NOT NULL DEFAULT 0,worker_epoch INTEGER,dispatch_state TEXT NOT NULL);
85
+ CREATE UNIQUE INDEX child_worker_identity ON child_workers(json_extract(binding,'$.worker_id'));
86
+ `);
87
+ for (const child of this.store.db
88
+ .prepare('SELECT * FROM child_tasks')
89
+ .all()) {
90
+ this.freeze(child, null, true);
91
+ this.store.db
92
+ .prepare('UPDATE child_workers SET dispatch_state=? WHERE child_id=?')
93
+ .run(child.state === 'accepted' ? 'interrupted' : 'terminal', child.id);
94
+ }
95
+ }
96
+ /** Caller owns the transaction that creates the child, its reservation and input artifact. */
97
+ freeze(child, inputArtifact, legacy = false, agent, repository, assignment, isolation, external) {
98
+ const admittedProfile = agent?.profile ?? DIAGNOSTIC_PROFILE;
99
+ const profileDigest = digest(admittedProfile);
100
+ this.store.db
101
+ .prepare('INSERT OR IGNORE INTO worker_profiles(id,revision,digest,document) VALUES (?,?,?,?)')
102
+ .run(admittedProfile.id, admittedProfile.revision, profileDigest, canonical(admittedProfile));
103
+ const profile = this.store.db
104
+ .prepare('SELECT digest,revoked FROM worker_profiles WHERE id=? AND revision=?')
105
+ .get(admittedProfile.id, admittedProfile.revision);
106
+ if (profile?.revoked || profile?.digest !== profileDigest)
107
+ throw new ServiceError('worker_profile_revoked', 'Diagnostic worker profile is revoked or changed');
108
+ const task = this.store.db
109
+ .prepare('SELECT id FROM durable_tasks WHERE session_id=?')
110
+ .get(child.parent_session);
111
+ const workspace = this.store.db
112
+ .prepare('SELECT id,revision,digest FROM workspaces WHERE session_id=?')
113
+ .get(child.parent_session);
114
+ const binding = {
115
+ schema_version: 1,
116
+ worker_id: randomUUID(),
117
+ runtime: external?.kind ?? 'native-pi',
118
+ owner_session: child.parent_session,
119
+ owner_task: task ? String(task.id) : null,
120
+ parent_attempt: child.parent_attempt,
121
+ step_key: child.step_key,
122
+ session_id: child.child_session,
123
+ attempt_id: child.child_attempt,
124
+ input_digest: child.input_digest,
125
+ input_artifact: inputArtifact,
126
+ profile: admittedProfile,
127
+ profile_digest: profileDigest,
128
+ capabilities: nativeCapabilities(admittedProfile, !!assignment, isolation),
129
+ capability_digest: digest(nativeCapabilities(admittedProfile, !!assignment, isolation)),
130
+ tools: JSON.parse(child.authority),
131
+ route: external
132
+ ? null
133
+ : (agent?.route ??
134
+ this.store.providerRoute(child.child_session) ??
135
+ this.store.providerRoute(child.parent_session)),
136
+ workspace: workspace
137
+ ? {
138
+ id: String(workspace.id),
139
+ revision: Number(workspace.revision),
140
+ digest: String(workspace.digest),
141
+ }
142
+ : null,
143
+ policy_digest: POLICY_DIGEST,
144
+ bounds: external
145
+ ? { scope: 'invocations', invocations: 1 }
146
+ : { scope: 'model-requests', requests: child.budget },
147
+ ...(external ? { external } : {}),
148
+ legacy,
149
+ ...(agent ? { agent } : {}),
150
+ ...(repository ? { repository } : {}),
151
+ ...(assignment ? { assignment } : {}),
152
+ ...(isolation ? { isolation } : {}),
153
+ };
154
+ if (external) {
155
+ binding.capabilities = externalCapabilities(external, binding.tools);
156
+ binding.capability_digest = digest(binding.capabilities);
157
+ }
158
+ else
159
+ assertModelRequestBoundary(binding.capabilities);
160
+ this.store.db
161
+ .prepare("INSERT INTO child_workers(child_id,binding,binding_digest,dispatch_state) VALUES (?,?,?,'intent')")
162
+ .run(child.id, canonical(binding), digest(binding));
163
+ // Migration has no event publication; the child itself remains the legacy receipt.
164
+ if (!legacy)
165
+ this.store.event(child.parent_session, 'child.worker_intent', {
166
+ child_id: child.id,
167
+ worker_id: binding.worker_id,
168
+ binding_digest: digest(binding),
169
+ });
170
+ }
171
+ view(childId) {
172
+ const row = this.store.db.prepare('SELECT * FROM child_workers WHERE child_id=?').get(childId);
173
+ if (!row)
174
+ throw new ServiceError('worker_binding_missing', 'Child has no worker binding');
175
+ return {
176
+ binding: JSON.parse(String(row.binding)),
177
+ binding_digest: String(row.binding_digest),
178
+ generation: Number(row.generation),
179
+ worker_epoch: row.worker_epoch === null ? null : Number(row.worker_epoch),
180
+ dispatch_state: row.dispatch_state,
181
+ };
182
+ }
183
+ validate(child, route) {
184
+ const view = this.view(child.id), b = view.binding;
185
+ if (b.agent) {
186
+ if (!this.profiles ||
187
+ b.agent.owner_session !== child.parent_session ||
188
+ b.agent.profile_digest !== b.profile_digest ||
189
+ canonical(b.agent.route) !== canonical(b.route))
190
+ throw new ServiceError('worker_binding_changed', 'Specialist admission belongs to another owner or route');
191
+ this.profiles.validate(b.agent);
192
+ }
193
+ const profile = this.store.db
194
+ .prepare('SELECT * FROM worker_profiles WHERE id=? AND revision=?')
195
+ .get(b.profile.id, b.profile.revision);
196
+ if (b.tools.some((t) => ['salaam_files', 'salaam_command'].includes(t)) && !b.isolation)
197
+ throw new ServiceError('worker_binding_changed', 'Coding workers require isolated workspace ownership');
198
+ if (b.isolation) {
199
+ if (!this.assertIsolation)
200
+ throw new ServiceError('worker_binding_changed', 'Execution boundary is unavailable');
201
+ this.assertIsolation(b);
202
+ }
203
+ if (b.assignment) {
204
+ if (!this.assertAssignment)
205
+ throw new ServiceError('worker_binding_changed', 'Assignment owner is unavailable');
206
+ this.assertAssignment(b);
207
+ }
208
+ const task = this.store.db
209
+ .prepare('SELECT id FROM durable_tasks WHERE session_id=?')
210
+ .get(child.parent_session);
211
+ const input = this.store.db
212
+ .prepare('SELECT s.text,a.digest,a.session_id FROM submissions s JOIN artifacts a ON a.id=? WHERE s.attempt_id=?')
213
+ .get(b.input_artifact, child.child_attempt);
214
+ if (b.schema_version !== 1 ||
215
+ digest(b) !== view.binding_digest ||
216
+ b.runtime !== 'native-pi' ||
217
+ b.owner_session !== child.parent_session ||
218
+ b.owner_task !== (task ? String(task.id) : null) ||
219
+ b.parent_attempt !== child.parent_attempt ||
220
+ b.session_id !== child.child_session ||
221
+ b.attempt_id !== child.child_attempt ||
222
+ b.step_key !== child.step_key ||
223
+ b.input_digest !== child.input_digest ||
224
+ !input ||
225
+ input.session_id !== child.child_session ||
226
+ input.digest !== textDigest(String(input.text)) ||
227
+ b.bounds.scope !== 'model-requests' ||
228
+ b.bounds.requests !== child.budget ||
229
+ canonical(b.tools) !== child.authority ||
230
+ b.tools.some((tool) => !b.profile.tools.includes(tool) && !(tool === 'salaam_team' && b.assignment)) ||
231
+ b.policy_digest !== POLICY_DIGEST ||
232
+ canonical(b.route) !== canonical(route) ||
233
+ b.capability_digest !== digest(nativeCapabilities(b.profile, !!b.assignment, b.isolation)) ||
234
+ digest(b.capabilities) !== b.capability_digest ||
235
+ (!b.agent && b.profile_digest !== digest(DIAGNOSTIC_PROFILE)) ||
236
+ digest(b.profile) !== b.profile_digest ||
237
+ !profile ||
238
+ profile.revoked ||
239
+ profile.digest !== b.profile_digest ||
240
+ digest(JSON.parse(String(profile.document))) !== b.profile_digest)
241
+ throw new ServiceError('worker_binding_changed', 'Frozen worker owner, profile, capabilities or configuration changed');
242
+ assertModelRequestBoundary(b.capabilities);
243
+ return view;
244
+ }
245
+ /** Commit dispatch intent before launching a process. It can be claimed only once. */
246
+ claim(child, epoch, route) {
247
+ this.store.transaction(() => {
248
+ const view = this.validate(child, route);
249
+ if (view.dispatch_state !== 'intent' ||
250
+ child.state !== 'accepted' ||
251
+ this.store.cancelled(child.child_attempt))
252
+ throw new ServiceError('worker_already_dispatched', 'Child worker dispatch cannot be replayed');
253
+ this.store.assertEpoch(child.child_session, epoch);
254
+ this.store.db
255
+ .prepare("UPDATE child_workers SET generation=generation+1,worker_epoch=?,dispatch_state='dispatching' WHERE child_id=?")
256
+ .run(epoch, child.id);
257
+ this.store.event(child.parent_session, 'child.worker_dispatching', {
258
+ child_id: child.id,
259
+ worker_id: view.binding.worker_id,
260
+ generation: view.generation + 1,
261
+ worker_epoch: epoch,
262
+ });
263
+ });
264
+ }
265
+ assert(child, epoch, route) {
266
+ const view = this.validate(child, route);
267
+ this.store.assertEpoch(child.child_session, epoch);
268
+ if (view.generation !== 1 ||
269
+ view.worker_epoch !== epoch ||
270
+ !['dispatching', 'accepted'].includes(view.dispatch_state) ||
271
+ child.state !== 'accepted' ||
272
+ this.store.cancelled(child.child_attempt))
273
+ throw new ServiceError('stale_worker', 'Worker generation no longer owns this child');
274
+ return view;
275
+ }
276
+ accepted(child, epoch, route) {
277
+ this.store.transaction(() => {
278
+ const view = this.assert(child, epoch, route);
279
+ if (view.dispatch_state !== 'dispatching')
280
+ throw new ServiceError('worker_already_dispatched', 'Duplicate worker acceptance');
281
+ this.store.db
282
+ .prepare("UPDATE child_workers SET dispatch_state='accepted' WHERE child_id=?")
283
+ .run(child.id);
284
+ this.store.event(child.parent_session, 'child.worker_accepted', {
285
+ child_id: child.id,
286
+ worker_id: view.binding.worker_id,
287
+ generation: view.generation,
288
+ worker_epoch: epoch,
289
+ });
290
+ });
291
+ }
292
+ /** Called with the terminal child result in the same transaction. */
293
+ finish(child, interrupted) {
294
+ this.store.db
295
+ .prepare('UPDATE child_workers SET dispatch_state=? WHERE child_id=?')
296
+ .run(interrupted ? 'interrupted' : 'terminal', child.id);
297
+ }
298
+ }