@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/service.ts CHANGED
@@ -14,7 +14,7 @@ import { Store } from './store.js';
14
14
  import { acquireStateLock } from './state-lock.js';
15
15
  import { Artifacts } from './artifacts.js';
16
16
  import { Supervisor, type SupervisorOptions } from './supervisor.js';
17
- import { canonical, integer, object, ServiceError, string } from './protocol.js';
17
+ import { canonical, digest, integer, object, ServiceError, string } from './protocol.js';
18
18
  import { readJson } from './transport.js';
19
19
  import { Dispatcher } from './dispatcher.js';
20
20
  import { ManagedWork } from './managed.js';
@@ -25,8 +25,28 @@ import { engineRequest } from './execution-boundary.js';
25
25
  import { DockerCodingExecutor, type ExecutionBackend } from './coding-executor.js';
26
26
  import { Workspaces } from './workspaces.js';
27
27
  import { CodingWork } from './coding.js';
28
+ import { AgentProfiles } from './agent-profiles.js';
29
+ import { RepositorySnapshots } from './repository-snapshots.js';
30
+ import { TeamWork } from './team-work.js';
31
+ import { WorkerWorkspaces } from './worker-workspaces.js';
32
+ import { ExternalWork } from './external-work.js';
33
+ import type { ExternalBackend } from './external-runtime.js';
34
+ import { DockerHarnessBackend } from './harness-backend.js';
35
+ import { A2AInbound } from './a2a-inbound.js';
36
+ import { A2ABackend, ExternalBackends } from './a2a-backend.js';
28
37
 
29
38
  export interface ServiceOptions extends Omit<SupervisorOptions, 'stateDir' | 'bridgeSocket'> {
39
+ externalBackend?: ExternalBackend;
40
+ harnessConfig?: string;
41
+ a2aConfig?: string;
42
+ a2aListenConfig?: string;
43
+ /** Trusted host admission, installed before any startup/recovery dispatch; throws deny a request. */
44
+ admitModelRequest?: (
45
+ session: string,
46
+ attempt: string | undefined,
47
+ input: number,
48
+ output: number,
49
+ ) => void;
30
50
  stateDir: string;
31
51
  effectConnector?: EffectConnector;
32
52
  approvalTtlMs?: number;
@@ -36,6 +56,8 @@ export interface ServiceOptions extends Omit<SupervisorOptions, 'stateDir' | 'br
36
56
  expectedExecution?: { profile_digest: string; engine_id: string };
37
57
  }
