@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
@@ -0,0 +1,441 @@
1
+ import {
2
+ readFileSync,
3
+ lstatSync,
4
+ realpathSync,
5
+ openSync,
6
+ closeSync,
7
+ fstatSync,
8
+ constants,
9
+ } from 'node:fs';
10
+ import { request as httpRequest } from 'node:http';
11
+ import { request as httpsRequest } from 'node:https';
12
+ import { isIP, BlockList } from 'node:net';
13
+ import { timingSafeEqual, randomUUID } from 'node:crypto';
14
+ import { digest, ServiceError } from './protocol.js';
15
+ import { repositoryPath } from './repository-snapshots.js';
16
+
17
+ export const A2A_PIN = {
18
+ specification: '1.0.0',
19
+ wire: '1.0',
20
+ sdk: '1.0.1',
21
+ transport: 'JSONRPC',
22
+ } as const;
23
+ export type A2AObject = Record<string, any>;
24
+ export interface A2APeer {
25
+ id: string;
26
+ endpoint: string;
27
+ card_url: string;
28
+ card_digest: string;
29
+ tenant: string;
30
+ skill: string;
31
+ addresses: string[];
32
+ local: boolean;
33
+ credential_file: string;
34
+ credential_digest: string;
35
+ files: string[];
36
+ retention: 'volatile' | 'durable' | 'unknown';
37
+ cancel: boolean;
38
+ streaming: boolean;
39
+ }
40
+ const fail = (message: string): never => {
41
+ throw new ServiceError('a2a_contract', message);
42
+ };
43
+ export function a2aObject(value: unknown): A2AObject {
44
+ if (!value || typeof value !== 'object' || Array.isArray(value))
45
+ return fail('Expected an A2A object');
46
+ return value as A2AObject;
47
+ }
48
+ export function a2aIdentity(value: unknown): string {
49
+ if (
50
+ typeof value !== 'string' ||
51
+ !value.length ||
52
+ value.length > 256 ||
53
+ /[\x00-\x1f\x7f]/.test(value)
54
+ )
55
+ return fail('Invalid A2A identity');
56
+ return value;
57
+ }
58
+ export function privateJSON(path: string): unknown {
59
+ if (realpathSync(path) !== path)
60
+ return fail('Configuration must use its canonical absolute path');
61
+ const s = lstatSync(path);
62
+ if (
63
+ !s.isFile() ||
64
+ s.nlink !== 1 ||
65
+ s.size > 262144 ||
66
+ s.mode & 0o077 ||
67
+ s.uid !== process.getuid?.()
68
+ )
69
+ return fail('Peer configuration and credentials must be owner-only regular files');
70
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
71
+ try {
72
+ const before = fstatSync(fd);
73
+ if (
74
+ before.dev !== s.dev ||
75
+ before.ino !== s.ino ||
76
+ before.nlink !== 1 ||
77
+ before.size !== s.size ||
78
+ before.uid !== s.uid ||
79
+ before.mode !== s.mode
80
+ )
81
+ return fail('Peer configuration changed during admission');
82
+ const bytes = readFileSync(fd, 'utf8'),
83
+ after = fstatSync(fd);
84
+ if (
85
+ after.size !== before.size ||
86
+ after.mtimeMs !== before.mtimeMs ||
87
+ after.ctimeMs !== before.ctimeMs
88
+ )
89
+ return fail('Peer configuration changed while reading');
90
+ return JSON.parse(bytes);
91
+ } finally {
92
+ closeSync(fd);
93
+ }
94
+ }
95
+ export function peerCredential(peer: A2APeer): string {
96
+ const value = a2aObject(privateJSON(peer.credential_file));
97
+ if (
98
+ Object.keys(value).sort().join(',') !== 'peer,token' ||
99
+ value.peer !== peer.id ||
100
+ typeof value.token !== 'string' ||
101
+ !/^[A-Za-z0-9._~-]{32,512}$/.test(value.token) ||
102
+ digest(value) !== peer.credential_digest
103
+ )
104
+ return fail('Peer credential identity changed; renew admission');
105
+ return value.token;
106
+ }
107
+ export function equalToken(a: string, b: string): boolean {
108
+ const x = Buffer.from(a),
109
+ y = Buffer.from(b);
110
+ return x.length === y.length && timingSafeEqual(x, y);
111
+ }
112
+ export function validatePeer(value: unknown): A2APeer {
113
+ const p = a2aObject(value);
114
+ const keys = [
115
+ 'id',
116
+ 'endpoint',
117
+ 'card_url',
118
+ 'card_digest',
119
+ 'tenant',
120
+ 'skill',
121
+ 'addresses',
122
+ 'local',
123
+ 'credential_file',
124
+ 'credential_digest',
125
+ 'files',
126
+ 'retention',
127
+ 'cancel',
128
+ 'streaming',
129
+ ];
130
+ if (Object.keys(p).sort().join(',') !== keys.sort().join(','))
131
+ return fail('Unknown or missing peer configuration field');
132
+ for (const key of ['id', 'skill']) a2aIdentity(p[key]);
133
+ if (typeof p.tenant !== 'string' || p.tenant.length > 256 || /[\x00-\x1f\x7f]/.test(p.tenant))
134
+ return fail('Invalid tenant');
135
+ for (const key of ['card_digest', 'credential_digest'])
136
+ if (!/^[a-f0-9]{64}$/.test(p[key])) return fail('Expected reviewed SHA-256 digest');
137
+ if (
138
+ typeof p.local !== 'boolean' ||
139
+ typeof p.cancel !== 'boolean' ||
140
+ typeof p.streaming !== 'boolean' ||
141
+ !['volatile', 'durable', 'unknown'].includes(p.retention)
142
+ )
143
+ return fail('Explicit peer capability and retention policy required');
144
+ const endpoint = new URL(p.endpoint),
145
+ card = new URL(p.card_url);
146
+ for (const u of [endpoint, card]) {
147
+ if (
148
+ u.username ||
149
+ u.password ||
150
+ u.search ||
151
+ u.hash ||
152
+ u.href !== (u === endpoint ? p.endpoint : p.card_url)
153
+ )
154
+ return fail('Canonical peer URLs cannot contain credentials, queries or fragments');
155
+ if (
156
+ u.protocol !== 'https:' &&
157
+ !(p.local && u.protocol === 'http:' && ['127.0.0.1', '[::1]'].includes(u.hostname))
158
+ )
159
+ return fail('HTTPS required except explicitly configured loopback rehearsal');
160
+ }
161
+ if (endpoint.origin !== card.origin) return fail('Agent Card must share the configured origin');
162
+ const denied = new BlockList();
163
+ denied.addSubnet('169.254.0.0', 16);
164
+ denied.addSubnet('0.0.0.0', 8);
165
+ denied.addSubnet('224.0.0.0', 4);
166
+ denied.addSubnet('fe80::', 10, 'ipv6');
167
+ denied.addSubnet('ff00::', 8, 'ipv6');
168
+ denied.addAddress('::', 'ipv6');
169
+ if (
170
+ !Array.isArray(p.addresses) ||
171
+ !p.addresses.length ||
172
+ p.addresses.length > 8 ||
173
+ new Set(p.addresses).size !== p.addresses.length
174
+ )
175
+ return fail('Pin explicit destination IP addresses');
176
+ for (const address of p.addresses) {
177
+ const family = isIP(address);
178
+ if (
179
+ !family ||
180
+ address.includes('%') ||
181
+ denied.check(address, family === 6 ? 'ipv6' : 'ipv4') ||
182
+ (!p.local && (address.startsWith('127.') || address === '::1'))
183
+ )
184
+ return fail('Forbidden peer destination');
185
+ if (family === 6 && address.toLowerCase().includes('ffff:'))
186
+ return fail('Mapped destination addresses are unsupported');
187
+ if (p.local && !['127.0.0.1', '::1'].includes(address))
188
+ return fail('Local rehearsal must remain on loopback');
189
+ }
190
+ if (
191
+ isIP(endpoint.hostname.replace(/^\[|\]$/g, '')) &&
192
+ !p.addresses.includes(endpoint.hostname.replace(/^\[|\]$/g, ''))
193
+ )
194
+ return fail('URL literal must match the pinned address');
195
+ if (!Array.isArray(p.files) || p.files.length > 64 || new Set(p.files).size !== p.files.length)
196
+ return fail('Bound explicit disclosure paths');
197
+ for (const path of p.files) repositoryPath(path);
198
+ peerCredential(p as A2APeer);
199
+ return structuredClone(p) as A2APeer;
200
+ }
201
+ /** No redirects, DNS fallback, cookies, inherited proxy or provider credentials. */
202
+ export class A2AHttp {
203
+ constructor(readonly peer: A2APeer) {}
204
+ async request(url: string, body?: unknown): Promise<A2AObject> {
205
+ const p = validatePeer(this.peer),
206
+ u = new URL(url);
207
+ if (url !== p.endpoint && url !== p.card_url) return fail('Unconfigured A2A destination');
208
+ const bytes = body === undefined ? undefined : Buffer.from(JSON.stringify(body));
209
+ if (bytes && bytes.length > 262144) return fail('A2A disclosure exceeds request bound');
210
+ const token = peerCredential(p);
211
+ return new Promise((resolve, reject) => {
212
+ const request = (u.protocol === 'https:' ? httpsRequest : httpRequest)(
213
+ u,
214
+ {
215
+ method: bytes ? 'POST' : 'GET',
216
+ agent: false,
217
+ headers: {
218
+ Authorization: `Bearer ${token}`,
219
+ 'A2A-Version': A2A_PIN.wire,
220
+ Accept: 'application/json',
221
+ ...(bytes
222
+ ? { 'Content-Type': 'application/json', 'Content-Length': String(bytes.length) }
223
+ : {}),
224
+ },
225
+ lookup: (_hostname, options, callback) => {
226
+ const address = p.addresses[0]!;
227
+ if (typeof options === 'object' && options.all)
228
+ callback(null, [{ address, family: isIP(address) }]);
229
+ else callback(null, address, isIP(address));
230
+ },
231
+ },
232
+ (response) => {
233
+ if (
234
+ response.statusCode !== 200 ||
235
+ !/^application\/json(?:;|$)/i.test(String(response.headers['content-type']))
236
+ ) {
237
+ response.destroy();
238
+ reject(Error('Peer returned an unsupported status or media type'));
239
+ return;
240
+ }
241
+ if (response.headers['a2a-version'] && response.headers['a2a-version'] !== A2A_PIN.wire) {
242
+ response.destroy();
243
+ reject(Error('Peer wire version changed'));
244
+ return;
245
+ }
246
+ let size = 0;
247
+ const chunks: Buffer[] = [];
248
+ response.on('data', (chunk: Buffer) => {
249
+ size += chunk.length;
250
+ if (size > 1048576) response.destroy(Error('Peer response exceeds bound'));
251
+ else chunks.push(chunk);
252
+ });
253
+ response.on('error', reject);
254
+ response.on('end', () => {
255
+ try {
256
+ resolve(a2aObject(JSON.parse(Buffer.concat(chunks).toString('utf8'))));
257
+ } catch (e) {
258
+ reject(e);
259
+ }
260
+ });
261
+ },
262
+ );
263
+ const timeout = body && a2aObject(body).method === 'SendMessage' ? 60000 : 10000;
264
+ const deadline = setTimeout(
265
+ () =>
266
+ request.destroy(
267
+ Error('A2A absolute request deadline exceeded; acceptance may be unknown'),
268
+ ),
269
+ timeout,
270
+ );
271
+ request.once('close', () => clearTimeout(deadline));
272
+ request.setTimeout(timeout, () =>
273
+ request.destroy(Error('A2A request timed out; acceptance may be unknown')),
274
+ );
275
+ request.on('error', reject);
276
+ request.end(bytes);
277
+ });
278
+ }
279
+ async card(): Promise<A2AObject> {
280
+ return this.assertCard(await this.request(this.peer.card_url));
281
+ }
282
+ assertCard(card: A2AObject): A2AObject {
283
+ if (digest(card) !== this.peer.card_digest)
284
+ return fail('Agent Card changed; operator review required');
285
+ if (
286
+ !Array.isArray(card.supportedInterfaces) ||
287
+ !card.supportedInterfaces.length ||
288
+ card.supportedInterfaces.some(
289
+ (i: any) =>
290
+ i.url !== this.peer.endpoint ||
291
+ i.protocolBinding !== 'JSONRPC' ||
292
+ i.protocolVersion !== '1.0' ||
293
+ (i.tenant ?? '') !== this.peer.tenant,
294
+ )
295
+ )
296
+ return fail('Agent Card interface differs from configured peer/version/tenant');
297
+ if (!Array.isArray(card.skills) || !card.skills.some((s: any) => s.id === this.peer.skill))
298
+ return fail('Configured peer skill is absent');
299
+ const caps = a2aObject(card.capabilities);
300
+ if (this.peer.streaming && caps.streaming !== true) return fail('Streaming capability changed');
301
+ return card;
302
+ }
303
+ async subscribe(task: string, consume: (event: A2AObject) => void): Promise<void> {
304
+ const p = validatePeer(this.peer),
305
+ u = new URL(p.endpoint),
306
+ id = randomUUID();
307
+ const bytes = Buffer.from(
308
+ JSON.stringify({
309
+ jsonrpc: '2.0',
310
+ id,
311
+ method: 'SubscribeToTask',
312
+ params: { id: task, tenant: p.tenant },
313
+ }),
314
+ );
315
+ await new Promise<void>((resolve, reject) => {
316
+ const request = (u.protocol === 'https:' ? httpsRequest : httpRequest)(
317
+ u,
318
+ {
319
+ method: 'POST',
320
+ agent: false,
321
+ headers: {
322
+ Authorization: `Bearer ${peerCredential(p)}`,
323
+ 'A2A-Version': '1.0',
324
+ Accept: 'text/event-stream',
325
+ 'Content-Type': 'application/json',
326
+ 'Content-Length': String(bytes.length),
327
+ },
328
+ lookup: (_hostname, options, callback) => {
329
+ const address = p.addresses[0]!;
330
+ if (typeof options === 'object' && options.all)
331
+ callback(null, [{ address, family: isIP(address) }]);
332
+ else callback(null, address, isIP(address));
333
+ },
334
+ },
335
+ (response) => {
336
+ if (
337
+ response.statusCode !== 200 ||
338
+ !/^text\/event-stream(?:;|$)/i.test(String(response.headers['content-type'])) ||
339
+ (response.headers['a2a-version'] && response.headers['a2a-version'] !== '1.0')
340
+ ) {
341
+ response.destroy();
342
+ reject(Error('Peer streaming unavailable'));
343
+ return;
344
+ }
345
+ const decoder = new TextDecoder('utf-8', { fatal: true });
346
+ let buffer = '',
347
+ size = 0,
348
+ events = 0,
349
+ terminalSeen = false,
350
+ done = false;
351
+ response.on('error', reject);
352
+ response.on('data', (chunk: Buffer) => {
353
+ try {
354
+ size += chunk.length;
355
+ if (size > 1048576) throw Error('Peer stream exceeds bound');
356
+ buffer += decoder.decode(chunk, { stream: true });
357
+ if (buffer.length > 524288) throw Error('Peer SSE frame exceeds bound');
358
+ for (;;) {
359
+ const match = /\r?\n\r?\n/.exec(buffer);
360
+ if (!match) break;
361
+ const frame = buffer.slice(0, match.index);
362
+ buffer = buffer.slice(match.index + match[0].length);
363
+ const data = frame
364
+ .split(/\r?\n/)
365
+ .filter((line) => line.startsWith('data:'))
366
+ .map((line) => line.slice(5).trimStart())
367
+ .join('\n');
368
+ if (!data) continue;
369
+ if (done)
370
+ throw new ServiceError('a2a_identity', 'Stream continued after its terminator');
371
+ if (data === '[DONE]') {
372
+ if (!terminalSeen)
373
+ throw new ServiceError('a2a_identity', 'Premature stream terminator');
374
+ done = true;
375
+ continue;
376
+ }
377
+ if (++events > 128) throw Error('Peer stream event bound exceeded');
378
+ const event = a2aObject(JSON.parse(data));
379
+ if (event.jsonrpc !== '2.0' || event.id !== id)
380
+ throw new ServiceError('a2a_identity', 'Unowned stream response');
381
+ if (event.error) throw Error('Peer stream operation failed');
382
+ const result = a2aObject(event.result);
383
+ consume(result);
384
+ if (
385
+ [
386
+ 'TASK_STATE_COMPLETED',
387
+ 'TASK_STATE_FAILED',
388
+ 'TASK_STATE_CANCELED',
389
+ 'TASK_STATE_REJECTED',
390
+ ].includes(result.statusUpdate?.status?.state ?? result.task?.status?.state)
391
+ )
392
+ terminalSeen = true;
393
+ }
394
+ } catch (e) {
395
+ response.destroy();
396
+ request.destroy();
397
+ reject(e);
398
+ }
399
+ });
400
+ response.on('end', () => {
401
+ try {
402
+ decoder.decode();
403
+ if (buffer.trim()) throw Error('Peer stream ended mid-frame');
404
+ resolve();
405
+ } catch (e) {
406
+ reject(e);
407
+ }
408
+ });
409
+ },
410
+ );
411
+ const deadline = setTimeout(
412
+ () => request.destroy(Error('A2A stream deadline; reconcile existing task')),
413
+ 15000,
414
+ );
415
+ request.once('close', () => clearTimeout(deadline));
416
+ request.on('error', reject);
417
+ request.end(bytes);
418
+ });
419
+ }
420
+ async rpc(
421
+ method: 'SendMessage' | 'GetTask' | 'CancelTask',
422
+ params: A2AObject,
423
+ ): Promise<A2AObject> {
424
+ const id = randomUUID(),
425
+ response = await this.request(this.peer.endpoint, {
426
+ jsonrpc: '2.0',
427
+ id,
428
+ method,
429
+ params: { ...params, tenant: this.peer.tenant },
430
+ });
431
+ if (
432
+ response.jsonrpc !== '2.0' ||
433
+ response.id !== id ||
434
+ Object.hasOwn(response, 'error') === Object.hasOwn(response, 'result')
435
+ )
436
+ return fail('Unowned or malformed A2A response');
437
+ if (response.error)
438
+ throw new ServiceError('a2a_remote_error', `Peer error ${Number(response.error.code)}`);
439
+ return a2aObject(response.result);
440
+ }
441
+ }