@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.mjs CHANGED
@@ -10,6 +10,7 @@ var CapabilityTypeSchema = z.string().min(1).regex(
10
10
  "capability type must be namespaced: {owner}/{package}[/{name}][.{variant}]"
11
11
  );
12
12
  var UserIdSchema = z.string().min(1);
13
+ var JsonSchemaSchema = z.unknown();
13
14
  var CapabilityScopeSchema = z.union([
14
15
  z.literal("any"),
15
16
  z.object({ onBehalfOf: UserIdSchema }).strict()
@@ -18,6 +19,36 @@ var CapabilitySchema = z.object({
18
19
  type: CapabilityTypeSchema,
19
20
  scope: CapabilityScopeSchema,
20
21
  concurrency: z.number().int().positive(),
22
+ /**
23
+ * Plain-language contract advertisement (HOL-2926). All three fields
24
+ * are optional and additive: older neurons keep sending
25
+ * `{ type, scope, concurrency }` and the gateway accepts both shapes.
26
+ * When present they flow (fire-and-forget) to Moku's
27
+ * `capability_catalog` so registry-sourced capabilities render with a
28
+ * real contract in the supervisor catalog instead of type-only stubs.
29
+ *
30
+ * Omit rather than null — the fields are `.optional()`, not
31
+ * `.nullable()`, and a `null` description fails validation.
32
+ */
33
+ description: z.string().optional(),
34
+ /**
35
+ * JSON Schema for the capability's input payload — opaque on the wire,
36
+ * but `null` is rejected: the reconcile push to Moku is last-writer-wins
37
+ * per present key, so a null here could wipe a stored schema.
38
+ * (`description` already rejects null via `z.string()`.)
39
+ */
40
+ // The `: boolean` return annotations stop TS (5.5+) from inferring the
41
+ // lambdas as type predicates, which would narrow the field from
42
+ // `unknown` to `{}` and ripple into every consumer.
43
+ inputSchema: JsonSchemaSchema.refine(
44
+ (v) => v !== null,
45
+ "omit inputSchema instead of sending null"
46
+ ).optional(),
47
+ /** JSON Schema for the capability's output payload — same rules as inputSchema. */
48
+ outputSchema: JsonSchemaSchema.refine(
49
+ (v) => v !== null,
50
+ "omit outputSchema instead of sending null"
51
+ ).optional(),
21
52
  /**
22
53
  * Per-capability lease TTL override in milliseconds (HOL-356).
23
54
  * The gateway uses this when minting a lease's `expiresAt`; absent
@@ -33,7 +64,6 @@ var CapabilitySchema = z.object({
33
64
  */
34
65
  leaseTtlMs: z.number().int().min(1e3).max(60 * 60 * 1e3).optional()
35
66
  }).strict();
36
- var JsonSchemaSchema = z.unknown();
37
67
  var LeaseIdSchema = z.string().min(1);
38
68
  var TaskPayloadSchema = z.object({
39
69
  taskId: z.string().min(1),
@@ -55,9 +85,10 @@ var NeuronIdSchema = z.string().min(1);
55
85
  var RegisterFrameSchema = z.object({
56
86
  type: z.literal("register"),
57
87
  neuronId: NeuronIdSchema,
58
- // Free-form by design — the gateway negotiates compatibility against its
59
- // own PROTOCOL_VERSION (semver), so a v1.1 server can still accept a v1.0
60
- // neuron. Do not pin this to the exported constant.
88
+ // Free-form by design — the gateway records this verbatim and performs
89
+ // no version negotiation; compatibility comes from additive-only frame
90
+ // evolution (see PROTOCOL_VERSION in transport.ts). Do not pin this to
91
+ // the exported constant.
61
92
  protocolVersion: z.string().min(1),
62
93
  capabilities: z.array(CapabilitySchema).min(1).refine(
63
94
  (caps) => new Set(caps.map((c) => c.type)).size === caps.length,
@@ -171,7 +202,7 @@ z.discriminatedUnion("type", [
171
202
  ]);
172
203
 
173
204
  // ../neuron-protocol/src/transport.ts
174
- var PROTOCOL_VERSION = "1.0.0";
205
+ var PROTOCOL_VERSION = "1.1.0";
175
206
  var SSE_PATH = "/neuron/events";
176
207
  var SSE_EVENT = {
177
208
  registered: "registered",
@@ -226,6 +257,12 @@ function toRegistrationCapability(def, opts) {
226
257
  type: def.type,
227
258
  scope,
228
259
  concurrency,
260
+ // Contract advertisement (HOL-2926). `description` is required on the
261
+ // def, so it is always present here; `opts.description` lets a
262
+ // deployment reword it without redefining the capability.
263
+ description: opts?.description ?? def.description,
264
+ inputSchema: def.inputSchema,
265
+ outputSchema: def.outputSchema,
229
266
  ...typeof leaseTtlMs === "number" ? { leaseTtlMs } : {}
230
267
  };
231
268
  }
@@ -245,7 +282,6 @@ var HeartbeatLoop = class {
245
282
  this.timer = setInterval(() => {
246
283
  void this.tick();
247
284
  }, this.intervalMs);
248
- if (typeof this.timer.unref === "function") this.timer.unref();
249
285
  }
250
286
  stop() {
251
287
  if (this.timer) {
@@ -496,9 +532,6 @@ var HttpTransport = class {
496
532
  this.reconnectTimer = null;
497
533
  void this.openStream();
498
534
  }, delay);
499
- if (typeof this.reconnectTimer === "object" && this.reconnectTimer !== null && "unref" in this.reconnectTimer && typeof this.reconnectTimer.unref === "function") {
500
- this.reconnectTimer.unref();
501
- }
502
535
  }
503
536
  };
504
537
  var TransportError = class extends Error {
@@ -530,8 +563,19 @@ function validateInput(zod, validator, jsonSchema, data) {
530
563
  return errors === null ? { ok: true, value: data } : { ok: false, errors };
531
564
  }
532
565
  async function runLease(opts) {
533
- const { lease, handler, zodInput, zodOutput, transport, validator, logger, signal } = opts;
534
- const inputCheck = validateInput(zodInput, validator, lease.inputSchema, lease.task.input);
566
+ const {
567
+ lease,
568
+ handler,
569
+ zodInput,
570
+ zodOutput,
571
+ inputSchema,
572
+ outputSchema,
573
+ transport,
574
+ validator,
575
+ logger,
576
+ signal
577
+ } = opts;
578
+ const inputCheck = validateInput(zodInput, validator, inputSchema, lease.task.input);
535
579
  if (!inputCheck.ok) {
536
580
  await sendNack(
537
581
  transport,
@@ -593,7 +637,7 @@ async function runLease(opts) {
593
637
  );
594
638
  return;
595
639
  }
596
- const outputCheck = validateInput(zodOutput, validator, lease.outputSchema, output);
640
+ const outputCheck = validateInput(zodOutput, validator, outputSchema, output);
597
641
  if (!outputCheck.ok) {
598
642
  await sendNack(
599
643
  transport,
@@ -660,32 +704,39 @@ function scopedLogger(logger, lease) {
660
704
  var Ajv = AjvDefault.default ?? AjvDefault;
661
705
  var addFormats = addFormatsDefault.default ?? addFormatsDefault;
662
706
  var SchemaValidator = class {
663
- ajv;
664
- cache = /* @__PURE__ */ new WeakMap();
665
- constructor() {
707
+ constructor(onCompileError) {
708
+ this.onCompileError = onCompileError;
666
709
  this.ajv = new Ajv({ allErrors: true, strict: false });
667
710
  addFormats(this.ajv);
668
711
  }
712
+ ajv;
713
+ cache = /* @__PURE__ */ new WeakMap();
714
+ // Schemas whose compilation threw. Cannot-compile is an environment or
715
+ // schema-authoring problem, not evidence about the data — so validate()
716
+ // FAILS OPEN for these (returns null) instead of reporting a mismatch.
717
+ // Nacking on compile failure turned every lease into an
718
+ // INPUT_SCHEMA_MISMATCH retry loop on runtimes where ajv's codegen is
719
+ // unavailable (Electron renderers with CSP `script-src 'self'` block
720
+ // `new Function`) — HOL-2937.
721
+ uncompilable = /* @__PURE__ */ new WeakSet();
669
722
  /**
670
- * Returns null if the value matches the schema (or no schema given);
671
- * otherwise an array of structured errors.
723
+ * Returns null if the value matches the schema, no schema was given, or
724
+ * the schema cannot be compiled in this runtime (fail-open — the gateway
725
+ * remains the enforcement point); otherwise an array of structured errors.
672
726
  */
673
727
  validate(schema, data) {
674
728
  if (!schema || typeof schema !== "object") return null;
729
+ if (this.uncompilable.has(schema)) return null;
675
730
  let validator = this.cache.get(schema);
676
731
  if (!validator) {
677
732
  try {
678
733
  validator = this.ajv.compile(schema);
679
734
  } catch (err) {
680
- return [
681
- {
682
- instancePath: "",
683
- schemaPath: "",
684
- keyword: "schema",
685
- params: {},
686
- message: `schema failed to compile: ${err instanceof Error ? err.message : String(err)}`
687
- }
688
- ];
735
+ this.uncompilable.add(schema);
736
+ this.onCompileError?.(
737
+ `schema failed to compile \u2014 skipping SDK-side validation for this schema: ${err instanceof Error ? err.message : String(err)}`
738
+ );
739
+ return null;
689
740
  }
690
741
  this.cache.set(schema, validator);
691
742
  }
@@ -818,7 +869,9 @@ var Neuron = class extends EventEmitter {
818
869
  super();
819
870
  this.logger = opts.logger ?? noopLogger;
820
871
  this.neuronId = opts.neuronId;
821
- this.validator = new SchemaValidator();
872
+ this.validator = new SchemaValidator(
873
+ (message) => this.logger.warn({ neuronId: this.neuronId }, message)
874
+ );
822
875
  this.transport = isWithTransport(opts) ? opts.transport : new HttpTransport({
823
876
  gatewayUrl: opts.gatewayUrl,
824
877
  neuronId: opts.neuronId,
@@ -890,7 +943,15 @@ var Neuron = class extends EventEmitter {
890
943
  );
891
944
  }
892
945
  handler = optsOrHandler;
893
- capability = { type: reg.type, scope: reg.scope, concurrency: reg.concurrency };
946
+ capability = {
947
+ type: reg.type,
948
+ scope: reg.scope,
949
+ concurrency: reg.concurrency,
950
+ ...reg.description !== void 0 ? { description: reg.description } : {},
951
+ ...reg.inputSchema !== void 0 ? { inputSchema: reg.inputSchema } : {},
952
+ ...reg.outputSchema !== void 0 ? { outputSchema: reg.outputSchema } : {},
953
+ ...typeof reg.leaseTtlMs === "number" ? { leaseTtlMs: reg.leaseTtlMs } : {}
954
+ };
894
955
  inputSchema = reg.inputSchema;
895
956
  outputSchema = reg.outputSchema;
896
957
  }
@@ -1001,7 +1062,15 @@ var Neuron = class extends EventEmitter {
1001
1062
  this.hasOpenedOnce = true;
1002
1063
  this.heartbeat.start();
1003
1064
  } catch (err) {
1004
- this.logger.error({ err }, "register POST failed; transport will retry on reconnect");
1065
+ const cause = err instanceof Error ? err : new Error(String(err));
1066
+ this.logger.error(
1067
+ { err: cause },
1068
+ "register POST rejected; SSE is open but the neuron is NOT registered and will receive no leases"
1069
+ );
1070
+ this.emit(
1071
+ "error",
1072
+ new Error(`registration rejected by gateway: ${cause.message}`, { cause })
1073
+ );
1005
1074
  }
1006
1075
  }
1007
1076
  handleConnectionClose(reason) {
@@ -1078,6 +1147,8 @@ var Neuron = class extends EventEmitter {
1078
1147
  handler: handler.handler,
1079
1148
  zodInput: handler.zodInput,
1080
1149
  zodOutput: handler.zodOutput,
1150
+ inputSchema: handler.inputSchema,
1151
+ outputSchema: handler.outputSchema,
1081
1152
  transport: this.transport,
1082
1153
  validator: this.validator,
1083
1154
  logger: this.logger,