@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/children.ts CHANGED
@@ -3,14 +3,21 @@ import type { Store } from './store.js';
3
3
  import type { Artifacts } from './artifacts.js';
4
4
  import type { Supervisor } from './supervisor.js';
5
5
  import { digest, ServiceError, type Attempt, type ChildTask } from './protocol.js';
6
+ import type { RepositorySnapshots } from './repository-snapshots.js';
7
+ import type { WorkerWorkspaces } from './worker-workspaces.js';
8
+ import type { ExternalRuntime } from './external-runtime.js';
9
+ import type { ExternalWork } from './external-work.js';
6
10
 
7
11
  export class ChildWork {
12
+ external?: ExternalWork;
13
+ workspaces?: WorkerWorkspaces;
8
14
  private paused = false;
9
15
  private starts = new Set<Promise<void>>();
10
16
  constructor(
11
17
  readonly store: Store,
12
18
  private artifacts: Artifacts,
13
19
  private supervisor: Supervisor,
20
+ private repositories?: RepositorySnapshots,
14
21
  ) {
15
22
  store.finishChild = (attempt) => this.finish(attempt);
16
23
  store.cancelChildren = (attempt) => this.cancelIntents(attempt);
@@ -21,16 +28,18 @@ export class ChildWork {
21
28
  .get(session) as unknown as ChildTask | undefined;
22
29
  }
23
30
  list(parent: string): ChildTask[] {
24
- return this.store.db
25
- .prepare('SELECT * FROM child_tasks WHERE parent_session=? ORDER BY rowid')
26
- .all(parent) as unknown as ChildTask[];
31
+ return (
32
+ this.store.db
33
+ .prepare('SELECT * FROM child_tasks WHERE parent_session=? ORDER BY rowid')
34
+ .all(parent) as unknown as ChildTask[]
35
+ ).map((child) => ({ ...child, worker: this.store.workerBoundary.view(child.id) }));
27
36
  }
28
37
  inspect(parent: string, id: string): ChildTask {
29
38
  const row = this.store.db
30
39
  .prepare('SELECT * FROM child_tasks WHERE id=? AND parent_session=?')
31
40
  .get(id, parent) as unknown as ChildTask | undefined;
32
41
  if (!row) throw new ServiceError('not_found', 'Child task not found', 404);
33
- return row;
42
+ return { ...row, worker: this.store.workerBoundary.view(row.id) };
34
43
  }
35
44
  spawn(
36
45
  parent: string,
@@ -40,6 +49,13 @@ export class ChildWork {
40
49
  prompt: string,
41
50
  budget: number,
42
51
  requested: string[],
52
+ profileId?: string,
53
+ admission?: {
54
+ authorize: () => void;
55
+ defer: true;
56
+ assignment: { id: string; generation: number };
57
+ runtime?: ExternalRuntime;
58
+ },
43
59
  ): ChildTask {
44
60
  if (!Number.isSafeInteger(budget) || budget < 1 || budget > 8)
45
61
  throw new ServiceError('invalid_budget', 'Child request budget must be 1–8');
@@ -52,12 +68,18 @@ export class ChildWork {
52
68
  )
53
69
  throw new ServiceError('invalid_child', 'Invalid child step or prompt');
54
70
  // Current child authority is deliberately read-only and closed-world.
55
- const authority = requested
56
- .filter((x) => x === 'salaam_probe')
57
- .filter((x, i, a) => a.indexOf(x) === i)
58
- .sort();
59
- const input = digest({ prompt, budget, requested: [...new Set(requested)].sort() });
60
- const child = this.store.transaction(() => {
71
+ const input = digest({
72
+ prompt,
73
+ budget,
74
+ requested: [...new Set(requested)].sort(),
75
+ ...(profileId ? { profile: profileId } : {}),
76
+ ...(admission ? { assignment: admission.assignment } : {}),
77
+ ...(admission?.runtime ? { runtime: admission.runtime } : {}),
78
+ });
79
+ let fresh = false;
80
+ if (this.store.db.isTransaction && !admission?.defer)
81
+ throw new Error('Nested child dispatch requires an owned deferred admission');
82
+ const prepare = () => {
61
83
  const old = this.store.db
62
84
  .prepare('SELECT * FROM child_tasks WHERE parent_session=? AND step_key=?')
63
85
  .get(parent, step) as unknown as ChildTask | undefined;
@@ -67,21 +89,49 @@ export class ChildWork {
67
89
  return old;
68
90
  }
69
91
  if (this.paused) throw new ServiceError('stopping', 'Child dispatch is stopping');
70
- this.supervisor.assertActive(parent, epoch, attempt);
92
+ if (admission) admission.authorize();
93
+ else this.supervisor.assertActive(parent, epoch, attempt);
94
+ const agent = profileId
95
+ ? this.store.workerBoundary.profiles?.resolve(parent, profileId)
96
+ : undefined;
97
+ if (profileId && !agent)
98
+ throw new ServiceError('authority_denied', 'Agent profile is unavailable');
99
+ if (!admission?.runtime)
100
+ this.supervisor.assertSessionProviderSupported(parent, agent?.output_cap);
101
+ if (!admission?.runtime && agent && budget > (agent.profile.max_requests ?? 8))
102
+ throw new ServiceError('invalid_budget', 'Budget exceeds the admitted profile limit');
103
+ const available = [
104
+ ...(agent?.profile.tools ?? ['salaam_probe']),
105
+ ...(admission && !admission.runtime ? ['salaam_team'] : []),
106
+ ];
107
+ const authority = [
108
+ ...new Set(
109
+ agent && !requested.length ? available : requested.filter((x) => available.includes(x)),
110
+ ),
111
+ ].sort();
112
+ const effectivePrompt = agent
113
+ ? `${agent.profile.prompt ?? 'Run the diagnostic probe.'}${admission?.runtime ? '\n\nRuntime binding: use the admitted external harness tools for the corresponding file and command actions. Its private draft is /workspace. Return evidence for explicit root integration; this invocation cannot verify the root task.' : ''}\n\nGoal:\n${prompt}\n\nExplicitly selected facts:\n${JSON.stringify(agent.facts)}`
114
+ : prompt;
115
+ if (Buffer.byteLength(effectivePrompt) > 65536)
116
+ throw new ServiceError('invalid_child', 'Specialist context exceeds 64 KiB');
71
117
  if (this.owned(parent))
72
118
  throw new ServiceError('depth_exceeded', 'Child depth is limited to one');
73
119
  if (!authority.length)
74
120
  throw new ServiceError('authority_denied', 'No requested read-only authority is available');
75
- if (this.list(parent).filter((x) => x.state === 'accepted').length >= 2)
121
+ if (this.activeCount(parent) >= 2)
76
122
  throw new ServiceError('child_limit', 'At most two children may be active');
77
- this.store.usage.budget(attempt);
78
- const changed = this.store.db
79
- .prepare(
80
- 'UPDATE attempt_budgets SET reserved_requests=reserved_requests+? WHERE attempt_id=? AND spent_requests+reserved_requests+?<=limit_requests',
81
- )
82
- .run(budget, attempt, budget);
83
- if (!changed.changes)
84
- throw new ServiceError('budget_exhausted', 'Parent cannot reserve this child budget');
123
+ if (this.activeCount() >= 8)
124
+ throw new ServiceError('child_limit', 'Daemon worker admission limit reached');
125
+ if (!admission?.runtime) {
126
+ this.store.usage.budget(attempt);
127
+ const changed = this.store.db
128
+ .prepare(
129
+ 'UPDATE attempt_budgets SET reserved_requests=reserved_requests+? WHERE attempt_id=? AND spent_requests+reserved_requests+?<=limit_requests',
130
+ )
131
+ .run(budget, attempt, budget);
132
+ if (!changed.changes)
133
+ throw new ServiceError('budget_exhausted', 'Parent cannot reserve this child budget');
134
+ }
85
135
  const id = randomUUID(),
86
136
  session = randomUUID(),
87
137
  submission = randomUUID(),
@@ -91,7 +141,7 @@ export class ChildWork {
91
141
  .run(session, this.store.session(parent).cwd, session);
92
142
  this.store.db
93
143
  .prepare('INSERT INTO submissions(id,session_id,text,attempt_id) VALUES (?,?,?,?)')
94
- .run(submission, session, prompt, childAttempt);
144
+ .run(submission, session, effectivePrompt, childAttempt);
95
145
  this.store.db
96
146
  .prepare(
97
147
  "INSERT INTO attempts(id,session_id,submission_id,state) VALUES (?,?,?,'accepted')",
@@ -112,8 +162,39 @@ export class ChildWork {
112
162
  JSON.stringify(authority),
113
163
  budget,
114
164
  );
115
- this.store.taskChildReserve?.(parent, childAttempt, budget);
116
- this.store.usage.budget(childAttempt, budget);
165
+ if (!admission?.runtime) {
166
+ this.store.taskChildReserve?.(parent, childAttempt, budget);
167
+ this.store.usage.budget(childAttempt, budget);
168
+ }
169
+ const artifact = this.artifacts.prepare(session, effectivePrompt);
170
+ artifact.commit();
171
+ const repository = agent?.profile.role
172
+ ? this.repositories?.capture(parent, session)
173
+ : undefined;
174
+ if (agent?.profile.role && !repository)
175
+ throw new ServiceError('workspace_unavailable', 'No specialist snapshot boundary');
176
+ const writing = authority.some((tool) => ['salaam_files', 'salaam_command'].includes(tool));
177
+ const isolation =
178
+ (writing || admission?.runtime) && repository
179
+ ? this.workspaces?.prepare(session, repository)
180
+ : undefined;
181
+ if (writing && !isolation)
182
+ throw new ServiceError('authority_denied', 'No isolated coding boundary');
183
+ this.store.workerBoundary.freeze(
184
+ this.owned(session)!,
185
+ artifact.id,
186
+ false,
187
+ agent,
188
+ repository,
189
+ admission?.assignment,
190
+ isolation,
191
+ admission?.runtime,
192
+ );
193
+ if (admission?.runtime) {
194
+ if (!this.external)
195
+ throw new ServiceError('runtime_unavailable', 'External runtime is unavailable');
196
+ this.external.prepare(this.owned(session)!);
197
+ }
117
198
  this.store.event(session, 'session.created', { parent_session: parent, child_id: id });
118
199
  this.store.event(session, 'submission.accepted', {
119
200
  attempt_id: childAttempt,
@@ -126,19 +207,42 @@ export class ChildWork {
126
207
  budget,
127
208
  authority,
128
209
  });
210
+ fresh = true;
129
211
  return this.inspect(parent, id);
130
- });
212
+ };
213
+ const child = this.store.db.isTransaction ? prepare() : this.store.transaction(prepare);
131
214
  // Repeated calls only return the durable binding. They never restart a child.
132
215
  if (
216
+ fresh &&
217
+ !admission?.defer &&
133
218
  this.store.session(child.child_session).epoch === 0 &&
134
219
  this.store.attempt(child.child_attempt).state === 'accepted'
135
220
  )
136
221
  this.start(child);
137
222
  return child;
138
223
  }
224
+ activeCount(parent?: string): number {
225
+ return Number(
226
+ this.store.db
227
+ .prepare(
228
+ `SELECT count(*) AS n FROM child_tasks c WHERE (? IS NULL OR c.parent_session=?) AND (c.state='accepted' OR EXISTS(SELECT 1 FROM workers w WHERE w.session_id=c.child_session AND w.state!='exited') OR EXISTS(SELECT 1 FROM external_runs e WHERE e.child_id=c.id AND e.stopped=0))`,
229
+ )
230
+ .get(parent ?? null, parent ?? null)!.n,
231
+ );
232
+ }
233
+ launchPrepared(child: ChildTask): void {
234
+ if (this.store.db.isTransaction) throw new Error('Commit child intent before launch');
235
+ if (this.store.workerBoundary.view(child.id).dispatch_state === 'intent') this.start(child);
236
+ }
237
+ cancelSessionInTransaction(parent: string): void {
238
+ const child = this.list(parent)[0];
239
+ if (child) this.cancelIntents(child.parent_attempt);
240
+ }
139
241
  private start(child: ChildTask): void {
140
- const task = this.supervisor
141
- .start(child.child_session)
242
+ const external = this.store.workerBoundary.view(child.id).binding.external;
243
+ const task = (
244
+ external ? this.external!.start(child) : this.supervisor.start(child.child_session)
245
+ )
142
246
  .catch((error) => {
143
247
  this.store.transaction(() => {
144
248
  const attempt = this.store.attempt(child.child_attempt);
@@ -149,6 +253,7 @@ export class ChildWork {
149
253
  this.store.db
150
254
  .prepare("UPDATE submissions SET state='interrupted' WHERE id=?")
151
255
  .run(attempt.submission_id);
256
+ this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
152
257
  this.finish(this.store.attempt(attempt.id));
153
258
  });
154
259
  })
@@ -172,6 +277,7 @@ export class ChildWork {
172
277
  this.store.db
173
278
  .prepare('UPDATE submissions SET state=? WHERE id=?')
174
279
  .run(state, attempt.submission_id);
280
+ this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
175
281
  this.finish(this.store.attempt(attempt.id));
176
282
  });
177
283
  }
@@ -192,6 +298,16 @@ export class ChildWork {
192
298
  state: attempt.state,
193
299
  text: attempt.result,
194
300
  error: attempt.error,
301
+ ...(this.store.workerBoundary.view(child.id).binding.agent
302
+ ? {
303
+ worker_id: this.store.workerBoundary.view(child.id).binding.worker_id,
304
+ report:
305
+ this.store.db
306
+ .prepare('SELECT artifact_id FROM specialist_reports WHERE worker_id=?')
307
+ .get(this.store.workerBoundary.view(child.id).binding.worker_id)?.artifact_id ??
308
+ null,
309
+ }
310
+ : {}),
195
311
  }),
196
312
  );
197
313
  artifact.commit();
@@ -199,6 +315,7 @@ export class ChildWork {
199
315
  this.store.db
200
316
  .prepare('UPDATE child_tasks SET state=?,result_id=?,artifact_id=? WHERE id=?')
201
317
  .run(attempt.state, result, artifact.id, child.id);
318
+ this.store.workerBoundary.finish(child, attempt.state === 'interrupted');
202
319
  this.store.event(child.parent_session, 'child.result_available', {
203
320
  child_id: child.id,
204
321
  result_id: result,
@@ -244,6 +361,7 @@ export class ChildWork {
244
361
  this.store.db
245
362
  .prepare("UPDATE submissions SET state='interrupted' WHERE id=?")
246
363
  .run(attempt.submission_id);
364
+ this.store.projectTaskAttempt?.(this.store.attempt(attempt.id));
247
365
  }
248
366
  this.finish(this.store.attempt(attempt.id));
249
367
  }
@@ -262,6 +380,7 @@ export class ChildWork {
262
380
  this.store.db
263
381
  .prepare("UPDATE submissions SET state='cancelled' WHERE id=?")
264
382
  .run(work.submission_id);
383
+ this.store.projectTaskAttempt?.(this.store.attempt(work.id));
265
384
  this.finish(this.store.attempt(work.id));
266
385
  }
267
386
  this.store.event(child.child_session, 'attempt.cancel_requested', {
@@ -274,10 +393,32 @@ export class ChildWork {
274
393
  const children = this.store.db
275
394
  .prepare('SELECT * FROM child_tasks WHERE parent_session=?')
276
395
  .all(this.store.attempt(attempt).session_id) as unknown as ChildTask[];
396
+ for (const child of children.filter((child) => this.store.cancelled(child.child_attempt)))
397
+ for (const run of this.supervisor.coding?.list(child.child_session) ?? [])
398
+ if (
399
+ ![
400
+ 'succeeded',
401
+ 'failed',
402
+ 'cancelled',
403
+ 'timed_out',
404
+ 'output_limited',
405
+ 'interrupted',
406
+ ].includes(run.state)
407
+ )
408
+ this.supervisor.coding!.cancel(
409
+ child.child_session,
410
+ run.id,
411
+ 'child-stop:' + run.id,
412
+ () => {},
413
+ );
277
414
  await Promise.all(
278
415
  children
279
416
  .filter((child) => this.store.cancelled(child.child_attempt))
280
- .map((child) => this.supervisor.cancelAttempt(child.child_attempt)),
417
+ .map((child) =>
418
+ this.store.workerBoundary.view(child.id).binding.external
419
+ ? this.external!.cancel(child)
420
+ : this.supervisor.cancelAttempt(child.child_attempt),
421
+ ),
281
422
  );
282
423
  }
283
424
  pause(): void {
@@ -1,189 +1,2 @@
1
- import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
- import {
3
- createAssistantMessageEventStream,
4
- type AssistantMessage,
5
- type AssistantMessageEvent,
6
- type Model,
7
- type ProviderHeaders,
8
- } from '@earendil-works/pi-ai';
9
- import { streamSimple as streamClaude } from '@earendil-works/pi-ai/api/anthropic-messages';
10
- import { randomUUID } from 'node:crypto';
11
- import { digest, textDigest } from './protocol.js';
12
- import { getClaudeModel, validateClaudePayload, type ProviderRoute } from './provider-config.js';
13
-
14
- export function registerClaude(
15
- pi: ExtensionAPI,
16
- route: ProviderRoute,
17
- frame: () => string,
18
- tools: string[],
19
- bridge: (method: string, data: unknown) => Promise<unknown>,
20
- fail: () => Promise<void>,
21
- ): void {
22
- const catalog = getClaudeModel(route.model);
23
- if (!catalog) throw new Error('Pinned Claude model missing');
24
- const key = process.env.ANTHROPIC_API_KEY;
25
- if (!key) throw new Error('Claude API key unavailable to worker');
26
- pi.registerProvider('anthropic', {
27
- baseUrl: 'https://api.anthropic.com',
28
- apiKey: '$ANTHROPIC_API_KEY',
29
- api: 'anthropic-messages',
30
- models: [{ ...catalog, maxTokens: route.max_tokens }],
31
- streamSimple(model, context, options) {
32
- const output = createAssistantMessageEventStream();
33
- const source = 'salaam-request:' + randomUUID();
34
- let dispatched = false,
35
- gateFailed = false,
36
- status: number | undefined;
37
- let last: AssistantMessage | undefined;
38
- const normalize = (message: AssistantMessage): AssistantMessage => ({
39
- ...message,
40
- responseId: source,
41
- });
42
- void (async () => {
43
- try {
44
- if (model.provider !== route.provider || model.id !== route.model)
45
- throw new Error('Unapproved Claude route');
46
- const native: Model<'anthropic-messages'> = {
47
- ...catalog,
48
- provider: 'anthropic',
49
- api: 'anthropic-messages',
50
- baseUrl: 'https://api.anthropic.com',
51
- compat: {
52
- ...catalog.compat,
53
- supportsMidConvoSystemMessages: false,
54
- supportsMidConvoToolChanges: false,
55
- supportsMidConvoEffort: false,
56
- },
57
- };
58
- const headers: ProviderHeaders = {};
59
- for (const [name, value] of Object.entries(options?.headers ?? {}))
60
- headers[name.toLowerCase()] = value;
61
- delete headers.authorization;
62
- delete headers['x-api-key'];
63
- delete headers['anthropic-workspace-id'];
64
- if (route.workspace_id) headers['anthropic-workspace-id'] = route.workspace_id;
65
- let approvedDigest: string | undefined;
66
- const upstream = streamClaude(native, context, {
67
- ...options,
68
- apiKey: key,
69
- env: {},
70
- headers,
71
- maxTokens: route.max_tokens,
72
- reasoning: 'low',
73
- maxRetries: 0,
74
- timeoutMs: 120000,
75
- onPayload: async (payload, rawModel) => {
76
- const replacement = await options?.onPayload?.(payload, rawModel);
77
- const actual = replacement === undefined ? payload : replacement;
78
- try {
79
- validateClaudePayload(actual, route, frame(), tools);
80
- approvedDigest = digest(actual);
81
- } catch {
82
- gateFailed = true;
83
- throw new Error('Claude payload denied');
84
- }
85
- return actual;
86
- },
87
- onResponse: async (response, rawModel) => {
88
- status = response.status;
89
- await options?.onResponse?.(response, rawModel);
90
- },
91
- fetch: async (input, init) => {
92
- try {
93
- const url = new URL(input instanceof Request ? input.url : String(input));
94
- if (
95
- url.href !== 'https://api.anthropic.com/v1/messages?beta=true' &&
96
- url.href !== 'https://api.anthropic.com/v1/messages'
97
- )
98
- throw new Error('Unapproved Claude endpoint');
99
- if (init?.method !== 'POST' || typeof init.body !== 'string' || dispatched)
100
- throw new Error('Claude request replay denied');
101
- const sentHeaders = new Headers(init.headers);
102
- if (sentHeaders.get('x-api-key') !== key || sentHeaders.has('authorization'))
103
- throw new Error('Unapproved Claude authentication');
104
- const payload = JSON.parse(init.body);
105
- validateClaudePayload(payload, route, frame(), tools);
106
- if (approvedDigest !== digest(payload))
107
- throw new Error('Claude payload changed after validation');
108
- dispatched = true;
109
- await bridge('model', {
110
- source_key: source,
111
- provider: 'anthropic',
112
- model: route.model,
113
- context_digest: textDigest(frame()),
114
- payload_digest: approvedDigest,
115
- payload,
116
- });
117
- } catch {
118
- gateFailed = true;
119
- throw new Error('Claude request denied by Salaam');
120
- }
121
- return globalThis.fetch(input, { ...init, redirect: 'error' });
122
- },
123
- });
124
- for await (const event of upstream) {
125
- if ('partial' in event) last = normalize(event.partial);
126
- if (event.type === 'done') {
127
- const message = normalize(event.message);
128
- last = message;
129
- await bridge('usage', {
130
- source_key: source,
131
- usage: message.usage,
132
- provider_response_id: event.message.responseId ?? null,
133
- });
134
- output.push({ ...event, message });
135
- } else if (event.type === 'error') {
136
- const message = normalize(event.error);
137
- const code = /^(?:Error:\s*)?(\d{3})\b/.exec(message.errorMessage ?? '')?.[1];
138
- status = status ?? (code ? Number(code) : undefined);
139
- delete message.diagnostics;
140
- message.errorMessage = options?.signal?.aborted
141
- ? 'Claude request cancelled'
142
- : `Claude request failed${status ? ` (HTTP ${status})` : ''}`;
143
- message.content = [];
144
- last = message;
145
- output.push({ ...event, error: message });
146
- } else
147
- output.push(
148
- ('partial' in event
149
- ? { ...event, partial: last! }
150
- : event) as AssistantMessageEvent,
151
- );
152
- }
153
- output.end();
154
- if (gateFailed) await fail();
155
- } catch {
156
- const error: AssistantMessage = {
157
- role: 'assistant',
158
- content: [],
159
- api: 'anthropic-messages',
160
- provider: 'anthropic',
161
- model: route.model,
162
- timestamp: Date.now(),
163
- stopReason: options?.signal?.aborted ? 'aborted' : 'error',
164
- usage: last?.usage ?? {
165
- input: 0,
166
- output: 0,
167
- cacheRead: 0,
168
- cacheWrite: 0,
169
- totalTokens: 0,
170
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
171
- },
172
- responseId: source,
173
- errorMessage: options?.signal?.aborted
174
- ? 'Claude request cancelled'
175
- : 'Claude required control failed',
176
- };
177
- output.push({
178
- type: 'error',
179
- reason: error.stopReason === 'aborted' ? 'aborted' : 'error',
180
- error,
181
- });
182
- output.end();
183
- if (!options?.signal?.aborted) await fail();
184
- }
185
- })();
186
- return output;
187
- },
188
- });
189
- }
1
+ // Kept as an internal compatibility entry point for existing extensions.
2
+ export { registerLiveProvider as registerClaude } from './live-provider.js';