@cortexkit/subc-client 0.3.0 → 0.3.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.
@@ -0,0 +1,652 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { AuthError, authenticateClient } from "./auth.js";
3
+ import { DEFAULT_RECONNECT_BACKOFF } from "./client.js";
4
+ import { ConnectionFileError, readConnectionFile } from "./connection-file.js";
5
+ import { buildFlags, buildFrame, buildFrameWithVersion, decodeHeader, encodeFrame, FrameType, HEADER_LEN, Priority, PROTOCOL_VERSION, } from "./envelope.js";
6
+ import { SocketClosedError, SocketTimeoutError, SocketWriteNotQueuedError, SocketWriteQueuedError, SubcSocket, } from "./socket.js";
7
+ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
8
+ const BODY_READ_TIMEOUT_MS = 30_000;
9
+ const WRITE_TIMEOUT_MS = 30_000;
10
+ const DEFAULT_RESTORED_DEBOUNCE_MS = 250;
11
+ export const HELLO_CORR = 1n;
12
+ export class SubcProviderError extends Error {
13
+ code;
14
+ constructor(message, code) {
15
+ super(message);
16
+ this.code = code;
17
+ }
18
+ }
19
+ export function managementSurfaceManifest(opts) {
20
+ const operations = opts.operations.map((operation) => typeof operation === "string"
21
+ ? { name: operation, kind: "query" }
22
+ : { name: operation.name, kind: operation.kind });
23
+ return {
24
+ module_id: opts.moduleId,
25
+ module_version: opts.moduleVersion ?? "0.0.0",
26
+ protocol_ver: PROTOCOL_VERSION,
27
+ trust_tier: "first_party",
28
+ provides: [
29
+ {
30
+ role: "management_surface",
31
+ operations,
32
+ config_schema: { type: "object" },
33
+ observability: [],
34
+ identity_scope: [],
35
+ },
36
+ ],
37
+ consumes: [],
38
+ scheduled_tasks: [],
39
+ bindings: {
40
+ storage: {
41
+ kind: "sqlite",
42
+ scope: "project",
43
+ owns_schema: false,
44
+ },
45
+ vault_grants: [],
46
+ identity: {
47
+ requires: [],
48
+ optional: [],
49
+ },
50
+ },
51
+ };
52
+ }
53
+ export function jsonProviderHandler(handler) {
54
+ return async (routeChannel, body) => {
55
+ const request = JSON.parse(Buffer.from(body).toString("utf8"));
56
+ const response = await handler(routeChannel, request);
57
+ return encodeJson(response);
58
+ };
59
+ }
60
+ export class SubcProvider {
61
+ sock;
62
+ currentConn;
63
+ opts;
64
+ closed;
65
+ resolveClosed = () => undefined;
66
+ closeStarted = false;
67
+ closedErr = null;
68
+ // In-flight data requests are keyed by generation, channel, and correlation id.
69
+ // A socket drop only makes the reply path stale; handlers may still finish their
70
+ // durable work, and their late sends are ignored by the generation guard.
71
+ inflight = new Map();
72
+ reconnecting = null;
73
+ generation = 1;
74
+ connectionEpoch = 1;
75
+ stateQueue = [];
76
+ drainingStateQueue = false;
77
+ restoredDebounceToken = 0;
78
+ /**
79
+ * The resolved storage descriptor the daemon delivered in HELLO_ACK, or
80
+ * `undefined` when no managed storage is configured. A module that persists
81
+ * hands this to the storage library.
82
+ */
83
+ storage;
84
+ constructor(sock, currentConn, opts, storage) {
85
+ this.sock = sock;
86
+ this.currentConn = currentConn;
87
+ this.opts = opts;
88
+ this.storage = storage;
89
+ this.closed = new Promise((resolve) => {
90
+ this.resolveClosed = resolve;
91
+ });
92
+ void this.readLoop(sock, this.generation);
93
+ this.enqueueConnectionState({ state: "connected", epoch: this.connectionEpoch });
94
+ }
95
+ get conn() {
96
+ return this.currentConn;
97
+ }
98
+ currentEpoch() {
99
+ return this.connectionEpoch;
100
+ }
101
+ /** Read the connection file, authenticate as a client, register the manifest with HELLO, and serve frames. */
102
+ static async connect(opts) {
103
+ if (opts.manifest.protocol_ver !== PROTOCOL_VERSION) {
104
+ throw new SubcProviderError(`manifest protocol_ver ${opts.manifest.protocol_ver} does not match client protocol ${PROTOCOL_VERSION}`, "invalid_manifest");
105
+ }
106
+ const normalized = normalizeProviderConnectOptions(opts);
107
+ const opened = await SubcProvider.openConnection(normalized);
108
+ return new SubcProvider(opened.sock, opened.conn, normalized, opened.ack.storage);
109
+ }
110
+ async close() {
111
+ if (!this.closeStarted) {
112
+ this.closeStarted = true;
113
+ this.cancelRestoredDebounce();
114
+ const sock = this.sock;
115
+ try {
116
+ await sendFrame(sock, buildFrame(FrameType.Goodbye, controlFlags(), 0, 0n, new Uint8Array(0)));
117
+ }
118
+ catch {
119
+ // The daemon may already have closed the connection; close() remains best-effort.
120
+ }
121
+ finally {
122
+ sock.close();
123
+ this.finishClosed();
124
+ }
125
+ }
126
+ await this.closed;
127
+ }
128
+ static async openConnection(opts) {
129
+ const conn = await readConnectionFile(opts.connectionFile);
130
+ const deadline = Date.now() + (opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS);
131
+ const endpoint = conn.endpoints[0];
132
+ const sock = await SubcSocket.connect(endpoint.host, endpoint.port, deadline);
133
+ try {
134
+ await authenticateClient(sock, conn, deadline);
135
+ await sendFrame(sock, buildHelloFrame(opts));
136
+ const ack = await expectHelloAck(sock, deadline);
137
+ return { sock, conn, ack };
138
+ }
139
+ catch (err) {
140
+ sock.close();
141
+ throw err;
142
+ }
143
+ }
144
+ async readLoop(sock, generation) {
145
+ try {
146
+ for (;;) {
147
+ // Header read waits indefinitely — idle time between frames is normal.
148
+ const headerBytes = await sock.readExact(HEADER_LEN, Number.POSITIVE_INFINITY);
149
+ const header = decodeHeader(headerBytes);
150
+ const body = header.len === 0
151
+ ? new Uint8Array(0)
152
+ : await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS);
153
+ const keepGoing = await this.dispatch({ header, body }, sock, generation);
154
+ if (!keepGoing) {
155
+ if (this.sock === sock && this.generation === generation)
156
+ this.closeStarted = true;
157
+ break;
158
+ }
159
+ }
160
+ }
161
+ catch (err) {
162
+ if (this.sock === sock && this.generation === generation && !this.closeStarted) {
163
+ this.handleUnexpectedDrop(sock, generation, err instanceof Error ? err : new SubcProviderError(String(err)));
164
+ return;
165
+ }
166
+ }
167
+ finally {
168
+ if (this.sock === sock && this.generation === generation) {
169
+ sock.close();
170
+ if (this.closeStarted)
171
+ this.finishClosed();
172
+ }
173
+ }
174
+ }
175
+ async dispatch(frame, sock, generation) {
176
+ switch (frame.header.ty) {
177
+ case FrameType.Ping:
178
+ if (frame.header.channel === 0) {
179
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Pong, frame.header.flags, 0, frame.header.corr, new Uint8Array(0)));
180
+ }
181
+ return true;
182
+ case FrameType.Goodbye:
183
+ if (frame.header.channel === 0)
184
+ return false;
185
+ // The route is gone: abort in-flight requests on that route so streaming
186
+ // handlers unwind, then notify the provider owner.
187
+ this.abortChannel(generation, frame.header.channel);
188
+ await this.opts.onRouteGone?.(frame.header.channel);
189
+ return true;
190
+ case FrameType.Cancel:
191
+ // The consumer cancelled one request: abort the matching handler. Its
192
+ // streaming handler observes ctx.signal and ends with a StreamEnd terminal.
193
+ this.inflight.get(routeKey(generation, frame.header.channel, frame.header.corr))?.abort();
194
+ return true;
195
+ case FrameType.Request:
196
+ if (frame.header.channel === 0) {
197
+ await this.handleControlRequest(frame, sock, generation);
198
+ }
199
+ else {
200
+ void this.handleDataRequest(frame, sock, generation).catch((err) => {
201
+ if (!this.closeStarted && this.sock === sock && this.generation === generation) {
202
+ console.warn("SubcProvider handler failed after its request was dispatched", err);
203
+ }
204
+ });
205
+ }
206
+ return true;
207
+ default:
208
+ return true;
209
+ }
210
+ }
211
+ /** Abort every in-flight request on a route channel for the current socket generation. */
212
+ abortChannel(generation, channel) {
213
+ const prefix = `${generation}:${channel}:`;
214
+ for (const [key, controller] of this.inflight) {
215
+ if (key.startsWith(prefix))
216
+ controller.abort();
217
+ }
218
+ }
219
+ abortGeneration(generation) {
220
+ const prefix = `${generation}:`;
221
+ for (const [key, controller] of this.inflight) {
222
+ if (key.startsWith(prefix))
223
+ controller.abort();
224
+ }
225
+ }
226
+ abortAllInflight() {
227
+ for (const controller of this.inflight.values())
228
+ controller.abort();
229
+ }
230
+ async handleControlRequest(frame, sock, generation) {
231
+ const request = parseJson(frame.body);
232
+ if (request.op !== "route.bind") {
233
+ throw new SubcProviderError(`unsupported module control request ${request.op ?? "<missing op>"}`);
234
+ }
235
+ const bindRequest = {
236
+ route_channel: numberField(request.route_channel, "route_channel"),
237
+ target: request.target,
238
+ identity: request.identity,
239
+ principal: request.principal,
240
+ };
241
+ const decision = await this.opts.onBind?.(bindRequest);
242
+ const rejection = bindRejection(decision);
243
+ if (rejection) {
244
+ await this.sendError(frame, rejection.code, rejection.message, controlFlags(), sock, generation);
245
+ return;
246
+ }
247
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Response, controlFlags(), 0, frame.header.corr, encodeJson({ op: "route.bind" })));
248
+ }
249
+ async handleDataRequest(frame, sock, generation) {
250
+ const { channel, corr, ver } = frame.header;
251
+ const key = routeKey(generation, channel, corr);
252
+ const controller = new AbortController();
253
+ this.inflight.set(key, controller);
254
+ const dataFlags = buildFlags(false, Priority.Interactive, false);
255
+ const ctx = {
256
+ signal: controller.signal,
257
+ currentEpoch: () => this.connectionEpoch,
258
+ emit: async (eventBody) => {
259
+ // Once aborted (cancel / route-gone / socket drop), drop further events silently.
260
+ if (controller.signal.aborted)
261
+ return;
262
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamData, dataFlags, channel, corr, eventBody));
263
+ },
264
+ };
265
+ try {
266
+ const body = await this.opts.handler(channel, frame.body, ctx);
267
+ if (body === undefined) {
268
+ // A streaming handler that ended: close the held-open request with a
269
+ // StreamEnd terminal (the consumer's subscription resolves).
270
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamEnd, dataFlags, channel, corr, new Uint8Array(0)));
271
+ }
272
+ else if (body instanceof Uint8Array) {
273
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, dataFlags, channel, corr, body));
274
+ }
275
+ else {
276
+ throw new SubcProviderError("provider handler must return a Uint8Array or void", "invalid_handler_response");
277
+ }
278
+ }
279
+ catch (err) {
280
+ await this.sendError(frame, err instanceof SubcProviderError && err.code ? err.code : "handler_error", err instanceof Error ? err.message : String(err), dataFlags, sock, generation);
281
+ }
282
+ finally {
283
+ if (this.inflight.get(key) === controller)
284
+ this.inflight.delete(key);
285
+ }
286
+ }
287
+ async sendError(frame, code, message, flags, sock, generation) {
288
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Error, flags, frame.header.channel, frame.header.corr, encodeJson({ code, message })));
289
+ }
290
+ async sendOn(sock, generation, frame) {
291
+ if (this.sock !== sock || this.generation !== generation || this.closeStarted || this.closedErr)
292
+ return;
293
+ await sendFrame(sock, frame);
294
+ }
295
+ handleUnexpectedDrop(sock, generation, cause) {
296
+ this.cancelRestoredDebounce();
297
+ this.abortGeneration(generation);
298
+ this.generation += 1;
299
+ sock.close();
300
+ this.enqueueConnectionState({ state: "down", cause });
301
+ this.scheduleReconnectAfterDrop(cause);
302
+ }
303
+ scheduleReconnectAfterDrop(trigger) {
304
+ if (this.closeStarted || this.reconnecting)
305
+ return;
306
+ const promise = this.reconnectWithRetry(trigger)
307
+ .catch((err) => {
308
+ if (!this.closeStarted)
309
+ this.failFatal(err instanceof Error ? err : new SubcProviderError(String(err)));
310
+ })
311
+ .finally(() => {
312
+ if (this.reconnecting === promise)
313
+ this.reconnecting = null;
314
+ });
315
+ this.reconnecting = promise;
316
+ }
317
+ async reconnectWithRetry(_trigger) {
318
+ let attempt = 0;
319
+ let delay = this.opts.reconnectBackoff.baseMs;
320
+ for (;;) {
321
+ if (this.closeStarted)
322
+ throw new SubcProviderError("provider closed");
323
+ attempt += 1;
324
+ this.enqueueConnectionState({ state: "reconnecting", attempt });
325
+ try {
326
+ const opened = await SubcProvider.openConnection(this.opts);
327
+ if (this.closeStarted) {
328
+ opened.sock.close();
329
+ throw new SubcProviderError("provider closed");
330
+ }
331
+ this.replaceConnection(opened);
332
+ return;
333
+ }
334
+ catch (err) {
335
+ if (this.closeStarted)
336
+ throw err;
337
+ if (!isProviderReconnectTransient(err))
338
+ throw err;
339
+ // Providers retry transient failures indefinitely by design (a module
340
+ // keeps trying until closed; only permanent errors fail-fatal), so an
341
+ // auth mismatch heals as soon as the daemon settles and a retry re-reads
342
+ // the rotated connection file.
343
+ await this.opts.sleep(delay);
344
+ delay = Math.min(delay * 2, this.opts.reconnectBackoff.capMs);
345
+ }
346
+ }
347
+ }
348
+ replaceConnection(opened) {
349
+ this.sock.close();
350
+ this.sock = opened.sock;
351
+ this.currentConn = opened.conn;
352
+ this.storage = opened.ack.storage;
353
+ this.closedErr = null;
354
+ this.connectionEpoch += 1;
355
+ const generation = this.generation;
356
+ void this.readLoop(opened.sock, generation);
357
+ this.scheduleRestored(generation, this.connectionEpoch);
358
+ }
359
+ scheduleRestored(generation, epoch) {
360
+ if (!this.opts.onConnectionState)
361
+ return;
362
+ const token = ++this.restoredDebounceToken;
363
+ void this.opts
364
+ .sleep(this.opts.restoredDebounceMs)
365
+ .then(() => {
366
+ if (token === this.restoredDebounceToken &&
367
+ !this.closeStarted &&
368
+ this.sock &&
369
+ this.generation === generation &&
370
+ this.connectionEpoch === epoch) {
371
+ this.enqueueConnectionState({ state: "restored", epoch });
372
+ }
373
+ })
374
+ .catch((err) => {
375
+ if (token === this.restoredDebounceToken && !this.closeStarted) {
376
+ console.warn("SubcProvider restored debounce timer failed", err);
377
+ }
378
+ });
379
+ }
380
+ cancelRestoredDebounce() {
381
+ this.restoredDebounceToken += 1;
382
+ }
383
+ enqueueConnectionState(event) {
384
+ if (!this.opts.onConnectionState)
385
+ return;
386
+ this.stateQueue.push(event);
387
+ if (!this.drainingStateQueue)
388
+ void this.drainConnectionStateQueue();
389
+ }
390
+ async drainConnectionStateQueue() {
391
+ if (this.drainingStateQueue)
392
+ return;
393
+ this.drainingStateQueue = true;
394
+ try {
395
+ while (this.stateQueue.length > 0) {
396
+ const event = this.stateQueue[0];
397
+ try {
398
+ await this.opts.onConnectionState?.(event);
399
+ this.stateQueue.shift();
400
+ }
401
+ catch (err) {
402
+ if (event.state === "restored") {
403
+ console.warn("SubcProvider restored callback failed; retrying delivery", err);
404
+ await pauseBeforeStateRetry();
405
+ continue;
406
+ }
407
+ console.warn("SubcProvider connection-state callback failed", err);
408
+ this.stateQueue.shift();
409
+ }
410
+ }
411
+ }
412
+ finally {
413
+ this.drainingStateQueue = false;
414
+ if (this.stateQueue.length > 0)
415
+ void this.drainConnectionStateQueue();
416
+ }
417
+ }
418
+ failFatal(err) {
419
+ if (!this.closedErr)
420
+ this.closedErr = err;
421
+ this.closeStarted = true;
422
+ this.cancelRestoredDebounce();
423
+ this.abortAllInflight();
424
+ this.sock.close();
425
+ this.finishClosed();
426
+ }
427
+ finishClosed() {
428
+ this.resolveClosed();
429
+ }
430
+ }
431
+ function routeKey(generation, channel, corr) {
432
+ return `${generation}:${channel}:${corr}`;
433
+ }
434
+ function launchNonce(opts) {
435
+ const nonce = opts.launchNonce ?? process.env[SUBC_LAUNCH_NONCE_ENV];
436
+ return nonce && nonce.length > 0 ? nonce : undefined;
437
+ }
438
+ const SUBC_LAUNCH_NONCE_ENV = "SUBC_LAUNCH_NONCE";
439
+ function normalizeProviderConnectOptions(opts) {
440
+ return {
441
+ connectionFile: opts.connectionFile,
442
+ manifest: opts.manifest,
443
+ handler: opts.handler,
444
+ handshakeTimeoutMs: opts.handshakeTimeoutMs,
445
+ controlOps: opts.controlOps,
446
+ onBind: opts.onBind,
447
+ onRouteGone: opts.onRouteGone,
448
+ reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
449
+ sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
450
+ restoredDebounceMs: opts.restoredDebounceMs ?? DEFAULT_RESTORED_DEBOUNCE_MS,
451
+ onConnectionState: opts.onConnectionState,
452
+ launchNonce: opts.launchNonce,
453
+ };
454
+ }
455
+ function buildHelloFrame(opts) {
456
+ const nonce = launchNonce(opts);
457
+ return buildFrame(FrameType.Hello, controlFlags(), 0, HELLO_CORR, encodeJson({
458
+ manifest: normalizeManifest(opts.manifest),
459
+ protocol_ver: PROTOCOL_VERSION,
460
+ control_ops: opts.controlOps === undefined ? null : opts.controlOps,
461
+ // Echo the one-time launch nonce subc injects for a reserved module
462
+ // (SUBC_LAUNCH_NONCE), so only the daemon-spawned process can register a
463
+ // reserved module_id. Omitted when unset (non-reserved / self-connecting).
464
+ ...(nonce ? { launch_nonce: nonce } : {}),
465
+ }));
466
+ }
467
+ function isProviderReconnectTransient(err) {
468
+ if (err instanceof SubcProviderError)
469
+ return err.code === "duplicate_module_id";
470
+ if (err instanceof SocketClosedError || err instanceof SocketTimeoutError)
471
+ return true;
472
+ if (err instanceof SocketWriteNotQueuedError || err instanceof SocketWriteQueuedError)
473
+ return true;
474
+ // AuthError is transient during reconnect: the daemon rotates its key on every
475
+ // restart, and with a fixed port a client racing the restart can read the
476
+ // pre-rotation file yet still connect — proof mismatch then means "stale key
477
+ // mid-rotation", not "impostor". Each retry re-reads the connection file, and
478
+ // server-proves-first protects every attempt. First-connect auth failures stay
479
+ // permanent in serve()'s initial openConnection (never routed through here).
480
+ if (err instanceof AuthError)
481
+ return true;
482
+ if (err instanceof ConnectionFileError)
483
+ return false;
484
+ const code = errorCode(err);
485
+ return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ENOENT";
486
+ }
487
+ function errorCode(err) {
488
+ if (typeof err === "object" && err !== null && "code" in err) {
489
+ const code = err.code;
490
+ if (typeof code === "string")
491
+ return code;
492
+ }
493
+ return undefined;
494
+ }
495
+ async function pauseBeforeStateRetry() {
496
+ await new Promise((resolve) => setTimeout(resolve, 0));
497
+ }
498
+ function controlFlags() {
499
+ return buildFlags(false, Priority.Passive, false);
500
+ }
501
+ async function sendFrame(sock, frame) {
502
+ await sock.write(encodeFrame(frame), Date.now() + WRITE_TIMEOUT_MS);
503
+ }
504
+ async function expectHelloAck(sock, deadline) {
505
+ const header = decodeHeader(await sock.readExact(HEADER_LEN, deadline));
506
+ const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, deadline);
507
+ const frame = { header, body };
508
+ switch (header.ty) {
509
+ case FrameType.HelloAck:
510
+ return parseJson(body);
511
+ case FrameType.Error: {
512
+ const error = parseJson(body);
513
+ throw new SubcProviderError(`subc rejected HELLO: ${error.code ?? "unknown"} — ${error.message ?? "subc error"}`, error.code);
514
+ }
515
+ default:
516
+ throw new SubcProviderError(`unexpected frame ${FrameType[frame.header.ty]} awaiting HELLO_ACK`);
517
+ }
518
+ }
519
+ function encodeJson(value) {
520
+ return new Uint8Array(Buffer.from(JSON.stringify(value), "utf8"));
521
+ }
522
+ function parseJson(bytes) {
523
+ return JSON.parse(Buffer.from(bytes).toString("utf8"));
524
+ }
525
+ function numberField(value, field) {
526
+ if (typeof value !== "number" || !Number.isInteger(value)) {
527
+ throw new SubcProviderError(`route.bind ${field} must be an integer`);
528
+ }
529
+ return value;
530
+ }
531
+ function bindRejection(decision) {
532
+ if (decision === undefined || decision === true)
533
+ return null;
534
+ if (decision === false) {
535
+ return { code: "route_rejected", message: "route.bind rejected by provider" };
536
+ }
537
+ if (decision.accept)
538
+ return null;
539
+ return {
540
+ code: decision.code ?? "route_rejected",
541
+ message: decision.message ?? "route.bind rejected by provider",
542
+ };
543
+ }
544
+ function normalizeManifest(manifest) {
545
+ return {
546
+ module_id: manifest.module_id,
547
+ module_version: manifest.module_version,
548
+ protocol_ver: manifest.protocol_ver,
549
+ trust_tier: manifest.trust_tier,
550
+ provides: manifest.provides.map(normalizeProviderRole),
551
+ consumes: manifest.consumes.map(normalizeConsumerRole),
552
+ scheduled_tasks: manifest.scheduled_tasks.map(normalizeScheduledTask),
553
+ bindings: {
554
+ storage: {
555
+ kind: manifest.bindings.storage.kind,
556
+ scope: manifest.bindings.storage.scope,
557
+ owns_schema: manifest.bindings.storage.owns_schema,
558
+ },
559
+ vault_grants: manifest.bindings.vault_grants.map((grant) => ({
560
+ secret: grant.secret,
561
+ reason: grant.reason,
562
+ })),
563
+ identity: {
564
+ requires: [...manifest.bindings.identity.requires],
565
+ optional: [...manifest.bindings.identity.optional],
566
+ },
567
+ },
568
+ };
569
+ }
570
+ function normalizeProviderRole(role) {
571
+ switch (role.role) {
572
+ case "tool_provider":
573
+ return {
574
+ role: "tool_provider",
575
+ tools: role.tools.map((tool) => ({
576
+ name: tool.name,
577
+ execution_mode: tool.execution_mode,
578
+ schema: tool.schema,
579
+ })),
580
+ identity_scope: [...role.identity_scope],
581
+ concurrency: role.concurrency,
582
+ emits_push: role.emits_push,
583
+ sub_supervises: role.sub_supervises,
584
+ };
585
+ case "pipeline_stage":
586
+ return {
587
+ role: "pipeline_stage",
588
+ stage: role.stage,
589
+ applies_to: {
590
+ provider: role.applies_to.provider,
591
+ model: role.applies_to.model,
592
+ },
593
+ interface: role.interface,
594
+ declares_frozen_floor: role.declares_frozen_floor,
595
+ needs_signals: [...role.needs_signals],
596
+ conformance_class: role.conformance_class,
597
+ };
598
+ case "management_surface":
599
+ return {
600
+ role: "management_surface",
601
+ operations: role.operations.map((operation) => ({
602
+ name: operation.name,
603
+ kind: operation.kind,
604
+ })),
605
+ config_schema: role.config_schema,
606
+ observability: role.observability.map((surface) => ({
607
+ name: surface.name,
608
+ kind: surface.kind,
609
+ })),
610
+ identity_scope: [...role.identity_scope],
611
+ };
612
+ case "internal_service":
613
+ return {
614
+ role: "internal_service",
615
+ service_id: role.service_id,
616
+ transport: role.transport,
617
+ agent_facing: role.agent_facing,
618
+ operations: [...role.operations],
619
+ };
620
+ }
621
+ }
622
+ function normalizeConsumerRole(role) {
623
+ switch (role.role) {
624
+ case "tool_client":
625
+ return { role: "tool_client", of: [...role.of] };
626
+ case "llm_client":
627
+ return { role: "llm_client", via: role.via, auth: role.auth };
628
+ case "service_client":
629
+ return { role: "service_client", of: [...role.of] };
630
+ }
631
+ }
632
+ function normalizeScheduledTask(task) {
633
+ return {
634
+ task_id: task.task_id,
635
+ eligibility: {
636
+ cooldown: task.eligibility.cooldown,
637
+ window: task.eligibility.window,
638
+ },
639
+ lease_scope: task.lease_scope,
640
+ renews_during_calls: task.renews_during_calls,
641
+ toolset: [...task.toolset],
642
+ model_policy: {
643
+ tier: task.model_policy.tier,
644
+ fallback_chain: [...task.model_policy.fallback_chain],
645
+ },
646
+ step_cap: task.step_cap,
647
+ circuit_breaker: {
648
+ identical_failures: task.circuit_breaker.identical_failures,
649
+ },
650
+ };
651
+ }
652
+ //# sourceMappingURL=provider.js.map