@holokai/neuron-sdk 0.2.0 → 0.4.0

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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { J as JsonSchema, C as Capability, a as CapabilityHandleOptions, S as ServerToNeuronFrame, N as NeuronLogger, T as Transport, A as AuthCallback, H as Handler, b as CapabilityRegistration, c as TransportCallbacks, d as ClientFrame, P as PostPath } from './transport-QqkooS7K.js';
2
- export { e as HandlerContext, f as HeartbeatInFlightEntry, L as LeasePayload, g as TaskPayload } from './transport-QqkooS7K.js';
1
+ import { J as JsonSchema, C as Capability, a as CapabilityHandleOptions, S as ServerToNeuronFrame, N as NeuronLogger, T as Transport, A as AuthCallback, H as Handler, b as CapabilityRegistration, c as TransportCallbacks, d as ClientFrame, P as PostPath } from './transport-Cs5tY2IO.js';
2
+ export { e as HandlerContext, f as HeartbeatInFlightEntry, L as LeasePayload, g as TaskPayload } from './transport-Cs5tY2IO.js';
3
3
  import { EventEmitter } from 'eventemitter3';
4
4
  import { z } from 'zod';
5
5
  import { ErrorObject } from 'ajv';
@@ -97,6 +97,11 @@ declare function toPlanShape(def: CapabilityDef): CapabilityPlanShape;
97
97
  * required at registration time — if neither the def nor `opts` provides
98
98
  * them, this throws so the misconfiguration surfaces at start time
99
99
  * rather than as a confusing wire-validation error.
100
+ *
101
+ * Since HOL-2926 the frame also carries the contract (`description` +
102
+ * compiled JSON Schemas): the gateway forwards it to Moku's capability
103
+ * catalog so registry-sourced capabilities render with a real contract
104
+ * in the supervisor catalog instead of type-only stubs.
100
105
  */
101
106
  declare function toRegistrationCapability(def: CapabilityDef, opts?: CapabilityHandleOptions): Capability;
102
107
 
@@ -141,7 +146,12 @@ interface NeuronEvents {
141
146
  delayMs: number;
142
147
  reason: string;
143
148
  }) => void;
144
- /** Fired on transport / parse errors. Reconnect handling is automatic. */
149
+ /**
150
+ * Fired on transport / parse errors (reconnect handling is automatic)
151
+ * and on register rejection (HOL-2926 — NOT automatically recovered:
152
+ * the SSE stream stays open, so the neuron sits connected but
153
+ * lease-less until the app reacts, e.g. by calling `stop()`).
154
+ */
145
155
  error: (err: Error) => void;
146
156
  /** Pass-through of every parsed inbound SSE frame. */
147
157
  frame: (frame: ServerToNeuronFrame) => void;
