@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/dist/src/cli.js CHANGED
@@ -1,23 +1,28 @@
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 { mkdirSync, openSync, closeSync, readFileSync, realpathSync, writeFileSync, fsyncSync, } from 'node:fs';
9
10
  import { VERSION } from './config.js';
10
11
  import { cliHelp, hasHelpFlag } from './cli-help.js';
11
12
  import { SalaamService } from './service.js';
12
13
  import { call } from './transport.js';
13
14
  import { attach, acquireControl, Outbox } from './attach.js';
14
- import { providerRoute } from './provider-config.js';
15
- import { credentialPath, loadApiKey, readSecret, saveApiKey } from './credentials.js';
15
+ import { providerRoute, PI_PROVIDERS, getProvider, providerHasOutputCap, } from './provider-config.js';
16
+ import { readSecret, validateApiKey } from './credentials.js';
17
+ import { authDirectory, ProviderCredentialStore, providerModels, loginProvider, resolveProviderAuth, } from './provider-auth.js';
18
+ import { bindProviderConfiguration } from './provider-model.js';
19
+ import { createInterface } from 'node:readline/promises';
16
20
  import { canonical } from './protocol.js';
17
21
  import { SocketEffectConnector } from './effect-connector.js';
18
22
  import { proveExecutionBoundary } from './execution-proof.js';
19
23
  import { BoundaryBroker, engineRequest } from './execution-boundary.js';
20
24
  import { workspaceDiff } from './workspace-diff.js';
