@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.
- package/CHANGELOG.md +12 -0
- package/app/agentGuide.test.ts +87 -0
- package/app/agentGuide.ts +61 -0
- package/app/deliveryGraph.ts +30 -1
- package/app/deliveryGraphCompiler.ts +39 -20
- package/app/deliveryGraphProposals.test.ts +60 -0
- package/app/deliveryGraphProposals.ts +61 -3
- package/app/deliveryGraphStage.ts +11 -1
- package/app/deliveryRunner.test.ts +120 -58
- package/app/deliveryRunner.ts +154 -55
- package/app/repoEnvelope.ts +73 -1
- package/app/sequenceIssues.test.ts +143 -1
- package/app/sequenceIssues.ts +246 -31
- package/e2e/addressable-guide.e2e.ts +43 -0
- package/openapi.yaml +310 -29
- package/operations/compileDeliveryGraph.test.ts +4 -0
- package/operations/dispatchDeliveryGraph.test.ts +50 -4
- package/operations/dispatchDeliveryGraph.ts +44 -27
- package/operations/getAgentGuide.test.ts +13 -0
- package/operations/getAgentGuide.ts +40 -5
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
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",
|