@@ -176,7 +186,7 @@ interface NeuronOptionsWithTransport extends NeuronCommonOptions {
176
186
  * surface so existing call sites keep compiling unchanged.
177
187
  */
178
188
  interface NeuronOptionsWithHttp extends NeuronCommonOptions {
179
- /** Base URL of the gateway, e.g. `https://api.holokai.dev`. */
189
+ /** Base URL of the gateway, e.g. `https://bigbrain.holokai.dev`. */
180
190
  gatewayUrl: string;
181
191
  /** Token provider — see `AuthCallback` for refresh semantics. */
182
192
  auth: AuthCallback;
@@ -276,7 +286,7 @@ declare const SSE_EVENT_NAMES: {
276
286
  };
277
287
 
278
288
  interface HttpTransportOptions {
279
- /** Base URL of the gateway (e.g. `https://api.holokai.dev`). */
289
+ /** Base URL of the gateway (e.g. `https://bigbrain.holokai.dev`). */
280
290
  gatewayUrl: string;
281
291
  /** Stable per-process identifier sent as `?neuronId=` on the SSE GET. */
282
292
  neuronId: string;
@@ -395,12 +405,15 @@ declare class ShutdownError extends Error {
395
405
  * the same schemas so a misbehaving party can't sneak by either side.
396
406
  */
397
407
  declare class SchemaValidator {
408
+ private readonly onCompileError?;
398
409
  private readonly ajv;
399
410
  private readonly cache;
400
- constructor();
411
+ private readonly uncompilable;
412
+ constructor(onCompileError?: ((message: string) => void) | undefined);
401
413
  /**
402
- * Returns null if the value matches the schema (or no schema given);
403
- * otherwise an array of structured errors.
414
+ * Returns null if the value matches the schema, no schema was given, or
415
+ * the schema cannot be compiled in this runtime (fail-open — the gateway
416
+ * remains the enforcement point); otherwise an array of structured errors.
404
417
  */
405
418
  validate(schema: JsonSchema | undefined, data: unknown): ErrorObject[] | null;
406
419
  }
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ var CapabilityTypeSchema = zod.z.string().min(1).regex(
17
17
  "capability type must be namespaced: {owner}/{package}[/{name}][.{variant}]"
18
18
  );
19
19
  var UserIdSchema = zod.z.string().min(1);
20
+ var JsonSchemaSchema = zod.z.unknown();
20
21
  var CapabilityScopeSchema = zod.z.union([
21
22
  zod.z.literal("any"),
22
23
  zod.z.object({ onBehalfOf: UserIdSchema }).strict()
@@ -25,6 +26,36 @@ var CapabilitySchema = zod.z.object({
25
26
  type: CapabilityTypeSchema,
26
27
  scope: CapabilityScopeSchema,
27
28
  concurrency: zod.z.number().int().positive(),
29
+ /**
30
+ * Plain-language contract advertisement (HOL-2926). All three fields
31
+ * are optional and additive: older neurons keep sending
32
+ * `{ type, scope, concurrency }` and the gateway accepts both shapes.
33
+ * When present they flow (fire-and-forget) to Moku's
34
+ * `capability_catalog` so registry-sourced capabilities render with a
35
+ * real contract in the supervisor catalog instead of type-only stubs.
36
+ *
37
+ * Omit rather than null — the fields are `.optional()`, not
38
+ * `.nullable()`, and a `null` description fails validation.
39
+ */
40
+ description: zod.z.string().optional(),
41
+ /**
42
+ * JSON Schema for the capability's input payload — opaque on the wire,
43
+ * but `null` is rejected: the reconcile push to Moku is last-writer-wins
44
+ * per present key, so a null here could wipe a stored schema.
45
+ * (`description` already rejects null via `z.string()`.)
46
+ */
47
+ // The `: boolean` return annotations stop TS (5.5+) from inferring the
48
+ // lambdas as type predicates, which would narrow the field from
49
+ // `unknown` to `{}` and ripple into every consumer.
50
+ inputSchema: JsonSchemaSchema.refine(
51
+ (v) => v !== null,
52
+ "omit inputSchema instead of sending null"
53
+ ).optional(),
54
+ /** JSON Schema for the capability's output payload — same rules as inputSchema. */
55
+ outputSchema: JsonSchemaSchema.refine(
56
+ (v) => v !== null,
57
+ "omit outputSchema instead of sending null"
58
+ ).optional(),
28
59
  /**
29
60
  * Per-capability lease TTL override in milliseconds (HOL-356).
30
61
  * The gateway uses this when minting a lease's `expiresAt`; absent
@@ -40,7 +71,6 @@ var CapabilitySchema = zod.z.object({
40
71
  */
41
72
  leaseTtlMs: zod.z.number().int().min(1e3).max(60 * 60 * 1e3).optional()
42
73
  }).strict();
43
- var JsonSchemaSchema = zod.z.unknown();
44
74
  var LeaseIdSchema = zod.z.string().min(1);
45
75
  var TaskPayloadSchema = zod.z.object({
46
76
  taskId: zod.z.string().min(1),
@@ -62,9 +92,10 @@ var NeuronIdSchema = zod.z.string().min(1);
62
92
  var RegisterFrameSchema = zod.z.object({
63
93
  type: zod.z.literal("register"),
64
94
  neuronId: NeuronIdSchema,
65
- // Free-form by design — the gateway negotiates compatibility against its
66
- // own PROTOCOL_VERSION (semver), so a v1.1 server can still accept a v1.0
67
- // neuron. Do not pin this to the exported constant.
95
+ // Free-form by design — the gateway records this verbatim and performs
96
+ // no version negotiation; compatibility comes from additive-only frame
97
+ // evolution (see PROTOCOL_VERSION in transport.ts). Do not pin this to
98
+ // the exported constant.
68
99
  protocolVersion: zod.z.string().min(1),
69
100
  capabilities: zod.z.array(CapabilitySchema).min(1).refine(
70
101
  (caps) => new Set(caps.map((c) => c.type)).size === caps.length,
@@ -178,7 +209,7 @@ zod.z.discriminatedUnion("type", [
178
209
  ]);
179
210
 
180
211
  // ../neuron-protocol/src/transport.ts
181
- var PROTOCOL_VERSION = "1.0.0";
212
+ var PROTOCOL_VERSION = "1.1.0";
182
213
  var SSE_PATH = "/neuron/events";
183
214
  var SSE_EVENT = {
184
215
  registered: "registered",
@@ -233,6 +264,12 @@ function toRegistrationCapability(def, opts) {
233
264
  type: def.type,
234
265
  scope,
235
266
  concurrency,
267
+ // Contract advertisement (HOL-2926). `description` is required on the
268
+ // def, so it is always present here; `opts.description` lets a
269
+ // deployment reword it without redefining the capability.
270
+ description: opts?.description ?? def.description,
271
+ inputSchema: def.inputSchema,
272
+ outputSchema: def.outputSchema,
236
273
  ...typeof leaseTtlMs === "number" ? { leaseTtlMs } : {}
237
274
  };
238
275
  }
@@ -252,7 +289,6 @@ var HeartbeatLoop = class {
252
289
  this.timer = setInterval(() => {
253
290
  void this.tick();
254
291
  }, this.intervalMs);
255
- if (typeof this.timer.unref === "function") this.timer.unref();
256
292
  }
257
293
  stop() {
258
294
  if (this.timer) {
@@ -503,9 +539,6 @@ var HttpTransport = class {
503
539
  this.reconnectTimer = null;
504
540
  void this.openStream();
505
541
  }, delay);
506
- if (typeof this.reconnectTimer === "object" && this.reconnectTimer !== null && "unref" in this.reconnectTimer && typeof this.reconnectTimer.unref === "function") {
507
- this.reconnectTimer.unref();
508
- }
509
542
  }
510
543
  };
511
544
  var TransportError = class extends Error {
@@ -537,8 +570,19 @@ function validateInput(zod, validator, jsonSchema, data) {
537
570
  return errors === null ? { ok: true, value: data } : { ok: false, errors };
538
571
  }
539
572
  async function runLease(opts) {
540
- const { lease, handler, zodInput, zodOutput, transport, validator, logger, signal } = opts;
541
- const inputCheck = validateInput(zodInput, validator, lease.inputSchema, lease.task.input);
573
+ const {
574
+ lease,
575
+ handler,
576
+ zodInput,
577
+ zodOutput,
578
+ inputSchema,
579
+ outputSchema,
580
+ transport,
581
+ validator,
582
+ logger,
583
+ signal
584
+ } = opts;
585
+ const inputCheck = validateInput(zodInput, validator, inputSchema, lease.task.input);
542
586
  if (!inputCheck.ok) {
543
587
  await sendNack(
544
588
  transport,
@@ -600,7 +644,7 @@ async function runLease(opts) {
600
644
  );
601
645
  return;
602
646
  }
603
- const outputCheck = validateInput(zodOutput, validator, lease.outputSchema, output);
647
+ const outputCheck = validateInput(zodOutput, validator, outputSchema, output);
604
648
  if (!outputCheck.ok) {
605
649
  await sendNack(
606
650
  transport,
@@ -667,32 +711,39 @@ function scopedLogger(logger, lease) {
667
711
  var Ajv = AjvDefault__default.default.default ?? AjvDefault__default.default;
668
712
  var addFormats = addFormatsDefault__default.default.default ?? addFormatsDefault__default.default;
669
713
  var SchemaValidator = class {
670
- ajv;
671
- cache = /* @__PURE__ */ new WeakMap();
672
- constructor() {
714
+ constructor(onCompileError) {
715
+ this.onCompileError = onCompileError;
673
716
  this.ajv = new Ajv({ allErrors: true, strict: false });
674
717
  addFormats(this.ajv);
675
718
  }
719
+ ajv;
720
+ cache = /* @__PURE__ */ new WeakMap();
721
+ // Schemas whose compilation threw. Cannot-compile is an environment or
722
+ // schema-authoring problem, not evidence about the data — so validate()
723
+ // FAILS OPEN for these (returns null) instead of reporting a mismatch.
724
+ // Nacking on compile failure turned every lease into an
725
+ // INPUT_SCHEMA_MISMATCH retry loop on runtimes where ajv's codegen is
726
+ // unavailable (Electron renderers with CSP `script-src 'self'` block
727
+ // `new Function`) — HOL-2937.
728
+ uncompilable = /* @__PURE__ */ new WeakSet();
676
729
  /**
677
- * Returns null if the value matches the schema (or no schema given);
678
- * otherwise an array of structured errors.
730
+ * Returns null if the value matches the schema, no schema was given, or
731
+ * the schema cannot be compiled in this runtime (fail-open — the gateway
732
+ * remains the enforcement point); otherwise an array of structured errors.
679
733
  */
680
734
  validate(schema, data) {
681
735
  if (!schema || typeof schema !== "object") return null;
736
+ if (this.uncompilable.has(schema)) return null;
682
737
  let validator = this.cache.get(schema);
683
738
  if (!validator) {
684
739
  try {
685
740
  validator = this.ajv.compile(schema);
686
741
  } catch (err) {
687
- return [
688
- {
689
- instancePath: "",
690
- schemaPath: "",
691
- keyword: "schema",
692
- params: {},
693
- message: `schema failed to compile: ${err instanceof Error ? err.message : String(err)}`
694
- }
695
- ];
742
+ this.uncompilable.add(schema);
743
+ this.onCompileError?.(
744
+ `schema failed to compile \u2014 skipping SDK-side validation for this schema: ${err instanceof Error ? err.message : String(err)}`
745
+ );
746
+ return null;
696
747
  }
697
748
  this.cache.set(schema, validator);
698
749
  }
@@ -825,7 +876,9 @@ var Neuron = class extends eventemitter3.EventEmitter {
825
876
  super();
826
877
  this.logger = opts.logger ?? noopLogger;
827
878
  this.neuronId = opts.neuronId;
828
- this.validator = new SchemaValidator();
879
+ this.validator = new SchemaValidator(
880
+ (message) => this.logger.warn({ neuronId: this.neuronId }, message)
881
+ );
829
882
  this.transport = isWithTransport(opts) ? opts.transport : new HttpTransport({
830
883
  gatewayUrl: opts.gatewayUrl,
831
884
  neuronId: opts.neuronId,
@@ -897,7 +950,15 @@ var Neuron = class extends eventemitter3.EventEmitter {
897
950
  );
898
951
  }
899
952
  handler = optsOrHandler;
900
- capability = { type: reg.type, scope: reg.scope, concurrency: reg.concurrency };
953
+ capability = {
954
+ type: reg.type,
955
+ scope: reg.scope,
956
+ concurrency: reg.concurrency,
957
+ ...reg.description !== void 0 ? { description: reg.description } : {},
958
+ ...reg.inputSchema !== void 0 ? { inputSchema: reg.inputSchema } : {},
959
+ ...reg.outputSchema !== void 0 ? { outputSchema: reg.outputSchema } : {},
960
+ ...typeof reg.leaseTtlMs === "number" ? { leaseTtlMs: reg.leaseTtlMs } : {}
961
+ };
901
962
  inputSchema = reg.inputSchema;
902
963
  outputSchema = reg.outputSchema;
903
964
  }
@@ -1008,7 +1069,15 @@ var Neuron = class extends eventemitter3.EventEmitter {
1008
1069
  this.hasOpenedOnce = true;
1009
1070
  this.heartbeat.start();
1010
1071
  } catch (err) {
1011
- this.logger.error({ err }, "register POST failed; transport will retry on reconnect");
1072
+ const cause = err instanceof Error ? err : new Error(String(err));
1073
+ this.logger.error(
1074
+ { err: cause },
1075
+ "register POST rejected; SSE is open but the neuron is NOT registered and will receive no leases"
1076
+ );
1077
+ this.emit(
1078
+ "error",
1079
+ new Error(`registration rejected by gateway: ${cause.message}`, { cause })
1080
+ );
1012
1081
  }
1013
1082
  }
1014
1083
  handleConnectionClose(reason) {
@@ -1085,6 +1154,8 @@ var Neuron = class extends eventemitter3.EventEmitter {
1085
1154
  handler: handler.handler,
1086
1155
  zodInput: handler.zodInput,
1087
1156
  zodOutput: handler.zodOutput,
1157
+ inputSchema: handler.inputSchema,
1158
+ outputSchema: handler.outputSchema,
1088
1159
  transport: this.transport,
1089
1160
  validator: this.validator,
1090
1161
  logger: this.logger,