@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/cli.ts CHANGED
@@ -1,24 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
  import { acp } from './acp.js';
3
3
  import { homedir } from 'node:os';
4
- import { join, resolve } from 'node:path';
4
+ import { join, resolve, dirname } from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
- import { randomUUID } from 'node:crypto';
6
+ import { randomUUID, randomBytes } from 'node:crypto';
7
+ import { digest, object } from './protocol.js';
7
8
  import { fileURLToPath } from 'node:url';
8
- import { mkdirSync, openSync, closeSync, readFileSync } from 'node:fs';
9
+ import {
10
+ mkdirSync,
11
+ openSync,
12
+ closeSync,
13
+ readFileSync,
14
+ realpathSync,
15
+ writeFileSync,
16
+ fsyncSync,
17
+ } from 'node:fs';
9
18
  import { ROOT, VERSION } from './config.js';
10
19
  import { cliHelp, hasHelpFlag } from './cli-help.js';
11
20
  import { SalaamService } from './service.js';
12
21
  import { call } from './transport.js';
13
22
  import { attach, acquireControl, Outbox } from './attach.js';
14
23
  import type { SessionSnapshot, Approval, Operation } from './protocol.js';
15
- import { providerRoute } from './provider-config.js';
16
- import { credentialPath, loadApiKey, readSecret, saveApiKey } from './credentials.js';
24
+ import {
25
+ providerRoute,
26
+ PI_PROVIDERS,
27
+ getProvider,
28
+ providerHasOutputCap,
29
+ } from './provider-config.js';
30
+ import { readSecret, validateApiKey } from './credentials.js';
31
+ import {
32
+ authDirectory,
33
+ ProviderCredentialStore,
34
+ providerModels,
35
+ loginProvider,
36
+ resolveProviderAuth,
37
+ } from './provider-auth.js';
38
+ import { bindProviderConfiguration } from './provider-model.js';
39
+ import { createInterface } from 'node:readline/promises';
40
+ import type { AuthInteraction } from '@earendil-works/pi-ai';
17
41
  import { canonical } from './protocol.js';
18
42
  import { SocketEffectConnector } from './effect-connector.js';
19
43
  import { proveExecutionBoundary } from './execution-proof.js';
20
44
  import { BoundaryBroker, engineRequest } from './execution-boundary.js';
21
45
  import { workspaceDiff, type WorkspaceChange } from './workspace-diff.js';