38
58
  export class SalaamService {
59
+ readonly external: ExternalWork;
60
+ a2aInbound?: A2AInbound;
39
61
  readonly stateDir: string;
40
62
  readonly socket: string;
41
63
  readonly bridgeSocket: string;
@@ -54,6 +76,10 @@ export class SalaamService {
54
76
  readonly dispatcher: Dispatcher;
55
77
  readonly managed: ManagedWork;
56
78
  readonly children: ChildWork;
79
+ readonly agents: AgentProfiles;
80
+ readonly repositories: RepositorySnapshots;
81
+ readonly team: TeamWork;
82
+ readonly workerWorkspaces: WorkerWorkspaces;
57
83
  readonly workspaces: Workspaces;
58
84
  readonly coding: CodingWork;
59
85
  readonly codingSocket: string | null;
@@ -82,6 +108,13 @@ export class SalaamService {
82
108
  this.tasks = new TaskContext(this.store, this.artifacts);
83
109
  this.taskWork = new TaskWork(this.store, this.tasks);
84
110
  this.taskBudget = new TaskBudget(this.store, this.taskWork);
111
+ if (options.admitModelRequest) {
112
+ const taskAdmission = this.store.taskModelAdmission!;
113
+ this.store.taskModelAdmission = (session, attempt, input, output) => {
114
+ taskAdmission(session, attempt, input, output);
115
+ options.admitModelRequest!(session, attempt, input, output);
116
+ };
117
+ }
85
118
  this.workspaces = new Workspaces(this.store, this.artifacts, options.codingRoot);
86
119
  if (options.codingSocket && !options.codingRoot)
87
120
  throw new Error('Command execution requires an explicit --coding-root');
@@ -90,6 +123,35 @@ export class SalaamService {
90
123
  (options.codingSocket
91
124
  ? new DockerCodingExecutor(engineRequest(options.codingSocket))
92
125
  : undefined);
126
+ if (
127
+ options.harnessConfig &&
128
+ (options.externalBackend ||
129
+ !(executor instanceof DockerCodingExecutor) ||
130
+ !options.codingSocket ||
131
+ !options.codingRoot)
132
+ )
133
+ throw Error(
134
+ 'Harness configuration requires the real configured Docker executor and an exclusive backend',
135
+ );
136
+ if (options.a2aConfig && (options.externalBackend || !options.codingRoot || !executor))
137
+ throw Error(
138
+ 'A2A configuration requires an owned coding root/executor and an exclusive backend',
139
+ );
140
+ const externalBackend =
141
+ options.externalBackend ??
142
+ new ExternalBackends([
143
+ ...(options.harnessConfig
144
+ ? [
145
+ new DockerHarnessBackend(
146
+ resolve(options.harnessConfig),
147
+ join(resolve(options.codingRoot!), 'external'),
148
+ options.codingSocket!,
149
+ executor as DockerCodingExecutor,
150
+ ),
151
+ ]
152
+ : []),
153
+ ...(options.a2aConfig ? [new A2ABackend(resolve(options.a2aConfig))] : []),
154
+ ]);
93
155
  this.coding = new CodingWork(
94
156
  this.store,
95
157
  this.workspaces,
@@ -119,18 +181,92 @@ export class SalaamService {
119
181
  effectsEnabled: !!options.effectConnector,
120
182
  });
121
183
  this.supervisor.tasks = this.tasks;
184
+ this.taskWork.providerCheck = (limits) =>
185
+ this.supervisor.assertTaskProviderSupported(limits);
122
186
  this.supervisor.coding = this.coding;
123
187
  this.managed = new ManagedWork(this.store, {
124
188
  connector: options.effectConnector,
125
189
  approvalTtlMs: options.approvalTtlMs,
126
- assertActive: (id, epoch, attempt) => this.supervisor.assertActive(id, epoch, attempt),
190
+ assertActive: (id, epoch, attempt) =>
191
+ this.external?.owns(id)
192
+ ? this.external.assertActive(id, epoch, attempt)
193
+ : this.supervisor.assertActive(id, epoch, attempt),
127
194
  reply: (id, epoch, request, confirmed) =>
128
195
  this.supervisor.replyUI(id, epoch, request, confirmed),
129
- failed: (id, error) => this.supervisor.failControl(id, error),
196
+ failed: (id, error) =>
197
+ this.external?.owns(id)
198
+ ? this.external.failControl(id, error)
199
+ : this.supervisor.failControl(id, error),
130
200
  });
131
201
  this.supervisor.managed = this.managed;
132
- this.children = new ChildWork(this.store, this.artifacts, this.supervisor);
202
+ this.agents = new AgentProfiles(
203
+ this.store,
204
+ this.supervisor.route,
205
+ async (route, session) => {
206
+ const prepared = await this.supervisor.prepareProviderRoute(route);
207
+ const task = this.taskWork.forSession(session);
208
+ if (task) this.supervisor.assertTaskProviderSupported(task.limits, prepared.output_cap);
209
+ return { route: prepared.route, output_cap: prepared.output_cap };
210
+ },
211
+ );
212
+ this.store.workerBoundary.profiles = this.agents;
213
+ this.agents.validateFrame = (session) => {
214
+ this.tasks.frame(session, 'x'.repeat(32));
215
+ };
216
+ this.repositories = new RepositorySnapshots(this.store, this.artifacts, this.workspaces);
217
+ this.supervisor.repositories = this.repositories;
218
+ this.children = new ChildWork(
219
+ this.store,
220
+ this.artifacts,
221
+ this.supervisor,
222
+ this.repositories,
223
+ );
133
224
  this.supervisor.children = this.children;
225
+ this.workerWorkspaces = new WorkerWorkspaces(
226
+ this.store,
227
+ this.workspaces,
228
+ this.repositories,
229
+ this.artifacts,
230
+ this.coding,
231
+ this.children,
232
+ );
233
+ this.children.workspaces = this.workerWorkspaces;
234
+ this.agents.assertExecution = () => {
235
+ this.workerWorkspaces.execution();
236
+ };
237
+ this.store.workerBoundary.assertIsolation = (binding) =>
238
+ this.workerWorkspaces.assert(binding);
239
+ this.taskWork.assertStepReview = (task, step) =>
240
+ this.workerWorkspaces.assertStepReview(task, step);
241
+ this.team = new TeamWork(
242
+ this.store,
243
+ this.taskWork,
244
+ this.taskBudget,
245
+ this.children,
246
+ this.agents,
247
+ );
248
+ this.supervisor.team = this.team;
249
+ this.external = new ExternalWork(
250
+ this.store,
251
+ this.artifacts,
252
+ this.children,
253
+ this.taskBudget,
254
+ this.managed,
255
+ this.workspaces,
256
+ externalBackend,
257
+ options.admitModelRequest,
258
+ );
259
+ this.children.external = this.external;
260
+ const guardSubmission = this.store.guardTaskSubmission!;
261
+ this.store.guardTaskSubmission = (session, step) => {
262
+ const child = this.children.owned(session);
263
+ if (child && this.store.workerBoundary.view(child.id).binding.external)
264
+ throw new ServiceError(
265
+ 'external_worker_owned',
266
+ 'Use the owning team assignment and an explicit continuation',
267
+ );
268
+ guardSubmission(session, step);
269
+ };
134
270
  this.dispatcher = new Dispatcher(this.store, this.supervisor);
135
271
  this.taskDriver = new TaskDriver(
136
272
  this.store,
@@ -150,6 +286,7 @@ export class SalaamService {
150
286
  );
151
287
  this.recurring = new RecurringWork(this.jobs);
152
288
  this.inbox = new OperatorInbox(this.store, (error) => {
289
+ this.external.failAll(error);
153
290
  for (const session of this.store.sessions())
154
291
  this.supervisor.failControl(session.id, error);
155
292
  });
@@ -207,19 +344,27 @@ export class SalaamService {
207
344
  service.coding.executor?.engineId !== options.expectedExecution.engine_id)
208
345
  )
209
346
  throw new Error('Host executor does not match reviewed manifest');
347
+ await service.supervisor.providerAuth();
348
+ await service.external.backend?.prepare?.();
210
349
  await service.supervisor.recover();
350
+ await service.external.recover();
211
351
  service.store.recoverWork();
212
352
  service.taskWork.recover();
213
353
  service.tasks.recover();
214
354
  service.workspaces.recover();
215
355
  service.coding.recover();
216
356
  service.children.recover();
357
+ service.workerWorkspaces.recover();
217
358
  service.managed.recover();
218
359
  await service.managed.reconcileAll();
219
360
  service.taskCheckpoints.recover();
220
361
  await service.jobs.recover();
221
362
  await service.recurring.recover();
222
363
  service.inbox.reconcile();
364
+ if (options.a2aListenConfig) {
365
+ service.a2aInbound = new A2AInbound(service, resolve(options.a2aListenConfig));
366
+ await service.a2aInbound.listen();
367
+ }
223
368
  await service.listen(service.bridgeServer, service.bridgeSocket);
224
369
  await service.listen(service.operator, service.socket);
225
370
  service.taskDriver.start();
@@ -253,6 +398,54 @@ export class SalaamService {
253
398
  if (this.stopping) throw new ServiceError('stopping', 'Service is stopping', 503);
254
399
  const url = new URL(req.url ?? '/', 'http://local');
255
400
  const parts = url.pathname.split('/').filter(Boolean);
401
+ if (
402
+ !bridge &&
403
+ req.method === 'GET' &&
404
+ parts[0] === 'v1' &&
405
+ parts[1] === 'a2a' &&
406
+ parts[2] === 'grants' &&
407
+ parts[3] &&
408
+ parts.length === 4
409
+ ) {
410
+ this.send(res, 200, A2AInbound.binding(this, parts[3]));
411
+ return;
412
+ }
413
+ if (!bridge && parts[0] === 'v1' && parts[1] === 'harnesses') {
414
+ if (req.method === 'GET' && parts.length === 2) {
415
+ this.send(res, 200, this.external.runtimes());
416
+ return;
417
+ }
418
+ if (parts[2]) {
419
+ const raw = this.store.db
420
+ .prepare('SELECT * FROM child_tasks WHERE id=?')
421
+ .get(parts[2]) as unknown as import('./protocol.js').ChildTask | undefined;
422
+ if (!raw || !this.store.workerBoundary.view(raw.id).binding.external)
423
+ throw new ServiceError('not_found', 'External worker not found', 404);
424
+ if (req.method === 'GET' && parts.length === 3) {
425
+ this.send(res, 200, {
426
+ child: this.children.inspect(raw.parent_session, raw.id),
427
+ execution: this.external.view(raw.id),
428
+ });
429
+ return;
430
+ }
431
+ if (req.method === 'POST' && parts.length === 4 && parts[3] === 'reconcile') {
432
+ const body = object(await readJson(req));
433
+ const result = await this.external.reconcile(
434
+ raw,
435
+ string(body.request_id, 'request_id', 128),
436
+ string(body.run_id, 'run_id', 128),
437
+ () =>
438
+ this.store.authorizeControl(raw.parent_session, {
439
+ client_id: string(body.client_id, 'client_id', 128),
440
+ token: string(body.token, 'token', 128),
441
+ }),
442
+ );
443
+ this.send(res, 200, result);
444
+ return;
445
+ }
446
+ }
447
+ throw new ServiceError('not_found', 'External worker route not found', 404);
448
+ }
256
449
  if (bridge) {
257
450
  if (req.method !== 'POST' || parts.length !== 3 || parts[0] !== 'worker')
258
451
  throw new ServiceError('not_found', 'Bridge route not found', 404);
@@ -462,7 +655,169 @@ export class SalaamService {
462
655
  }
463
656
  throw new ServiceError('not_found', 'Job route not found', 404);
464
657
  }
658
+ if (
659
+ parts[0] === 'v1' &&
660
+ parts[1] === 'integrations' &&
661
+ parts[2] &&
662
+ parts.length === 4 &&
663
+ parts[3] === 'resolve' &&
664
+ req.method === 'POST'
665
+ ) {
666
+ const body = object(await readJson(req)),
667
+ controller = {
668
+ client_id: string(body.client_id, 'client_id', 128),
669
+ token: string(body.token, 'token', 128),
670
+ },
671
+ parent = this.workerWorkspaces.owner(parts[2]);
672
+ this.send(
673
+ res,
674
+ 200,
675
+ this.workerWorkspaces.resolve(
676
+ parts[2],
677
+ string(body.request_id, 'request_id', 128),
678
+ integer(body.workspace_revision, 'workspace_revision'),
679
+ string(body.workspace_digest, 'workspace_digest', 128),
680
+ () => this.store.authorizeControl(parent, controller),
681
+ ),
682
+ );
683
+ return;
684
+ }
685
+ if (
686
+ parts[0] === 'v1' &&
687
+ parts[1] === 'integrations' &&
688
+ parts[2] &&
689
+ parts.length === 3 &&
690
+ req.method === 'GET'
691
+ ) {
692
+ this.send(res, 200, this.workerWorkspaces.inspect(parts[2]));
693
+ return;
694
+ }
695
+ if (parts[0] === 'v1' && parts[1] === 'workers' && parts[2] && parts.length === 4) {
696
+ const child = this.store.db
697
+ .prepare('SELECT parent_session FROM child_tasks WHERE id=?')
698
+ .get(parts[2]);
699
+ if (!child) throw new ServiceError('not_found', 'Worker not found', 404);
700
+ const parent = String(child.parent_session);
701
+ if (parts[3] === 'proposal' && req.method === 'GET') {
702
+ this.send(
703
+ res,
704
+ 200,
705
+ this.workerWorkspaces.proposal(
706
+ parent,
707
+ parts[2],
708
+ integer(Number(url.searchParams.get('offset') ?? '0'), 'offset'),
709
+ ),
710
+ );
711
+ return;
712
+ }
713
+ if (parts[3] === 'integrate' && req.method === 'POST') {
714
+ const body = object(await readJson(req)),
715
+ controller = {
716
+ client_id: string(body.client_id, 'client_id', 128),
717
+ token: string(body.token, 'token', 128),
718
+ };
719
+ this.send(
720
+ res,
721
+ 200,
722
+ this.workerWorkspaces.integrate(
723
+ parent,
724
+ parts[2],
725
+ string(body.request_id, 'request_id', 128),
726
+ integer(body.workspace_revision, 'workspace_revision'),
727
+ string(body.workspace_digest, 'workspace_digest', 128),
728
+ string(body.patch_digest, 'patch_digest', 128),
729
+ () => this.store.authorizeControl(parent, controller),
730
+ ),
731
+ );
732
+ return;
733
+ }
734
+ }
465
735
  if (parts[0] === 'v1' && parts[1] === 'tasks') {
736
+ if (parts[2] && parts[3] === 'team') {
737
+ const task = this.taskWork.get(parts[2]);
738
+ if (req.method === 'GET' && parts.length === 4) {
739
+ this.send(res, 200, this.team.view(task.id));
740
+ return;
741
+ }
742
+ if (req.method === 'POST') {
743
+ const body = object(await readJson(req));
744
+ const request = string(body.request_id, 'request_id', 128);
745
+ const controller = {
746
+ client_id: string(body.client_id, 'client_id', 128),
747
+ token: string(body.token, 'token', 128),
748
+ };
749
+ const authorize = () => this.store.authorizeControl(task.session_id, controller);
750
+ let result: unknown;
751
+ if (parts.length === 5 && parts[4] === 'assign') {
752
+ if (body.runtime !== undefined && body.requests !== undefined)
753
+ throw new ServiceError(
754
+ 'worker_budget_scope',
755
+ 'Use invocations for an external runtime; requests always means model requests',
756
+ 400,
757
+ );
758
+ result = this.team.assign(
759
+ task.id,
760
+ string(body.step_id, 'step_id', 128),
761
+ string(body.profile_id, 'profile_id', 128),
762
+ integer(
763
+ body.runtime === undefined ? body.requests : body.invocations,
764
+ body.runtime === undefined ? 'requests' : 'invocations',
765
+ ),
766
+ request,
767
+ integer(body.expected_revision, 'expected_revision'),
768
+ controller,
769
+ body.runtime === undefined ? undefined : string(body.runtime, 'runtime', 128),
770
+ );
771
+ } else if (parts.length === 5 && parts[4] === 'send') {
772
+ result = this.team.send(
773
+ task.id,
774
+ 'operator',
775
+ string(body.recipient, 'recipient', 128),
776
+ string(body.text, 'text', 4096),
777
+ request,
778
+ authorize,
779
+ );
780
+ } else if (parts.length === 6 && parts[4]) {
781
+ const assignment = this.team.get(parts[4]);
782
+ if (assignment.task_id !== task.id)
783
+ throw new ServiceError('team_message_scope', 'Assignment belongs to another task');
784
+ if (parts[5] === 'run')
785
+ result = this.team.run(
786
+ assignment.id,
787
+ request,
788
+ integer(body.expected_revision, 'expected_revision'),
789
+ integer(body.generation, 'generation'),
790
+ body.instruction == null ? null : string(body.instruction, 'instruction', 4096),
791
+ body.continues_attempt_id == null
792
+ ? null
793
+ : string(body.continues_attempt_id, 'continues_attempt_id', 128),
794
+ controller,
795
+ );
796
+ else if (parts[5] === 'reject')
797
+ result = this.team.reject(
798
+ assignment.id,
799
+ request,
800
+ integer(body.expected_revision, 'expected_revision'),
801
+ integer(body.generation, 'generation'),
802
+ string(body.reason, 'reason', 4096),
803
+ controller,
804
+ );
805
+ else if (parts[5] === 'receive')
806
+ result = this.team.receive(assignment.id, request, authorize);
807
+ else if (parts[5] === 'ack')
808
+ result = this.team.acknowledge(
809
+ assignment.id,
810
+ string(body.message_id, 'message_id', 128),
811
+ request,
812
+ authorize,
813
+ );
814
+ else throw new ServiceError('not_found', 'Team action not found', 404);
815
+ } else throw new ServiceError('not_found', 'Team route not found', 404);
816
+ this.send(res, 200, result);
817
+ return;
818
+ }
819
+ throw new ServiceError('not_found', 'Team route not found', 404);
820
+ }
466
821
  if (req.method === 'GET' && parts.length === 2) {
467
822
  const session = string(url.searchParams.get('session_id'), 'session_id', 128);
468
823
  this.store.session(session);
@@ -640,6 +995,26 @@ export class SalaamService {
640
995
  const sessions = this.store.sessions().map((x) => this.supervisor.inspect(x.id));
641
996
  this.send(res, 200, {
642
997
  service: 'healthy',
998
+ a2a: {
999
+ inbound_enabled: !!this.a2aInbound,
1000
+ listener: this.a2aInbound?.server.address() ?? null,
1001
+ wire_version: '1.0',
1002
+ remote_execution: 'unobserved',
1003
+ inbound_authority: 'one-use-native-analysis-assignment',
1004
+ },
1005
+ external_workers: {
1006
+ control: this.external.status(),
1007
+ runtimes: this.external.runtimes(),
1008
+ opaque_budget: 'explicit-invocations-only',
1009
+ unknown_model_usage: true,
1010
+ uncertain_stops: Number(
1011
+ this.store.db
1012
+ .prepare(
1013
+ "SELECT count(*) AS n FROM external_runs WHERE stopped=0 AND state='unknown'",
1014
+ )
1015
+ .get()!.n,
1016
+ ),
1017
+ },
643
1018
  job_execution: {
644
1019
  durable_jobs: true,
645
1020
  one_shot_triggers: !!this.coding.executor,
@@ -651,16 +1026,18 @@ export class SalaamService {
651
1026
  control_error: this.jobs.controlError,
652
1027
  },
653
1028
  task_execution: {
654
- durable_tasks: true,
1029
+ durable_tasks: this.supervisor.providerCapabilities().durable_tasks,
655
1030
  explicit_steps: true,
656
1031
  automatic_progression: 'operator-bounded-opt-in',
657
1032
  verified_completion: !!this.coding.executor,
658
1033
  verification_supported: true,
659
1034
  task_budgets: true,
1035
+ task_token_budgets: this.supervisor.providerCapabilities().output_token_cap,
660
1036
  checkpoints: true,
661
1037
  deterministic_compaction: 'operator-bounded-idle-only',
662
1038
  },
663
1039
  provider_route: this.supervisor.route,
1040
+ provider_capabilities: this.supervisor.providerCapabilities(),
664
1041
  effect_connector: this.managed.options.connector?.id ?? null,
665
1042
  coding_root: this.workspaces.root ?? null,
666
1043
  coding_socket: this.codingSocket,
@@ -750,6 +1127,60 @@ export class SalaamService {
750
1127
  }
751
1128
  if (parts[0] === 'v1' && parts[1] === 'sessions' && parts[2]) {
752
1129
  const id = parts[2];
1130
+ if (parts[3] === 'agents') {
1131
+ if (req.method === 'GET') {
1132
+ const profile = url.searchParams.get('profile');
1133
+ this.send(
1134
+ res,
1135
+ 200,
1136
+ profile
1137
+ ? {
1138
+ ...this.agents.inspect(id, profile),
1139
+ digest: digest(this.agents.inspect(id, profile)),
1140
+ }
1141
+ : this.agents.list(id).map((p) => ({ ...p, digest: digest(p) })),
1142
+ );
1143
+ return;
1144
+ }
1145
+ const body = object(await readJson(req));
1146
+ const controller = {
1147
+ client_id: string(body.client_id, 'client_id', 128),
1148
+ token: string(body.token, 'token', 128),
1149
+ };
1150
+ const request = string(body.request_id, 'request_id', 128);
1151
+ if (parts[4] === 'admit') {
1152
+ const facts =
1153
+ body.fact_ids === undefined
1154
+ ? []
1155
+ : Array.isArray(body.fact_ids)
1156
+ ? body.fact_ids.map((x) => string(x, 'fact_id', 128))
1157
+ : (() => {
1158
+ throw new ServiceError('invalid_field', 'fact_ids must be an array');
1159
+ })();
1160
+ this.send(
1161
+ res,
1162
+ 201,
1163
+ await this.agents.admit(
1164
+ id,
1165
+ request,
1166
+ string(body.profile_id, 'profile_id', 128),
1167
+ string(body.expected_digest, 'expected_digest', 64),
1168
+ facts,
1169
+ controller,
1170
+ ),
1171
+ );
1172
+ return;
1173
+ }
1174
+ if (parts[4] === 'revoke') {
1175
+ this.send(
1176
+ res,
1177
+ 200,
1178
+ this.agents.revoke(id, string(body.grant_id, 'grant_id', 128), request, controller),
1179
+ );
1180
+ return;
1181
+ }
1182
+ throw new ServiceError('not_found', 'Agent operation not found', 404);
1183
+ }
753
1184
  if (parts[3] === 'activity' && req.method === 'GET') {
754
1185
  this.store.session(id);
755
1186
  const rows = this.store.db
@@ -1011,12 +1442,14 @@ export class SalaamService {
1011
1442
  },
1012
1443
  );
1013
1444
  this.coding.cancelAttempt(attempt);
1014
- void this.children
1015
- .cancelOwned(attempt)
1016
- .then(() => this.supervisor.cancelAttempt(attempt))
1017
- .catch((error) =>
1018
- this.supervisor.failControl(this.store.attempt(attempt).session_id, error),
1019
- );
1445
+ const owned = this.children.owned(this.store.attempt(attempt).session_id);
1446
+ void (
1447
+ owned && this.store.workerBoundary.view(owned.id).binding.external
1448
+ ? this.external.cancel(owned)
1449
+ : this.children.cancelOwned(attempt).then(() => this.supervisor.cancelAttempt(attempt))
1450
+ ).catch((error) =>
1451
+ this.supervisor.failControl(this.store.attempt(attempt).session_id, error),
1452
+ );
1020
1453
  this.send(res, 202, receipt);
1021
1454
  return;
1022
1455
  }
@@ -1255,6 +1688,8 @@ export class SalaamService {
1255
1688
  this.children.pause();
1256
1689
  const errors: unknown[] = [];
1257
1690
  try {
1691
+ await this.a2aInbound?.close();
1692
+ await this.external.close();
1258
1693
  await this.coding.close();
1259
1694
  try {
1260
1695
  await this.children.close();