@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/tui-view.ts CHANGED
@@ -29,7 +29,11 @@ export function modelLabel(client: OperatorClient): string {
29
29
  const route = client.health?.provider_route;
30
30
  if (!route) return 'Connect your workspace';
31
31
  if (['offline', 'salaam-proof'].includes(route.provider)) return 'Offline demo';
32
- return route.model.replace('claude-sonnet-', 'Claude Sonnet ').replace(/(\d)-(\d)$/, '$1.$2');
32
+ const warning =
33
+ client.health?.provider_capabilities?.output_token_cap === false ? ' · no output cap' : '';
34
+ return (
35
+ route.model.replace('claude-sonnet-', 'Claude Sonnet ').replace(/(\d)-(\d)$/, '$1.$2') + warning
36
+ );
33
37
  }
34
38
  export class Dynamic implements Component {
35
39
  constructor(private readonly content: (width: number) => string[]) {}
@@ -507,6 +511,107 @@ export class DocumentView implements Component {
507
511
  ...this.text(job.reason, width),
508
512
  );
509
513
  }
514
+ const team = c.snapshot.team as
515
+ | {
516
+ assignments: {
517
+ id: string;
518
+ profile_id: string;
519
+ generation: number;
520
+ step_state: string;
521
+ stale: boolean;
522
+ runtime?: { id: string; provider_route?: { provider: string; model: string } } | null;
523
+ execution?: {
524
+ state: string;
525
+ stopped: boolean;
526
+ stop_scope?: string;
527
+ peer?: {
528
+ peer_id: string;
529
+ task_id: string | null;
530
+ cancel_state: string | null;
531
+ remote_state: string | null;
532
+ } | null;
533
+ } | null;
534
+ }[];
535
+ messages: {
536
+ id: string;
537
+ sender: string;
538
+ recipient: string;
539
+ delivered_at: number | null;
540
+ acknowledged_at: number | null;
541
+ }[];
542
+ }
543
+ | undefined;
544
+ if (team?.assignments.length) {
545
+ lines.push(...this.title('TEAM BOARD', width));
546
+ for (const a of team.assignments) {
547
+ lines.push(
548
+ ...this.text(
549
+ `${shortId(a.id)} · ${a.profile_id} · claim ${a.generation} · ${a.step_state}${a.stale ? ' · stale plan' : ''}`,
550
+ width,
551
+ ),
552
+ );
553
+ if (a.runtime)
554
+ lines.push(
555
+ ...this.text(a.runtime.id + ' · invocation budget · model usage unknown', width),
556
+ );
557
+ if (a.execution?.peer)
558
+ lines.push(
559
+ ...this.text(
560
+ 'Peer ' +
561
+ a.execution.peer.peer_id +
562
+ ' · task ' +
563
+ (a.execution.peer.task_id ?? 'acceptance unknown'),
564
+ width,
565
+ ),
566
+ ...this.text(
567
+ (a.execution.peer.remote_state ?? 'remote state unknown') +
568
+ ' · ' +
569
+ (a.execution.peer.cancel_state ?? 'no cancel requested'),
570
+ width,
571
+ ),
572
+ );
573
+ if (a.runtime?.provider_route)
574
+ lines.push(
575
+ ...this.text(
576
+ 'Backend ' +
577
+ a.runtime.provider_route.provider +
578
+ '/' +
579
+ a.runtime.provider_route.model,
580
+ width,
581
+ ),
582
+ );
583
+ if (a.execution)
584
+ lines.push(
585
+ ...this.text(
586
+ a.execution.state +
587
+ ' · ' +
588
+ (a.execution.stop_scope === 'never-dispatched'
589
+ ? 'not dispatched'
590
+ : a.execution.stop_scope === 'remote-terminal-receipt'
591
+ ? a.execution.stopped
592
+ ? 'remote terminal receipt'
593
+ : 'remote stop unconfirmed'
594
+ : a.execution.stopped
595
+ ? 'stop confirmed'
596
+ : 'stop unconfirmed'),
597
+ width,
598
+ ),
599
+ );
600
+ }
601
+ lines.push(
602
+ ...this.text(
603
+ 'Messages are passive. Starting or continuing work requires a separate grant.',
604
+ width,
605
+ ),
606
+ );
607
+ for (const m of team.messages)
608
+ lines.push(
609
+ ...this.text(
610
+ `${shortId(m.id)} · ${m.sender === 'operator' ? 'operator' : shortId(m.sender)} → ${shortId(m.recipient)} · ${m.acknowledged_at ? 'acknowledged' : m.delivered_at ? 'delivered' : 'stored'}`,
611
+ width,
612
+ ),
613
+ );
614
+ }
510
615
  const task = c.snapshot.work_task;