46
+ import {
47
+ probeHarness,
48
+ HARNESS_TARGETS,
49
+ type HarnessPin,
50
+ type HarnessTarget,
51
+ } from './harness-probe.js';
22
52
  interface WorkspaceDiffPage {
23
53
  revision: number;
24
54
  digest: string;
@@ -63,6 +93,406 @@ async function main(): Promise<void> {
63
93
  if (foreground) args.splice(args.indexOf('--foreground'), 1);
64
94
  const socket = join(state, 'operator.sock');
65
95
  const [group, verb, id] = args;
96
+ if (group === 'a2a' && verb === 'credential') {
97
+ const file = option('--file');
98
+ if (!id || !file) throw Error('Supply PEER_ID and --file NEW_OWNER_ONLY_FILE');
99
+ const { a2aIdentity } = await import('./a2a-peer.js');
100
+ a2aIdentity(id);
101
+ const path = resolve(file),
102
+ value = { peer: id, token: randomBytes(32).toString('base64url') };
103
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
104
+ const fd = openSync(path, 'wx', 0o600);
105
+ try {
106
+ writeFileSync(fd, JSON.stringify(value) + '\n');
107
+ fsyncSync(fd);
108
+ } finally {
109
+ closeSync(fd);
110
+ }
111
+ console.log(
112
+ JSON.stringify(
113
+ { peer: id, file: realpathSync(path), credential_digest: digest(value) },
114
+ null,
115
+ 2,
116
+ ),
117
+ );
118
+ return;
119
+ }
120
+ if (group === 'a2a' && verb === 'discover') {
121
+ const peer = option('--peer'),
122
+ endpoint = option('--endpoint'),
123
+ card_url = option('--card-url'),
124
+ credential = option('--credential-file'),
125
+ addresses = option('--addresses');
126
+ if (!peer || !endpoint || !card_url || !credential || !addresses)
127
+ throw Error('Supply --peer, --endpoint, --card-url, --credential-file and --addresses');
128
+ const { privateJSON, validatePeer, A2AHttp } = await import('./a2a-peer.js');
129
+ const credential_file = realpathSync(resolve(credential)),
130
+ value = privateJSON(credential_file);
131
+ const pin = validatePeer({
132
+ id: peer,
133
+ endpoint,
134
+ card_url,
135
+ card_digest: digest(null),
136
+ tenant: option('--tenant') ?? '',
137
+ skill: 'pending-review',
138
+ addresses: addresses.split(','),
139
+ local: args.includes('--local'),
140
+ credential_file,
141
+ credential_digest: digest(value),
142
+ files: [],
143
+ retention: 'unknown',
144
+ cancel: false,
145
+ streaming: false,
146
+ });
147
+ console.log(JSON.stringify(await new A2AHttp(pin).request(card_url), null, 2));
148
+ return;
149
+ }
150
+ if (group === 'a2a' && verb === 'pin') {
151
+ const peer = option('--peer'),
152
+ endpoint = option('--endpoint'),
153
+ cardFile = option('--card-file'),
154
+ credential = option('--credential-file'),
155
+ addresses = option('--addresses'),
156
+ skill = option('--skill');
157
+ if (!peer || !endpoint || !cardFile || !credential || !addresses || !skill)
158
+ throw Error(
159
+ 'Supply --peer, --endpoint, --card-file, --credential-file, --addresses and --skill',
160
+ );
161
+ const { privateJSON, validatePeer, A2AHttp } = await import('./a2a-peer.js');
162
+ const card = JSON.parse(readFileSync(resolve(cardFile), 'utf8')),
163
+ credential_file = realpathSync(resolve(credential)),
164
+ value = privateJSON(credential_file),
165
+ card_url = option('--card-url');
166
+ if (!card_url) throw Error('Supply the exact --card-url that was reviewed');
167
+ const pin = validatePeer({
168
+ id: peer,
169
+ endpoint,
170
+ card_url,
171
+ card_digest: digest(card),
172
+ tenant: option('--tenant') ?? '',
173
+ skill,
174
+ addresses: addresses.split(','),
175
+ local: args.includes('--local'),
176
+ credential_file,
177
+ credential_digest: digest(value),
178
+ files: option('--files')?.split(',').filter(Boolean) ?? [],
179
+ retention: option('--retention') ?? 'unknown',
180
+ cancel: args.includes('--supports-cancel'),
181
+ streaming: args.includes('--supports-streaming'),
182
+ });
183
+ new A2AHttp(pin).assertCard(card);
184
+ console.log(JSON.stringify(pin, null, 2));
185
+ return;
186
+ }
187
+ if (group === 'a2a' && verb === 'grant') {
188
+ const assignment = option('--assignment'),
189
+ peer = option('--peer'),
190
+ credential = option('--credential-file');
191
+ if (!assignment || !peer || !credential)
192
+ throw Error('Supply --assignment, --peer and --credential-file');
193
+ const { privateJSON } = await import('./a2a-peer.js');
194
+ const credential_file = realpathSync(resolve(credential)),
195
+ value = object(privateJSON(credential_file));
196
+ if (
197
+ value.peer !== peer ||
198
+ Object.keys(value).sort().join(',') !== 'peer,token' ||
199
+ typeof value.token !== 'string' ||
200
+ !/^[A-Za-z0-9._~-]{32,512}$/.test(value.token)
201
+ )
202
+ throw Error('Use a dedicated peer credential JSON {peer,token}');
203
+ const peerClient = option('--peer-client');
204
+ if (peerClient && peerClient !== 'openclaw-2026.10.5')
205
+ throw Error('Only the explicitly pinned OpenClaw client adapter is available');
206
+ const binding = await call(socket, `/v1/a2a/grants/${encodeURIComponent(assignment)}`);
207
+ console.log(
208
+ JSON.stringify(
209
+ {
210
+ peer,
211
+ tenant: option('--tenant') ?? '',
212
+ skill: option('--skill') ?? 'review',
213
+ assignment_id: assignment,
214
+ binding_digest: digest(binding),
215
+ credential_file,
216
+ credential_digest: digest(value),
217
+ ...(peerClient ? { client: peerClient } : {}),
218
+ },
219
+ null,
220
+ 2,
221
+ ),
222
+ );
223
+ return;
224
+ }
225
+ if (group === 'a2a' && verb === 'peers') {
226
+ console.log(
227
+ JSON.stringify(
228
+ (await call<any[]>(socket, '/v1/harnesses')).filter((r) => r.kind === 'a2a'),
229
+ null,
230
+ 2,
231
+ ),
232
+ );
233
+ return;
234
+ }
235
+ if (group === 'harness' && verb === 'probe') {
236
+ const path = option('--pin');
237
+ if (!path) throw new Error('Supply an operator-reviewed --pin FILE');
238
+ const pin = JSON.parse(readFileSync(resolve(path), 'utf8')) as HarnessPin;
239
+ console.log(JSON.stringify(await probeHarness(pin), null, 2));
240
+ return;
241
+ }
242
+ if (group === 'harness' && verb === 'prepare') {
243
+ const path = option('--runtime-root'),
244
+ arch = option('--arch');
245
+ if (!path || !arch) throw Error('Supply a new --runtime-root and execution-host --arch');
246
+ const { prepareHarnessRuntime } = await import('./harness-prepare.js');
247
+ console.log(JSON.stringify(await prepareHarnessRuntime(path, arch), null, 2));
248
+ return;
249
+ }
250
+ if (group === 'harness' && verb === 'config') {
251
+ const target = option('--target'),
252
+ path = option('--runtime-root');
253
+ if (!target || !Object.hasOwn(HARNESS_TARGETS, target) || !path)
254
+ throw Error('Supply a named --target and dedicated --runtime-root');
255
+ const runtime_root = realpathSync(resolve(path));
256
+ const { assertHarnessArtifactRoot, harnessRuntimeDigest } =
257
+ await import('./harness-backend.js');
258
+ assertHarnessArtifactRoot(runtime_root);
259
+ console.log(
260
+ JSON.stringify(
261
+ {
262
+ schema: 1,
263
+ runtimes: [
264
+ {
265
+ id: option('--id') ?? target,
266
+ target,
267
+ version: HARNESS_TARGETS[target as HarnessTarget].version,
268
+ runtime_root,
269
+ runtime_digest: await harnessRuntimeDigest(runtime_root),
270
+ model: 'claude-sonnet-4-6',
271
+ output: 4096,
272
+ requests: 12,
273
+ },
274
+ ],
275
+ },
276
+ null,
277
+ 2,
278
+ ),
279
+ );
280
+ return;
281
+ }
282
+ if (
283
+ ['harness', 'a2a'].includes(group ?? '') &&
284
+ ['list', 'inspect', 'reconcile'].includes(verb ?? '')
285
+ ) {
286
+ if (verb === 'list') {
287
+ console.log(JSON.stringify(await call(socket, '/v1/harnesses'), null, 2));
288
+ return;
289
+ }
290
+ if (!id) throw Error('Supply an external CHILD_ID');
291
+ const current = await call<any>(socket, `/v1/harnesses/${id}`);
292
+ if (verb === 'inspect') {
293
+ console.log(JSON.stringify(current, null, 2));
294
+ return;
295
+ }
296
+ const run_id = option('--run-id');
297
+ if (!run_id) throw Error('Supply the exact --run-id from harness inspect');
298
+ const client_id = randomUUID(),
299
+ owner = current.child.parent_session,
300
+ lease = await acquireControl(socket, owner, client_id, args.includes('--takeover'));
301
+ try {
302
+ console.log(
303
+ JSON.stringify(
304
+ await call(socket, `/v1/harnesses/${id}/reconcile`, {
305
+ client_id,
306
+ token: lease.token,
307
+ run_id,
308
+ request_id: option('--request-id') ?? randomUUID(),
309
+ }),
310
+ null,
311
+ 2,
312
+ ),
313
+ );
314
+ } finally {
315
+ await call(socket, `/v1/sessions/${owner}/control`, {
316
+ request_id: randomUUID(),
317
+ client_id,
318
+ token: lease.token,
319
+ action: 'release',
320
+ expected_epoch: lease.epoch,
321
+ }).catch(() => {});
322
+ }
323
+ return;
324
+ }
325
+ if (group === 'workers') {
326
+ if (!id) throw new Error('Supply a child or integration receipt ID');
327
+ if (verb === 'proposal' || verb === 'integration') {
328
+ const result = await call<any>(
329
+ socket,
330
+ verb === 'proposal'
331
+ ? `/v1/workers/${id}/proposal?offset=${Number(option('--offset') ?? 0)}`
332
+ : `/v1/integrations/${id}`,
333
+ );
334
+ if (verb === 'proposal' && !args.includes('--json')) {
335
+ const { changes, diff, ...metadata } = result;
336
+ console.log(
337
+ JSON.stringify(
338
+ {
339
+ ...metadata,
340
+ files: changes.map((c: any) => ({
341
+ path: c.path,
342
+ before_hash: digest(c.before),
343
+ after_hash: digest(c.after),
344
+ })),
345
+ },
346
+ null,
347
+ 2,
348
+ ),
349
+ );
350
+ for (const patch of diff) console.log(patch);
351
+ } else console.log(JSON.stringify(result, null, 2));
352
+ return;
353
+ }
354
+ if (!['integrate', 'resolve'].includes(verb ?? ''))
355
+ throw new Error('Use workers proposal, integrate, integration or resolve');
356
+ const owner =
357
+ verb === 'integrate'
358
+ ? (await call<{ parent_session: string }>(socket, `/v1/children/${id}`)).parent_session
359
+ : (await call<{ parent_session: string }>(socket, `/v1/integrations/${id}`)).parent_session;
360
+ const revision = option('--revision'),
361
+ workspace_digest = option('--root-digest'),
362
+ patch_digest = option('--patch-digest');
363
+ if (revision === undefined || !workspace_digest || (verb === 'integrate' && !patch_digest))
364
+ throw new Error(
365
+ 'Supply reviewed --revision N, --root-digest SHA256 and --patch-digest SHA256 for integration',
366
+ );
367
+ const client_id = randomUUID(),
368
+ lease = await acquireControl(socket, owner, client_id, args.includes('--takeover'));
369
+ try {
370
+ const result = await call(
371
+ socket,
372
+ verb === 'integrate' ? `/v1/workers/${id}/integrate` : `/v1/integrations/${id}/resolve`,
373
+ {
374
+ client_id,
375
+ token: lease.token,
376
+ request_id: option('--request-id') ?? randomUUID(),
377
+ workspace_revision: Number(revision),
378
+ workspace_digest,
379
+ patch_digest,
380
+ },
381
+ );
382
+ console.log(JSON.stringify(result, null, 2));
383
+ } finally {
384
+ await call(socket, `/v1/sessions/${owner}/control`, {
385
+ request_id: randomUUID(),
386
+ client_id,
387
+ token: lease.token,
388
+ action: 'release',
389
+ expected_epoch: lease.epoch,
390
+ }).catch(() => {});
391
+ }
392
+ return;
393
+ }
394
+ if (group === 'team') {
395
+ if (!id) throw new Error('Supply TASK_ID');
396
+ if (verb === 'show') {
397
+ console.log(JSON.stringify(await call(socket, `/v1/tasks/${id}/team`), null, 2));
398
+ return;
399
+ }
400
+ if (!['assign', 'run', 'reject', 'send', 'receive', 'ack'].includes(verb ?? ''))
401
+ throw new Error('Use team show, assign, run, send, receive or ack');
402
+ const task = await call<{ session_id: string }>(socket, `/v1/tasks/${id}`);
403
+ const client_id = randomUUID(),
404
+ lease = await acquireControl(socket, task.session_id, client_id, args.includes('--takeover'));
405
+ try {
406
+ const assignment = option('--assignment');
407
+ if (['run', 'reject', 'receive', 'ack'].includes(verb!) && !assignment)
408
+ throw new Error('Supply --assignment ID');
409
+ const revision = option('--revision'),
410
+ generation = option('--generation');
411
+ if (['assign', 'run', 'reject'].includes(verb!) && revision === undefined)
412
+ throw new Error('Supply the reviewed --revision N');
413
+ if (['run', 'reject'].includes(verb!) && generation === undefined)
414
+ throw new Error('Supply the reviewed --generation N');
415
+ const path = assignment ? `${assignment}/${verb}` : verb;
416
+ const result = await call(socket, `/v1/tasks/${id}/team/${path}`, {
417
+ client_id,
418
+ token: lease.token,
419
+ request_id: option('--request-id') ?? randomUUID(),
420
+ expected_revision: revision === undefined ? undefined : Number(revision),
421
+ generation: generation === undefined ? undefined : Number(generation),
422
+ ...(verb === 'assign'
423
+ ? {
424
+ step_id: option('--step'),
425
+ profile_id: option('--profile'),
426
+ ...(() => {
427
+ const runtime = option('--runtime');
428
+ return runtime
429
+ ? { runtime, invocations: Number(option('--invocations')) }
430
+ : { requests: Number(option('--requests')) };
431
+ })(),
432
+ }
433
+ : {}),
434
+ ...(verb === 'run'
435
+ ? { instruction: option('--instruction'), continues_attempt_id: option('--continues') }
436
+ : {}),
437
+ ...(verb === 'send' ? { recipient: option('--to'), text: option('--text') } : {}),
438
+ ...(verb === 'ack' ? { message_id: option('--message') } : {}),
439
+ ...(verb === 'reject' ? { reason: option('--reason') } : {}),
440
+ });
441
+ console.log(JSON.stringify(result, null, 2));
442
+ } finally {
443
+ await call(socket, `/v1/sessions/${task.session_id}/control`, {
444
+ request_id: randomUUID(),
445
+ client_id,
446
+ token: lease.token,
447
+ action: 'release',
448
+ expected_epoch: lease.epoch,
449
+ }).catch(() => {});
450
+ }
451
+ return;
452
+ }
453
+ if (group === 'agents') {
454
+ const session = option('--session');
455
+ if (!session) throw new Error('Supply --session SESSION_ID');
456
+ if (verb === 'list' || verb === 'inspect') {
457
+ console.log(
458
+ JSON.stringify(
459
+ await call(
460
+ socket,
461
+ `/v1/sessions/${session}/agents${verb === 'inspect' ? '?profile=' + encodeURIComponent(id ?? '') : ''}`,
462
+ ),
463
+ null,
464
+ 2,
465
+ ),
466
+ );
467
+ return;
468
+ }
469
+ if (!id || !['admit', 'revoke'].includes(verb ?? ''))
470
+ throw new Error('Use agents list, inspect, admit or revoke');
471
+ const client_id = randomUUID(),
472
+ lease = await acquireControl(socket, session, client_id, args.includes('--takeover'));
473
+ try {
474
+ const expected_digest = option('--digest');
475
+ if (verb === 'admit' && !expected_digest)
476
+ throw new Error('Review agents inspect and supply --digest SHA256');
477
+ const fact_ids = option('--facts')?.split(',').filter(Boolean) ?? [];
478
+ const result = await call(socket, `/v1/sessions/${session}/agents/${verb}`, {
479
+ request_id: option('--request-id') ?? randomUUID(),
480
+ client_id,
481
+ token: lease.token,
482
+ ...(verb === 'admit' ? { profile_id: id, expected_digest, fact_ids } : { grant_id: id }),
483
+ });
484
+ console.log(JSON.stringify(result, null, 2));
485
+ } finally {
486
+ await call(socket, `/v1/sessions/${session}/control`, {
487
+ request_id: randomUUID(),
488
+ client_id,
489
+ token: lease.token,
490
+ action: 'release',
491
+ expected_epoch: lease.epoch,
492
+ }).catch(() => {});
493
+ }
494
+ return;
495
+ }
66
496
  if (group === 'host') {
67
497
  const { planHost, readHost, hostUnit, runHost } = await import('./host.js');
68
498
  if (verb === 'plan') {
@@ -300,11 +730,148 @@ async function main(): Promise<void> {
300
730
  await acp(state, { coding: args.includes('--coding') });
301
731
  return;
302
732
  }
303
- if (group === 'auth' && verb === 'set' && id === 'anthropic') {
304
- saveApiKey(await readSecret());
305
- console.log('Claude API key saved to ' + credentialPath() + ' (mode 600).');
733
+ if (group === 'providers' && verb === 'list') {
734
+ console.log(
735
+ JSON.stringify(
736
+ PI_PROVIDERS.map((p) => ({
737
+ id: p.id,
738
+ name: p.name,
739
+ chat_models: p.getModels().length,
740
+ apis: [...new Set(p.getModels().map((m) => m.api))],
741
+ auth: [p.auth.apiKey ? 'api_key' : null, p.auth.oauth ? 'oauth' : null].filter(Boolean),
742
+ limitation: !p.getModels().length
743
+ ? 'Classifier only; no chat route'
744
+ : p.id === 'openai-codex'
745
+ ? 'No provider-enforced output cap; tasks must explicitly disable token budgeting'
746
+ : null,
747
+ })),
748
+ null,
749
+ 2,
750
+ ),
751
+ );
306
752
  return;
307
753
  }
754
+ if (group === 'models' && verb === 'list') {
755
+ const providers = id ? [getProvider(id)] : PI_PROVIDERS;
756
+ if (providers.some((p) => !p)) throw new Error('Unknown Pi provider');
757
+ console.log(
758
+ JSON.stringify(
759
+ providers.flatMap((p) =>
760
+ p!.getModels().map((m) => ({
761
+ provider: p!.id,
762
+ id: m.id,
763
+ name: m.name,
764
+ api: m.api,
765
+ context_window: m.contextWindow,
766
+ max_output_tokens: m.maxTokens,
767
+ })),
768
+ ),
769
+ null,
770
+ 2,
771
+ ),
772
+ );
773
+ return;
774
+ }
775
+ if (group === 'auth') {
776
+ const store = new ProviderCredentialStore();
777
+ if (verb === 'status') {
778
+ const models = providerModels(store),
779
+ entries = id ? [getProvider(id)] : PI_PROVIDERS;
780
+ if (entries.some((p) => !p)) throw new Error('Unknown Pi provider');
781
+ const statuses = [];
782
+ for (const p of entries) {
783
+ try {
784
+ const auth = await models.checkAuth(p!.id);
785
+ statuses.push({
786
+ provider: p!.id,
787
+ configured: !!auth,
788
+ type: auth?.type ?? null,
789
+ source: auth?.source ?? null,
790
+ });
791
+ } catch {
792
+ statuses.push({ provider: p!.id, configured: false, error: 'Credential check failed' });
793
+ }
794
+ }
795
+ console.log(JSON.stringify(statuses, null, 2));
796
+ return;
797
+ }
798
+ if (!id || !getProvider(id)) throw new Error('Choose a provider from salaam providers list');
799
+ if (verb === 'logout') {
800
+ await store.delete(id);
801
+ console.log(
802
+ 'Stored credential removed for ' + id + '. Environment credentials remain external.',
803
+ );
804
+ return;
805
+ }
806
+ if (verb === 'set') {
807
+ if (!getProvider(id)!.auth.apiKey)
808
+ throw new Error('This provider needs salaam auth login ' + id + ' --oauth');
809
+ const key = (await readSecret(id + ' API key (hidden): ')).trim();
810
+ if (
811
+ !key ||
812
+ key.length > 4096 ||
813
+ /[^\x21-\x7e]/.test(key) ||
814
+ (id === 'anthropic' && key.includes('sk-ant-oat'))
815
+ )
816
+ throw new Error('Supply a provider API key, not an OAuth token');
817
+ if (id === 'anthropic') validateApiKey(key);
818
+ await store.modify(id, async () => ({ type: 'api_key', key }));
819
+ console.log(
820
+ 'API key saved privately for ' + id + ' under ' + authDirectory() + ' (mode 600).',
821
+ );
822
+ return;
823
+ }
824
+ if (verb === 'login') {
825
+ if (!process.stdin.isTTY)
826
+ throw new Error(
827
+ 'Interactive login needs a terminal; use auth set PROVIDER for a piped API key',
828
+ );
829
+ const provider = getProvider(id)!;
830
+ const type = args.includes('--oauth') || !provider.auth.apiKey ? 'oauth' : 'api_key';
831
+ const interaction: AuthInteraction = {
832
+ async prompt(prompt) {
833
+ if (prompt.type === 'secret')
834
+ return readSecret(prompt.message + ' (hidden): ', prompt.signal);
835
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
836
+ try {
837
+ const choices =
838
+ prompt.type === 'select'
839
+ ? '\n' + prompt.options.map((v, i) => `${i + 1}. ${v.label} (${v.id})`).join('\n')
840
+ : '';
841
+ const answer = await rl.question(prompt.message + choices + '\n> ', {
842
+ signal: prompt.signal,
843
+ });
844
+ return prompt.type === 'select' && /^\d+$/.test(answer)
845
+ ? (prompt.options[Number(answer) - 1]?.id ?? answer)
846
+ : answer;
847
+ } finally {
848
+ rl.close();
849
+ }
850
+ },
851
+ notify(event) {
852
+ if (event.type === 'auth_url')
853
+ console.error(
854
+ 'Open in your browser: ' +
855
+ event.url +
856
+ (event.instructions ? '\n' + event.instructions : ''),
857
+ );
858
+ else if (event.type === 'device_code')
859
+ console.error('Open ' + event.verificationUri + ' and enter ' + event.userCode);
860
+ else console.error(event.message);
861
+ },
862
+ };
863
+ await loginProvider(id, type, interaction);
864
+ console.log(
865
+ 'Signed in to ' +
866
+ id +
867
+ ' with ' +
868
+ type +
869
+ '. Restart the service to use a different provider route.',
870
+ );
871
+ return;
872
+ }
873
+ throw new Error('Run salaam auth --help');
874
+ }
308
875
  if (group === 'schedules' && verb) {
309
876
  if (verb === 'list' || (id && ['inspect', 'occurrences'].includes(verb))) {
310
877
  console.log(
@@ -874,26 +1441,42 @@ async function main(): Promise<void> {
874
1441
  const model = option('--model');
875
1442
  const cap = option('--max-output-tokens');
876
1443
  const workspace = option('--workspace-id');
877
- const route = providerRoute(
1444
+ let route = providerRoute(
878
1445
  provider,
879
1446
  model,
880
1447
  cap === undefined ? undefined : Number(cap),
881
1448
  workspace,
882
1449
  );
1450
+ if (route.provider !== 'salaam-proof') {
1451
+ const auth = await resolveProviderAuth(route);
1452
+ if (cap !== undefined && !providerHasOutputCap(route, auth))
1453
+ throw new Error(
1454
+ 'This authentication cannot enforce --max-output-tokens; omit it and explicitly disable token budgeting in task plans',
1455
+ );
1456
+ route = bindProviderConfiguration(route, auth);
1457
+ }
883
1458
  const effectSocket = option('--effect-socket');
884
1459
  const opaque = args.includes('--effect-opaque');
885
1460
  const codingRoot = option('--coding-root'),
886
- codingSocket = option('--coding-socket');
1461
+ codingSocket = option('--coding-socket'),
1462
+ harnessConfig = option('--harness-config'),
1463
+ a2aConfig = option('--a2a-config'),
1464
+ a2aListenConfig = option('--a2a-listen-config');
887
1465
  if (foreground) {
888
1466
  const service = await SalaamService.open({
889
1467
  stateDir: state,
890
1468
  providerRoute: route,
891
1469
  codingRoot: codingRoot ? resolve(codingRoot) : undefined,
892
1470
  codingSocket: codingSocket ? resolve(codingSocket) : undefined,
1471
+ harnessConfig: harnessConfig ? resolve(harnessConfig) : undefined,
1472
+ a2aConfig: a2aConfig ? resolve(a2aConfig) : undefined,
1473
+ a2aListenConfig: a2aListenConfig ? resolve(a2aListenConfig) : undefined,
893
1474
  effectConnector: effectSocket
894
1475
  ? new SocketEffectConnector(resolve(effectSocket), { opaque })
895
1476
  : undefined,
896
1477
  });
1478
+ const warning = service.supervisor.providerCapabilities().warning;
1479
+ if (warning) console.error(warning);
897
1480
  process.stdout.write(JSON.stringify({ socket, service: 'started' }) + '\n');
898
1481
  for (const signal of ['SIGINT', 'SIGTERM'] as const)
899
1482
  process.once(signal, () => {
@@ -924,7 +1507,6 @@ async function main(): Promise<void> {
924
1507
  console.log('Salaam service already running');
925
1508
  return;
926
1509
  }
927
- if (route.provider === 'anthropic') loadApiKey();
928
1510
  mkdirSync(state, { recursive: true, mode: 0o700 });
929
1511
  const fd = openSync(join(state, 'service.log'), 'a', 0o600);
930
1512
  const child = spawn(
@@ -945,6 +1527,9 @@ async function main(): Promise<void> {
945
1527
  ...(opaque ? ['--effect-opaque'] : []),
946
1528
  ...(codingRoot ? ['--coding-root', resolve(codingRoot)] : []),
947
1529
  ...(codingSocket ? ['--coding-socket', resolve(codingSocket)] : []),
1530
+ ...(harnessConfig ? ['--harness-config', resolve(harnessConfig)] : []),
1531
+ ...(a2aConfig ? ['--a2a-config', resolve(a2aConfig)] : []),
1532
+ ...(a2aListenConfig ? ['--a2a-listen-config', resolve(a2aListenConfig)] : []),
948
1533
  ],
949
1534
  { detached: true, stdio: ['ignore', fd, fd] },
950
1535
  );
@@ -952,7 +1537,12 @@ async function main(): Promise<void> {
952
1537
  child.unref();
953
1538
  for (let i = 0; i < 150; i++) {
954
1539
  try {
955
- await call(socket, '/v1/health');
1540
+ const health = await call<{ provider_capabilities?: { warning: string | null } }>(
1541
+ socket,
1542
+ '/v1/health',
1543
+ );
1544
+ if (health.provider_capabilities?.warning)
1545
+ console.error(health.provider_capabilities.warning);
956
1546
  console.log(JSON.stringify({ socket, service: 'started' }));
957
1547
  return;
958
1548
  } catch {