25
+ import { probeHarness, HARNESS_TARGETS, } from './harness-probe.js';
21
26
  async function main() {
22
27
  const args = process.argv.slice(2);
23
28
  if (args[0] === '--version' || args[0] === '-V' || args[0] === 'version') {
@@ -54,6 +59,334 @@ async function main() {
54
59
  args.splice(args.indexOf('--foreground'), 1);
55
60
  const socket = join(state, 'operator.sock');
56
61
  const [group, verb, id] = args;
62
+ if (group === 'a2a' && verb === 'credential') {
63
+ const file = option('--file');
64
+ if (!id || !file)
65
+ throw Error('Supply PEER_ID and --file NEW_OWNER_ONLY_FILE');
66
+ const { a2aIdentity } = await import('./a2a-peer.js');
67
+ a2aIdentity(id);
68
+ const path = resolve(file), value = { peer: id, token: randomBytes(32).toString('base64url') };
69
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
70
+ const fd = openSync(path, 'wx', 0o600);
71
+ try {
72
+ writeFileSync(fd, JSON.stringify(value) + '\n');
73
+ fsyncSync(fd);
74
+ }
75
+ finally {
76
+ closeSync(fd);
77
+ }
78
+ console.log(JSON.stringify({ peer: id, file: realpathSync(path), credential_digest: digest(value) }, null, 2));
79
+ return;
80
+ }
81
+ if (group === 'a2a' && verb === 'discover') {
82
+ const peer = option('--peer'), endpoint = option('--endpoint'), card_url = option('--card-url'), credential = option('--credential-file'), addresses = option('--addresses');
83
+ if (!peer || !endpoint || !card_url || !credential || !addresses)
84
+ throw Error('Supply --peer, --endpoint, --card-url, --credential-file and --addresses');
85
+ const { privateJSON, validatePeer, A2AHttp } = await import('./a2a-peer.js');
86
+ const credential_file = realpathSync(resolve(credential)), value = privateJSON(credential_file);
87
+ const pin = validatePeer({
88
+ id: peer,
89
+ endpoint,
90
+ card_url,
91
+ card_digest: digest(null),
92
+ tenant: option('--tenant') ?? '',
93
+ skill: 'pending-review',
94
+ addresses: addresses.split(','),
95
+ local: args.includes('--local'),
96
+ credential_file,
97
+ credential_digest: digest(value),
98
+ files: [],
99
+ retention: 'unknown',
100
+ cancel: false,
101
+ streaming: false,
102
+ });
103
+ console.log(JSON.stringify(await new A2AHttp(pin).request(card_url), null, 2));
104
+ return;
105
+ }
106
+ if (group === 'a2a' && verb === 'pin') {
107
+ const peer = option('--peer'), endpoint = option('--endpoint'), cardFile = option('--card-file'), credential = option('--credential-file'), addresses = option('--addresses'), skill = option('--skill');
108
+ if (!peer || !endpoint || !cardFile || !credential || !addresses || !skill)
109
+ throw Error('Supply --peer, --endpoint, --card-file, --credential-file, --addresses and --skill');
110
+ const { privateJSON, validatePeer, A2AHttp } = await import('./a2a-peer.js');
111
+ const card = JSON.parse(readFileSync(resolve(cardFile), 'utf8')), credential_file = realpathSync(resolve(credential)), value = privateJSON(credential_file), card_url = option('--card-url');
112
+ if (!card_url)
113
+ throw Error('Supply the exact --card-url that was reviewed');
114
+ const pin = validatePeer({
115
+ id: peer,
116
+ endpoint,
117
+ card_url,
118
+ card_digest: digest(card),
119
+ tenant: option('--tenant') ?? '',
120
+ skill,
121
+ addresses: addresses.split(','),
122
+ local: args.includes('--local'),
123
+ credential_file,
124
+ credential_digest: digest(value),
125
+ files: option('--files')?.split(',').filter(Boolean) ?? [],
126
+ retention: option('--retention') ?? 'unknown',
127
+ cancel: args.includes('--supports-cancel'),
128
+ streaming: args.includes('--supports-streaming'),
129
+ });
130
+ new A2AHttp(pin).assertCard(card);
131
+ console.log(JSON.stringify(pin, null, 2));
132
+ return;
133
+ }
134
+ if (group === 'a2a' && verb === 'grant') {
135
+ const assignment = option('--assignment'), peer = option('--peer'), credential = option('--credential-file');
136
+ if (!assignment || !peer || !credential)
137
+ throw Error('Supply --assignment, --peer and --credential-file');
138
+ const { privateJSON } = await import('./a2a-peer.js');
139
+ const credential_file = realpathSync(resolve(credential)), value = object(privateJSON(credential_file));
140
+ if (value.peer !== peer ||
141
+ Object.keys(value).sort().join(',') !== 'peer,token' ||
142
+ typeof value.token !== 'string' ||
143
+ !/^[A-Za-z0-9._~-]{32,512}$/.test(value.token))
144
+ throw Error('Use a dedicated peer credential JSON {peer,token}');
145
+ const peerClient = option('--peer-client');
146
+ if (peerClient && peerClient !== 'openclaw-2026.10.5')
147
+ throw Error('Only the explicitly pinned OpenClaw client adapter is available');
148
+ const binding = await call(socket, `/v1/a2a/grants/${encodeURIComponent(assignment)}`);
149
+ console.log(JSON.stringify({
150
+ peer,
151
+ tenant: option('--tenant') ?? '',
152
+ skill: option('--skill') ?? 'review',
153
+ assignment_id: assignment,
154
+ binding_digest: digest(binding),
155
+ credential_file,
156
+ credential_digest: digest(value),
157
+ ...(peerClient ? { client: peerClient } : {}),
158
+ }, null, 2));
159
+ return;
160
+ }
161
+ if (group === 'a2a' && verb === 'peers') {
162
+ console.log(JSON.stringify((await call(socket, '/v1/harnesses')).filter((r) => r.kind === 'a2a'), null, 2));
163
+ return;
164
+ }
165
+ if (group === 'harness' && verb === 'probe') {
166
+ const path = option('--pin');
167
+ if (!path)
168
+ throw new Error('Supply an operator-reviewed --pin FILE');
169
+ const pin = JSON.parse(readFileSync(resolve(path), 'utf8'));
170
+ console.log(JSON.stringify(await probeHarness(pin), null, 2));
171
+ return;
172
+ }
173
+ if (group === 'harness' && verb === 'prepare') {
174
+ const path = option('--runtime-root'), arch = option('--arch');
175
+ if (!path || !arch)
176
+ throw Error('Supply a new --runtime-root and execution-host --arch');
177
+ const { prepareHarnessRuntime } = await import('./harness-prepare.js');
178
+ console.log(JSON.stringify(await prepareHarnessRuntime(path, arch), null, 2));
179
+ return;
180
+ }
181
+ if (group === 'harness' && verb === 'config') {
182
+ const target = option('--target'), path = option('--runtime-root');
183
+ if (!target || !Object.hasOwn(HARNESS_TARGETS, target) || !path)
184
+ throw Error('Supply a named --target and dedicated --runtime-root');
185
+ const runtime_root = realpathSync(resolve(path));
186
+ const { assertHarnessArtifactRoot, harnessRuntimeDigest } = await import('./harness-backend.js');
187
+ assertHarnessArtifactRoot(runtime_root);
188
+ console.log(JSON.stringify({
189
+ schema: 1,
190
+ runtimes: [
191
+ {
192
+ id: option('--id') ?? target,
193
+ target,
194
+ version: HARNESS_TARGETS[target].version,
195
+ runtime_root,
196
+ runtime_digest: await harnessRuntimeDigest(runtime_root),
197
+ model: 'claude-sonnet-4-6',
198
+ output: 4096,
199
+ requests: 12,
200
+ },
201
+ ],
202
+ }, null, 2));
203
+ return;
204
+ }
205
+ if (['harness', 'a2a'].includes(group ?? '') &&
206
+ ['list', 'inspect', 'reconcile'].includes(verb ?? '')) {
207
+ if (verb === 'list') {
208
+ console.log(JSON.stringify(await call(socket, '/v1/harnesses'), null, 2));
209
+ return;
210
+ }
211
+ if (!id)
212
+ throw Error('Supply an external CHILD_ID');
213
+ const current = await call(socket, `/v1/harnesses/${id}`);
214
+ if (verb === 'inspect') {
215
+ console.log(JSON.stringify(current, null, 2));
216
+ return;
217
+ }
218
+ const run_id = option('--run-id');
219
+ if (!run_id)
220
+ throw Error('Supply the exact --run-id from harness inspect');
221
+ const client_id = randomUUID(), owner = current.child.parent_session, lease = await acquireControl(socket, owner, client_id, args.includes('--takeover'));
222
+ try {
223
+ console.log(JSON.stringify(await call(socket, `/v1/harnesses/${id}/reconcile`, {
224
+ client_id,
225
+ token: lease.token,
226
+ run_id,
227
+ request_id: option('--request-id') ?? randomUUID(),
228
+ }), null, 2));
229
+ }
230
+ finally {
231
+ await call(socket, `/v1/sessions/${owner}/control`, {
232
+ request_id: randomUUID(),
233
+ client_id,
234
+ token: lease.token,
235
+ action: 'release',
236
+ expected_epoch: lease.epoch,
237
+ }).catch(() => { });
238
+ }
239
+ return;
240
+ }
241
+ if (group === 'workers') {
242
+ if (!id)
243
+ throw new Error('Supply a child or integration receipt ID');
244
+ if (verb === 'proposal' || verb === 'integration') {
245
+ const result = await call(socket, verb === 'proposal'
246
+ ? `/v1/workers/${id}/proposal?offset=${Number(option('--offset') ?? 0)}`
247
+ : `/v1/integrations/${id}`);
248
+ if (verb === 'proposal' && !args.includes('--json')) {
249
+ const { changes, diff, ...metadata } = result;
250
+ console.log(JSON.stringify({
251
+ ...metadata,
252
+ files: changes.map((c) => ({
253
+ path: c.path,
254
+ before_hash: digest(c.before),
255
+ after_hash: digest(c.after),
256
+ })),
257
+ }, null, 2));
258
+ for (const patch of diff)
259
+ console.log(patch);
260
+ }
261
+ else
262
+ console.log(JSON.stringify(result, null, 2));
263
+ return;
264
+ }
265
+ if (!['integrate', 'resolve'].includes(verb ?? ''))
266
+ throw new Error('Use workers proposal, integrate, integration or resolve');
267
+ const owner = verb === 'integrate'
268
+ ? (await call(socket, `/v1/children/${id}`)).parent_session
269
+ : (await call(socket, `/v1/integrations/${id}`)).parent_session;
270
+ const revision = option('--revision'), workspace_digest = option('--root-digest'), patch_digest = option('--patch-digest');
271
+ if (revision === undefined || !workspace_digest || (verb === 'integrate' && !patch_digest))
272
+ throw new Error('Supply reviewed --revision N, --root-digest SHA256 and --patch-digest SHA256 for integration');
273
+ const client_id = randomUUID(), lease = await acquireControl(socket, owner, client_id, args.includes('--takeover'));
274
+ try {
275
+ const result = await call(socket, verb === 'integrate' ? `/v1/workers/${id}/integrate` : `/v1/integrations/${id}/resolve`, {
276
+ client_id,
277
+ token: lease.token,
278
+ request_id: option('--request-id') ?? randomUUID(),
279
+ workspace_revision: Number(revision),
280
+ workspace_digest,
281
+ patch_digest,
282
+ });
283
+ console.log(JSON.stringify(result, null, 2));
284
+ }
285
+ finally {
286
+ await call(socket, `/v1/sessions/${owner}/control`, {
287
+ request_id: randomUUID(),
288
+ client_id,
289
+ token: lease.token,
290
+ action: 'release',
291
+ expected_epoch: lease.epoch,
292
+ }).catch(() => { });
293
+ }
294
+ return;
295
+ }
296
+ if (group === 'team') {
297
+ if (!id)
298
+ throw new Error('Supply TASK_ID');
299
+ if (verb === 'show') {
300
+ console.log(JSON.stringify(await call(socket, `/v1/tasks/${id}/team`), null, 2));
301
+ return;
302
+ }
303
+ if (!['assign', 'run', 'reject', 'send', 'receive', 'ack'].includes(verb ?? ''))
304
+ throw new Error('Use team show, assign, run, send, receive or ack');
305
+ const task = await call(socket, `/v1/tasks/${id}`);
306
+ const client_id = randomUUID(), lease = await acquireControl(socket, task.session_id, client_id, args.includes('--takeover'));
307
+ try {
308
+ const assignment = option('--assignment');
309
+ if (['run', 'reject', 'receive', 'ack'].includes(verb) && !assignment)
310
+ throw new Error('Supply --assignment ID');
311
+ const revision = option('--revision'), generation = option('--generation');
312
+ if (['assign', 'run', 'reject'].includes(verb) && revision === undefined)
313
+ throw new Error('Supply the reviewed --revision N');
314
+ if (['run', 'reject'].includes(verb) && generation === undefined)
315
+ throw new Error('Supply the reviewed --generation N');
316
+ const path = assignment ? `${assignment}/${verb}` : verb;
317
+ const result = await call(socket, `/v1/tasks/${id}/team/${path}`, {
318
+ client_id,
319
+ token: lease.token,
320
+ request_id: option('--request-id') ?? randomUUID(),
321
+ expected_revision: revision === undefined ? undefined : Number(revision),
322
+ generation: generation === undefined ? undefined : Number(generation),
323
+ ...(verb === 'assign'
324
+ ? {
325
+ step_id: option('--step'),
326
+ profile_id: option('--profile'),
327
+ ...(() => {
328
+ const runtime = option('--runtime');
329
+ return runtime
330
+ ? { runtime, invocations: Number(option('--invocations')) }
331
+ : { requests: Number(option('--requests')) };
332
+ })(),
333
+ }
334
+ : {}),
335
+ ...(verb === 'run'
336
+ ? { instruction: option('--instruction'), continues_attempt_id: option('--continues') }
337
+ : {}),
338
+ ...(verb === 'send' ? { recipient: option('--to'), text: option('--text') } : {}),
339
+ ...(verb === 'ack' ? { message_id: option('--message') } : {}),
340
+ ...(verb === 'reject' ? { reason: option('--reason') } : {}),
341
+ });
342
+ console.log(JSON.stringify(result, null, 2));
343
+ }
344
+ finally {
345
+ await call(socket, `/v1/sessions/${task.session_id}/control`, {
346
+ request_id: randomUUID(),
347
+ client_id,
348
+ token: lease.token,
349
+ action: 'release',
350
+ expected_epoch: lease.epoch,
351
+ }).catch(() => { });
352
+ }
353
+ return;
354
+ }
355
+ if (group === 'agents') {
356
+ const session = option('--session');
357
+ if (!session)
358
+ throw new Error('Supply --session SESSION_ID');
359
+ if (verb === 'list' || verb === 'inspect') {
360
+ console.log(JSON.stringify(await call(socket, `/v1/sessions/${session}/agents${verb === 'inspect' ? '?profile=' + encodeURIComponent(id ?? '') : ''}`), null, 2));
361
+ return;
362
+ }
363
+ if (!id || !['admit', 'revoke'].includes(verb ?? ''))
364
+ throw new Error('Use agents list, inspect, admit or revoke');
365
+ const client_id = randomUUID(), lease = await acquireControl(socket, session, client_id, args.includes('--takeover'));
366
+ try {
367
+ const expected_digest = option('--digest');
368
+ if (verb === 'admit' && !expected_digest)
369
+ throw new Error('Review agents inspect and supply --digest SHA256');
370
+ const fact_ids = option('--facts')?.split(',').filter(Boolean) ?? [];
371
+ const result = await call(socket, `/v1/sessions/${session}/agents/${verb}`, {
372
+ request_id: option('--request-id') ?? randomUUID(),
373
+ client_id,
374
+ token: lease.token,
375
+ ...(verb === 'admit' ? { profile_id: id, expected_digest, fact_ids } : { grant_id: id }),
376
+ });
377
+ console.log(JSON.stringify(result, null, 2));
378
+ }
379
+ finally {
380
+ await call(socket, `/v1/sessions/${session}/control`, {
381
+ request_id: randomUUID(),
382
+ client_id,
383
+ token: lease.token,
384
+ action: 'release',
385
+ expected_epoch: lease.epoch,
386
+ }).catch(() => { });
387
+ }
388
+ return;
389
+ }
57
390
  if (group === 'host') {
58
391
  const { planHost, readHost, hostUnit, runHost } = await import('./host.js');
59
392
  if (verb === 'plan') {
@@ -262,11 +595,127 @@ async function main() {
262
595
  await acp(state, { coding: args.includes('--coding') });
263
596
  return;
264
597
  }
265
- if (group === 'auth' && verb === 'set' && id === 'anthropic') {
266
- saveApiKey(await readSecret());
267
- console.log('Claude API key saved to ' + credentialPath() + ' (mode 600).');
598
+ if (group === 'providers' && verb === 'list') {
599
+ console.log(JSON.stringify(PI_PROVIDERS.map((p) => ({
600
+ id: p.id,
601
+ name: p.name,
602
+ chat_models: p.getModels().length,
603
+ apis: [...new Set(p.getModels().map((m) => m.api))],
604
+ auth: [p.auth.apiKey ? 'api_key' : null, p.auth.oauth ? 'oauth' : null].filter(Boolean),
605
+ limitation: !p.getModels().length
606
+ ? 'Classifier only; no chat route'
607
+ : p.id === 'openai-codex'
608
+ ? 'No provider-enforced output cap; tasks must explicitly disable token budgeting'
609
+ : null,
610
+ })), null, 2));
268
611
  return;
269
612
  }
613
+ if (group === 'models' && verb === 'list') {
614
+ const providers = id ? [getProvider(id)] : PI_PROVIDERS;
615
+ if (providers.some((p) => !p))
616
+ throw new Error('Unknown Pi provider');
617
+ console.log(JSON.stringify(providers.flatMap((p) => p.getModels().map((m) => ({
618
+ provider: p.id,
619
+ id: m.id,
620
+ name: m.name,
621
+ api: m.api,
622
+ context_window: m.contextWindow,
623
+ max_output_tokens: m.maxTokens,
624
+ }))), null, 2));
625
+ return;
626
+ }
627
+ if (group === 'auth') {
628
+ const store = new ProviderCredentialStore();
629
+ if (verb === 'status') {
630
+ const models = providerModels(store), entries = id ? [getProvider(id)] : PI_PROVIDERS;
631
+ if (entries.some((p) => !p))
632
+ throw new Error('Unknown Pi provider');
633
+ const statuses = [];
634
+ for (const p of entries) {
635
+ try {
636
+ const auth = await models.checkAuth(p.id);
637
+ statuses.push({
638
+ provider: p.id,
639
+ configured: !!auth,
640
+ type: auth?.type ?? null,
641
+ source: auth?.source ?? null,
642
+ });
643
+ }
644
+ catch {
645
+ statuses.push({ provider: p.id, configured: false, error: 'Credential check failed' });
646
+ }
647
+ }
648
+ console.log(JSON.stringify(statuses, null, 2));
649
+ return;
650
+ }
651
+ if (!id || !getProvider(id))
652
+ throw new Error('Choose a provider from salaam providers list');
653
+ if (verb === 'logout') {
654
+ await store.delete(id);
655
+ console.log('Stored credential removed for ' + id + '. Environment credentials remain external.');
656
+ return;
657
+ }
658
+ if (verb === 'set') {
659
+ if (!getProvider(id).auth.apiKey)
660
+ throw new Error('This provider needs salaam auth login ' + id + ' --oauth');
661
+ const key = (await readSecret(id + ' API key (hidden): ')).trim();
662
+ if (!key ||
663
+ key.length > 4096 ||
664
+ /[^\x21-\x7e]/.test(key) ||
665
+ (id === 'anthropic' && key.includes('sk-ant-oat')))
666
+ throw new Error('Supply a provider API key, not an OAuth token');
667
+ if (id === 'anthropic')
668
+ validateApiKey(key);
669
+ await store.modify(id, async () => ({ type: 'api_key', key }));
670
+ console.log('API key saved privately for ' + id + ' under ' + authDirectory() + ' (mode 600).');
671
+ return;
672
+ }
673
+ if (verb === 'login') {
674
+ if (!process.stdin.isTTY)
675
+ throw new Error('Interactive login needs a terminal; use auth set PROVIDER for a piped API key');
676
+ const provider = getProvider(id);
677
+ const type = args.includes('--oauth') || !provider.auth.apiKey ? 'oauth' : 'api_key';
678
+ const interaction = {
679
+ async prompt(prompt) {
680
+ if (prompt.type === 'secret')
681
+ return readSecret(prompt.message + ' (hidden): ', prompt.signal);
682
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
683
+ try {
684
+ const choices = prompt.type === 'select'
685
+ ? '\n' + prompt.options.map((v, i) => `${i + 1}. ${v.label} (${v.id})`).join('\n')
686
+ : '';
687
+ const answer = await rl.question(prompt.message + choices + '\n> ', {
688
+ signal: prompt.signal,
689
+ });
690
+ return prompt.type === 'select' && /^\d+$/.test(answer)
691
+ ? (prompt.options[Number(answer) - 1]?.id ?? answer)
692
+ : answer;
693
+ }
694
+ finally {
695
+ rl.close();
696
+ }
697
+ },
698
+ notify(event) {
699
+ if (event.type === 'auth_url')
700
+ console.error('Open in your browser: ' +
701
+ event.url +
702
+ (event.instructions ? '\n' + event.instructions : ''));
703
+ else if (event.type === 'device_code')
704
+ console.error('Open ' + event.verificationUri + ' and enter ' + event.userCode);
705
+ else
706
+ console.error(event.message);
707
+ },
708
+ };
709
+ await loginProvider(id, type, interaction);
710
+ console.log('Signed in to ' +
711
+ id +
712
+ ' with ' +
713
+ type +
714
+ '. Restart the service to use a different provider route.');
715
+ return;
716
+ }
717
+ throw new Error('Run salaam auth --help');
718
+ }
270
719
  if (group === 'schedules' && verb) {
271
720
  if (verb === 'list' || (id && ['inspect', 'occurrences'].includes(verb))) {
272
721
  console.log(JSON.stringify(await call(socket, verb === 'list'
@@ -705,20 +1154,32 @@ async function main() {
705
1154
  const model = option('--model');
706
1155
  const cap = option('--max-output-tokens');
707
1156
  const workspace = option('--workspace-id');
708
- const route = providerRoute(provider, model, cap === undefined ? undefined : Number(cap), workspace);
1157
+ let route = providerRoute(provider, model, cap === undefined ? undefined : Number(cap), workspace);
1158
+ if (route.provider !== 'salaam-proof') {
1159
+ const auth = await resolveProviderAuth(route);
1160
+ if (cap !== undefined && !providerHasOutputCap(route, auth))
1161
+ throw new Error('This authentication cannot enforce --max-output-tokens; omit it and explicitly disable token budgeting in task plans');
1162
+ route = bindProviderConfiguration(route, auth);
1163
+ }
709
1164
  const effectSocket = option('--effect-socket');
710
1165
  const opaque = args.includes('--effect-opaque');
711
- const codingRoot = option('--coding-root'), codingSocket = option('--coding-socket');
1166
+ const codingRoot = option('--coding-root'), codingSocket = option('--coding-socket'), harnessConfig = option('--harness-config'), a2aConfig = option('--a2a-config'), a2aListenConfig = option('--a2a-listen-config');
712
1167
  if (foreground) {
713
1168
  const service = await SalaamService.open({
714
1169
  stateDir: state,
715
1170
  providerRoute: route,
716
1171
  codingRoot: codingRoot ? resolve(codingRoot) : undefined,
717
1172
  codingSocket: codingSocket ? resolve(codingSocket) : undefined,
1173
+ harnessConfig: harnessConfig ? resolve(harnessConfig) : undefined,
1174
+ a2aConfig: a2aConfig ? resolve(a2aConfig) : undefined,
1175
+ a2aListenConfig: a2aListenConfig ? resolve(a2aListenConfig) : undefined,
718
1176
  effectConnector: effectSocket
719
1177
  ? new SocketEffectConnector(resolve(effectSocket), { opaque })
720
1178
  : undefined,
721
1179
  });
1180
+ const warning = service.supervisor.providerCapabilities().warning;
1181
+ if (warning)
1182
+ console.error(warning);
722
1183
  process.stdout.write(JSON.stringify({ socket, service: 'started' }) + '\n');
723
1184
  for (const signal of ['SIGINT', 'SIGTERM'])
724
1185
  process.once(signal, () => {
@@ -744,8 +1205,6 @@ async function main() {
744
1205
  console.log('Salaam service already running');
745
1206
  return;
746
1207
  }
747
- if (route.provider === 'anthropic')
748
- loadApiKey();
749
1208
  mkdirSync(state, { recursive: true, mode: 0o700 });
750
1209
  const fd = openSync(join(state, 'service.log'), 'a', 0o600);
751
1210
  const child = spawn(process.execPath, [
@@ -764,12 +1223,17 @@ async function main() {
764
1223
  ...(opaque ? ['--effect-opaque'] : []),
765
1224
  ...(codingRoot ? ['--coding-root', resolve(codingRoot)] : []),
766
1225
  ...(codingSocket ? ['--coding-socket', resolve(codingSocket)] : []),
1226
+ ...(harnessConfig ? ['--harness-config', resolve(harnessConfig)] : []),
1227
+ ...(a2aConfig ? ['--a2a-config', resolve(a2aConfig)] : []),
1228
+ ...(a2aListenConfig ? ['--a2a-listen-config', resolve(a2aListenConfig)] : []),
767
1229
  ], { detached: true, stdio: ['ignore', fd, fd] });
768
1230
  closeSync(fd);
769
1231
  child.unref();
770
1232
  for (let i = 0; i < 150; i++) {
771
1233
  try {
772
- await call(socket, '/v1/health');
1234
+ const health = await call(socket, '/v1/health');
1235
+ if (health.provider_capabilities?.warning)
1236
+ console.error(health.provider_capabilities.warning);
773
1237
  console.log(JSON.stringify({ socket, service: 'started' }));
774
1238
  return;
775
1239
  }
@@ -2,6 +2,7 @@ import { readFileSync, existsSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { digest, textDigest } from './protocol.js';
5
+ import { PI_PROVIDERS } from './provider-config.js';
5
6
  import { packageDigest } from './package-digest.js';
6
7
  let root = dirname(fileURLToPath(import.meta.url));
7
8
  while (!existsSync(join(root, 'pi-pin.json'))) {
@@ -16,7 +17,7 @@ export const PI_ROOT = resolve(dirname(fileURLToPath(import.meta.resolve('@earen
16
17
  export const PI_CLI = join(PI_ROOT, 'dist/bundle/cli.js');
17
18
  export const HOUSE_EXTENSION = join(ROOT, 'dist/src/house-extension.js');
18
19
  export const POLICY = {
19
- version: 6,
20
+ version: 14,
20
21
  tools: [
21
22
  'salaam_probe',
22
23
  'salaam_review',
@@ -27,6 +28,9 @@ export const POLICY = {
27
28
  'salaam_history',
28
29
  'salaam_files',
29
30
  'salaam_command',
31
+ 'salaam_repository',
32
+ 'salaam_findings',
33
+ 'salaam_team',
30
34
  ],
31
35
  children: {
32
36
  maxDepth: 1,
@@ -34,13 +38,21 @@ export const POLICY = {
34
38
  parentRequestBudget: 16,
35
39
  maxChildRequestBudget: 8,
36
40
  authority: ['salaam_probe'],
41
+ worker_boundary: 'frozen-native-model-request-v1',
42
+ specialist_profiles: 'operator-admitted-snapshot-v1',
43
+ team_claims: 'task-step-generation-v1',
44
+ daemon_workers: 8,
45
+ messages: 'passive-durable-explicit-ack-v1',
46
+ writing: 'owned-draft-explicit-hash-bound-integration-v1',
47
+ external: 'operator-pinned-whole-runtime-explicit-invocation-v1',
48
+ a2a: 'pinned-peer-one-turn-analysis-explicit-invocation-v1',
37
49
  },
38
50
  providers: [
39
51
  'salaam-proof/scripted',
40
- 'anthropic/claude-sonnet-5-5',
41
- 'anthropic/claude-sonnet-4-6',
52
+ ...PI_PROVIDERS.flatMap((provider) => provider.getModels().map((model) => provider.id + '/' + model.id)).sort(),
42
53
  ],
43
54
  provider_retries: 0,
55
+ task_token_budget: 'capped-default-explicit-null-opt-out-v1',
44
56
  compaction: { strategy: 'explicit-task-checkpoint-v1', automatic: false, keepRecentTokens: 512 },
45
57
  external_effects: 'explicit-connector-and-operation-approval',
46
58
  coding: {
@@ -72,9 +84,20 @@ export function bundleDigest() {
72
84
  policy: POLICY_DIGEST,
73
85
  lock: textDigest(readFileSync(lockPath, 'utf8')),
74
86
  house: textDigest(readFileSync(HOUSE_EXTENSION, 'utf8')),
87
+ provider_usage: textDigest(readFileSync(join(ROOT, 'dist/src/provider-usage.js'), 'utf8')),
88
+ provider_model: textDigest(readFileSync(join(ROOT, 'dist/src/provider-model.js'), 'utf8')),
89
+ live_provider: textDigest(readFileSync(join(ROOT, 'dist/src/live-provider.js'), 'utf8')),
90
+ provider_payload: textDigest(readFileSync(join(ROOT, 'dist/src/provider-payload.js'), 'utf8')),
91
+ provider_transport: textDigest(readFileSync(join(ROOT, 'dist/src/provider-transport.js'), 'utf8')),
92
+ provider_auth: textDigest(readFileSync(join(ROOT, 'dist/src/provider-auth.js'), 'utf8')),
75
93
  claude: textDigest(readFileSync(join(ROOT, 'dist/src/claude-provider.js'), 'utf8')),
76
94
  provider_config: textDigest(readFileSync(join(ROOT, 'dist/src/provider-config.js'), 'utf8')),
77
95
  protocol: textDigest(readFileSync(join(ROOT, 'dist/src/protocol.js'), 'utf8')),
96
+ worker_boundary: textDigest(readFileSync(join(ROOT, 'dist/src/worker-boundary.js'), 'utf8')),
97
+ agent_profiles: textDigest(readFileSync(join(ROOT, 'dist/src/agent-profiles.js'), 'utf8')),
98
+ team_work: textDigest(readFileSync(join(ROOT, 'dist/src/team-work.js'), 'utf8')),
99
+ worker_workspaces: textDigest(readFileSync(join(ROOT, 'dist/src/worker-workspaces.js'), 'utf8')),
100
+ repository_snapshots: textDigest(readFileSync(join(ROOT, 'dist/src/repository-snapshots.js'), 'utf8')),
78
101
  transport: textDigest(readFileSync(join(ROOT, 'dist/src/transport.js'), 'utf8')),
79
102
  });
80
103
  }
@@ -58,7 +58,7 @@ export function saveApiKey(value, path = credentialPath()) {
58
58
  closeSync(directory);
59
59
  }
60
60
  }
61
- export async function readSecret() {
61
+ export async function readSecret(label = 'Claude API key (hidden): ', signal) {
62
62
  if (!process.stdin.isTTY) {
63
63
  let input = '';
64
64
  for await (const chunk of process.stdin) {
@@ -68,13 +68,14 @@ export async function readSecret() {
68
68
  }
69
69
  return input;
70
70
  }
71
- process.stderr.write('Claude API key (hidden): ');
71
+ process.stderr.write(label);
72
72
  const stdin = process.stdin, wasRaw = stdin.isRaw;
73
73
  stdin.setRawMode(true);
74
74
  stdin.resume();
75
75
  return new Promise((resolve, reject) => {
76
76
  let key = '';
77
77
  const done = (error) => {
78
+ signal?.removeEventListener('abort', aborted);
78
79
  stdin.off('data', data);
79
80
  stdin.setRawMode(wasRaw);
80
81
  stdin.pause();
@@ -103,6 +104,10 @@ export async function readSecret() {
103
104
  }
104
105
  }
105
106
  };
107
+ const aborted = () => done(new Error('Credential entry cancelled'));
108
+ signal?.addEventListener('abort', aborted, { once: true });
106
109
  stdin.on('data', data);
110
+ if (signal?.aborted)
111
+ aborted();
107
112
  });
108
113
  }
@@ -0,0 +1 @@
1
+ export {};