@sapiom/harness 0.8.8 → 0.9.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +11 -1
  3. package/dist/core/agent-project-discovery.d.ts +235 -1
  4. package/dist/core/agent-project-discovery.d.ts.map +1 -1
  5. package/dist/core/agent-project-discovery.js +303 -1
  6. package/dist/core/agent-project-discovery.js.map +1 -1
  7. package/dist/core/canvas-interconnections.d.ts +31 -0
  8. package/dist/core/canvas-interconnections.d.ts.map +1 -1
  9. package/dist/core/canvas-interconnections.js +294 -21
  10. package/dist/core/canvas-interconnections.js.map +1 -1
  11. package/dist/core/canvas-render.d.ts +32 -0
  12. package/dist/core/canvas-render.d.ts.map +1 -1
  13. package/dist/core/canvas-render.js +50 -41
  14. package/dist/core/canvas-render.js.map +1 -1
  15. package/dist/core/definition-name.d.ts +12 -0
  16. package/dist/core/definition-name.d.ts.map +1 -1
  17. package/dist/core/definition-name.js +14 -5
  18. package/dist/core/definition-name.js.map +1 -1
  19. package/dist/core/system-graph-inventory.d.ts +68 -0
  20. package/dist/core/system-graph-inventory.d.ts.map +1 -0
  21. package/dist/core/system-graph-inventory.js +312 -0
  22. package/dist/core/system-graph-inventory.js.map +1 -0
  23. package/dist/core/system-graph-relationships.d.ts +41 -0
  24. package/dist/core/system-graph-relationships.d.ts.map +1 -0
  25. package/dist/core/system-graph-relationships.js +98 -0
  26. package/dist/core/system-graph-relationships.js.map +1 -0
  27. package/dist/core/system-graph-store.d.ts +45 -0
  28. package/dist/core/system-graph-store.d.ts.map +1 -0
  29. package/dist/core/system-graph-store.js +222 -0
  30. package/dist/core/system-graph-store.js.map +1 -0
  31. package/dist/core/system-graph-watcher.d.ts +51 -0
  32. package/dist/core/system-graph-watcher.d.ts.map +1 -0
  33. package/dist/core/system-graph-watcher.js +448 -0
  34. package/dist/core/system-graph-watcher.js.map +1 -0
  35. package/dist/core/system-graph.d.ts +43 -0
  36. package/dist/core/system-graph.d.ts.map +1 -0
  37. package/dist/core/system-graph.js +321 -0
  38. package/dist/core/system-graph.js.map +1 -0
  39. package/dist/core/workflow-registry.d.ts +84 -6
  40. package/dist/core/workflow-registry.d.ts.map +1 -1
  41. package/dist/core/workflow-registry.js +191 -91
  42. package/dist/core/workflow-registry.js.map +1 -1
  43. package/dist/core/workspace-watcher.d.ts +21 -13
  44. package/dist/core/workspace-watcher.d.ts.map +1 -1
  45. package/dist/core/workspace-watcher.js +79 -81
  46. package/dist/core/workspace-watcher.js.map +1 -1
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/profiles/default.d.ts.map +1 -1
  51. package/dist/profiles/default.js +4 -2
  52. package/dist/profiles/default.js.map +1 -1
  53. package/dist/server/agent-move.d.ts +134 -0
  54. package/dist/server/agent-move.d.ts.map +1 -0
  55. package/dist/server/agent-move.js +343 -0
  56. package/dist/server/agent-move.js.map +1 -0
  57. package/dist/server/index.d.ts.map +1 -1
  58. package/dist/server/index.js +275 -22
  59. package/dist/server/index.js.map +1 -1
  60. package/dist/server/rest.d.ts +3 -0
  61. package/dist/server/rest.d.ts.map +1 -1
  62. package/dist/server/rest.js +3 -0
  63. package/dist/server/rest.js.map +1 -1
  64. package/dist/server/studio-rail.d.ts +76 -0
  65. package/dist/server/studio-rail.d.ts.map +1 -0
  66. package/dist/server/studio-rail.js +234 -0
  67. package/dist/server/studio-rail.js.map +1 -0
  68. package/dist/server/system-graph.d.ts +14 -0
  69. package/dist/server/system-graph.d.ts.map +1 -0
  70. package/dist/server/system-graph.js +40 -0
  71. package/dist/server/system-graph.js.map +1 -0
  72. package/dist/server/workflow-graph.d.ts +43 -0
  73. package/dist/server/workflow-graph.d.ts.map +1 -0
  74. package/dist/server/workflow-graph.js +200 -0
  75. package/dist/server/workflow-graph.js.map +1 -0
  76. package/dist/shared/system-graph.d.ts +59 -0
  77. package/dist/shared/system-graph.d.ts.map +1 -0
  78. package/dist/shared/system-graph.js +76 -0
  79. package/dist/shared/system-graph.js.map +1 -0
  80. package/dist/shared/types.d.ts +45 -0
  81. package/dist/shared/types.d.ts.map +1 -1
  82. package/dist/shared/types.js.map +1 -1
  83. package/dist/web/assets/index-2Fab6dzf.js +508 -0
  84. package/dist/web/assets/index-D0doaFvT.css +32 -0
  85. package/dist/web/index.html +2 -2
  86. package/package.json +5 -5
  87. package/dist/web/assets/index-DrTPW10E.js +0 -494
  88. package/dist/web/assets/index-jGBbXx9R.css +0 -32
