@cortexkit/subc-client 0.8.1 → 0.11.1

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.
package/dist/client.d.ts CHANGED
@@ -30,15 +30,61 @@ export interface RouteOpenOptions {
30
30
  /** Optional consumer-declared reverse-request capabilities for this route.open. This is a declaration, not a verified privilege; providers must treat an omitted field as no reverse-request capability. Known MCP method-family values today are "elicitation", "sampling", and "roots". */
31
31
  consumerCapabilities?: string[];
32
32
  }
33
+ export interface CatalogCapabilityRequirement {
34
+ capability: string;
35
+ need: "required" | "optional";
36
+ }
37
+ export interface CatalogCapabilities {
38
+ provides: string[];
39
+ requires: CatalogCapabilityRequirement[];
40
+ must_never_reach: string[];
41
+ }
42
+ /** A self-signal's anchor relative to the external surface it shapes. */
43
+ export type CatalogSignalAnchor = "fixed_interval" | {
44
+ event: {
45
+ event: string;
46
+ };
47
+ };
48
+ /** The source of a self-signal's effective cadence. */
49
+ export type CatalogSignalCadence = {
50
+ literal: {
51
+ interval_ms: number;
52
+ };
53
+ } | {
54
+ derived: {
55
+ source: string;
56
+ };
57
+ };
58
+ /** A self-signal declaration mirrored from a registered module manifest. */
59
+ export interface CatalogSelfSignalDeclaration {
60
+ name: string;
61
+ /** Open string for forward-compatible self-signal kinds. */
62
+ kind: string;
63
+ effect: "observe" | "mutate";
64
+ anchored_to: CatalogSignalAnchor;
65
+ cadence?: CatalogSignalCadence | null;
66
+ domain?: string | null;
67
+ note?: string | null;
68
+ }
33
69
  export interface CatalogEntry {
34
70
  module_id: string;
35
71
  roles: unknown[];
36
72
  control_ops: string[];
73
+ /** Static capability claims mirrored from the registered module manifest. */
74
+ capabilities?: CatalogCapabilities | null;
75
+ /** Self-signal declarations mirrored verbatim from the registered manifest. */
76
+ self_signals?: CatalogSelfSignalDeclaration[] | null;
37
77
  }
38
78
  export interface RequestOptions {
39
79
  priority?: Priority;
40
80
  admissionClass?: AdmissionClass;
41
81
  timeoutMs?: number;
82
+ /**
83
+ * Set when the request body is opaque bytes rather than JSON. This flag is the
84
+ * receiver's only signal about the body representation; reply decoding follows
85
+ * the response frame's flag instead of this request option.
86
+ */
87
+ binary?: boolean;
42
88
  /** Called for each interim PUSH / StreamData frame before the terminal reply. */
43
89
  onProgress?: (body: Uint8Array) => void;
44
90
  }
@@ -106,6 +152,13 @@ export declare class SubcCallError extends Error {
106
152
  readonly code?: string | undefined;
107
153
  readonly cause?: unknown | undefined;
108
154
  constructor(kind: SubcCallErrorKind, message: string, code?: string | undefined, cause?: unknown | undefined);
155
+ /**
156
+ * Machine-parsable payload from the wire `ErrorBody.detail`, surfaced from
157
+ * the underlying Error frame when one caused this failure. Typed refusal
158
+ * reasons (for example certification refusals) ride this field; consumers
159
+ * classify on it and must not have to reach into `cause`.
160
+ */
161
+ get detail(): unknown;
109
162
  }
110
163
  /**
111
164
  * A live subscription to a provider's event stream, riding a single held-open
@@ -122,7 +175,21 @@ export interface Subscription {
122
175
  }
123
176
  export declare class SubcError extends Error {
124
177
  readonly code?: string | undefined;
125
- constructor(message: string, code?: string | undefined);
178
+ /**
179
+ * Machine-parsable payload from the wire `ErrorBody.detail` field, carried
180
+ * verbatim. Providers use it for typed refusal reasons (for example
181
+ * synapse's certification-refusal reasons); dropping it here would strand
182
+ * those reasons at the transport boundary.
183
+ */
184
+ readonly detail?: unknown | undefined;
185
+ constructor(message: string, code?: string | undefined,
186
+ /**
187
+ * Machine-parsable payload from the wire `ErrorBody.detail` field, carried
188
+ * verbatim. Providers use it for typed refusal reasons (for example
189
+ * synapse's certification-refusal reasons); dropping it here would strand
190
+ * those reasons at the transport boundary.
191
+ */
192
+ detail?: unknown | undefined);
126
193
  }
