@nanobpm/nano-workforce 0.86.0 → 0.87.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
@@ -1,3 +1,10 @@
1
+ # [0.87.0](https://github.com/nanobpm/nano-workforce/compare/v0.86.0...v0.87.0) (2026-08-18)
2
+
3
+
4
+ ### Features
5
+
6
+ * **agentic:** frictionless zero-config cockpit — LOCAL mode accepts a tokenless upgrade ([#283](https://github.com/nanobpm/nano-workforce/issues/283)) ([b23dfbb](https://github.com/nanobpm/nano-workforce/commit/b23dfbb018241e4719c6ea664f6428d094069aa6)), closes [#278](https://github.com/nanobpm/nano-workforce/issues/278) [#224](https://github.com/nanobpm/nano-workforce/issues/224) [#282](https://github.com/nanobpm/nano-workforce/issues/282)
7
+
1
8
  # [0.86.0](https://github.com/nanobpm/nano-workforce/compare/v0.85.3...v0.86.0) (2026-08-18)
2
9
 
3
10
 
@@ -294,6 +294,77 @@ test("LOCAL mode still rejects a wrong token (4401)", async (t) => {
294
294
  assertEquals(channel.hub.connectionCount, 0);
295
295
  });
296
296
 
297
+ // --- Frictionless zero-config: LOCAL mode accepts a TOKENLESS upgrade (#282) ---
298
+ // The well-known LOCAL token is public, so requiring the client to echo it is pure friction: a
299
+ // genuinely zero-config cockpit (no `?token=`) must attach on the trusted LAN. LOCAL mode therefore
300
+ // treats a missing token as presenting the well-known token.
301
+
302
+ test("LOCAL mode (secure:false): a TOKENLESS upgrade opens (zero-config, #282)", async (t) => {
303
+ const { server, port } = await startHttp();
304
+ const channel = await mountAgenticChannel({
305
+ server,
306
+ secret: "",
307
+ secure: false,
308
+ data: undefined,
309
+ log: noopLog(),
310
+ });
311
+ t.after(async () => {
312
+ await channel.teardown();
313
+ await closeServer(server);
314
+ });
315
+
316
+ // No query at all — the browser cockpit's zero-config default (`defaultRelayUrl` sends no token).
317
+ const ws = await connect(port, "");
318
+ assertEquals(ws.readyState, WebSocket.OPEN);
319
+ assertEquals(channel.hub.connectionCount, 1);
320
+ ws.close();
321
+ });
322
+
323
+ test("SECURE mode still REJECTS a tokenless upgrade (4401) — substitution is LOCAL-only (#282)", async (t) => {
324
+ const { server, port } = await startHttp();
325
+ const channel = await mountAgenticChannel({
326
+ server,
327
+ secret: SECRET,
328
+ secure: true,
329
+ data: undefined,
330
+ log: noopLog(),
331
+ });
332
+ t.after(async () => {
333
+ await channel.teardown();
334
+ await closeServer(server);
335
+ });
336
+
337
+ const closedCode = await rejectionCode(port, "");
338
+ assertEquals(closedCode, 4401);
339
+ assertEquals(channel.hub.connectionCount, 0);
340
+ });
341
+
342
+ test("LOCAL mode with a CUSTOM token still REJECTS a tokenless upgrade (no bypass, #282)", async (t) => {
343
+ const { server, port } = await startHttp();
344
+ // A caller opting into a non-default token in LOCAL mode (secure:false + a custom secret) is NOT
345
+ // the frictionless well-known-token posture — the tokenless substitution must NOT apply, so a
346
+ // client cannot bypass the custom token by omitting `?token=`.
347
+ const channel = await mountAgenticChannel({
348
+ server,
349
+ secret: "custom-local-token",
350
+ secure: false,
351
+ data: undefined,
352
+ log: noopLog(),
353
+ });
354
+ t.after(async () => {
355
+ await channel.teardown();
356
+ await closeServer(server);
357
+ });
358
+
359
+ const closedCode = await rejectionCode(port, "");
360
+ assertEquals(closedCode, 4401);
361
+ assertEquals(channel.hub.connectionCount, 0);
362
+ // The custom token itself still upgrades.
363
+ const ws = await connect(port, "?token=custom-local-token");
364
+ assertEquals(ws.readyState, WebSocket.OPEN);
365
+ ws.close();
366
+ });
367
+
297
368
  /** A capturing `Logger`: records every `(level, msg)` pair the sink receives. */
298
369
  function capturingLog(): { log: ReturnType<typeof noopLog>; records: Array<{ level: string; msg: string }> } {
299
370
  const records: Array<{ level: string; msg: string }> = [];
@@ -15,6 +15,7 @@
15
15
  // is untouched; advisory semantics preserved (a family never gates a BPMN sequence flow).
16
16
  import type { Server } from "node:http";
17
17
  import type { AddressInfo } from "node:net";
18
+ import type { HandshakeRequest } from "@nanobpm/agentic/channel";
18
19
  import {
19
20
  AgenticHub,
20
21
  sharedSecretAuthenticator,
@@ -133,7 +134,22 @@ export async function mountAgenticChannel(
133
134
  // credential is intentionally NOT required — nano-workforce never verified it (accept-any), so it
134
135
  // was pure configuration friction; a real ADR 0028 capability check can reintroduce it later by
135
136
  // passing a verifier.
136
- const authenticator = sharedSecretAuthenticator({ secret, requireCredential: false });
137
+ const baseAuthenticator = sharedSecretAuthenticator({ secret, requireCredential: false });
138
+ // LOCAL mode is frictionless-first (#282): the well-known token is PUBLIC, so requiring a client to
139
+ // echo it adds no security — only friction. A genuinely zero-config cockpit (`defaultRelayUrl`
140
+ // sends no `?token=`) must attach on the trusted LAN, so a TOKENLESS upgrade is treated as
141
+ // presenting the well-known token. This substitution is scoped to the DEFAULT LOCAL posture only
142
+ // (`secret === LOCAL_AGENTIC_TOKEN`): a caller that opts into a CUSTOM token in LOCAL mode
143
+ // (`secure:false` + a non-default `secret`) still requires that token — a tokenless client can't
144
+ // bypass it. SECURE mode is likewise unaffected: it keeps demanding the real secret, so a tokenless
145
+ // (or wrong) upgrade is still rejected (4401).
146
+ const substituteWellKnownToken = !secure && secret === LOCAL_AGENTIC_TOKEN;
147
+ const authenticator = substituteWellKnownToken
148
+ ? (req: HandshakeRequest) =>
149
+ req.token === undefined && req.query?.token === undefined
150
+ ? baseAuthenticator({ ...req, token: secret })
151
+ : baseAuthenticator(req)
152
+ : baseAuthenticator;
137
153
  const hub = new AgenticHub({
138
154
  transport,
139
155
  // A valid identity token upgrades (4401 on mismatch). SECURE mode's token is the real secret;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.86.0",
3
+ "version": "0.87.0",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",