@@ -0,0 +1,234 @@
1
+ /**
2
+ * `.sapiom/studio-rail.json` — the Group axis's stored arrangement, one file
3
+ * per project root (SAP-2929; design.md § Persistence).
4
+ *
5
+ * This router is deliberately DUMB about the file's meaning. It reads, writes
6
+ * and removes an opaque UTF-8 blob; every rule about what the blob means —
7
+ * defensive parsing, pruning member paths no agent claims, and above all the
8
+ * difference between `groups: null` ("nothing stored, detection owns this") and
9
+ * `groups: []` ("the user materialized groups and then deleted them all") —
10
+ * lives in ONE place, `web/src/lib/agent-groups.ts`, where it is unit-tested.
11
+ *
12
+ * That split is the point. In the reference prototype the null/empty
13
+ * distinction was collapsed by a second, well-meaning serializer on the write
14
+ * path: the first page load wrote `groups: []` for a state that was `null`, and
15
+ * from the second load onward every agent fell into `Ungrouped`, in every
16
+ * project, permanently. A server that re-encodes the shape is a second place
17
+ * for that to happen. So the wire format here is `{ raw }` — the exact text —
18
+ * and "un-materialized" reaches disk as DELETE, never as a write.
19
+ *
20
+ * Containment: `root` must be a project root the studio actually knows about
21
+ * (`listKnownRoots`, injected — recentDirs, the configured project root, and
22
+ * live session cwds, which is exactly the set `web/src/lib/project-tree.ts`
23
+ * turns into project rows). Resolution happens before any disk access, so this
24
+ * route cannot be turned into an arbitrary-path file reader or writer. The
25
+ * written path is confined under `<root>/.sapiom/` by `resolveWithinRoot`, so a
26
+ * symlinked `.sapiom` cannot land the file outside the project.
27
+ *
28
+ * Mounted under the same `/api` boot-token middleware as the rest of the REST
29
+ * surface (server/index.ts).
30
+ */
31
+ import * as path from "node:path";
32
+ import * as fs from "node:fs/promises";
33
+ import { Router } from "express";
34
+ import { detectWorkflowLaunches } from "../core/canvas-interconnections.js";
35
+ import { hasTraversalSegment, resolveWithinRoot } from "../core/path-safety.js";
36
+ /** The directory the file lives in: already a live convention (canvas renders,
37
+ * harness-context.json) and already skipped by every source scan. */
38
+ const SAPIOM_DIR = ".sapiom";
39
+ export const STUDIO_RAIL_FILE = "studio-rail.json";
40
+ /**
41
+ * A stored arrangement is a handful of ids, labels and absolute paths. The cap
42
+ * is a backstop against a client (or a hand edit) turning a preferences file
43
+ * into a place to park a megabyte, not a real limit on how many groups a
44
+ * project may have.
45
+ */
46
+ export const MAX_STUDIO_RAIL_BYTES = 256 * 1024;
47
+ /** Trailing separators are spelling, not identity — `<root>/` and `<root>`
48
+ * are one directory, and the client stores whichever the user typed. */
49
+ const canonical = (p) => {
50
+ const resolved = path.resolve(p);
51
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
52
+ };
53
+ /**
54
+ * The absolute, resolved root when `candidate` is a project root the studio
55
+ * knows about — null otherwise, which every route answers as a 400 rather than
56
+ * reaching for the disk.
57
+ */
58
+ function resolveKnownRoot(candidate, knownRoots) {
59
+ if (typeof candidate !== "string" || candidate.trim() === "")
60
+ return null;
61
+ if (hasTraversalSegment(candidate))
62
+ return null;
63
+ if (!path.isAbsolute(candidate))
64
+ return null;
65
+ const wanted = canonical(candidate);
66
+ return knownRoots.some((root) => canonical(root) === wanted)
67
+ ? path.resolve(candidate)
68
+ : null;
69
+ }
70
+ /** `<root>/.sapiom/studio-rail.json`, or null if that escapes `root`. */
71
+ export function studioRailPath(root) {
72
+ return resolveWithinRoot(root, path.join(SAPIOM_DIR, STUDIO_RAIL_FILE));
73
+ }
74
+ /**
75
+ * The stored blob, or null.
76
+ *
77
+ * An absent file, an unreadable directory and an oversized file all read as
78
+ * null — "nothing stored", which the model turns into the derived groups. A
79
+ * rail that throws on a bad preferences file is a rail you cannot open in order
80
+ * to fix it.
81
+ */
82
+ export async function readStudioRailFile(root) {
83
+ const file = studioRailPath(root);
84
+ if (file == null)
85
+ return null;
86
+ try {
87
+ const stat = await fs.stat(file);
88
+ if (!stat.isFile() || stat.size > MAX_STUDIO_RAIL_BYTES)
89
+ return null;
90
+ return await fs.readFile(file, "utf8");
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ /** Writes the blob verbatim, creating `.sapiom/` if it is not there yet. */
97
+ export async function writeStudioRailFile(root, raw) {
98
+ const file = studioRailPath(root);
99
+ if (file == null)
100
+ throw new Error("studio-rail path escapes the project root");
101
+ await fs.mkdir(path.dirname(file), { recursive: true });
102
+ await fs.writeFile(file, raw, "utf8");
103
+ }
104
+ /**
105
+ * Removes the file. Missing is success: "there is no stored arrangement" is the
106
+ * requested end state, and DELETE is how an un-materialized state — including
107
+ * the one `Reset to detected` produces — reaches disk. Skipping the write
108
+ * instead would let the old arrangement outlive the reset.
109
+ */
110
+ export async function removeStudioRailFile(root) {
111
+ const file = studioRailPath(root);
112
+ if (file == null)
113
+ return;
114
+ try {
115
+ await fs.rm(file);
116
+ }
117
+ catch {
118
+ // ENOENT and a read-only checkout alike: nothing stored is the goal state.
119
+ }
120
+ }
121
+ /**
122
+ * Launch edges across every registered agent, as parent-name to child-slug.
123
+ *
124
+ * Uses the shared syntax-only extractor (`core/canvas-interconnections.ts`) —
125
+ * the same detector the canvas and system graph use for agent relationships. A
126
+ * second edge detector would be a second answer to "what does this launch",
127
+ * and the Group axis and system graph are supposed to read one graph.
128
+ *
129
+ * `knownStepIds` is empty on purpose: that argument exists so a capability chip
130
+ * can be attributed to the step it sits in, and a group does not care which
131
+ * step launched the child, only that the launch exists.
132
+ *
133
+ * `child` is the `definition` slug as written at the call site. Matching it to
134
+ * an agent is the client's job (a slug can name an agent by `definitionSlug` or
135
+ * by folder name); the client ignores an unmatched slug.
136
+ */
137
+ export async function detectLaunchEdges(workflows) {
138
+ const edges = [];
139
+ const seen = new Set();
140
+ for (const workflow of workflows) {
141
+ let launches;
142
+ try {
143
+ launches = await detectWorkflowLaunches(workflow.path, new Set());
144
+ }
145
+ catch {
146
+ continue;
147
+ }
148
+ for (const launch of launches) {
149
+ // A project that launches the same definition from three steps is ONE
150
+ // edge: the group is about the relationship, not its multiplicity.
151
+ const key = JSON.stringify([workflow.name, launch.slug]);
152
+ if (launch.slug === workflow.name || seen.has(key))
153
+ continue;
154
+ seen.add(key);
155
+ edges.push({ parent: workflow.name, child: launch.slug });
156
+ }
157
+ }
158
+ return edges;
159
+ }
160
+ /**
161
+ * GET /api/studio-rail?root=abs -> { root, raw: string | null }
162
+ * PUT /api/studio-rail?root=abs { raw } -> { ok: true }
163
+ * DELETE /api/studio-rail?root=abs -> { ok: true }
164
+ * GET /api/studio-rail/launch-edges -> { edges: [{ parent, child }] }
165
+ */
166
+ export function createStudioRailRouter(deps) {
167
+ const router = Router();
168
+ // BEFORE the `?root=` routes: `/api/studio-rail/launch-edges` carries no
169
+ // root, and a root-less request must never be answered with a file read.
170
+ router.get("/api/studio-rail/launch-edges", async (_req, res, next) => {
171
+ try {
172
+ const edges = await detectLaunchEdges(deps.listWorkflows());
173
+ res.json({ edges });
174
+ }
175
+ catch (err) {
176
+ next(err);
177
+ }
178
+ });
179
+ router.get("/api/studio-rail", async (req, res, next) => {
180
+ const root = resolveKnownRoot(req.query.root, await deps.listKnownRoots());
181
+ if (root == null) {
182
+ res.status(400).json({ error: "root must be a known project root" });
183
+ return;
184
+ }
185
+ try {
186
+ res.json({
187
+ root,
188
+ raw: await readStudioRailFile(root),
189
+ });
190
+ }
191
+ catch (err) {
192
+ next(err);
193
+ }
194
+ });
195
+ router.put("/api/studio-rail", async (req, res, next) => {
196
+ const root = resolveKnownRoot(req.query.root, await deps.listKnownRoots());
197
+ if (root == null) {
198
+ res.status(400).json({ error: "root must be a known project root" });
199
+ return;
200
+ }
201
+ const raw = req.body?.raw;
202
+ if (typeof raw !== "string") {
203
+ res.status(400).json({ error: "raw must be a string" });
204
+ return;
205
+ }
206
+ if (Buffer.byteLength(raw, "utf8") > MAX_STUDIO_RAIL_BYTES) {
207
+ res.status(413).json({ error: "studio-rail.json is too large" });
208
+ return;
209
+ }
210
+ try {
211
+ await writeStudioRailFile(root, raw);
212
+ res.json({ ok: true });
213
+ }
214
+ catch (err) {
215
+ next(err);
216
+ }
217
+ });
218
+ router.delete("/api/studio-rail", async (req, res, next) => {
219
+ const root = resolveKnownRoot(req.query.root, await deps.listKnownRoots());
220
+ if (root == null) {
221
+ res.status(400).json({ error: "root must be a known project root" });
222
+ return;
223
+ }
224
+ try {
225
+ await removeStudioRailFile(root);
226
+ res.json({ ok: true });
227
+ }
228
+ catch (err) {
229
+ next(err);
230
+ }
231
+ });
232
+ return router;
233
+ }
234
+ //# sourceMappingURL=studio-rail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studio-rail.js","sourceRoot":"","sources":["../../src/server/studio-rail.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { Router } from "express";
2
+ import type { WorkspaceScope, WorkspaceScopeResolver } from "../core/system-graph.js";
3
+ import type { SystemGraphStore } from "../core/system-graph-store.js";
4
+ import { type SystemGraphSnapshot } from "../shared/system-graph.js";
5
+ export interface SystemGraphRouterOptions {
6
+ scopeResolver: WorkspaceScopeResolver;
7
+ store: SystemGraphStore;
8
+ onScopeAccess?: (scope: WorkspaceScope) => void | Promise<void>;
9
+ /** Re-run registry prerequisites before an explicit user retry. */
10
+ onScopeRefresh?: (scope: WorkspaceScope) => SystemGraphSnapshot | Promise<SystemGraphSnapshot>;
11
+ }
12
+ /** Mounted beneath the boot-token-protected `/api` boundary. */
13
+ export declare function createSystemGraphRouter(options: SystemGraphRouterOptions): Router;
14
+ //# sourceMappingURL=system-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"system-graph.d.ts","sourceRoot":"","sources":["../../src/server/system-graph.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EAIP,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EACV,cAAc,EACd,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,sBAAsB,CAAC;IACtC,KAAK,EAAE,gBAAgB,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,mEAAmE;IACnE,cAAc,CAAC,EAAE,CACf,KAAK,EAAE,cAAc,KAClB,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACzD;AAED,gEAAgE;AAChE,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,wBAAwB,GAChC,MAAM,CA4CR"}
@@ -0,0 +1,40 @@
1
+ import { Router, } from "express";
2
+ import { SYSTEM_GRAPH_CACHE_HEADER, } from "../shared/system-graph.js";
3
+ /** Mounted beneath the boot-token-protected `/api` boundary. */
4
+ export function createSystemGraphRouter(options) {
5
+ const router = Router();
6
+ const route = "/workspaces/:workspaceKey/system-graph";
7
+ const serve = async (req, res, next, refresh) => {
8
+ try {
9
+ const scope = await options.scopeResolver.resolve(req.params.workspaceKey);
10
+ if (!scope) {
11
+ res.status(404).json({ error: "Workspace not found" });
12
+ return;
13
+ }
14
+ try {
15
+ await options.onScopeAccess?.(scope);
16
+ }
17
+ catch {
18
+ // Watcher setup is best-effort. A graph read must remain available
19
+ // even when automatic freshness cannot be armed.
20
+ }
21
+ const snapshot = refresh
22
+ ? await (options.onScopeRefresh?.(scope) ??
23
+ options.store.refresh(scope))
24
+ : await options.store.get(scope);
25
+ const cacheStatus = snapshot.state === "ready" ? "complete" : "degraded";
26
+ res.set(SYSTEM_GRAPH_CACHE_HEADER, cacheStatus).json(snapshot);
27
+ }
28
+ catch (err) {
29
+ next(err);
30
+ }
31
+ };
32
+ router.get(route, (req, res, next) => {
33
+ void serve(req, res, next, false);
34
+ });
35
+ router.post(`${route}/refresh`, (req, res, next) => {
36
+ void serve(req, res, next, true);
37
+ });
38
+ return router;
39
+ }
40
+ //# sourceMappingURL=system-graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"system-graph.js","sourceRoot":"","sources":["../../src/server/system-graph.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,GAIP,MAAM,SAAS,CAAC;AAOjB,OAAO,EACL,yBAAyB,GAG1B,MAAM,2BAA2B,CAAC;AAYnC,gEAAgE;AAChE,MAAM,UAAU,uBAAuB,CACrC,OAAiC;IAEjC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,MAAM,KAAK,GAAG,wCAAwC,CAAC;IAEvD,MAAM,KAAK,GAAG,KAAK,EACjB,GAAY,EACZ,GAAa,EACb,IAAkB,EAClB,OAAgB,EACD,EAAE;QACjB,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,OAAO,CAC/C,GAAG,CAAC,MAAM,CAAC,YAAY,CACxB,CAAC;YACF,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;gBACvD,OAAO;YACT,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,aAAa,EAAE,CAAC,KAAK,CAAC,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACP,mEAAmE;gBACnE,iDAAiD;YACnD,CAAC;YACD,MAAM,QAAQ,GAAG,OAAO;gBACtB,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,KAAK,CAAC;oBACpC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBACjC,CAAC,CAAC,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACnC,MAAM,WAAW,GACf,QAAQ,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;YACvD,GAAG,CAAC,GAAG,CAAC,yBAAyB,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,CAAC;QACZ,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACnC,KAAK,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACjD,KAAK,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { type Router as ExpressRouter } from "express";
2
+ import { type AgentProjectMarkerInspection } from "../core/agent-project-discovery.js";
3
+ import { deriveWorkflowCanvas, type RenderableWorkflow } from "../core/canvas-render.js";
4
+ import type { CanvasGraph } from "../core/canvas-graph.js";
5
+ import type { CanvasEnrichment } from "../core/canvas-enrichment.js";
6
+ /** Why a board is not a graph. `ok` is the only status carrying one. */
7
+ export type WorkflowGraphStatus = "ok" | "empty" | "preparing" | "error";
8
+ export interface WorkflowGraphResponse {
9
+ /** The resolved absolute agent directory the board was derived from. */
10
+ path: string;
11
+ /** The registry's display name — the board's panel title. */
12
+ name: string;
13
+ status: WorkflowGraphStatus;
14
+ /** The extracted graph; null for every status but "ok". */
15
+ graph: CanvasGraph | null;
16
+ /** Deterministic enrichment derived from `graph`; null when there is none. */
17
+ enrichment: CanvasEnrichment | null;
18
+ /** Human-readable explanation for "empty"/"error"; null otherwise. */
19
+ reason: string | null;
20
+ /** True when the graph came from the extraction cache — no child process ran. */
21
+ cached: boolean;
22
+ /**
23
+ * The finished canvas document, byte-identical to what `/canvas/:sessionId/`
24
+ * serves for a session bound to this workflow. Present for EVERY status —
25
+ * an empty board is still a renderable page, not a hole.
26
+ */
27
+ document: string;
28
+ }
29
+ export interface WorkflowGraphRouterDeps {
30
+ /**
31
+ * Resolves an absolute agent path against the live registry; null when the
32
+ * path is not registered. Called before anything touches disk.
33
+ */
34
+ resolveWorkflow(agentPath: string): RenderableWorkflow | null;
35
+ /** Seam for tests — defaults to the real `sapiom.json` inspection. */
36
+ inspectMarker?(dir: string): Promise<AgentProjectMarkerInspection>;
37
+ /** Seam for tests — defaults to the real derivation (which runs esbuild). */
38
+ deriveCanvas?: typeof deriveWorkflowCanvas;
39
+ /** Seam for tests — defaults to `fs.realpath`. */
40
+ realpath?(p: string): Promise<string>;
41
+ }
42
+ export declare function createWorkflowGraphRouter(deps: WorkflowGraphRouterDeps): ExpressRouter;
43
+ //# sourceMappingURL=workflow-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-graph.d.ts","sourceRoot":"","sources":["../../src/server/workflow-graph.ts"],"names":[],"mappings":"AA4DA,OAAO,EAAU,KAAK,MAAM,IAAI,aAAa,EAAE,MAAM,SAAS,CAAC;AAE/D,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,oBAAoB,EAAE,KAAK,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACzF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,wEAAwE;AACxE,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,CAAC;AAEzE,MAAM,WAAW,qBAAqB;IACpC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,mBAAmB,CAAC;IAC5B,2DAA2D;IAC3D,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,8EAA8E;IAC9E,UAAU,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACpC,sEAAsE;IACtE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,iFAAiF;IACjF,MAAM,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAAC;IAC9D,sEAAsE;IACtE,aAAa,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IACnE,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,oBAAoB,CAAC;IAC3C,kDAAkD;IAClD,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvC;AAsDD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,uBAAuB,GAAG,aAAa,CA4FtF"}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * `GET /api/workflows/:path/graph` — the WORKFLOW-keyed canvas route (IA-01).
3
+ *
4
+ * The canvas has only ever been reachable through a session: boards live at
5
+ * `/canvas/:harnessSessionId/` and resolve by the session's current binding
6
+ * (server/canvas.ts), so an agent that has never hosted a session has no board
7
+ * at all. Reading agent F's board while working in agent B's session is
8
+ * therefore impossible, which is what the selection-driven rail needs.
9
+ *
10
+ * This route is the second, session-free entry point onto the SAME derivation.
11
+ * It renders nothing new: it calls `deriveWorkflowCanvas` (core/canvas-render.ts),
12
+ * the exact pipeline the write path uses, so `document` here is byte-identical
13
+ * to the render file a bound session's canvas serves for the same workflow.
14
+ * Nothing is written to disk.
15
+ *
16
+ * Keying: `:path` is the agent's absolute directory path, URI-encoded into one
17
+ * segment — `encodeURIComponent(agentPath)`, the same convention
18
+ * `/api/workflows/:id/input-contract` and `/api/workflows/:id/deploy` already
19
+ * use (web/src/lib/api.ts). Express matches on the raw path and decodes the
20
+ * param, so an encoded `/` never splits the route.
21
+ *
22
+ * Containment: the request string is a LOOKUP KEY AND NOTHING ELSE. It is
23
+ * matched against the live registry and then dropped; every filesystem call
24
+ * below runs on `workflow.path` — the path the registry itself authored when
25
+ * it scanned the disk. So the barrier is not "we checked the client's string
26
+ * carefully", it is "the client's string never reaches `fs`", which survives a
27
+ * future edit that reorders the checks and is a barrier static analysis can
28
+ * see rather than one it has to be told about.
29
+ *
30
+ * The lexical guards below (`..` segment, absolute-only) stay: they reject the
31
+ * escape shapes outright rather than let normalization quietly land them on
32
+ * some other registered path. Defence in depth, not the barrier. The marker's
33
+ * realpath is likewise confined to the agent directory, so a symlinked
34
+ * `sapiom.json` cannot turn this into a file reader.
35
+ *
36
+ * Failure modes are deliberately distinct, and every one of them is
37
+ * distinguishable from "no such route":
38
+ *
39
+ * 400 the path is missing, relative, or carries a `..` segment; or
40
+ * `sapiom.json` resolves outside the agent directory
41
+ * 404 the path is not a registered workflow
42
+ * 200 `status: "empty"` — registered, but no readable/parseable
43
+ * `sapiom.json` (absent ⇒ empty, not an error)
44
+ * 200 `status: "preparing"`— dependencies not installed yet
45
+ * 200 `status: "error"` — extraction ran and failed; `reason` says why
46
+ * 200 `status: "ok"` — graph + document
47
+ *
48
+ * EVERY failure below the registry lookup lands in that `error` shape. The
49
+ * derivation spawns a child process, parses a manifest this process did not
50
+ * write, and walks user directories that can EACCES — so it throws, and
51
+ * Express 4 does not catch a rejected async handler. Unhandled, Node's default
52
+ * `--unhandled-rejections=throw` takes the harness process down and the
53
+ * request is never answered: the canvas spins forever and then the studio
54
+ * dies. A board that cannot be derived is a board with a reason on it.
55
+ *
56
+ * Mounted under the same `/api` boot-token middleware as the rest of the REST
57
+ * surface (server/index.ts).
58
+ */
59
+ import * as path from "node:path";
60
+ import * as fsp from "node:fs/promises";
61
+ import { Router } from "express";
62
+ import { inspectAgentProjectMarker, } from "../core/agent-project-discovery.js";
63
+ import { renderCanvasMessageDocument } from "../core/canvas-template.js";
64
+ import { deriveWorkflowCanvas } from "../core/canvas-render.js";
65
+ import { hasTraversalSegment, resolveWithinRoot } from "../core/path-safety.js";
66
+ /** Why a registered agent has no graph, phrased for a person reading the pane. */
67
+ const EMPTY_REASONS = {
68
+ gone: "This agent's directory is no longer on disk.",
69
+ absent: "This agent has no sapiom.json, so there is no graph to render yet.",
70
+ invalid: "This agent's sapiom.json is not valid JSON, so its graph can't be read.",
71
+ unreadable: "This agent's sapiom.json could not be read.",
72
+ };
73
+ /** The empty board — the same message document server/canvas.ts serves, with
74
+ * the specific reason as its subtitle so the pane is never mutely blank. */
75
+ function emptyResponse(agentPath, name, reason) {
76
+ return {
77
+ path: agentPath,
78
+ name,
79
+ status: "empty",
80
+ graph: null,
81
+ enrichment: null,
82
+ reason,
83
+ cached: false,
84
+ document: renderCanvasMessageDocument("Nothing rendered yet", reason),
85
+ };
86
+ }
87
+ /**
88
+ * The board a failed derivation gets: the declared `error` status, the reason
89
+ * that was thrown, and a renderable page rather than a hole.
90
+ *
91
+ * 200, not 500. `status` already distinguishes every outcome this route has,
92
+ * the pane renders `document` for all of them, and a 5xx would be read by the
93
+ * SPA's `ApiError` path as "this route is broken" rather than "this one
94
+ * agent's graph could not be extracted" — the same distinction
95
+ * `server/actions.ts` preserves when its input-contract extraction fails.
96
+ */
97
+ function errorResponse(agentPath, name, reason) {
98
+ return {
99
+ path: agentPath,
100
+ name,
101
+ status: "error",
102
+ graph: null,
103
+ enrichment: null,
104
+ reason,
105
+ cached: false,
106
+ document: renderCanvasMessageDocument("Couldn't render this board", reason),
107
+ };
108
+ }
109
+ /** The thrown value as one line a person can read in the pane. */
110
+ function failureReason(err) {
111
+ const detail = (err instanceof Error ? err.message : String(err)).trim();
112
+ return `Studio couldn't read this agent's graph: ${detail || "the derivation failed."}`;
113
+ }
114
+ export function createWorkflowGraphRouter(deps) {
115
+ const inspectMarker = deps.inspectMarker ?? inspectAgentProjectMarker;
116
+ const deriveCanvas = deps.deriveCanvas ?? deriveWorkflowCanvas;
117
+ const realpath = deps.realpath ?? ((p) => fsp.realpath(p));
118
+ const router = Router();
119
+ router.get("/api/workflows/:path/graph", async (req, res) => {
120
+ const raw = req.params.path;
121
+ if (typeof raw !== "string" || raw.trim() === "") {
122
+ res.status(400).json({ error: "agent path is required" });
123
+ return;
124
+ }
125
+ // Rejected on the RAW value, before resolution: a `..` climb must be an
126
+ // error, not something normalization silently lands on another registered
127
+ // path. (path-safety.ts's segment-aware test — "a..b" is a normal name.)
128
+ if (hasTraversalSegment(raw)) {
129
+ res.status(400).json({ error: "agent path must not contain a '..' segment" });
130
+ return;
131
+ }
132
+ if (!path.isAbsolute(raw)) {
133
+ res.status(400).json({ error: "agent path must be absolute" });
134
+ return;
135
+ }
136
+ // The lookup, and the LAST use of the request string. `workflow` is the
137
+ // registry's own row, so `workflow.path` is a path this server wrote when
138
+ // it scanned the disk — and that is the value every `fs` call below runs
139
+ // on. The request never reaches the filesystem, resolved or otherwise.
140
+ const workflow = deps.resolveWorkflow(path.resolve(raw));
141
+ if (!workflow) {
142
+ res.status(404).json({ error: "agent not found" });
143
+ return;
144
+ }
145
+ const agentPath = path.resolve(workflow.path);
146
+ const name = workflow.name || path.basename(agentPath);
147
+ let realDir;
148
+ try {
149
+ realDir = await realpath(agentPath);
150
+ }
151
+ catch {
152
+ // Registered but gone (the registry prunes lazily). That is an EMPTY
153
+ // board, not a 404 — 404 means "not registered", and conflating the two
154
+ // would make a consumer show "no such agent" for one it can see listed.
155
+ res.json(emptyResponse(agentPath, name, EMPTY_REASONS.gone));
156
+ return;
157
+ }
158
+ // Symlink guard on the one file this route reads by name. A symlinked
159
+ // agent DIRECTORY is legitimate (the user registered it, and realDir is
160
+ // its target), but a `sapiom.json` symlinked out of the project would make
161
+ // a path-keyed read endpoint into a file reader — refuse that outright.
162
+ const marker = await realpath(path.join(realDir, "sapiom.json")).catch(() => null);
163
+ if (marker !== null && resolveWithinRoot(realDir, marker) === null) {
164
+ res.status(400).json({ error: "sapiom.json resolves outside the agent directory" });
165
+ return;
166
+ }
167
+ // From here on the work is a child process, a manifest this process did
168
+ // not write, and a walk of user directories. All three throw, and an
169
+ // uncaught rejection in an Express 4 async handler kills the harness
170
+ // instead of answering the request.
171
+ try {
172
+ const inspection = await inspectMarker(realDir);
173
+ if (inspection.status !== "valid") {
174
+ res.json(emptyResponse(agentPath, name, EMPTY_REASONS[inspection.status]));
175
+ return;
176
+ }
177
+ const derived = await deriveCanvas({
178
+ path: workflow.path,
179
+ name,
180
+ definitionId: workflow.definitionId ?? null,
181
+ activeBuildRunStatus: workflow.activeBuildRunStatus ?? null,
182
+ });
183
+ res.json({
184
+ path: agentPath,
185
+ name,
186
+ status: derived.status,
187
+ graph: derived.graph,
188
+ enrichment: derived.enrichment,
189
+ reason: derived.reason,
190
+ cached: derived.cached,
191
+ document: derived.document,
192
+ });
193
+ }
194
+ catch (err) {
195
+ res.json(errorResponse(agentPath, name, failureReason(err)));
196
+ }
197
+ });
198
+ return router;
199
+ }
200
+ //# sourceMappingURL=workflow-graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-graph.js","sourceRoot":"","sources":["../../src/server/workflow-graph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Public contract for the local workspace dependency graph. Graph payloads
3
+ * never carry filesystem roots; WorkspaceScopeSummary only joins AppState's
4
+ * existing cwd-backed folder projection to an opaque key.
5
+ */
6
+ export type WorkspaceKey = string;
7
+ export type AgentKey = string;
8
+ /**
9
+ * The one browser/server rule for a workspace-relative local AgentKey.
10
+ * Callers receive null when the source is not inside the supplied scope.
11
+ */
12
+ export declare function workspaceRelativeLocalKey(scopeRoot: string, sourceRoot: string): AgentKey | null;
13
+ /** Internal HTTP metadata; it is deliberately not part of SystemGraph JSON. */
14
+ export declare const SYSTEM_GRAPH_CACHE_HEADER = "X-Sapiom-System-Graph-Cache";
15
+ export type SystemGraphCacheStatus = "complete" | "degraded";
16
+ export interface WorkspaceScopeSummary {
17
+ workspaceKey: WorkspaceKey;
18
+ /** Used only to join the existing workspace-folder projection in AppState. */
19
+ cwd: string;
20
+ }
21
+ export interface SystemGraphNode {
22
+ id: string;
23
+ agentKey: AgentKey;
24
+ label: string;
25
+ }
26
+ export type AgentInvocationMode = "blocking" | "async";
27
+ export interface SystemGraphEdge {
28
+ from: string;
29
+ to: string;
30
+ kind: "invokes";
31
+ basis: "static";
32
+ mode: AgentInvocationMode;
33
+ }
34
+ export interface GraphWarning {
35
+ code: "unresolved-target" | "dynamic-target" | "duplicate-edge" | "projection-failed" | "duplicate-agent-key" | "inventory-extraction-failed";
36
+ message: string;
37
+ agentKey?: AgentKey;
38
+ }
39
+ export interface SystemGraph {
40
+ kind: "system";
41
+ scope: {
42
+ kind: "working-tree";
43
+ workspaceKey: WorkspaceKey;
44
+ };
45
+ nodes: SystemGraphNode[];
46
+ edges: SystemGraphEdge[];
47
+ warnings: GraphWarning[];
48
+ }
49
+ export type SystemGraphLifecycleState = "building" | "ready" | "stale" | "degraded";
50
+ /** Path-free lifecycle envelope for one workspace projection. */
51
+ export interface SystemGraphSnapshot {
52
+ workspaceKey: WorkspaceKey;
53
+ /** Monotonic within one server process and workspace. */
54
+ revision: number;
55
+ state: SystemGraphLifecycleState;
56
+ /** Null only before a usable projection exists. */
57
+ graph: SystemGraph | null;
58
+ }
59
+ //# sourceMappingURL=system-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"system-graph.d.ts","sourceRoot":"","sources":["../../src/shared/system-graph.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAClC,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AAuD9B;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,QAAQ,GAAG,IAAI,CA0BjB;AAED,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,gCAAgC,CAAC;AACvE,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,UAAU,CAAC;AAE7D,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,YAAY,CAAC;IAC3B,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,OAAO,CAAC;AAEvD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,QAAQ,CAAC;IAChB,IAAI,EAAE,mBAAmB,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EACA,mBAAmB,GACnB,gBAAgB,GAChB,gBAAgB,GAChB,mBAAmB,GACnB,qBAAqB,GACrB,6BAA6B,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE;QACL,IAAI,EAAE,cAAc,CAAC;QACrB,YAAY,EAAE,YAAY,CAAC;KAC5B,CAAC;IACF,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AACD,MAAM,MAAM,yBAAyB,GACjC,UAAU,GACV,OAAO,GACP,OAAO,GACP,UAAU,CAAC;AAEf,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,YAAY,CAAC;IAC3B,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,yBAAyB,CAAC;IACjC,mDAAmD;IACnD,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;CAC3B"}