@nanobpm/nano-workforce 0.180.0 → 0.182.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.
@@ -82,6 +82,19 @@ test("blank/whitespace section is treated as no section (TOC)", async () => {
82
82
  assertEquals(r.body.kind, "toc");
83
83
  });
84
84
 
85
+ test("a pagination cursor beyond MAX_SAFE_INTEGER → 400 (unsafe integers lose precision)", async () => {
86
+ // 9007199254740993 === 9007199254740992 in IEEE-754 double, so `Number.isInteger` accepts it
87
+ // while it no longer represents the caller's requested character offset. It must be rejected.
88
+ const unsafe = "9007199254740993";
89
+ const rStart = (await handler(input({ section: "delivery-graphs", start: unsafe }), app)) as any;
90
+ assertEquals(rStart.status, 400);
91
+ assert(Array.isArray(rStart.body.issues) && rStart.body.issues.some((i: any) => i.path === "start"));
92
+
93
+ const rLength = (await handler(input({ section: "delivery-graphs", length: unsafe }), app)) as any;
94
+ assertEquals(rLength.status, 400);
95
+ assert(Array.isArray(rLength.body.issues) && rLength.body.issues.some((i: any) => i.path === "length"));
96
+ });
97
+
85
98
  test("shared-secret guard: rejects when the secret is set and header is wrong", async () => {
86
99
  const prev = process.env.NANO_PR_WEBHOOK_SECRET;
87
100
  process.env.NANO_PR_WEBHOOK_SECRET = "s3cr3t";
@@ -15,13 +15,29 @@
15
15
  //
16
16
  // The optional shared-secret guard mirrors /agent and /version: enforced HERE only when
17
17
  // NANO_PR_WEBHOOK_SECRET is set (the runtime does not enforce OpenAPI `security`).
18
- import { guideToc, renderGuideSection, resolveEngineBase } from "../app/agentGuide.ts";
18
+ import { GUIDE_SECTION_PAGE_DEFAULT, guideToc, renderGuideSection, renderGuideSectionChunk, resolveEngineBase } from "../app/agentGuide.ts";
19
19
  import { resolveApiBase } from "../app/resolveApiBase.ts";
20
20
  import { buildVersionInfo, envVar } from "../app/version.ts";
21
21
  import { defineOperation } from "../nano-generated/operations.ts";
22
22
 
23
23
  const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
24
24
 
25
+ /** Parse a `start`/`length` pagination query param: a present value must be a non-negative SAFE
26
+ * integer (`length` additionally >= 1). `Number.isSafeInteger` (not `Number.isInteger`) is required
27
+ * because these values are used as character cursors: an integer beyond `Number.MAX_SAFE_INTEGER`
28
+ * (e.g. `9007199254740993`) loses precision, so accepting it would silently misinterpret the
29
+ * caller's requested window. Returns the parsed number, `undefined` when absent, or a
30
+ * path-qualified validation issue. */
31
+ function parsePageArg(raw: unknown, path: string, min: number): { value?: number; issue?: { path: string; message: string } } {
32
+ if (raw === undefined || raw === null || (typeof raw === "string" && raw.trim() === "")) return {};
33
+ const s = typeof raw === "string" ? raw.trim() : String(raw);
34
+ const n = Number(s);
35
+ if (!Number.isSafeInteger(n) || n < min) {
36
+ return { issue: { path, message: `\`${path}\` must be an integer >= ${min} (character offset); got "${s}"` } };
37
+ }
38
+ return { value: n };
39
+ }
40
+
25
41
  export default defineOperation("getAgentGuide", ({ query, req }, app) => {
26
42
  if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
27
43
  app.log.warn("getAgentGuide rejected: missing/invalid shared secret");
@@ -46,9 +62,23 @@ export default defineOperation("getAgentGuide", ({ query, req }, app) => {
46
62
  };
47
63
  }
48
64
 
49
- // A section id just that section, or a 400 that names the valid ids.
50
- const instructions = renderGuideSection(section, baseUrl);
51
- if (instructions === undefined) {
65
+ // Pagination window (issue #740): a caller engages it by passing `start` and/or `length` (character
66
+ // offsets). Absent both, the whole section is returned unchanged (byte-for-byte identical to before).
67
+ const startArg = parsePageArg(query.start, "start", 0);
68
+ const lengthArg = parsePageArg(query.length, "length", 1);
69
+ const pageIssues = [startArg.issue, lengthArg.issue].filter((i): i is { path: string; message: string } => i !== undefined);
70
+ if (pageIssues.length > 0) {
71
+ app.log.warn("getAgentGuide rejected: invalid pagination args", { issues: pageIssues.length });
72
+ return { status: 400, body: { error: "invalid pagination arguments", issues: pageIssues } };
73
+ }
74
+ const paginate = startArg.value !== undefined || lengthArg.value !== undefined;
75
+
76
+ // A section id → just that section (or a bounded page of it), or a 400 that names the valid ids.
77
+ const chunk = paginate ? renderGuideSectionChunk(section, baseUrl, startArg.value ?? 0, lengthArg.value ?? GUIDE_SECTION_PAGE_DEFAULT) : undefined;
78
+ // The resolved body text: a page's slice when paginating, else the whole section. `undefined` from
79
+ // either path means the section id is unknown → the uniform 400 below.
80
+ const resolved = chunk ? chunk.instructions : paginate ? undefined : renderGuideSection(section, baseUrl);
81
+ if (resolved === undefined) {
52
82
  // Derive the valid ids from the PARSED table of contents — the sections this deployment can
53
83
  // actually serve — not the static registry. When the guide doc is unreadable (RAW_GUIDE
54
84
  // fallback, no `##` headings) the TOC is empty and NO id is retrievable, so say so explicitly
@@ -85,7 +115,12 @@ export default defineOperation("getAgentGuide", ({ query, req }, app) => {
85
115
  id: section,
86
116
  title,
87
117
  format: "markdown",
88
- instructions,
118
+ instructions: resolved,
119
+ // Pagination cursor state — present ONLY when the caller engaged a window, so an un-paginated
120
+ // `section=<id>` call's body stays byte-for-byte identical (issue #740).
121
+ ...(chunk
122
+ ? { start: chunk.start, length: chunk.length, totalLength: chunk.totalLength, nextStart: chunk.nextStart }
123
+ : {}),
89
124
  },
90
125
  },
91
126
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.180.0",
3
+ "version": "0.182.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",