@nanobpm/nano-workforce 0.150.2 → 0.150.3
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,9 @@
|
|
|
1
|
+
## [0.150.3](https://github.com/nanobpm/nano-workforce/compare/v0.150.2...v0.150.3) (2026-08-28)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **compileDeliveryGraph:** key reviewUrl to the request origin ([#577](https://github.com/nanobpm/nano-workforce/issues/577)) ([#581](https://github.com/nanobpm/nano-workforce/issues/581)) ([8c04c95](https://github.com/nanobpm/nano-workforce/commit/8c04c95b9d3617b3d71d28e54a7f24cae54c6e2c))
|
|
6
|
+
|
|
1
7
|
## [0.150.2](https://github.com/nanobpm/nano-workforce/compare/v0.150.1...v0.150.2) (2026-08-28)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
stageProposal,
|
|
26
26
|
sweepExpiredProposals,
|
|
27
27
|
} from "./deliveryGraphProposals.ts";
|
|
28
|
+
import { publicBaseUrl } from "./blackboard.ts";
|
|
28
29
|
|
|
29
30
|
const APP_ROOT = resolve(import.meta.dirname, "..");
|
|
30
31
|
|
|
@@ -87,6 +88,12 @@ test("proposalReviewUrl: a navigational deep-link to the cockpit page — NOT a
|
|
|
87
88
|
assert(!/\/actions\//.test(url), "reviewUrl points at a page, never an API action");
|
|
88
89
|
});
|
|
89
90
|
|
|
91
|
+
test("proposalReviewUrl: with no base falls back to publicBaseUrl() — the text-ingress (no request) path (#577)", () => {
|
|
92
|
+
// The staging text-ingress path has no HTTP request to derive an origin from, so the default base
|
|
93
|
+
// must stay the deployment-wide NANO_WORKFORCE_BASE_URL via publicBaseUrl().
|
|
94
|
+
assertEquals(proposalReviewUrl("abc123"), `${publicBaseUrl()}/app/pages/delivery-graphs#proposal-abc123`);
|
|
95
|
+
});
|
|
96
|
+
|
|
90
97
|
test("buildProposalRow: stamps status staged, boolean→0/1, and TTL from createdAt", () => {
|
|
91
98
|
const r = row({ sideEffecting: true, createdAt: "2024-01-01T00:00:00.000Z" });
|
|
92
99
|
assertEquals(r.status, "staged");
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Tests for app/resolveApiBase.ts — the single canonical control-API base reconstruction shared by
|
|
2
|
-
// getAgentInstructions and getAgentSkill
|
|
2
|
+
// getAgentInstructions and getAgentSkill, plus the human-facing resolvePublicOrigin (#577). Covers
|
|
3
|
+
// proxy-header handling, scheme restriction, host sanitisation, x-forwarded-prefix sanitisation,
|
|
3
4
|
// host-absent fallback, and mount-suffix stripping for both mount depths.
|
|
4
5
|
import { test } from "node:test";
|
|
5
6
|
import { assertEquals } from "#test-assert";
|
|
6
|
-
import { resolveApiBase } from "./resolveApiBase.ts";
|
|
7
|
+
import { resolveApiBase, resolvePublicOrigin } from "./resolveApiBase.ts";
|
|
7
8
|
|
|
8
9
|
function req(headers: Record<string, string>, path: string) {
|
|
9
10
|
return { path, headers: new Headers(headers) };
|
|
@@ -42,6 +43,7 @@ test("strips multiple trailing slashes after the mount suffix", () => {
|
|
|
42
43
|
assertEquals(resolveApiBase(req({ host: "h" }, "/app/api/agent/skill///"), "agent/skill"), "http://h/app/api");
|
|
43
44
|
});
|
|
44
45
|
|
|
46
|
+
// ── resolveApiBase: x-forwarded-prefix sanitisation (#580) via the shared sanitiseForwardedPrefix ──
|
|
45
47
|
test("prepends a validated x-forwarded-prefix to the reconstructed base", () => {
|
|
46
48
|
const r = req(
|
|
47
49
|
{ host: "nano.ngrok-free.dev", "x-forwarded-prefix": "/console/app-view/Workforce" },
|
|
@@ -97,3 +99,79 @@ test("prefix composes with x-forwarded-proto and x-forwarded-host", () => {
|
|
|
97
99
|
);
|
|
98
100
|
assertEquals(resolveApiBase(r, "agent/skill"), "https://nano.ngrok-free.dev/console/app-view/Workforce/app/api");
|
|
99
101
|
});
|
|
102
|
+
|
|
103
|
+
// ── resolvePublicOrigin: the human-facing ORIGIN (+ proxy prefix), no /app/api suffix (#577) ──
|
|
104
|
+
// Shares sanitiseForwardedPrefix with resolveApiBase, so the prefix policy (absolute-only, reject
|
|
105
|
+
// scheme/authority/traversal entirely, percent-aware) is identical on both surfaces — no drift.
|
|
106
|
+
test("resolvePublicOrigin: bare origin from the host header", () => {
|
|
107
|
+
assertEquals(resolvePublicOrigin(req({ host: "wf.example.com" }, "/app/api/actions/compile-delivery-graph")), "http://wf.example.com");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("resolvePublicOrigin: honours x-forwarded-proto and x-forwarded-host", () => {
|
|
111
|
+
const r = req({ host: "internal", "x-forwarded-host": "example.test", "x-forwarded-proto": "https" }, "/app/api/actions/compile-delivery-graph");
|
|
112
|
+
assertEquals(resolvePublicOrigin(r), "https://example.test");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("resolvePublicOrigin: restricts x-forwarded-proto to http/https", () => {
|
|
116
|
+
const r = req({ host: "wf.example.com", "x-forwarded-proto": "javascript" }, "/app/api/actions/compile-delivery-graph");
|
|
117
|
+
assertEquals(resolvePublicOrigin(r), "http://wf.example.com");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("resolvePublicOrigin: appends the reverse-proxy x-forwarded-prefix", () => {
|
|
121
|
+
const r = req(
|
|
122
|
+
{ "x-forwarded-host": "nano.ngrok-free.dev", "x-forwarded-proto": "https", "x-forwarded-prefix": "/console/app-view/Workforce" },
|
|
123
|
+
"/app/api/actions/compile-delivery-graph",
|
|
124
|
+
);
|
|
125
|
+
assertEquals(resolvePublicOrigin(r), "https://nano.ngrok-free.dev/console/app-view/Workforce");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("resolvePublicOrigin: normalises a trailing slash on x-forwarded-prefix", () => {
|
|
129
|
+
const r = req({ host: "h", "x-forwarded-prefix": "/console/app-view/Workforce/" }, "/app/api/actions/compile-delivery-graph");
|
|
130
|
+
assertEquals(resolvePublicOrigin(r), "http://h/console/app-view/Workforce");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("resolvePublicOrigin: treats a slash-only x-forwarded-prefix as empty (no double slash)", () => {
|
|
134
|
+
const r = req({ host: "h", "x-forwarded-prefix": "///" }, "/app/api/actions/compile-delivery-graph");
|
|
135
|
+
assertEquals(resolvePublicOrigin(r), "http://h");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("resolvePublicOrigin: rejects a path-traversal x-forwarded-prefix entirely", () => {
|
|
139
|
+
const r = req({ host: "h", "x-forwarded-prefix": "/console/../../etc" }, "/app/api/actions/compile-delivery-graph");
|
|
140
|
+
assertEquals(resolvePublicOrigin(r), "http://h");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("resolvePublicOrigin: rejects a scheme/authority x-forwarded-prefix", () => {
|
|
144
|
+
const r = req({ host: "h", "x-forwarded-prefix": "https://evil.example/hijack" }, "/app/api/actions/compile-delivery-graph");
|
|
145
|
+
assertEquals(resolvePublicOrigin(r), "http://h");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("resolvePublicOrigin: rejects a relative (non-absolute) x-forwarded-prefix", () => {
|
|
149
|
+
const r = req({ host: "h", "x-forwarded-prefix": "@evil.example" }, "/app/api/actions/compile-delivery-graph");
|
|
150
|
+
assertEquals(resolvePublicOrigin(r), "http://h");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("resolvePublicOrigin: falls back to a localhost origin when the Host header is absent", () => {
|
|
154
|
+
assertEquals(resolvePublicOrigin(req({}, "/app/api/actions/compile-delivery-graph")), "http://localhost:3000");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── host sanitisation: the untrusted x-forwarded-host/host authority is reflected into the URL ──
|
|
158
|
+
test("rejects a userinfo-injecting host (falls back to localhost)", () => {
|
|
159
|
+
const r = req({ "x-forwarded-host": "evil.com@real.example" }, "/app/api/actions/compile-delivery-graph");
|
|
160
|
+
assertEquals(resolvePublicOrigin(r), "http://localhost:3000");
|
|
161
|
+
assertEquals(resolveApiBase(req({ "x-forwarded-host": "evil.com@real.example" }, "/app/api/agent"), "agent"), "http://localhost:3000/app/api");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("rejects a path-injecting host (falls back to localhost)", () => {
|
|
165
|
+
const r = req({ "x-forwarded-host": "real.example/extra-path" }, "/app/api/actions/compile-delivery-graph");
|
|
166
|
+
assertEquals(resolvePublicOrigin(r), "http://localhost:3000");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("accepts a host:port authority", () => {
|
|
170
|
+
const r = req({ "x-forwarded-host": "wf.example.com:8443", "x-forwarded-proto": "https" }, "/app/api/actions/compile-delivery-graph");
|
|
171
|
+
assertEquals(resolvePublicOrigin(r), "https://wf.example.com:8443");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("accepts a bracketed IPv6 host authority", () => {
|
|
175
|
+
const r = req({ "x-forwarded-host": "[2001:db8::1]:3000" }, "/app/api/actions/compile-delivery-graph");
|
|
176
|
+
assertEquals(resolvePublicOrigin(r), "http://[2001:db8::1]:3000");
|
|
177
|
+
});
|
package/app/resolveApiBase.ts
CHANGED
|
@@ -17,33 +17,72 @@
|
|
|
17
17
|
* "/app/api" when the path is nothing but the suffix.
|
|
18
18
|
*/
|
|
19
19
|
export function resolveApiBase(req: { path: string; headers: Headers }, mountSuffix: string): string {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const proto = rawProto === "http" || rawProto === "https" ? rawProto : "http";
|
|
23
|
-
const host = (req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? "").split(",")[0].trim();
|
|
24
|
-
// The external path prefix stripped by a reverse proxy (e.g. the console app-view proxy mounts us
|
|
25
|
-
// under "/console/app-view/{project}"). X-Forwarded-Prefix is the de-facto standard header for it.
|
|
26
|
-
// It is untrusted, proxy-supplied input that ends up in a URL handed to an agent, so validate it as
|
|
27
|
-
// strictly as x-forwarded-proto above: accept only an absolute path of URL-safe path characters —
|
|
28
|
-
// rejecting anything with a scheme, an authority ("//host"), or a "."/".." traversal segment — then
|
|
29
|
-
// drop trailing slashes so it composes cleanly with the base path. Anything else falls back to an
|
|
30
|
-
// empty prefix, i.e. today's behaviour.
|
|
31
|
-
//
|
|
32
|
-
// Percent-encoding can smuggle those forms past a literal check: "%2e%2e" decodes to "..", and
|
|
33
|
-
// "%2f%2f" decodes to an authority-introducing "//". So normalise the common encoded spellings of
|
|
34
|
-
// "." and "/" (case-insensitively) before rejecting dot-segments and "//"; the still-encoded raw
|
|
35
|
-
// value is what we reflect once it validates.
|
|
36
|
-
const rawPrefix = (req.headers.get("x-forwarded-prefix") ?? "").split(",")[0].trim();
|
|
37
|
-
const decodedPrefix = rawPrefix.replace(/%2e/gi, ".").replace(/%2f/gi, "/");
|
|
38
|
-
const prefix =
|
|
39
|
-
/^\/(?!\/)[A-Za-z0-9._~\-/%]*$/.test(rawPrefix) &&
|
|
40
|
-
!decodedPrefix.includes("//") &&
|
|
41
|
-
!/(^|\/)\.\.?(\/|$)/.test(decodedPrefix)
|
|
42
|
-
? rawPrefix.replace(/\/+$/, "")
|
|
43
|
-
: "";
|
|
20
|
+
const { proto, host } = requestProtoHost(req);
|
|
21
|
+
const prefix = sanitiseForwardedPrefix(req.headers.get("x-forwarded-prefix"));
|
|
44
22
|
// The op is mounted at "<base>/<mountSuffix>"; strip the trailing segments to recover the base path.
|
|
45
23
|
const suffix = mountSuffix.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
46
24
|
const stripRe = new RegExp(`/${suffix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/*$`);
|
|
47
25
|
const basePath = req.path.replace(stripRe, "") || "/app/api";
|
|
48
26
|
return host ? `${proto}://${host}${prefix}${basePath}` : `http://localhost:3000${prefix}${basePath}`;
|
|
49
27
|
}
|
|
28
|
+
|
|
29
|
+
/** The public ORIGIN (+ proxy prefix) this request arrived on — the base for a navigational link
|
|
30
|
+
* handed back to the caller (e.g. a cockpit deep-link), WITHOUT the `/app/api` mount suffix that
|
|
31
|
+
* {@link resolveApiBase} keeps. Where `resolveApiBase` reconstructs the control-API base an *agent*
|
|
32
|
+
* calls back on, this reconstructs the human-facing origin: proto + host (same proxy-header trust as
|
|
33
|
+
* `resolveApiBase`) plus any reverse-proxy path prefix advertised via `x-forwarded-prefix`, so a
|
|
34
|
+
* link built as `${resolvePublicOrigin(req)}/app/pages/…` opens on the exact origin the operator is
|
|
35
|
+
* driving this app from (e.g. a tunnel), not a static deployment-wide base. Falls back to a
|
|
36
|
+
* localhost origin when the Host header is absent (a raw unit-test request). */
|
|
37
|
+
export function resolvePublicOrigin(req: { path: string; headers: Headers }): string {
|
|
38
|
+
const { proto, host } = requestProtoHost(req);
|
|
39
|
+
const prefix = sanitiseForwardedPrefix(req.headers.get("x-forwarded-prefix"));
|
|
40
|
+
return host ? `${proto}://${host}${prefix}` : `http://localhost:3000${prefix}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Sanitise the untrusted, proxy/user-controlled `x-forwarded-prefix` into a safe leading-slash,
|
|
44
|
+
* no-trailing-slash path segment (or ""). The ONE canonical prefix sanitiser shared by
|
|
45
|
+
* {@link resolveApiBase} and {@link resolvePublicOrigin} (AGENTS.md "derivation over duplication") —
|
|
46
|
+
* the prefix is the reverse-proxy path the public URL was mounted under (e.g.
|
|
47
|
+
* "/console/app-view/Workforce") and is reflected into a caller-facing URL, so it must not smuggle a
|
|
48
|
+
* scheme, an authority ("//host"), or a "."/".." traversal segment into the URL. Accept only an
|
|
49
|
+
* absolute path of URL-safe path characters, then drop trailing slashes so it composes cleanly with
|
|
50
|
+
* the base path; anything else falls back to an empty prefix. Percent-encoding can smuggle those
|
|
51
|
+
* forms past a literal check ("%2e%2e" decodes to "..", "%2f%2f" to an authority-introducing "//"),
|
|
52
|
+
* so normalise the common encoded spellings of "." and "/" (case-insensitively) before rejecting
|
|
53
|
+
* dot-segments and "//"; the still-encoded raw value is what we reflect once it validates. Because
|
|
54
|
+
* the return is always either "" or a leading-"/" path, it can never alter the `${proto}://${host}`
|
|
55
|
+
* authority. */
|
|
56
|
+
function sanitiseForwardedPrefix(raw: string | null): string {
|
|
57
|
+
const rawPrefix = (raw ?? "").split(",")[0].trim();
|
|
58
|
+
const decodedPrefix = rawPrefix.replace(/%2e/gi, ".").replace(/%2f/gi, "/");
|
|
59
|
+
return /^\/(?!\/)[A-Za-z0-9._~\-/%]*$/.test(rawPrefix) &&
|
|
60
|
+
!decodedPrefix.includes("//") &&
|
|
61
|
+
!/(^|\/)\.\.?(\/|$)/.test(decodedPrefix)
|
|
62
|
+
? rawPrefix.replace(/\/+$/, "")
|
|
63
|
+
: "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The trusted (proto, host) pair for a request — the ONE place proxy-header handling lives so
|
|
67
|
+
* `resolveApiBase` and `resolvePublicOrigin` can't drift (AGENTS.md "derivation over duplication").
|
|
68
|
+
* Only `http`/`https` are trusted from the user-controlled `x-forwarded-proto`; the host prefers
|
|
69
|
+
* `x-forwarded-host` over `host`. `host` is "" when neither header is present or the advertised host
|
|
70
|
+
* is not a valid authority (see {@link sanitiseHost}). */
|
|
71
|
+
function requestProtoHost(req: { headers: Headers }): { proto: string; host: string } {
|
|
72
|
+
const rawProto = (req.headers.get("x-forwarded-proto") ?? "http").split(",")[0].trim().toLowerCase();
|
|
73
|
+
const proto = rawProto === "http" || rawProto === "https" ? rawProto : "http";
|
|
74
|
+
const rawHost = (req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? "").split(",")[0].trim();
|
|
75
|
+
return { proto, host: sanitiseHost(rawHost) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Sanitise the untrusted, proxy/user-controlled host (`x-forwarded-host`/`host`) into a bare
|
|
79
|
+
* authority — a registered name or IPv4 with an optional `:port`, or a bracketed IPv6 literal with
|
|
80
|
+
* an optional `:port` — or "" when it carries anything else. The host is reflected verbatim into the
|
|
81
|
+
* `${proto}://${host}` authority of a caller-facing URL, so a hostile value like
|
|
82
|
+
* `evil.com@real.example` (userinfo injection) or `real.example/extra-path` (path injection) must be
|
|
83
|
+
* rejected outright rather than smuggled through. */
|
|
84
|
+
function sanitiseHost(host: string): string {
|
|
85
|
+
if (!host) return "";
|
|
86
|
+
const valid = /^(?:[A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\])(?::\d+)?$/.test(host);
|
|
87
|
+
return valid ? host : "";
|
|
88
|
+
}
|
|
@@ -30,8 +30,9 @@ async function withApp(fn: (app: AppApi, data: DataLayer) => Promise<void>): Pro
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
async function call(app: AppApi, body: unknown) {
|
|
34
|
-
|
|
33
|
+
async function call(app: AppApi, body: unknown, headers: Record<string, string> = {}) {
|
|
34
|
+
const req = { path: "/app/api/actions/compile-delivery-graph", headers: new Headers(headers) };
|
|
35
|
+
return (await handler({ req: req as any, params: {}, query: {}, body } as any, app)) as any;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const GOOD = {
|
|
@@ -79,6 +80,33 @@ test("compile-delivery-graph: the response exposes NO dispatch handle — no run
|
|
|
79
80
|
});
|
|
80
81
|
});
|
|
81
82
|
|
|
83
|
+
// ── #577: reviewUrl is a caller-facing link → keyed to the request origin, not the static base ──
|
|
84
|
+
test("compile-delivery-graph: reviewUrl is on the request's forwarded origin, not NANO_WORKFORCE_BASE_URL", async () => {
|
|
85
|
+
await withApp(async (app) => {
|
|
86
|
+
const res = await call(app, GOOD, { "x-forwarded-proto": "https", "x-forwarded-host": "example.test" });
|
|
87
|
+
assertEquals(res.status, 200);
|
|
88
|
+
assertEquals(
|
|
89
|
+
res.body.reviewUrl,
|
|
90
|
+
`https://example.test/app/pages/delivery-graphs#proposal-${res.body.digest}`,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("compile-delivery-graph: reviewUrl honours the reverse-proxy x-forwarded-prefix", async () => {
|
|
96
|
+
await withApp(async (app) => {
|
|
97
|
+
const res = await call(app, GOOD, {
|
|
98
|
+
"x-forwarded-proto": "https",
|
|
99
|
+
"x-forwarded-host": "nano.ngrok-free.dev",
|
|
100
|
+
"x-forwarded-prefix": "/console/app-view/Workforce",
|
|
101
|
+
});
|
|
102
|
+
assertEquals(res.status, 200);
|
|
103
|
+
assertEquals(
|
|
104
|
+
res.body.reviewUrl,
|
|
105
|
+
`https://nano.ngrok-free.dev/console/app-view/Workforce/app/pages/delivery-graphs#proposal-${res.body.digest}`,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
82
110
|
test("compile-delivery-graph: re-compiling the same graph is idempotent — one staged proposal, TTL anchored to the first stage", async () => {
|
|
83
111
|
await withApp(async (app, data) => {
|
|
84
112
|
const first = await call(app, GOOD);
|
|
@@ -21,12 +21,13 @@ import {
|
|
|
21
21
|
stageProposal,
|
|
22
22
|
} from "../app/deliveryGraphProposals.ts";
|
|
23
23
|
import { deliveryGraphDigest } from "../app/deliveryRunner.ts";
|
|
24
|
+
import { resolvePublicOrigin } from "../app/resolveApiBase.ts";
|
|
24
25
|
import { defineOperation } from "../nano-generated/operations.ts";
|
|
25
26
|
|
|
26
27
|
const STAGED_MESSAGE =
|
|
27
28
|
"The graph compiled and is staged for operator review. Ask the operator to preview and approve — or request modifications — in the cockpit. Dispatch is an operator action; there is no start endpoint.";
|
|
28
29
|
|
|
29
|
-
export default defineOperation("compileDeliveryGraph", async ({ body }, app) => {
|
|
30
|
+
export default defineOperation("compileDeliveryGraph", async ({ body, req }, app) => {
|
|
30
31
|
// The runtime validates the body's SHAPE against `DeliveryGraph` before we run; the compiler adds
|
|
31
32
|
// the SEMANTIC checks (acyclicity, edge integrity, fact resolution). A directly-invoked delegate
|
|
32
33
|
// could still pass `undefined` — the compiler reads its input as `unknown` and maps that to a clean
|
|
@@ -75,7 +76,10 @@ export default defineOperation("compileDeliveryGraph", async ({ body }, app) =>
|
|
|
75
76
|
message: STAGED_MESSAGE,
|
|
76
77
|
digest,
|
|
77
78
|
preview,
|
|
78
|
-
|
|
79
|
+
// Navigational, human-facing link → keyed to the ORIGIN this request arrived on (tunnel,
|
|
80
|
+
// proxy prefix, …), not the static deployment-wide NANO_WORKFORCE_BASE_URL, so the operator
|
|
81
|
+
// driving this instance can actually open it (#577).
|
|
82
|
+
reviewUrl: proposalReviewUrl(digest, resolvePublicOrigin(req)),
|
|
79
83
|
},
|
|
80
84
|
};
|
|
81
85
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.150.
|
|
3
|
+
"version": "0.150.3",
|
|
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",
|