127
194
  export interface ConnectOptions {
128
195
  connectionFile: string;
@@ -135,6 +202,14 @@ export interface ConnectOptions {
135
202
  reconnectBackoff?: ReconnectBackoff;
136
203
  /** Injectable sleep for timer-free reconnect tests. */
137
204
  sleep?: (ms: number) => Promise<void>;
205
+ /**
206
+ * How long managed calls keep retrying route.open in-place on retryable
207
+ * refusals (module_reloading, module_warming, target_unavailable, …) before
208
+ * settling not_sent. This deadline is the ONLY binder on those retries —
209
+ * module reloads legitimately take tens of seconds (the daemon's drain alone
210
+ * defaults to 30s), so the default matches the daemon's drain ceiling.
211
+ */
212
+ routeOpenRetryDeadlineMs?: number;
138
213
  /**
139
214
  * Window the post-deadline liveness probe waits for ANY inbound frame after
140
215
  * sending its channel-0 Ping before convicting the socket as half-open.
@@ -171,6 +246,23 @@ export interface ControlPush {
171
246
  /** The full parsed body, `op` included, for op-specific fields. */
172
247
  body: Record<string, unknown>;
173
248
  }
249
+ /** A known route-close reason, excluding forward-compatible unknown values. */
250
+ export type KnownRouteCloseReason = "reload" | "restart" | "disable" | "crash" | "capability_denied";
251
+ /** A route-close reason decoded from a daemon control push. */
252
+ export type RouteCloseReason = KnownRouteCloseReason | "unknown";
253
+ /** Whether the close reason alone permits reopening a route automatically. */
254
+ export type RouteCloseDisposition = "may_reopen" | "must_not_reopen";
255
+ /**
256
+ * Decode a daemon close reason without rejecting a future wire value.
257
+ *
258
+ * An unknown reason remains observable as `unknown`; callers must use the
259
+ * strict disposition instead of treating an unrecognized policy change as safe.
260
+ */
261
+ export declare function parseRouteCloseReason(reason: unknown): RouteCloseReason;
262
+ /** Unknown close reasons take the strictest action and must not trigger a reopen. */
263
+ export declare function classifyRouteCloseReason(reason: unknown): RouteCloseDisposition;
264
+ /** Decode a close reason from a route lifecycle control push, if present. */
265
+ export declare function routeCloseReason(push: ControlPush): RouteCloseReason | undefined;
174
266
  export declare class SubcClient {
175
267
  private sock;
176
268
  private currentConn;
@@ -193,6 +285,15 @@ export declare class SubcClient {
193
285
  static connect(opts: ConnectOptions): Promise<SubcClient>;
194
286
  /** List modules subc knows about (channel-0 catalog.list). */
195
287
  catalogList(moduleId?: string): Promise<CatalogEntry[]>;
288
+ /**
289
+ * Resolve the sole catalog claimant for a capability.
290
+ *
291
+ * This expresses singular intent even when the fleet permits plural claims;
292
+ * ambiguity is returned instead of selecting an arbitrary module.
293
+ */
294
+ resolveProvider(capability: string): Promise<string>;
295
+ /** Resolve every catalog claimant in deterministic module-id order. */
296
+ resolveProviders(capability: string): Promise<string[]>;
196
297
  /** Open a route and return its connection-bound immutable handle. */
197
298
  routeOpen(target: RouteTarget, identity: BindIdentity, opts?: RouteOpenOptions): Promise<RouteHandle>;
198
299
  /** Send a data-plane request on exactly the supplied route generation. */
@@ -202,6 +303,14 @@ export declare class SubcClient {
202
303
  * reconnecting and re-opening cached routes after connection drops.
203
304
  */
204
305
  call<Response = unknown>(moduleId: string, method: string, params?: unknown, opts?: ManagedCallOptions): Promise<Response>;
306
+ /**
307
+ * Managed route + raw opaque-body request convenience. Unlike call(), this
308
+ * sends bytes without a JSON method envelope because the BINARY flag is the
309
+ * receiver's only signal that the body is not JSON. A BINARY reply resolves
310
+ * to Uint8Array; a non-binary reply is decoded as JSON.
311
+ */
312
+ callBinary(moduleId: string, body: Uint8Array, opts?: ManagedCallOptions): Promise<Uint8Array | unknown>;
313
+ private managedCall;
205
314
  /** Open a held request on exactly one route generation. */
206
315
  subscribe(handle: RouteHandle, body: unknown, onEvent: (event: Uint8Array) => void, opts?: SubscribeOptions): Subscription;
207
316
  /** Send a pure-header cancellation for an in-flight request. */
@@ -316,6 +425,8 @@ export declare class SubcClient {
316
425
  private allocateCorr;
317
426
  private closeConnectionAfterCleanupFailure;
318
427
  private encode;
428
+ /** Decode a terminal response according to the representation on the wire. */
429
+ private decodeReply;
319
430
  private parseJson;
320
431
  }
321
432
  export declare function isConsumerReconnectTransient(err: unknown): boolean;
@@ -325,12 +436,15 @@ export declare function isConsumerReconnectTransient(err: unknown): boolean;
325
436
  * is booting, mid-reload, transiently absent, or the bind relay timed out. A
326
437
  * daemon-rejected route.open is provably pre-send (no data frame ever left the
327
438
  * client), so these classify as not_sent; the managed path retries them in-place
328
- * within ROUTE_OPEN_RETRY_DEADLINE_MS. Permanent rejections (bad_consumer_identity,
329
- * config_divergence, unknown_target, ...) are excluded — they are pre-send but
330
- * would never succeed, so retrying them would only storm the daemon. Kept
331
- * byte-identical to subc-client-rs is_retryable_route_open_code for cross-client
439
+ * within ROUTE_OPEN_RETRY_DEADLINE_MS. Permanent rejections (module_removed,
440
+ * bad_consumer_identity, config_divergence, unknown_target, ...) are excluded — they are pre-send but
441
+ * would never succeed, so retrying them would only storm the daemon. In
442
+ * particular, capability_forbidden is an explicit non-retryable policy refusal.
443
+ * Kept byte-identical to subc-client-rs is_retryable_route_open_code for cross-client
332
444
  * classification parity.
333
445
  */
334
446
  export declare function isRetryableRouteOpenCode(code: string | undefined): boolean;
335
447
  export declare function connectionFileExists(path: string): Promise<boolean>;
448
+ /** Validate the exact capability-grammar identifier before catalog I/O. */
449
+ export declare function isValidCapabilityIdentifier(identifier: string): boolean;
336
450
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -12,7 +12,7 @@ import { promises as fs } from "node:fs";
12
12
  import { debuglog } from "node:util";
13
13
  import { AuthError, authenticateClient } from "./auth.js";
14
14
  import { ConnectionFileError, readConnectionFile } from "./connection-file.js";
15
- import { AdmissionClass, buildFrame, buildFlags, encodeFrame, FrameType, Priority, } from "./envelope.js";
15
+ import { AdmissionClass, buildFrame, buildFlags, encodeFrame, FrameType, hasBinary, Priority, } from "./envelope.js";
16
16
  import { belongsToConnection, createRouteHandle, newConnectionToken, sameRouteHandle, StaleRouteHandleError, } from "./route-handle.js";
17
17
  import { SocketClosedError, SocketTimeoutError, SocketWriteNotQueuedError, SocketWriteQueuedError, SubcSocket, writeBorrowed, writeTrackedBorrowed, } from "./socket.js";
18
18
  const debug = debuglog("subc-client");
@@ -89,14 +89,75 @@ export class SubcCallError extends Error {
89
89
  this.cause = cause;
90
90
  this.name = "SubcCallError";
91
91
  }
92
+ /**
93
+ * Machine-parsable payload from the wire `ErrorBody.detail`, surfaced from
94
+ * the underlying Error frame when one caused this failure. Typed refusal
95
+ * reasons (for example certification refusals) ride this field; consumers
96
+ * classify on it and must not have to reach into `cause`.
97
+ */
98
+ get detail() {
99
+ return this.cause instanceof SubcError ? this.cause.detail : undefined;
100
+ }
92
101
  }
93
102
  export class SubcError extends Error {
94
103
  code;
95
- constructor(message, code) {
104
+ detail;
105
+ constructor(message, code,
106
+ /**
107
+ * Machine-parsable payload from the wire `ErrorBody.detail` field, carried
108
+ * verbatim. Providers use it for typed refusal reasons (for example
109
+ * synapse's certification-refusal reasons); dropping it here would strand
110
+ * those reasons at the transport boundary.
111
+ */
112
+ detail) {
96
113
  super(message);
97
114
  this.code = code;
115
+ this.detail = detail;
98
116
  }
99
117
  }
118
+ function requireBinaryBody(body) {
119
+ if (body instanceof Uint8Array)
120
+ return body;
121
+ const type = body === null ? "null" : Array.isArray(body) ? "array" : typeof body;
122
+ throw new SubcError(`binary request body must be a Uint8Array; got ${type}`, "binary_body_required");
123
+ }
124
+ /**
125
+ * Decode a daemon close reason without rejecting a future wire value.
126
+ *
127
+ * An unknown reason remains observable as `unknown`; callers must use the
128
+ * strict disposition instead of treating an unrecognized policy change as safe.
129
+ */
130
+ export function parseRouteCloseReason(reason) {
131
+ switch (reason) {
132
+ case "reload":
133
+ case "restart":
134
+ case "disable":
135
+ case "crash":
136
+ case "capability_denied":
137
+ return reason;
138
+ default:
139
+ return "unknown";
140
+ }
141
+ }
142
+ /** Unknown close reasons take the strictest action and must not trigger a reopen. */
143
+ export function classifyRouteCloseReason(reason) {
144
+ switch (parseRouteCloseReason(reason)) {
145
+ case "reload":
146
+ case "restart":
147
+ return "may_reopen";
148
+ case "disable":
149
+ case "crash":
150
+ case "capability_denied":
151
+ case "unknown":
152
+ return "must_not_reopen";
153
+ }
154
+ }
155
+ /** Decode a close reason from a route lifecycle control push, if present. */
156
+ export function routeCloseReason(push) {
157
+ if (push.op !== "route.closing" && push.op !== "route.closed")
158
+ return undefined;
159
+ return parseRouteCloseReason(push.body.reason);
160
+ }
100
161
  export class SubcClient {
101
162
  sock;
102
163
  currentConn;
@@ -139,6 +200,33 @@ export class SubcClient {
139
200
  const parsed = this.parseJson(reply);
140
201
  return parsed.modules ?? [];
141
202
  }
203
+ /**
204
+ * Resolve the sole catalog claimant for a capability.
205
+ *
206
+ * This expresses singular intent even when the fleet permits plural claims;
207
+ * ambiguity is returned instead of selecting an arbitrary module.
208
+ */
209
+ async resolveProvider(capability) {
210
+ const claimants = await this.resolveProviders(capability);
211
+ if (claimants.length === 0) {
212
+ throw new SubcError(`no catalog claimant for capability ${capability}`, "capability_unprovided");
213
+ }
214
+ if (claimants.length > 1) {
215
+ throw new SubcError(`multiple catalog claimants for capability ${capability}: ${claimants.join(", ")}`, "capability_ambiguous");
216
+ }
217
+ return claimants[0];
218
+ }
219
+ /** Resolve every catalog claimant in deterministic module-id order. */
220
+ async resolveProviders(capability) {
221
+ if (!isValidCapabilityIdentifier(capability)) {
222
+ throw new SubcError(`malformed capability identifier ${JSON.stringify(capability)}`, "invalid_capability_identifier");
223
+ }
224
+ const modules = await this.catalogList();
225
+ return modules
226
+ .filter((module) => module.capabilities?.provides.includes(capability) ?? false)
227
+ .map((module) => module.module_id)
228
+ .sort();
229
+ }
142
230
  /** Open a route and return its connection-bound immutable handle. */
143
231
  async routeOpen(target, identity, opts = {}) {
144
232
  const consumerIdentity = routeOpenConsumerIdentity(opts);
@@ -185,23 +273,40 @@ export class SubcClient {
185
273
  /** Send a data-plane request on exactly the supplied route generation. */
186
274
  async request(handle, body, opts = {}) {
187
275
  this.assertLiveHandle(handle);
188
- const bytes = body instanceof Uint8Array ? body : this.encode(body);
276
+ const binary = opts.binary ?? false;
277
+ const bytes = binary ? requireBinaryBody(body) : body instanceof Uint8Array ? body : this.encode(body);
189
278
  const priority = opts.priority ?? Priority.Interactive;
190
279
  const admission = opts.admissionClass ?? AdmissionClass.Normal;
191
- const reply = await this.send(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
192
- return this.parseJson(reply);
280
+ const reply = await this.send(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress, undefined, undefined, binary);
281
+ return this.decodeReply(reply);
193
282
  }
194
283
  /**
195
284
  * Managed route + request convenience. Opens and caches a route for the module,
196
285
  * reconnecting and re-opening cached routes after connection drops.
197
286
  */
198
287
  async call(moduleId, method, params, opts = {}) {
288
+ if (opts.binary) {
289
+ throw new SubcError("call() builds a JSON body and cannot send a binary request; use callBinary(moduleId, body, opts) with a Uint8Array", "binary_call_requires_call_binary");
290
+ }
199
291
  const body = params === undefined ? { method } : { method, params };
292
+ return (await this.managedCall(moduleId, body, opts));
293
+ }
294
+ /**
295
+ * Managed route + raw opaque-body request convenience. Unlike call(), this
296
+ * sends bytes without a JSON method envelope because the BINARY flag is the
297
+ * receiver's only signal that the body is not JSON. A BINARY reply resolves
298
+ * to Uint8Array; a non-binary reply is decoded as JSON.
299
+ */
300
+ async callBinary(moduleId, body, opts = {}) {
301
+ const bytes = requireBinaryBody(body);
302
+ return this.managedCall(moduleId, bytes, { ...opts, binary: true });
303
+ }
304
+ async managedCall(moduleId, body, opts) {
200
305
  let retriedUnknownChannel = false;
201
306
  for (;;) {
202
307
  const routeHandle = await this.cachedRouteHandle(moduleId, opts);
203
308
  try {
204
- return (await this.managedRequest(routeHandle, body, opts));
309
+ return await this.managedRequest(routeHandle, body, opts);
205
310
  }
206
311
  catch (err) {
207
312
  if (!(err instanceof SubcCallError))
@@ -408,7 +513,7 @@ export class SubcClient {
408
513
  async controlRpc(body, acceptFrame, onLateResponse) {
409
514
  return this.send(null, body, Priority.Interactive, AdmissionClass.Normal, undefined, undefined, acceptFrame, onLateResponse);
410
515
  }
411
- send(handle, body, priority, admission, timeoutMs, onProgress, acceptFrame, onLateResponse) {
516
+ send(handle, body, priority, admission, timeoutMs, onProgress, acceptFrame, onLateResponse, binary = false) {
412
517
  if (handle)
413
518
  this.assertLiveHandle(handle);
414
519
  if (this.closedErr)
@@ -423,7 +528,7 @@ export class SubcClient {
423
528
  const key = pendingKey(handle, corr);
424
529
  const channel = handle?.channel ?? 0;
425
530
  const epoch = handle?.epoch ?? 0;
426
- const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), channel, epoch, corr, body);
531
+ const frame = buildFrame(FrameType.Request, buildFlags(binary, priority, false, admission), channel, epoch, corr, body);
427
532
  return new Promise((resolve, reject) => {
428
533
  const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
429
534
  const pending = {
@@ -479,12 +584,13 @@ export class SubcClient {
479
584
  setImmediate(arbitrate);
480
585
  }
481
586
  async managedRequest(handle, body, opts) {
482
- const bytes = body instanceof Uint8Array ? body : this.encode(body);
587
+ const binary = opts.binary ?? false;
588
+ const bytes = binary ? requireBinaryBody(body) : body instanceof Uint8Array ? body : this.encode(body);
483
589
  const priority = opts.priority ?? Priority.Interactive;
484
590
  const admission = opts.admissionClass ?? AdmissionClass.Normal;
485
591
  try {
486
- const reply = await this.sendManaged(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
487
- return this.parseJson(reply);
592
+ const reply = await this.sendManaged(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress, binary);
593
+ return this.decodeReply(reply);
488
594
  }
489
595
  catch (error) {
490
596
  if (error instanceof SubcCallError)
@@ -492,7 +598,7 @@ export class SubcClient {
492
598
  throw this.terminalCallError("managed call failed", error);
493
599
  }
494
600
  }
495
- sendManaged(handle, body, priority, admission, timeoutMs, onProgress) {
601
+ sendManaged(handle, body, priority, admission, timeoutMs, onProgress, binary = false) {
496
602
  try {
497
603
  this.assertLiveHandle(handle);
498
604
  }
@@ -510,7 +616,7 @@ export class SubcClient {
510
616
  return Promise.reject(this.notSentCallError("request correlation allocator was exhausted", error));
511
617
  }
512
618
  const key = pendingKey(handle, corr);
513
- const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), handle.channel, handle.epoch, corr, body);
619
+ const frame = buildFrame(FrameType.Request, buildFlags(binary, priority, false, admission), handle.channel, handle.epoch, corr, body);
514
620
  let handedToSocket = false;
515
621
  const classifyFailure = (error) => {
516
622
  if (!handedToSocket)
@@ -572,7 +678,7 @@ export class SubcClient {
572
678
  return cached.opening;
573
679
  }
574
680
  async openCachedRoute(cached) {
575
- const routeRetryDeadline = Date.now() + ROUTE_OPEN_RETRY_DEADLINE_MS;
681
+ const routeRetryDeadline = Date.now() + this.opts.routeOpenRetryDeadlineMs;
576
682
  let routeRetryDelay = this.opts.reconnectBackoff.baseMs;
577
683
  let routeRetryAttempt = 0;
578
684
  for (;;) {
@@ -611,13 +717,20 @@ export class SubcClient {
611
717
  continue;
612
718
  }
613
719
  if (!this.closeStarted && error instanceof SubcError && isRetryableRouteOpenCode(error.code)) {
720
+ // The deadline is the ONLY binder here. An attempt cap used to share
721
+ // this condition, and because the capped backoff sums to ~3.1s it
722
+ // strictly dominated the 30s deadline — the advertised reload
723
+ // patience was never delivered, so every module restart whose reload
724
+ // exceeded ~3s failed managed callers with module_reloading. Module
725
+ // reloads legitimately take tens of seconds (drain alone defaults to
726
+ // 30s); the backoff cap below bounds retry pressure instead.
614
727
  routeRetryAttempt += 1;
615
- if (routeRetryAttempt < this.opts.reconnectBackoff.maxAttempts && Date.now() < routeRetryDeadline) {
728
+ if (Date.now() < routeRetryDeadline) {
616
729
  await this.opts.sleep(routeRetryDelay);
617
730
  routeRetryDelay = Math.min(routeRetryDelay * 2, this.opts.reconnectBackoff.capMs);
618
731
  continue;
619
732
  }
620
- throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${error.code} (retry budget exhausted)`, error);
733
+ throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${error.code} (retry deadline exhausted after ${routeRetryAttempt} attempts)`, error);
621
734
  }
622
735
  throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, error);
623
736
  }
@@ -786,20 +899,34 @@ export class SubcClient {
786
899
  void this.readLoop(opened.sock, this.generation);
787
900
  }
788
901
  async reopenCachedRoutes() {
789
- for (const cached of this.routes.values())
790
- cached.handle = null;
791
- for (const cached of this.routes.values()) {
792
- if (cached.closed)
793
- continue;
794
- const handle = await this.routeOpen(cached.target, cached.identity, {
795
- consumerIdentity: cached.consumerIdentity ?? null,
796
- });
797
- if (cached.closed) {
798
- this.liveRoutes.delete(handle.channel);
799
- this.sendRouteGoodbye(handle);
902
+ const routeKeys = [...this.routes.keys()];
903
+ for (const key of routeKeys) {
904
+ const cached = this.routes.get(key);
905
+ if (cached)
906
+ cached.handle = null;
907
+ }
908
+ for (const key of routeKeys) {
909
+ const cached = this.routes.get(key);
910
+ if (!cached || cached.closed)
800
911
  continue;
912
+ try {
913
+ const handle = await this.routeOpen(cached.target, cached.identity, {
914
+ consumerIdentity: cached.consumerIdentity ?? null,
915
+ });
916
+ if (cached.closed) {
917
+ this.liveRoutes.delete(handle.channel);
918
+ this.sendRouteGoodbye(handle);
919
+ continue;
920
+ }
921
+ cached.handle = handle;
922
+ }
923
+ catch (error) {
924
+ if (isRouteOpenRefusal(error)) {
925
+ this.routes.delete(key);
926
+ continue;
927
+ }
928
+ throw error;
801
929
  }
802
- cached.handle = handle;
803
930
  }
804
931
  }
805
932
  // A request timeout carries the local socket port and (channel, corr) so a
@@ -841,6 +968,13 @@ export class SubcClient {
841
968
  // post-deadline liveness probe reads this stamp. Stamped after readLoop's
842
969
  // generation gate, so a stale socket's late frames never vouch for the
843
970
  // live one.
971
+ //
972
+ // PLACEMENT IS LOAD-BEARING: every inbound frame passes this one point
973
+ // before demux, which is the whole reason a single stamp works. A future
974
+ // fast path or drain-and-dispatch refactor that routes frames around
975
+ // dispatch() makes the stamp skippable, and the liveness watermark
976
+ // quietly stops meaning "the link delivered bytes" -- the cheapest
977
+ // correctness property here is also the easiest to lose in a refactor.
844
978
  this.lastInboundAtMs = Date.now();
845
979
  if (frame.header.channel === 0 && frame.header.ty === FrameType.Push) {
846
980
  // Daemon-originated control push (route.closing / route.closed / future
@@ -976,7 +1110,7 @@ export class SubcClient {
976
1110
  errorFromFrame(frame) {
977
1111
  try {
978
1112
  const parsed = JSON.parse(Buffer.from(frame.body).toString("utf8"));
979
- return new SubcError(parsed.message ?? "subc error", parsed.code);
1113
+ return new SubcError(parsed.message ?? "subc error", parsed.code, parsed.detail);
980
1114
  }
981
1115
  catch {
982
1116
  return new SubcError(Buffer.from(frame.body).toString("utf8") || "subc error");
@@ -1061,6 +1195,10 @@ export class SubcClient {
1061
1195
  encode(value) {
1062
1196
  return Buffer.from(JSON.stringify(value), "utf8");
1063
1197
  }
1198
+ /** Decode a terminal response according to the representation on the wire. */
1199
+ decodeReply(frame) {
1200
+ return hasBinary(frame.header.flags) ? frame.body : this.parseJson(frame);
1201
+ }
1064
1202
  parseJson(frame) {
1065
1203
  const b = frame.body;
1066
1204
  return JSON.parse(Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString("utf8"));
@@ -1089,19 +1227,31 @@ export function isConsumerReconnectTransient(err) {
1089
1227
  const code = errorCode(err);
1090
1228
  return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ENOENT";
1091
1229
  }
1230
+ /**
1231
+ * A coded SubcError came from a complete wire Error frame, so the connection
1232
+ * survived long enough to refuse only this route. An uncoded SubcError can mean
1233
+ * the socket or protocol exchange failed mid-open and must still fail reconnect.
1234
+ */
1235
+ function isRouteOpenRefusal(err) {
1236
+ return err instanceof SubcError && typeof err.code === "string";
1237
+ }
1092
1238
  /**
1093
1239
  * The closed set of route.open rejection codes that mean "the target is
1094
1240
  * momentarily unavailable but the request could succeed on retry" — the target
1095
1241
  * is booting, mid-reload, transiently absent, or the bind relay timed out. A
1096
1242
  * daemon-rejected route.open is provably pre-send (no data frame ever left the
1097
1243
  * client), so these classify as not_sent; the managed path retries them in-place
1098
- * within ROUTE_OPEN_RETRY_DEADLINE_MS. Permanent rejections (bad_consumer_identity,
1099
- * config_divergence, unknown_target, ...) are excluded — they are pre-send but
1100
- * would never succeed, so retrying them would only storm the daemon. Kept
1101
- * byte-identical to subc-client-rs is_retryable_route_open_code for cross-client
1244
+ * within ROUTE_OPEN_RETRY_DEADLINE_MS. Permanent rejections (module_removed,
1245
+ * bad_consumer_identity, config_divergence, unknown_target, ...) are excluded — they are pre-send but
1246
+ * would never succeed, so retrying them would only storm the daemon. In
1247
+ * particular, capability_forbidden is an explicit non-retryable policy refusal.
1248
+ * Kept byte-identical to subc-client-rs is_retryable_route_open_code for cross-client
1102
1249
  * classification parity.
1103
1250
  */
1104
1251
  export function isRetryableRouteOpenCode(code) {
1252
+ // A capability deny is policy, never a transient target-availability failure.
1253
+ if (code === "capability_forbidden")
1254
+ return false;
1105
1255
  return (code === "unknown_module" ||
1106
1256
  code === "module_reloading" ||
1107
1257
  code === "module_warming" ||
@@ -1125,11 +1275,32 @@ function normalizeConnectOptions(opts) {
1125
1275
  targetKind: opts.targetKind ?? DEFAULT_MANAGED_TARGET_KIND,
1126
1276
  reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
1127
1277
  sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
1278
+ routeOpenRetryDeadlineMs: opts.routeOpenRetryDeadlineMs ?? ROUTE_OPEN_RETRY_DEADLINE_MS,
1128
1279
  timeoutArbitrationGraceMs: opts.timeoutArbitrationGraceMs ?? TIMEOUT_ARBITRATION_GRACE_MS,
1129
1280
  livenessProbeWindowMs: opts.livenessProbeWindowMs ?? LIVENESS_PROBE_WINDOW_MS,
1130
1281
  onControlPush: opts.onControlPush,
1131
1282
  };
1132
1283
  }
1284
+ /** Validate the exact capability-grammar identifier before catalog I/O. */
1285
+ export function isValidCapabilityIdentifier(identifier) {
1286
+ if (/\s/u.test(identifier))
1287
+ return false;
1288
+ const separator = identifier.indexOf("/v");
1289
+ if (separator <= 0 || identifier.indexOf("/v", separator + 1) !== -1)
1290
+ return false;
1291
+ const name = identifier.slice(0, separator);
1292
+ const version = identifier.slice(separator + 2);
1293
+ if (new TextEncoder().encode(name).byteLength > 64 || version.length === 0)
1294
+ return false;
1295
+ if (!/^[a-z][a-z0-9-]*[a-z0-9]$/u.test(name) && !/^[a-z]$/u.test(name))
1296
+ return false;
1297
+ if (name.includes("--"))
1298
+ return false;
1299
+ if (!/^\d+$/u.test(version) || (version.length > 1 && version.startsWith("0")))
1300
+ return false;
1301
+ const numericVersion = Number(version);
1302
+ return Number.isSafeInteger(numericVersion) && numericVersion >= 1 && numericVersion <= 0xffff_ffff;
1303
+ }
1133
1304
  function routeCacheKey(target, identity, consumerIdentity) {
1134
1305
  const consumerPart = consumerIdentity
1135
1306
  ? `${consumerIdentity.module_id}\0${consumerIdentity.launch_nonce}`