norns-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/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to `norns-sdk` are documented in this file.
4
4
 
5
+ ## [0.4.0] - 2026-10-23
6
+
7
+ ### Changed
8
+ - `mode` is no longer sent on agent registration and is deprecated on
9
+ `defineAgent()`. Norns never read it: a conversation starts from a
10
+ `conversationKey` on the message, not from the definition.
11
+
12
+ ## [0.3.0] - 2026-10-22
13
+
14
+ ### Added
15
+ - A worker holding a scoped key (`nrnw_...`, issued for one gard's worker)
16
+ skips the agent registration on `start()`, since the key cannot reach the
17
+ REST API; the definitions are registered with a tenant key instead.
18
+ `isScopedKey()` is exported.
19
+ - `KeyRevokedError`: `worker.stopped` rejects with it when Norns revokes the
20
+ key the worker joined on.
21
+
5
22
  ## [0.2.0] - 2026-10-16
6
23
 
7
24
  ### Added
package/README.md CHANGED
@@ -44,7 +44,6 @@ const supportBot = defineAgent({
44
44
  model: "claude-sonnet-5",
45
45
  systemPrompt: "You are a customer support agent. Look up docs and help customers.",
46
46
  tools: [searchDocs, sendEmail],
47
- mode: "conversation",
48
47
  });
49
48
 
50
49
  const worker = new NornsWorker({ agents: [supportBot] });
@@ -90,6 +89,12 @@ execute: async ({ port }, ctx) => {
90
89
 
91
90
  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
91
 
92
+ 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`.
93
+
94
+ ```ts
95
+ const worker = new NornsWorker({ agents: [coder], gard: 3, claimToken: "tok_...", capabilities: ["tools"] });
96
+ ```
97
+
93
98
  ### Shutdown
94
99
 
95
100
  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.
@@ -202,7 +207,6 @@ const agent = defineAgent({
202
207
  model: "claude-sonnet-5",
203
208
  systemPrompt: "You are helpful.",
204
209
  tools: [search, sendEmail],
205
- mode: "conversation", // "task" or "conversation"
206
210
  checkpointPolicy: "on_tool_call", // "every_step", "on_tool_call", "manual"
207
211
  contextWindow: 20,
208
212
  maxSteps: 50,
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
@@ -73,7 +75,6 @@ function defineAgent(options) {
73
75
  model: options.model ?? DEFAULT_MODEL,
74
76
  systemPrompt: options.systemPrompt ?? "",
75
77
  tools: Object.freeze([...tools]),
76
- mode: options.mode ?? "task",
77
78
  checkpointPolicy: options.checkpointPolicy ?? "on_tool_call",
78
79
  contextStrategy: options.contextStrategy ?? "sliding_window",
79
80
  contextWindow: options.contextWindow ?? 20,
@@ -88,7 +89,6 @@ function defineAgent(options) {
88
89
  }
89
90
  function agentBody(agent) {
90
91
  const modelConfig = {
91
- mode: agent.mode,
92
92
  checkpoint_policy: agent.checkpointPolicy,
93
93
  context_strategy: agent.contextStrategy,
94
94
  context_window: agent.contextWindow,
@@ -245,6 +245,12 @@ var GardDestroyedError = class extends NornsError {
245
245
  this.name = "GardDestroyedError";
246
246
  }
247
247
  };
248
+ var KeyRevokedError = class extends NornsError {
249
+ constructor() {
250
+ super("the worker's scoped key was revoked");
251
+ this.name = "KeyRevokedError";
252
+ }
253
+ };
248
254
  var StreamClosedError = class extends NornsError {
249
255
  reason;
250
256
  constructor(reason) {
@@ -573,6 +579,9 @@ function resolveApiKey(apiKey) {
573
579
  if (!value) throw new NornsError("Norns API key missing: pass `apiKey` or set NORNS_API_KEY");
574
580
  return value;
575
581
  }
582
+ function isScopedKey(apiKey) {
583
+ return apiKey.startsWith("nrnw_");
584
+ }
576
585
  function wsBase(url) {
577
586
  return url.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
578
587
  }
@@ -1283,9 +1292,15 @@ var NornsWorker = class {
1283
1292
  return this.#starting;
1284
1293
  }
1285
1294
  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})`);
1295
+ if (isScopedKey(this.#apiKey)) {
1296
+ this.#logger.info(
1297
+ "Worker key is scoped (nrnw_): skipping agent registration. Register the agent definitions with a tenant key."
1298
+ );
1299
+ } else {
1300
+ for (const agent of this.#agents) {
1301
+ const info = await this.#client.agents.ensure(agent);
1302
+ this.#logger.info(`Registered agent '${agent.name}' (id=${info.id})`);
1303
+ }
1289
1304
  }
1290
1305
  if (this.#capabilities.includes("llm")) {
1291
1306
  for (const model of new Set(this.#agents.map((a) => a.model))) {
@@ -1336,6 +1351,10 @@ var NornsWorker = class {
1336
1351
  this.#logger.error("This worker's gard was destroyed; shutting down");
1337
1352
  this.#fail(new GardDestroyedError(this.gard));
1338
1353
  });
1354
+ channel.on("key_revoked", () => {
1355
+ this.#logger.error("This worker's key was revoked; shutting down");
1356
+ this.#fail(new KeyRevokedError());
1357
+ });
1339
1358
  channel.onError((reason) => {
1340
1359
  if (this.#state === "running")
1341
1360
  this.#logger.warn(`Channel error (${errorMessage(reason)}); rejoining`);
@@ -1649,6 +1668,7 @@ var NornsWorker = class {
1649
1668
  DEFAULT_MODEL,
1650
1669
  GardDestroyedError,
1651
1670
  JoinError,
1671
+ KeyRevokedError,
1652
1672
  NornsApiError,
1653
1673
  NornsClient,
1654
1674
  NornsError,
@@ -1659,6 +1679,7 @@ var NornsWorker = class {
1659
1679
  consoleLogger,
1660
1680
  defineAgent,
1661
1681
  inferProvider,
1682
+ isScopedKey,
1662
1683
  jsonSchema,
1663
1684
  silentLogger,
1664
1685
  tool