norns-sdk 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `norns-sdk` are documented in this file.
4
4
 
5
+ ## [0.3.0] - 2026-10-22
6
+
7
+ ### Added
8
+ - A worker holding a scoped key (`nrnw_...`, issued for one gard's worker)
9
+ skips the agent registration on `start()`, since the key cannot reach the
10
+ REST API; the definitions are registered with a tenant key instead.
11
+ `isScopedKey()` is exported.
12
+ - `KeyRevokedError`: `worker.stopped` rejects with it when Norns revokes the
13
+ key the worker joined on.
14
+
5
15
  ## [0.2.0] - 2026-10-16
6
16
 
7
17
  ### Added
package/README.md CHANGED
@@ -90,6 +90,12 @@ execute: async ({ port }, ctx) => {
90
90
 
91
91
  A fatally rejected claim (bad token, destroyed gard) rejects `start()` with `JoinError` instead of reconnect-looping. If the gard is destroyed while the worker is connected, `worker.stopped` rejects with `GardDestroyedError`.
92
92
 
93
+ A worker inside a gard should hold a [scoped key](https://github.com/nornscode/norns/blob/main/docs/gards.md#scoped-worker-keys) (`nrnw_...`) rather than the tenant key: it can join the worker socket, claim its gard and serve tasks, and nothing else. With one, `start()` skips the agent registration, since the key cannot reach the REST API; register the definitions with a tenant key instead. Pass `capabilities: ["tools"]` for a worker with no LLM access, matching what the key allows. If the key is revoked while the worker is connected, `worker.stopped` rejects with `KeyRevokedError`.
94
+
95
+ ```ts
96
+ const worker = new NornsWorker({ agents: [coder], gard: 3, claimToken: "tok_...", capabilities: ["tools"] });
97
+ ```
98
+
93
99
  ### Shutdown
94
100
 
95
101
  On SIGTERM or SIGINT the worker drains instead of dying mid-task: it tells Norns to stop sending it work, finishes the tasks it holds, reports their results, leaves the channel, and the process exits. Tasks still running after `shutdownTimeoutMs` (default 30 seconds, or `NORNS_SHUTDOWN_TIMEOUT` in seconds) are dropped and Norns re-dispatches them. A second signal exits at once.
package/dist/index.cjs CHANGED
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  DEFAULT_MODEL: () => DEFAULT_MODEL,
24
24
  GardDestroyedError: () => GardDestroyedError,
25
25
  JoinError: () => JoinError,
26
+ KeyRevokedError: () => KeyRevokedError,
26
27
  NornsApiError: () => NornsApiError,
27
28
  NornsClient: () => NornsClient,
28
29
  NornsError: () => NornsError,
@@ -33,6 +34,7 @@ __export(index_exports, {
33
34
  consoleLogger: () => consoleLogger,
34
35
  defineAgent: () => defineAgent,
35
36
  inferProvider: () => inferProvider,
37
+ isScopedKey: () => isScopedKey,
36
38
  jsonSchema: () => import_ai4.jsonSchema,
37
39
  silentLogger: () => silentLogger,
38
40
  tool: () => tool
@@ -245,6 +247,12 @@ var GardDestroyedError = class extends NornsError {
245
247
  this.name = "GardDestroyedError";
246
248
  }
247
249
  };
250
+ var KeyRevokedError = class extends NornsError {
251
+ constructor() {
252
+ super("the worker's scoped key was revoked");
253
+ this.name = "KeyRevokedError";
254
+ }
255
+ };
248
256
  var StreamClosedError = class extends NornsError {
249
257
  reason;
250
258
  constructor(reason) {
@@ -573,6 +581,9 @@ function resolveApiKey(apiKey) {
573
581
  if (!value) throw new NornsError("Norns API key missing: pass `apiKey` or set NORNS_API_KEY");
574
582
  return value;
575
583
  }
584
+ function isScopedKey(apiKey) {
585
+ return apiKey.startsWith("nrnw_");
586
+ }
576
587
  function wsBase(url) {
577
588
  return url.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
578
589
  }
@@ -1283,9 +1294,15 @@ var NornsWorker = class {
1283
1294
  return this.#starting;
1284
1295
  }
1285
1296
  async #start() {
1286
- for (const agent of this.#agents) {
1287
- const info = await this.#client.agents.ensure(agent);
1288
- this.#logger.info(`Registered agent '${agent.name}' (id=${info.id})`);
1297
+ if (isScopedKey(this.#apiKey)) {
1298
+ this.#logger.info(
1299
+ "Worker key is scoped (nrnw_): skipping agent registration. Register the agent definitions with a tenant key."
1300
+ );
1301
+ } else {
1302
+ for (const agent of this.#agents) {
1303
+ const info = await this.#client.agents.ensure(agent);
1304
+ this.#logger.info(`Registered agent '${agent.name}' (id=${info.id})`);
1305
+ }
1289
1306
  }
1290
1307
  if (this.#capabilities.includes("llm")) {
1291
1308
  for (const model of new Set(this.#agents.map((a) => a.model))) {
@@ -1336,6 +1353,10 @@ var NornsWorker = class {
1336
1353
  this.#logger.error("This worker's gard was destroyed; shutting down");
1337
1354
  this.#fail(new GardDestroyedError(this.gard));
1338
1355
  });
1356
+ channel.on("key_revoked", () => {
1357
+ this.#logger.error("This worker's key was revoked; shutting down");
1358
+ this.#fail(new KeyRevokedError());
1359
+ });
1339
1360
  channel.onError((reason) => {
1340
1361
  if (this.#state === "running")
1341
1362
  this.#logger.warn(`Channel error (${errorMessage(reason)}); rejoining`);
@@ -1649,6 +1670,7 @@ var NornsWorker = class {
1649
1670
  DEFAULT_MODEL,
1650
1671
  GardDestroyedError,
1651
1672
  JoinError,
1673
+ KeyRevokedError,
1652
1674
  NornsApiError,
1653
1675
  NornsClient,
1654
1676
  NornsError,
@@ -1659,6 +1681,7 @@ var NornsWorker = class {
1659
1681
  consoleLogger,
1660
1682
  defineAgent,
1661
1683
  inferProvider,
1684
+ isScopedKey,
1662
1685
  jsonSchema,
1663
1686
  silentLogger,
1664
1687
  tool