511
616
  if (task) {
512
617
  lines.push(
@@ -530,12 +635,17 @@ export class DocumentView implements Component {
530
635
  width,
531
636
  ),
532
637
  ...this.text(
533
- 'Declared token units ' +
534
- task.runtime.token_units +
535
- ' + ' +
536
- task.runtime.reserved_tokens +
537
- ' reserved / ' +
538
- task.limits.token_units,
638
+ task.limits.token_units === null
639
+ ? 'Token budget disabled · ' +
640
+ (task.limits.output_token_bound === null
641
+ ? 'no task output-token bound'
642
+ : 'output bound ' + task.limits.output_token_bound)
643
+ : 'Declared token units ' +
644
+ task.runtime.token_units +
645
+ ' + ' +
646
+ task.runtime.reserved_tokens +
647
+ ' reserved / ' +
648
+ task.limits.token_units,
539
649
  width,
540
650
  ),
541
651
  ...this.text(
@@ -547,11 +657,34 @@ export class DocumentView implements Component {
547
657
  width,
548
658
  ),
549
659
  );
550
- if (task.verification)
551
- lines.push(
552
- ...this.title('VERIFICATION', width),
553
- ...this.text(JSON.stringify(task.verification, null, 2), width),
554
- );
660
+ if (task.verification) {
661
+ const verification = task.verification as {
662
+ checks?: {
663
+ id: string;
664
+ check_key: string;
665
+ state: string;
666
+ fresh: boolean;
667
+ workspace_revision: number;
668
+ }[];
669
+ completion?: { state: string; fresh: boolean } | null;
670
+ };
671
+ lines.push(...this.title('VERIFICATION', width));
672
+ for (const check of verification.checks ?? [])
673
+ lines.push(
674
+ ...this.text(
675
+ `${check.check_key} · ${check.state} · ${check.fresh ? 'current' : 'historical'} · r${check.workspace_revision} · ${shortId(check.id)}`,
676
+ width,
677
+ ),
678
+ );
679
+ if (verification.completion)
680
+ lines.push(
681
+ ...this.text(
682
+ `Task ${verification.completion.state} · ${verification.completion.fresh ? 'current verification' : 'verification needs refresh'}`,
683
+ width,
684
+ ),
685
+ );
686
+ lines.push(...this.text('Ctrl+K → Inspect task for complete check receipts.', width));
687
+ }
555
688
  for (const constraint of task.constraints)
556
689
  lines.push(...this.text('Constraint: ' + constraint, width));
557
690
  for (const step of task.steps)
@@ -585,6 +718,13 @@ export class DocumentView implements Component {
585
718
  t.fill('', width),
586
719
  );
587
720
  lines.push(...this.title('CHILD WORK', width));
721
+ for (const agent of c.snapshot.agents ?? [])
722
+ lines.push(
723
+ ...this.text(
724
+ `${agent.profile_id} · revision ${agent.revision} · ${agent.state} · ${agent.route.provider}/${agent.route.model} · ${agent.tools.join(', ')}`,
725
+ width,
726
+ ),
727
+ );
588
728
  if (!c.snapshot.children.length)
589
729
  lines.push(...this.text('No child work in this session.', width));
590
730
  for (const child of c.snapshot.children)
@@ -598,6 +738,18 @@ export class DocumentView implements Component {
598
738
  (child.acknowledged ? ' · acknowledged' : ''),
599
739
  width,
600
740
  ),
741
+ ...(child.worker
742
+ ? this.text(
743
+ `${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'}`,
744
+ width,
745
+ )
746
+ : []),
747
+ ...(child.worker?.binding.isolation
748
+ ? this.text(
749
+ `Private draft ${shortId(child.worker.binding.isolation.workspace_id)} · execution ${child.worker.binding.isolation.execution.evidence} · root integration requires a reviewed proposal digest`,
750
+ width,
751
+ )
752
+ : []),
601
753
  );
602
754
  lines.push(t.fill('', width), ...this.title('USAGE', width));
603
755
  if (c.usage)
package/src/tui.ts CHANGED
@@ -410,7 +410,10 @@ export class SalaamApp {
410
410
  private async startService(): Promise<void> {
411
411
  await this.client.list();
412
412
  if (this.client.connection === 'connected') return;
413
- const provider = existsSync(credentialPath()) ? 'anthropic' : 'offline';
413
+ const provider =
414
+ existsSync(credentialPath()) || existsSync(credentialPath() + '.json')
415
+ ? 'anthropic'
416
+ : 'offline';
414
417
  await promisify(execFile)(
415
418
  process.execPath,
416
419
  [
@@ -826,7 +829,7 @@ export class SalaamApp {
826
829
  const max_steps = Number(value);
827
830
  this.confirm(
828
831
  'Run this bounded plan',
829
- `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.`,
832
+ `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.`,
830
833
  'Run bounded task',
831
834
  () => this.client.taskAction('auto', { max_steps, compact: true }, reviewed),
832
835
  );
package/src/usage.ts CHANGED
@@ -115,6 +115,23 @@ export class UsageLedger {
115
115
  typeof m?.responseId === 'string' && m.responseId.startsWith('salaam-request:')
116
116
  ? m.responseId
117
117
  : `pi:${session}:${entry.id}`;
118
+ // Managed live streams report receipts through the authenticated bridge.
119
+ // Pi normalizes a missing receipt to zeros; importing that transcript must
120
+ // never upgrade a conservatively unknown observation to a zero charge.
121
+ if (key.startsWith('salaam-request:')) {
122
+ const managed = this.store.db
123
+ .prepare('SELECT session_id,provider FROM usage_observations WHERE source_key=?')
124
+ .get(key);
125
+ if (managed && managed.provider !== 'salaam-proof') {
126
+ if (managed.session_id !== session)
127
+ throw new ServiceError('invalid_usage', 'Usage does not belong to this session');
128
+ continue;
129
+ }
130
+ // A rejected live request can still produce a Pi error transcript. Its
131
+ // absence from admission is definitive: no request was dispatched.
132
+ const route = this.store.providerRoute(session);
133
+ if (!managed && route && route.provider !== 'salaam-proof') continue;
134
+ }
118
135
  // An aborted provider message may carry placeholder zeros. They are not evidence of zero spend.
119
136
  const complete = m && !['error', 'aborted'].includes(String(m.stopReason));
120
137
  if ((complete && m.usage) || entry.usage)
@@ -0,0 +1,395 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { Store } from './store.js';
3
+ import { canonical, digest, textDigest, ServiceError, type ChildTask } from './protocol.js';
4
+ import { POLICY_DIGEST } from './config.js';
5
+ import type { ProviderRoute } from './provider-config.js';
6
+ import type { AgentProfile, AgentGrant, AgentProfiles } from './agent-profiles.js';
7
+ import type { ExternalRuntime } from './external-runtime.js';
8
+
9
+ /** Local contract v1. These are enforced capabilities, not upstream feature claims. */
10
+ export const NATIVE_CAPABILITIES = {
11
+ schema_version: 1,
12
+ runtime: 'native-pi',
13
+ model_requests: true,
14
+ output_cap: 'authentication-dependent',
15
+ usage_receipts: true,
16
+ resume_dispatch: false,
17
+ confirmed_stop: false,
18
+ files: false,
19
+ commands: false,
20
+ isolation: 'managed-process-no-coding-authority',
21
+ retention: 'durable-local',
22
+ } as const;
23
+ export const DIAGNOSTIC_PROFILE = {
24
+ schema_version: 1,
25
+ id: 'salaam/diagnostic',
26
+ revision: 1,
27
+ tools: ['salaam_probe'],
28
+ max_depth: 1,
29
+ } as const;
30
+ export function nativeCapabilities(
31
+ profile: AgentProfile,
32
+ coordination = false,
33
+ isolation?: WorkerBinding['isolation'],
34
+ ) {
35
+ const caps = profile.role
36
+ ? {
37
+ ...NATIVE_CAPABILITIES,
38
+ repository_read: profile.tools.includes('salaam_repository'),
39
+ findings: profile.tools.includes('salaam_findings'),
40
+ }
41
+ : NATIVE_CAPABILITIES;
42
+ return {
43
+ ...caps,
44
+ ...(coordination ? { team_messages: true } : {}),
45
+ ...(isolation
46
+ ? {
47
+ files: profile.tools.includes('salaam_files'),
48
+ commands: profile.tools.includes('salaam_command'),
49
+ isolation:
50
+ isolation.execution.evidence === 'fixture'
51
+ ? 'simulated-owned-copy'
52
+ : 'owned-copy-and-proven-executor',
53
+ execution_profile: isolation.execution.profile_digest,
54
+ execution_engine: isolation.execution.engine_id,
55
+ }
56
+ : {}),
57
+ };
58
+ }
59
+
60
+ export function externalCapabilities(runtime: ExternalRuntime, tools: readonly string[]) {
61
+ return {
62
+ schema_version: 1 as const,
63
+ runtime: runtime.kind,
64
+ model_requests: false as const,
65
+ output_cap: runtime.output_cap,
66
+ files: runtime.files && tools.includes('salaam_files'),
67
+ commands: runtime.commands && tools.includes('salaam_command'),
68
+ model_usage: 'unknown' as const,
69
+ isolation:
70
+ runtime.kind === 'a2a'
71
+ ? 'remote-peer-no-local-tool-authority'
72
+ : runtime.evidence === 'fixture'
73
+ ? 'simulated-whole-runtime'
74
+ : 'proven-whole-runtime',
75
+ };
76
+ }
77
+ export interface WorkerBinding {
78
+ schema_version: 1;
79
+ worker_id: string;
80
+ runtime: 'native-pi' | 'harness' | 'a2a';
81
+ owner_session: string;
82
+ owner_task: string | null;
83
+ parent_attempt: string;
84
+ step_key: string;
85
+ session_id: string;
86
+ attempt_id: string;
87
+ input_digest: string;
88
+ input_artifact: string | null;
89
+ profile: AgentProfile;
90
+ profile_digest: string;
91
+ capabilities: ReturnType<typeof nativeCapabilities> | ReturnType<typeof externalCapabilities>;
92
+ capability_digest: string;
93
+ tools: string[];
94
+ route: ProviderRoute | null;
95
+ workspace: { id: string; revision: number; digest: string } | null;
96
+ policy_digest: string;
97
+ bounds: { scope: 'model-requests'; requests: number } | { scope: 'invocations'; invocations: 1 };
98
+ external?: ExternalRuntime;
99
+ legacy: boolean;
100
+ agent?: AgentGrant;
101
+ repository?: { artifact_id: string; base_artifact: string; digest: string };
102
+ assignment?: { id: string; generation: number };
103
+ isolation?: {
104
+ workspace_id: string;
105
+ baseline_artifact: string;
106
+ digest: string;
107
+ execution: { profile_digest: string; engine_id: string; evidence: 'proven' | 'fixture' };
108
+ };
109
+ }
110
+ export interface WorkerBoundaryView {
111
+ binding: WorkerBinding;
112
+ binding_digest: string;
113
+ generation: number;
114
+ worker_epoch: number | null;
115
+ dispatch_state: 'intent' | 'dispatching' | 'accepted' | 'interrupted' | 'terminal';
116
+ }
117
+
118
+ /** Never equate one opaque invocation with one model request. Invocation admission is separate. */
119
+ export function assertModelRequestBoundary(capabilities: {
120
+ schema_version: number;
121
+ model_requests: boolean;
122
+ }): void {
123
+ if (capabilities.schema_version !== 1)
124
+ throw new ServiceError('worker_capability_version', 'Unsupported worker capability version');
125
+ if (capabilities.model_requests !== true)
126
+ throw new ServiceError(
127
+ 'worker_budget_scope',
128
+ 'Model-request bounds require enforced admission; opaque workers need explicit invocation bounds',
129
+ );
130
+ }
131
+
132
+ export class WorkerBoundary {
133
+ profiles?: AgentProfiles;
134
+ assertAssignment?: (binding: WorkerBinding) => void;
135
+ assertIsolation?: (binding: WorkerBinding) => void;
136
+ constructor(private store: Store) {}
137
+ /** Called within the schema migration transaction. Cached responses and child rows stay intact. */
138
+ migrate(): void {
139
+ this.store.db.exec(`
140
+ 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));
141
+ 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);
142
+ CREATE UNIQUE INDEX child_worker_identity ON child_workers(json_extract(binding,'$.worker_id'));
143
+ `);
144
+ for (const child of this.store.db
145
+ .prepare('SELECT * FROM child_tasks')
146
+ .all() as unknown as ChildTask[]) {
147
+ this.freeze(child, null, true);
148
+ this.store.db
149
+ .prepare('UPDATE child_workers SET dispatch_state=? WHERE child_id=?')
150
+ .run(child.state === 'accepted' ? 'interrupted' : 'terminal', child.id);
151
+ }
152
+ }
153
+ /** Caller owns the transaction that creates the child, its reservation and input artifact. */
154
+ freeze(
155
+ child: ChildTask,
156
+ inputArtifact: string | null,
157
+ legacy = false,
158
+ agent?: AgentGrant,
159
+ repository?: WorkerBinding['repository'],
160
+ assignment?: WorkerBinding['assignment'],
161
+ isolation?: WorkerBinding['isolation'],
162
+ external?: ExternalRuntime,
163
+ ): void {
164
+ const admittedProfile = agent?.profile ?? DIAGNOSTIC_PROFILE;
165
+ const profileDigest = digest(admittedProfile);
166
+ this.store.db
167
+ .prepare(
168
+ 'INSERT OR IGNORE INTO worker_profiles(id,revision,digest,document) VALUES (?,?,?,?)',
169
+ )
170
+ .run(admittedProfile.id, admittedProfile.revision, profileDigest, canonical(admittedProfile));
171
+ const profile = this.store.db
172
+ .prepare('SELECT digest,revoked FROM worker_profiles WHERE id=? AND revision=?')
173
+ .get(admittedProfile.id, admittedProfile.revision);
174
+ if (profile?.revoked || profile?.digest !== profileDigest)
175
+ throw new ServiceError(
176
+ 'worker_profile_revoked',
177
+ 'Diagnostic worker profile is revoked or changed',
178
+ );
179
+ const task = this.store.db
180
+ .prepare('SELECT id FROM durable_tasks WHERE session_id=?')
181
+ .get(child.parent_session);
182
+ const workspace = this.store.db
183
+ .prepare('SELECT id,revision,digest FROM workspaces WHERE session_id=?')
184
+ .get(child.parent_session);
185
+ const binding: WorkerBinding = {
186
+ schema_version: 1,
187
+ worker_id: randomUUID(),
188
+ runtime: external?.kind ?? 'native-pi',
189
+ owner_session: child.parent_session,
190
+ owner_task: task ? String(task.id) : null,
191
+ parent_attempt: child.parent_attempt,
192
+ step_key: child.step_key,
193
+ session_id: child.child_session,
194
+ attempt_id: child.child_attempt,
195
+ input_digest: child.input_digest,
196
+ input_artifact: inputArtifact,
197
+ profile: admittedProfile,
198
+ profile_digest: profileDigest,
199
+ capabilities: nativeCapabilities(admittedProfile, !!assignment, isolation),
200
+ capability_digest: digest(nativeCapabilities(admittedProfile, !!assignment, isolation)),
201
+ tools: JSON.parse(child.authority),
202
+ route: external
203
+ ? null
204
+ : (agent?.route ??
205
+ this.store.providerRoute(child.child_session) ??
206
+ this.store.providerRoute(child.parent_session)),
207
+ workspace: workspace
208
+ ? {
209
+ id: String(workspace.id),
210
+ revision: Number(workspace.revision),
211
+ digest: String(workspace.digest),
212
+ }
213
+ : null,
214
+ policy_digest: POLICY_DIGEST,
215
+ bounds: external
216
+ ? { scope: 'invocations', invocations: 1 }
217
+ : { scope: 'model-requests', requests: child.budget },
218
+ ...(external ? { external } : {}),
219
+ legacy,
220
+ ...(agent ? { agent } : {}),
221
+ ...(repository ? { repository } : {}),
222
+ ...(assignment ? { assignment } : {}),
223
+ ...(isolation ? { isolation } : {}),
224
+ };
225
+ if (external) {
226
+ binding.capabilities = externalCapabilities(external, binding.tools);
227
+ binding.capability_digest = digest(binding.capabilities);
228
+ } else assertModelRequestBoundary(binding.capabilities);
229
+ this.store.db
230
+ .prepare(
231
+ "INSERT INTO child_workers(child_id,binding,binding_digest,dispatch_state) VALUES (?,?,?,'intent')",
232
+ )
233
+ .run(child.id, canonical(binding), digest(binding));
234
+ // Migration has no event publication; the child itself remains the legacy receipt.
235
+ if (!legacy)
236
+ this.store.event(child.parent_session, 'child.worker_intent', {
237
+ child_id: child.id,
238
+ worker_id: binding.worker_id,
239
+ binding_digest: digest(binding),
240
+ });
241
+ }
242
+ view(childId: string): WorkerBoundaryView {
243
+ const row = this.store.db.prepare('SELECT * FROM child_workers WHERE child_id=?').get(childId);
244
+ if (!row) throw new ServiceError('worker_binding_missing', 'Child has no worker binding');
245
+ return {
246
+ binding: JSON.parse(String(row.binding)),
247
+ binding_digest: String(row.binding_digest),
248
+ generation: Number(row.generation),
249
+ worker_epoch: row.worker_epoch === null ? null : Number(row.worker_epoch),
250
+ dispatch_state: row.dispatch_state as WorkerBoundaryView['dispatch_state'],
251
+ };
252
+ }
253
+ private validate(child: ChildTask, route: ProviderRoute): WorkerBoundaryView {
254
+ const view = this.view(child.id),
255
+ b = view.binding;
256
+ if (b.agent) {
257
+ if (
258
+ !this.profiles ||
259
+ b.agent.owner_session !== child.parent_session ||
260
+ b.agent.profile_digest !== b.profile_digest ||
261
+ canonical(b.agent.route) !== canonical(b.route)
262
+ )
263
+ throw new ServiceError(
264
+ 'worker_binding_changed',
265
+ 'Specialist admission belongs to another owner or route',
266
+ );
267
+ this.profiles.validate(b.agent);
268
+ }
269
+ const profile = this.store.db
270
+ .prepare('SELECT * FROM worker_profiles WHERE id=? AND revision=?')
271
+ .get(b.profile.id, b.profile.revision);
272
+ if (b.tools.some((t) => ['salaam_files', 'salaam_command'].includes(t)) && !b.isolation)
273
+ throw new ServiceError(
274
+ 'worker_binding_changed',
275
+ 'Coding workers require isolated workspace ownership',
276
+ );
277
+ if (b.isolation) {
278
+ if (!this.assertIsolation)
279
+ throw new ServiceError('worker_binding_changed', 'Execution boundary is unavailable');
280
+ this.assertIsolation(b);
281
+ }
282
+ if (b.assignment) {
283
+ if (!this.assertAssignment)
284
+ throw new ServiceError('worker_binding_changed', 'Assignment owner is unavailable');
285
+ this.assertAssignment(b);
286
+ }
287
+ const task = this.store.db
288
+ .prepare('SELECT id FROM durable_tasks WHERE session_id=?')
289
+ .get(child.parent_session);
290
+ const input = this.store.db
291
+ .prepare(
292
+ 'SELECT s.text,a.digest,a.session_id FROM submissions s JOIN artifacts a ON a.id=? WHERE s.attempt_id=?',
293
+ )
294
+ .get(b.input_artifact, child.child_attempt);
295
+ if (
296
+ b.schema_version !== 1 ||
297
+ digest(b) !== view.binding_digest ||
298
+ b.runtime !== 'native-pi' ||
299
+ b.owner_session !== child.parent_session ||
300
+ b.owner_task !== (task ? String(task.id) : null) ||
301
+ b.parent_attempt !== child.parent_attempt ||
302
+ b.session_id !== child.child_session ||
303
+ b.attempt_id !== child.child_attempt ||
304
+ b.step_key !== child.step_key ||
305
+ b.input_digest !== child.input_digest ||
306
+ !input ||
307
+ input.session_id !== child.child_session ||
308
+ input.digest !== textDigest(String(input.text)) ||
309
+ b.bounds.scope !== 'model-requests' ||
310
+ b.bounds.requests !== child.budget ||
311
+ canonical(b.tools) !== child.authority ||
312
+ b.tools.some(
313
+ (tool) => !b.profile.tools.includes(tool) && !(tool === 'salaam_team' && b.assignment),
314
+ ) ||
315
+ b.policy_digest !== POLICY_DIGEST ||
316
+ canonical(b.route) !== canonical(route) ||
317
+ b.capability_digest !== digest(nativeCapabilities(b.profile, !!b.assignment, b.isolation)) ||
318
+ digest(b.capabilities) !== b.capability_digest ||
319
+ (!b.agent && b.profile_digest !== digest(DIAGNOSTIC_PROFILE)) ||
320
+ digest(b.profile) !== b.profile_digest ||
321
+ !profile ||
322
+ profile.revoked ||
323
+ profile.digest !== b.profile_digest ||
324
+ digest(JSON.parse(String(profile.document))) !== b.profile_digest
325
+ )
326
+ throw new ServiceError(
327
+ 'worker_binding_changed',
328
+ 'Frozen worker owner, profile, capabilities or configuration changed',
329
+ );
330
+ assertModelRequestBoundary(b.capabilities);
331
+ return view;
332
+ }
333
+ /** Commit dispatch intent before launching a process. It can be claimed only once. */
334
+ claim(child: ChildTask, epoch: number, route: ProviderRoute): void {
335
+ this.store.transaction(() => {
336
+ const view = this.validate(child, route);
337
+ if (
338
+ view.dispatch_state !== 'intent' ||
339
+ child.state !== 'accepted' ||
340
+ this.store.cancelled(child.child_attempt)
341
+ )
342
+ throw new ServiceError(
343
+ 'worker_already_dispatched',
344
+ 'Child worker dispatch cannot be replayed',
345
+ );
346
+ this.store.assertEpoch(child.child_session, epoch);
347
+ this.store.db
348
+ .prepare(
349
+ "UPDATE child_workers SET generation=generation+1,worker_epoch=?,dispatch_state='dispatching' WHERE child_id=?",
350
+ )
351
+ .run(epoch, child.id);
352
+ this.store.event(child.parent_session, 'child.worker_dispatching', {
353
+ child_id: child.id,
354
+ worker_id: view.binding.worker_id,
355
+ generation: view.generation + 1,
356
+ worker_epoch: epoch,
357
+ });
358
+ });
359
+ }
360
+ assert(child: ChildTask, epoch: number, route: ProviderRoute): WorkerBoundaryView {
361
+ const view = this.validate(child, route);
362
+ this.store.assertEpoch(child.child_session, epoch);
363
+ if (
364
+ view.generation !== 1 ||
365
+ view.worker_epoch !== epoch ||
366
+ !['dispatching', 'accepted'].includes(view.dispatch_state) ||
367
+ child.state !== 'accepted' ||
368
+ this.store.cancelled(child.child_attempt)
369
+ )
370
+ throw new ServiceError('stale_worker', 'Worker generation no longer owns this child');
371
+ return view;
372
+ }
373
+ accepted(child: ChildTask, epoch: number, route: ProviderRoute): void {
374
+ this.store.transaction(() => {
375
+ const view = this.assert(child, epoch, route);
376
+ if (view.dispatch_state !== 'dispatching')
377
+ throw new ServiceError('worker_already_dispatched', 'Duplicate worker acceptance');
378
+ this.store.db
379
+ .prepare("UPDATE child_workers SET dispatch_state='accepted' WHERE child_id=?")
380
+ .run(child.id);
381
+ this.store.event(child.parent_session, 'child.worker_accepted', {
382
+ child_id: child.id,
383
+ worker_id: view.binding.worker_id,
384
+ generation: view.generation,
385
+ worker_epoch: epoch,
386
+ });
387
+ });
388
+ }
389
+ /** Called with the terminal child result in the same transaction. */
390
+ finish(child: ChildTask, interrupted: boolean): void {
391
+ this.store.db
392
+ .prepare('UPDATE child_workers SET dispatch_state=? WHERE child_id=?')
393
+ .run(interrupted ? 'interrupted' : 'terminal', child.id);
394
+ }
395
+ }