@sapiom/harness 0.1.5 → 0.1.6

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 (86) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/cli/bin.js +24 -7
  3. package/dist/cli/bin.js.map +1 -1
  4. package/dist/cli/consent.js +1 -1
  5. package/dist/core/adapters/adapter.d.ts +2 -2
  6. package/dist/core/api-key-provider.d.ts +79 -0
  7. package/dist/core/api-key-provider.d.ts.map +1 -0
  8. package/dist/core/api-key-provider.js +91 -0
  9. package/dist/core/api-key-provider.js.map +1 -0
  10. package/dist/core/canvas-run-state.d.ts +9 -5
  11. package/dist/core/canvas-run-state.d.ts.map +1 -1
  12. package/dist/core/canvas-run-state.js +42 -13
  13. package/dist/core/canvas-run-state.js.map +1 -1
  14. package/dist/core/definition-slug-resolver.d.ts +13 -0
  15. package/dist/core/definition-slug-resolver.d.ts.map +1 -1
  16. package/dist/core/definition-slug-resolver.js +29 -0
  17. package/dist/core/definition-slug-resolver.js.map +1 -1
  18. package/dist/core/render-local-run.d.ts +82 -0
  19. package/dist/core/render-local-run.d.ts.map +1 -0
  20. package/dist/core/render-local-run.js +105 -0
  21. package/dist/core/render-local-run.js.map +1 -0
  22. package/dist/core/render-log-slice.d.ts +30 -0
  23. package/dist/core/render-log-slice.d.ts.map +1 -0
  24. package/dist/core/render-log-slice.js +49 -0
  25. package/dist/core/render-log-slice.js.map +1 -0
  26. package/dist/core/render-run-state.d.ts.map +1 -1
  27. package/dist/core/render-run-state.js +30 -49
  28. package/dist/core/render-run-state.js.map +1 -1
  29. package/dist/core/run-local-bootstrap.d.ts +93 -0
  30. package/dist/core/run-local-bootstrap.d.ts.map +1 -0
  31. package/dist/core/run-local-bootstrap.js +155 -0
  32. package/dist/core/run-local-bootstrap.js.map +1 -0
  33. package/dist/core/run-state.d.ts +16 -1
  34. package/dist/core/run-state.d.ts.map +1 -1
  35. package/dist/core/run-state.js +75 -25
  36. package/dist/core/run-state.js.map +1 -1
  37. package/dist/core/stub-feedback.d.ts +52 -0
  38. package/dist/core/stub-feedback.d.ts.map +1 -0
  39. package/dist/core/stub-feedback.js +33 -0
  40. package/dist/core/stub-feedback.js.map +1 -0
  41. package/dist/server/actions.d.ts +139 -0
  42. package/dist/server/actions.d.ts.map +1 -0
  43. package/dist/server/actions.js +430 -0
  44. package/dist/server/actions.js.map +1 -0
  45. package/dist/server/auth-routes.d.ts +92 -0
  46. package/dist/server/auth-routes.d.ts.map +1 -0
  47. package/dist/server/auth-routes.js +151 -0
  48. package/dist/server/auth-routes.js.map +1 -0
  49. package/dist/server/index.d.ts.map +1 -1
  50. package/dist/server/index.js +0 -0
  51. package/dist/server/index.js.map +1 -1
  52. package/dist/server/rest.d.ts.map +1 -1
  53. package/dist/server/rest.js +0 -2
  54. package/dist/server/rest.js.map +1 -1
  55. package/dist/server/runs.d.ts +16 -15
  56. package/dist/server/runs.d.ts.map +1 -1
  57. package/dist/server/runs.js +15 -80
  58. package/dist/server/runs.js.map +1 -1
  59. package/dist/server/static.d.ts +15 -1
  60. package/dist/server/static.d.ts.map +1 -1
  61. package/dist/server/static.js +54 -4
  62. package/dist/server/static.js.map +1 -1
  63. package/dist/shared/types.d.ts +79 -35
  64. package/dist/shared/types.d.ts.map +1 -1
  65. package/dist/shared/types.js.map +1 -1
  66. package/dist/web/assets/index-DCtyI4VW.css +32 -0
  67. package/dist/web/assets/index-WSw5RvYR.js +445 -0
  68. package/dist/web/canvas/sess-boot/index.html +305 -0
  69. package/dist/web/index.html +4 -9
  70. package/dist/web/welcome-hero-dark.png +0 -0
  71. package/dist/web/welcome-hero-light.png +0 -0
  72. package/package.json +6 -3
  73. package/dist/core/run-spend.d.ts +0 -55
  74. package/dist/core/run-spend.d.ts.map +0 -1
  75. package/dist/core/run-spend.js +0 -109
  76. package/dist/core/run-spend.js.map +0 -1
  77. package/dist/core/run-transactions.d.ts +0 -60
  78. package/dist/core/run-transactions.d.ts.map +0 -1
  79. package/dist/core/run-transactions.js +0 -129
  80. package/dist/core/run-transactions.js.map +0 -1
  81. package/dist/server/skills.d.ts +0 -51
  82. package/dist/server/skills.d.ts.map +0 -1
  83. package/dist/server/skills.js +0 -312
  84. package/dist/server/skills.js.map +0 -1
  85. package/dist/web/assets/index-DM84J83Y.js +0 -234
  86. package/dist/web/assets/index-DemtWW4L.css +0 -32
@@ -0,0 +1,155 @@
1
+ /**
2
+ * run-local bootstrap — the child-process entrypoint behind
3
+ * `POST /api/runs/local`.
4
+ *
5
+ * It runs an agent entirely in-process against stub capabilities
6
+ * (`runLocalFromDir` from @sapiom/agent-core) and writes the result as an
7
+ * NDJSON stream to stdout: one line per {@link LocalStepTrace}, then a single
8
+ * terminal summary line `{ outcome, output, error, unusedStubs, stubWarnings }`.
9
+ * Fully offline and zero-cost — run-local resolves every `ctx.sapiom.*` call
10
+ * from stubs and never touches the network.
11
+ *
12
+ * Why a separate child process (not an in-process import):
13
+ * 1. `runLocalFromDir` esbuild-bundles and dynamically `import()`s a workflow
14
+ * project the harness doesn't control, so a stray top-level side effect,
15
+ * infinite loop, or crash in someone else's step body is bounded to this
16
+ * one child instead of taking the long-lived harness server down.
17
+ * 2. It sidesteps a Vite/Vitest limitation: the dynamic `import(\`file://…\`)`
18
+ * inside the loader gets intercepted by the SSR dynamic-import-vars
19
+ * transform when pulled into a Vitest module graph, and mishandles the
20
+ * tmpdir `file://` URL on darwin. A plain child `node` process never goes
21
+ * through that transform. (Mirrors the reasoning in canvas-manifest-check.)
22
+ *
23
+ * Contract with the route ({@link createActionsRouter}):
24
+ * - Request arrives as one JSON object on **stdin**: `{ sourceDir, input?,
25
+ * stubs?, maxAttemptsPerStep? }`.
26
+ * - Output is line-oriented JSON on **stdout** — the route forwards each line
27
+ * through unchanged, so the shapes here ARE the wire shapes the SPA parses.
28
+ * - Diagnostics go to **stderr**; the route keeps a bounded tail for failures.
29
+ * - Exit 0 once the terminal line is written (even for a failed run — a failed
30
+ * *run* is a successful *invocation*); exit 1 only when no run happened
31
+ * (bad request, load error) after writing a terminal `error` line.
32
+ */
33
+ import { runLocalFromDir, } from "@sapiom/agent-core";
34
+ /** Read the entire request payload from a readable stream as one UTF-8 string. */
35
+ async function readAll(stream) {
36
+ const chunks = [];
37
+ for await (const chunk of stream) {
38
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
39
+ }
40
+ return Buffer.concat(chunks).toString("utf8");
41
+ }
42
+ /**
43
+ * Parse the stdin payload into a {@link RunLocalRequest}. Throws a plain Error
44
+ * with a caller-safe message (no key material, no provider names) on anything
45
+ * that isn't a JSON object carrying a non-empty string `sourceDir`.
46
+ */
47
+ export function parseRunLocalRequest(raw) {
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(raw);
51
+ }
52
+ catch {
53
+ throw new Error("run-local request is not valid JSON");
54
+ }
55
+ if (typeof parsed !== "object" || parsed === null) {
56
+ throw new Error("run-local request must be a JSON object");
57
+ }
58
+ const body = parsed;
59
+ const sourceDir = body.sourceDir;
60
+ if (typeof sourceDir !== "string" || sourceDir.trim() === "") {
61
+ throw new Error("run-local request requires a non-empty sourceDir");
62
+ }
63
+ return {
64
+ sourceDir,
65
+ input: body.input,
66
+ stubs: body.stubs,
67
+ maxAttemptsPerStep: typeof body.maxAttemptsPerStep === "number"
68
+ ? body.maxAttemptsPerStep
69
+ : undefined,
70
+ };
71
+ }
72
+ /** Serialize one step trace as a single NDJSON line (newline-terminated). */
73
+ function traceLine(step) {
74
+ return JSON.stringify(step) + "\n";
75
+ }
76
+ /** Serialize the terminal summary as a single NDJSON line. */
77
+ function summaryLine(line) {
78
+ return JSON.stringify(line) + "\n";
79
+ }
80
+ /**
81
+ * Run the request and emit its NDJSON stream to `out`. Returns the process exit
82
+ * code: 0 when a run executed (any outcome), 1 when the run could not be
83
+ * invoked (a terminal `error` line is emitted first). Never throws — every
84
+ * failure becomes an in-band terminal line, because a half-written stream with
85
+ * a thrown stack on stderr is far harder for the route to reason about than a
86
+ * clean terminal line plus an exit code.
87
+ */
88
+ export async function runBootstrap(request, out) {
89
+ try {
90
+ const result = await runLocalFromDir({
91
+ sourceDir: request.sourceDir,
92
+ input: request.input,
93
+ stubs: request.stubs,
94
+ maxAttemptsPerStep: request.maxAttemptsPerStep,
95
+ });
96
+ // One line per step-attempt, in execution order — the consumer parses them
97
+ // incrementally rather than buffering the whole trace as a single blob.
98
+ for (const step of result.steps) {
99
+ out.write(traceLine(step));
100
+ }
101
+ const summary = {
102
+ kind: "summary",
103
+ outcome: result.outcome,
104
+ output: result.output,
105
+ error: result.error,
106
+ unusedStubs: result.unusedStubs,
107
+ stubWarnings: result.stubWarnings,
108
+ };
109
+ out.write(summaryLine(summary));
110
+ return 0;
111
+ }
112
+ catch (err) {
113
+ const errorLine = {
114
+ kind: "error",
115
+ outcome: "failed",
116
+ error: err instanceof Error ? err.message : String(err),
117
+ };
118
+ out.write(JSON.stringify(errorLine) + "\n");
119
+ return 1;
120
+ }
121
+ }
122
+ /**
123
+ * Entrypoint: read the request from stdin, run it, exit with the run's code.
124
+ * A stdin/read failure (or a request that can't be parsed) also degrades to a
125
+ * terminal `error` line + exit 1 — the route always sees a well-formed final
126
+ * line regardless of how the child failed.
127
+ */
128
+ async function main() {
129
+ let request;
130
+ try {
131
+ request = parseRunLocalRequest(await readAll(process.stdin));
132
+ }
133
+ catch (err) {
134
+ const errorLine = {
135
+ kind: "error",
136
+ outcome: "failed",
137
+ error: err instanceof Error ? err.message : String(err),
138
+ };
139
+ process.stdout.write(JSON.stringify(errorLine) + "\n");
140
+ process.exitCode = 1;
141
+ return;
142
+ }
143
+ process.exitCode = await runBootstrap(request, process.stdout);
144
+ }
145
+ // Only self-invoke when run as the child entrypoint, never when imported by a
146
+ // unit test (which drives runBootstrap / parseRunLocalRequest directly). The
147
+ // built entry is dist/core/run-local-bootstrap.js; import.meta.url ends with
148
+ // this module's own file, and process.argv[1] is the script node was told to
149
+ // run — they share a basename only for the real child launch.
150
+ const invokedAsScript = typeof process.argv[1] === "string" &&
151
+ import.meta.url === new URL(`file://${process.argv[1]}`).href;
152
+ if (invokedAsScript) {
153
+ void main();
154
+ }
155
+ //# sourceMappingURL=run-local-bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-local-bootstrap.js","sourceRoot":"","sources":["../../src/core/run-local-bootstrap.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,7 @@
16
16
  * elsewhere in the harness; consolidate into one shared helper when convenient.
17
17
  */
18
18
  import type { RunView } from "../shared/types.js";
19
+ import { type ApiKeyProvider } from "./api-key-provider.js";
19
20
  /** Resolve the agents surface base URL from the environment. */
20
21
  export declare function resolveAgentsBaseUrl(): string;
21
22
  export type RunStateResult = {
@@ -27,7 +28,12 @@ export type RunStateResult = {
27
28
  error: string;
28
29
  };
29
30
  export interface RunStateFetcherOpts {
30
- apiKey: string | null;
31
+ /**
32
+ * Sapiom API key. Accepts either a plain `string | null` (the boot-time key)
33
+ * or an {@link ApiKeyProvider} — pass a provider to get the refresh-on-401
34
+ * recovery path. A bare string never refreshes.
35
+ */
36
+ apiKey: string | null | ApiKeyProvider;
31
37
  baseUrl?: string;
32
38
  /** Injectable fetch implementation — defaults to global fetch. Test seam. */
33
39
  fetchImpl?: typeof fetch;
@@ -35,11 +41,20 @@ export interface RunStateFetcherOpts {
35
41
  export interface RunStateFetcher {
36
42
  fetch(executionId: string): Promise<RunStateResult>;
37
43
  }
44
+ /** Upstream statuses that mean "the API key was rejected" — worth one refresh
45
+ * + retry before we give up. 401 is the common case; 403 covers a key that
46
+ * authenticated but lost authorization (e.g. rotated to a new org). */
47
+ export declare function isAuthRejection(status: number): boolean;
38
48
  /**
39
49
  * Create a fetcher that resolves an execution's current render state from the
40
50
  * agents surface. The fetcher is intentionally non-throwing: all error paths
41
51
  * return a typed {@link RunStateResult} with `ok: false` so the router can
42
52
  * forward the appropriate HTTP status without a try/catch at the call site.
53
+ *
54
+ * When constructed with an {@link ApiKeyProvider}, a 401/403 from the upstream
55
+ * call triggers exactly one credential refresh + retry: if the shared store has
56
+ * a newer key (e.g. the user re-logged-in), the retry uses it and the Studio
57
+ * recovers in place instead of surfacing a dead-end auth error.
43
58
  */
44
59
  export declare function createRunStateFetcher(opts: RunStateFetcherOpts): RunStateFetcher;
45
60
  //# sourceMappingURL=run-state.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../src/core/run-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGlD,gEAAgE;AAChE,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,mBAAmB,GACxB,eAAe,CA6CjB"}
1
+ {"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../src/core/run-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EACL,KAAK,cAAc,EAEpB,MAAM,uBAAuB,CAAC;AAgB/B,gEAAgE;AAChE,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,cAAc,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACrD;AAED;;wEAEwE;AACxE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,mBAAmB,GACxB,eAAe,CA4EjB"}
@@ -17,23 +17,80 @@
17
17
  */
18
18
  import { decodeExecutionProjection } from "@sapiom/agent-core";
19
19
  import { renderRunState } from "./render-run-state.js";
20
+ import { staticApiKeyProvider, } from "./api-key-provider.js";
21
+ /**
22
+ * Normalize the polymorphic `apiKey` option to an {@link ApiKeyProvider}. A
23
+ * plain string (or null) becomes a no-refresh provider so existing static-key
24
+ * callers keep working unchanged; a provider is used as-is.
25
+ */
26
+ function toApiKeyProvider(apiKey) {
27
+ if (apiKey === null || typeof apiKey === "string") {
28
+ return staticApiKeyProvider(apiKey);
29
+ }
30
+ return apiKey;
31
+ }
20
32
  /** Resolve the agents surface base URL from the environment. */
21
33
  export function resolveAgentsBaseUrl() {
22
34
  return (process.env.SAPIOM_AGENTS_URL ??
23
35
  process.env.SAPIOM_TOOLS_BASE ??
24
36
  "https://tools.sapiom.ai");
25
37
  }
38
+ /** Upstream statuses that mean "the API key was rejected" — worth one refresh
39
+ * + retry before we give up. 401 is the common case; 403 covers a key that
40
+ * authenticated but lost authorization (e.g. rotated to a new org). */
41
+ export function isAuthRejection(status) {
42
+ return status === 401 || status === 403;
43
+ }
26
44
  /**
27
45
  * Create a fetcher that resolves an execution's current render state from the
28
46
  * agents surface. The fetcher is intentionally non-throwing: all error paths
29
47
  * return a typed {@link RunStateResult} with `ok: false` so the router can
30
48
  * forward the appropriate HTTP status without a try/catch at the call site.
49
+ *
50
+ * When constructed with an {@link ApiKeyProvider}, a 401/403 from the upstream
51
+ * call triggers exactly one credential refresh + retry: if the shared store has
52
+ * a newer key (e.g. the user re-logged-in), the retry uses it and the Studio
53
+ * recovers in place instead of surfacing a dead-end auth error.
31
54
  */
32
55
  export function createRunStateFetcher(opts) {
33
- const { apiKey, baseUrl = resolveAgentsBaseUrl(), fetchImpl = fetch } = opts;
56
+ const { baseUrl = resolveAgentsBaseUrl(), fetchImpl = fetch } = opts;
57
+ const provider = toApiKeyProvider(opts.apiKey);
58
+ const requestOnce = async (executionId, apiKey) => {
59
+ try {
60
+ const res = await fetchImpl(`${baseUrl}/agents/v1/executions/${encodeURIComponent(executionId)}`, { headers: { "x-sapiom-api-key": apiKey } });
61
+ return { kind: "ok", res };
62
+ }
63
+ catch {
64
+ return {
65
+ kind: "err",
66
+ result: { ok: false, status: 502, error: "gateway unreachable" },
67
+ };
68
+ }
69
+ };
70
+ const decode = async (res) => {
71
+ if (res.status === 404) {
72
+ return { ok: false, status: 404, error: "execution not found" };
73
+ }
74
+ if (!res.ok) {
75
+ return {
76
+ ok: false,
77
+ status: 502,
78
+ error: `gateway responded ${res.status}`,
79
+ };
80
+ }
81
+ try {
82
+ const raw = (await res.json());
83
+ const runView = renderRunState(decodeExecutionProjection(raw));
84
+ return { ok: true, runView };
85
+ }
86
+ catch {
87
+ return { ok: false, status: 502, error: "could not decode execution" };
88
+ }
89
+ };
34
90
  return {
35
91
  async fetch(executionId) {
36
92
  // No API key — do not touch the network; the harness is not signed in.
93
+ let apiKey = provider.getKey();
37
94
  if (!apiKey) {
38
95
  return {
39
96
  ok: false,
@@ -41,31 +98,24 @@ export function createRunStateFetcher(opts) {
41
98
  error: "harness is not signed in to Sapiom",
42
99
  };
43
100
  }
44
- let res;
45
- try {
46
- res = await fetchImpl(`${baseUrl}/agents/v1/executions/${encodeURIComponent(executionId)}`, { headers: { "x-sapiom-api-key": apiKey } });
47
- }
48
- catch {
49
- return { ok: false, status: 502, error: "gateway unreachable" };
50
- }
51
- if (res.status === 404) {
52
- return { ok: false, status: 404, error: "execution not found" };
53
- }
54
- if (!res.ok) {
55
- return {
56
- ok: false,
57
- status: 502,
58
- error: `gateway responded ${res.status}`,
59
- };
60
- }
61
- try {
62
- const raw = (await res.json());
63
- const runView = renderRunState(decodeExecutionProjection(raw));
64
- return { ok: true, runView };
65
- }
66
- catch {
67
- return { ok: false, status: 502, error: "could not decode execution" };
101
+ const first = await requestOnce(executionId, apiKey);
102
+ if (first.kind === "err")
103
+ return first.result;
104
+ // Refresh-on-401: re-read the shared credential store once and retry with
105
+ // the newer key when the rejection was an auth failure and refresh
106
+ // actually produced a different key. Any other status (or an unchanged
107
+ // key) falls through to normal decoding/error mapping — no wasted retry.
108
+ if (isAuthRejection(first.res.status)) {
109
+ const refreshed = await provider.refresh();
110
+ if (refreshed && refreshed !== apiKey) {
111
+ apiKey = refreshed;
112
+ const second = await requestOnce(executionId, apiKey);
113
+ if (second.kind === "err")
114
+ return second.result;
115
+ return decode(second.res);
116
+ }
68
117
  }
118
+ return decode(first.res);
69
119
  },
70
120
  };
71
121
  }
@@ -1 +1 @@
1
- {"version":3,"file":"run-state.js","sourceRoot":"","sources":["../../src/core/run-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAG/D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,gEAAgE;AAChE,MAAM,UAAU,oBAAoB;IAClC,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,iBAAiB;QAC7B,OAAO,CAAC,GAAG,CAAC,iBAAiB;QAC7B,yBAAyB,CAC1B,CAAC;AACJ,CAAC;AAiBD;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAyB;IAEzB,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,oBAAoB,EAAE,EAAE,SAAS,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC;IAE7E,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,WAAmB;YAC7B,uEAAuE;YACvE,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,MAAM,EAAE,GAAG;oBACX,KAAK,EAAE,oCAAoC;iBAC5C,CAAC;YACJ,CAAC;YAED,IAAI,GAAa,CAAC;YAClB,IAAI,CAAC;gBACH,GAAG,GAAG,MAAM,SAAS,CACnB,GAAG,OAAO,yBAAyB,kBAAkB,CAAC,WAAW,CAAC,EAAE,EACpE,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAAE,MAAM,EAAE,EAAE,CAC5C,CAAC;YACJ,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;YAClE,CAAC;YAED,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBACvB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;YAClE,CAAC;YAED,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,MAAM,EAAE,GAAG;oBACX,KAAK,EAAE,qBAAqB,GAAG,CAAC,MAAM,EAAE;iBACzC,CAAC;YACJ,CAAC;YAED,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAC;gBAC1D,MAAM,OAAO,GAAG,cAAc,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC;gBAC/D,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;YAC/B,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC;YACzE,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"run-state.js","sourceRoot":"","sources":["../../src/core/run-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAG/D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAEL,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAE/B;;;;GAIG;AACH,SAAS,gBAAgB,CACvB,MAAsC;IAEtC,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAClD,OAAO,oBAAoB,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,oBAAoB;IAClC,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,iBAAiB;QAC7B,OAAO,CAAC,GAAG,CAAC,iBAAiB;QAC7B,yBAAyB,CAC1B,CAAC;AACJ,CAAC;AAsBD;;wEAEwE;AACxE,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAyB;IAEzB,MAAM,EAAE,OAAO,GAAG,oBAAoB,EAAE,EAAE,SAAS,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC;IACrE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAE/C,MAAM,WAAW,GAAG,KAAK,EACvB,WAAmB,EACnB,MAAc,EAGd,EAAE;QACF,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,SAAS,CACzB,GAAG,OAAO,yBAAyB,kBAAkB,CAAC,WAAW,CAAC,EAAE,EACpE,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAAE,MAAM,EAAE,EAAE,CAC5C,CAAC;YACF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;gBACL,IAAI,EAAE,KAAK;gBACX,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,qBAAqB,EAAE;aACjE,CAAC;QACJ,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,KAAK,EAAE,GAAa,EAA2B,EAAE;QAC9D,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;QAClE,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,GAAG;gBACX,KAAK,EAAE,qBAAqB,GAAG,CAAC,MAAM,EAAE;aACzC,CAAC;QACJ,CAAC;QACD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAC;YAC1D,MAAM,OAAO,GAAG,cAAc,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC;YAC/D,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC;QACzE,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,WAAmB;YAC7B,uEAAuE;YACvE,IAAI,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;YAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO;oBACL,EAAE,EAAE,KAAK;oBACT,MAAM,EAAE,GAAG;oBACX,KAAK,EAAE,oCAAoC;iBAC5C,CAAC;YACJ,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YACrD,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK;gBAAE,OAAO,KAAK,CAAC,MAAM,CAAC;YAE9C,0EAA0E;YAC1E,mEAAmE;YACnE,uEAAuE;YACvE,yEAAyE;YACzE,IAAI,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;gBACtC,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;gBAC3C,IAAI,SAAS,IAAI,SAAS,KAAK,MAAM,EAAE,CAAC;oBACtC,MAAM,GAAG,SAAS,CAAC;oBACnB,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;oBACtD,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK;wBAAE,OAAO,MAAM,CAAC,MAAM,CAAC;oBAChD,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC;YAED,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * stub-feedback — the pure, read-only derivations behind the run-inspector's
3
+ * stub affordances (WB15-2). Given a {@link RunView} (which already carries the
4
+ * run-level stub facts set by {@link renderLocalRun}), decide:
5
+ * - whether a given step should show the "stubbed" chip, and
6
+ * - what the read-only stub-hygiene notice should say (or nothing).
7
+ *
8
+ * Pure and deterministic: no LLM, no I/O, no clock — every result is a function
9
+ * of the RunView alone, so both are Stryker targets with mutation-first tests.
10
+ *
11
+ * Read-only by design: this surfaces what a stub run DID (served a call, or a
12
+ * supplied stub matched nothing / had the wrong shape). Editing stubs is a
13
+ * deferred fast-follow and lives nowhere here.
14
+ *
15
+ * Honesty is the whole point: the chip appears only for a step that actually ran
16
+ * in a stub-served run (never for a prod run, never for a step the run never
17
+ * reached), and the notice is `null` unless there is a real problem to report —
18
+ * so a clean run shows no chrome at all.
19
+ */
20
+ import type { RunView } from "../shared/types.js";
21
+ /**
22
+ * Should the "stubbed" chip render for the step named `stepName`?
23
+ *
24
+ * True only when BOTH hold:
25
+ * 1. the run was stub-served (`run.stubbed` — set by renderLocalRun for an
26
+ * offline local run; never by renderRunState for a prod run), and
27
+ * 2. that step actually ran this run (it appears in `run.steps`).
28
+ *
29
+ * A local run resolves every `ctx.sapiom.*` call from a stub, so a step that ran
30
+ * was stub-served — that is the honest granularity (agent-core records no
31
+ * per-CALL stub attribution). A step the run never reached, or any step of a
32
+ * real run, gets no chip. A null run (nothing observed yet) is never stubbed.
33
+ */
34
+ export declare function stepIsStubbed(run: RunView | null, stepName: string): boolean;
35
+ /**
36
+ * The read-only stub-hygiene notice for a run, or `null` when there is nothing
37
+ * honest to show. Present only the signals that carry real content:
38
+ * - `unusedStubs`: supplied stub keys that matched no capability call (a no-op
39
+ * mock — almost always a typo or the wrong path form), and
40
+ * - `stubWarnings`: stub values that matched a key but had the wrong shape.
41
+ *
42
+ * Returns `null` unless at least one non-empty signal exists, so the caller can
43
+ * render nothing for a clean run (honesty — no empty "0 issues" panel). The
44
+ * returned arrays are always non-empty when present (empty ones are dropped),
45
+ * so the component never has to re-check length.
46
+ */
47
+ export interface StubNotice {
48
+ unusedStubs?: NonNullable<RunView["unusedStubs"]>;
49
+ stubWarnings?: NonNullable<RunView["stubWarnings"]>;
50
+ }
51
+ export declare function stubNotice(run: RunView | null): StubNotice | null;
52
+ //# sourceMappingURL=stub-feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stub-feedback.d.ts","sourceRoot":"","sources":["../../src/core/stub-feedback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG5E;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC;IAClD,YAAY,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC;CACrD;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,GAAG,UAAU,GAAG,IAAI,CAYjE"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Should the "stubbed" chip render for the step named `stepName`?
3
+ *
4
+ * True only when BOTH hold:
5
+ * 1. the run was stub-served (`run.stubbed` — set by renderLocalRun for an
6
+ * offline local run; never by renderRunState for a prod run), and
7
+ * 2. that step actually ran this run (it appears in `run.steps`).
8
+ *
9
+ * A local run resolves every `ctx.sapiom.*` call from a stub, so a step that ran
10
+ * was stub-served — that is the honest granularity (agent-core records no
11
+ * per-CALL stub attribution). A step the run never reached, or any step of a
12
+ * real run, gets no chip. A null run (nothing observed yet) is never stubbed.
13
+ */
14
+ export function stepIsStubbed(run, stepName) {
15
+ if (!run || run.stubbed !== true)
16
+ return false;
17
+ return run.steps.some((s) => s.name === stepName);
18
+ }
19
+ export function stubNotice(run) {
20
+ if (!run)
21
+ return null;
22
+ const notice = {};
23
+ if (run.unusedStubs && run.unusedStubs.length > 0) {
24
+ notice.unusedStubs = run.unusedStubs;
25
+ }
26
+ if (run.stubWarnings && run.stubWarnings.length > 0) {
27
+ notice.stubWarnings = run.stubWarnings;
28
+ }
29
+ // Empty object == no real signal; report honest absence as null so the
30
+ // inspector renders no notice block at all.
31
+ return notice.unusedStubs || notice.stubWarnings ? notice : null;
32
+ }
33
+ //# sourceMappingURL=stub-feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stub-feedback.js","sourceRoot":"","sources":["../../src/core/stub-feedback.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,GAAmB,EAAE,QAAgB;IACjE,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC/C,OAAO,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;AACpD,CAAC;AAmBD,MAAM,UAAU,UAAU,CAAC,GAAmB;IAC5C,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,IAAI,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,CAAC,WAAW,GAAG,GAAG,CAAC,WAAW,CAAC;IACvC,CAAC;IACD,IAAI,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpD,MAAM,CAAC,YAAY,GAAG,GAAG,CAAC,YAAY,CAAC;IACzC,CAAC;IACD,uEAAuE;IACvE,4CAA4C;IAC5C,OAAO,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AACnE,CAAC"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Actions router — backs the direct, in-app agent action macros:
3
+ * POST /api/workflows/:id/deploy → deploy the linked agent (build + poll).
4
+ * POST /api/runs → start a prod execution → { executionId }.
5
+ *
6
+ * These are the "direct" replacements for the old CLI/agent-driven macros: the
7
+ * harness server calls the Sapiom backend itself (via {@link deploy} / {@link run}
8
+ * from @sapiom/agent-core), so an action never spawns a subprocess agent and never
9
+ * consumes the user's LLM credits. The Sapiom API key is held server-side and
10
+ * never forwarded to the browser — exactly like {@link createRunsRouter}: the
11
+ * SPA hits these local `/api/*` routes (no key in the request) and the router
12
+ * presents the key to the backend on its behalf.
13
+ *
14
+ * Deploy streams its build lifecycle as NDJSON (one JSON object per line, the
15
+ * same line-oriented convention the local-run stream uses) so the canvas can
16
+ * show "building…" the moment the build kicks off and a terminal line when it
17
+ * settles. Prod-run is a single request/response returning `{ executionId }`,
18
+ * which the existing live-canvas path then polls via the runs router.
19
+ *
20
+ * Run-local (`POST /api/runs/local`) is the offline sibling: it spawns the
21
+ * run-local bootstrap child, which runs the workflow in-process against stub
22
+ * capabilities and streams NDJSON back — one {@link LocalStepTrace} per line,
23
+ * then a terminal summary carrying `unusedStubs`/`stubWarnings`. It needs no
24
+ * API key and makes no network call, so it works signed-out and at zero cost.
25
+ */
26
+ import { Router } from "express";
27
+ import { createClient, deploy as coreDeploy, run as coreRun, readConfig as coreReadConfig } from "@sapiom/agent-core";
28
+ import { type ApiKeyProvider } from "../core/api-key-provider.js";
29
+ /**
30
+ * A registered workflow the actions router can act on — the subset of
31
+ * {@link WorkflowInfo} deploy needs. Resolved by the injected
32
+ * {@link ActionsRouterOpts.resolveWorkflow} so the router stays decoupled from
33
+ * the registry (mirrors the rest router's `findWorkflow` seam).
34
+ */
35
+ export interface ActionWorkflow {
36
+ /** Absolute path to the agent project directory (deploy's `projectDir`). */
37
+ path: string;
38
+ }
39
+ /**
40
+ * One line of the deploy NDJSON stream. `building` is emitted once the build is
41
+ * triggered; exactly one terminal line (`ready` | `error`) closes the stream.
42
+ * `capability`-agnostic and credential-free by construction.
43
+ */
44
+ export type DeployStreamEvent = {
45
+ phase: "building";
46
+ definitionId: string;
47
+ } | {
48
+ phase: "ready";
49
+ definitionId: string;
50
+ buildRunId: string;
51
+ status: string;
52
+ } | {
53
+ phase: "error";
54
+ code: string;
55
+ message: string;
56
+ hint?: string;
57
+ };
58
+ /**
59
+ * Injectable core operations. Real implementations are the @sapiom/agent-core
60
+ * exports; tests substitute fakes so no route ever touches git or the network.
61
+ * Undocumented for prod — a test seam only, mirroring `fetchImpl` in runs.ts.
62
+ */
63
+ export interface ActionsCoreDeps {
64
+ createClient: typeof createClient;
65
+ deploy: typeof coreDeploy;
66
+ run: typeof coreRun;
67
+ readConfig: typeof coreReadConfig;
68
+ }
69
+ /**
70
+ * The slice of node's ChildProcess the run-local route uses — injectable so
71
+ * tests drive a fake child (a scripted stdout stream) without spawning a real
72
+ * `node` process. Mirrors {@link TaskProcess} in task-manager.ts.
73
+ */
74
+ export interface RunLocalChildProcess {
75
+ /** Where the request JSON is written; closed immediately after. */
76
+ stdin: NodeJS.WritableStream | null;
77
+ /** Line-oriented NDJSON the route forwards to the HTTP response. */
78
+ stdout: NodeJS.ReadableStream | null;
79
+ /** Diagnostics; a bounded tail is kept for failure reporting. */
80
+ stderr: NodeJS.ReadableStream | null;
81
+ on(event: "exit", listener: (code: number | null) => void): unknown;
82
+ on(event: "error", listener: (err: Error) => void): unknown;
83
+ }
84
+ /** Spawn the run-local bootstrap child. Test seam — defaults to `node`ing the
85
+ * compiled bootstrap. */
86
+ export type RunLocalSpawnFn = () => RunLocalChildProcess;
87
+ /**
88
+ * Resolve the compiled run-local bootstrap entry. This module lives at
89
+ * `dist/server/actions.js` (built) or `src/server/actions.ts` (tsx dev /
90
+ * vitest) — the bootstrap is its sibling one directory over in `core/`, with
91
+ * the same `.js`/`.ts` extension as this file. Reading the extension off
92
+ * `import.meta.url` (rather than hard-coding `.js`) keeps a real dev-server
93
+ * spawn resolvable too. Exported for unit coverage of the path math.
94
+ */
95
+ export declare function resolveRunLocalBootstrapPath(moduleUrl: string): string;
96
+ export interface ActionsRouterOpts {
97
+ /**
98
+ * Sapiom credential the deploy/prod-run actions authenticate with. Accepts
99
+ * either a plain `string | null` (the boot-time key) or an
100
+ * {@link ApiKeyProvider}; pass a provider — exactly like
101
+ * {@link createRunsRouter} — so a rejected key can refresh + retry and so each
102
+ * request reads the current key rather than a boot-time snapshot. This is the
103
+ * API key (`sk_…`), NOT the local boot token. `null` (or a provider whose
104
+ * `getKey()` is null) means the harness is not signed in.
105
+ */
106
+ apiKey: string | null | ApiKeyProvider;
107
+ /**
108
+ * Backend host for @sapiom/agent-core's GatewayClient (the CORE surface —
109
+ * `/v1/workflows` is appended by the client). Resolved from env by default.
110
+ * Test seam.
111
+ */
112
+ coreBaseUrl?: string;
113
+ /**
114
+ * Resolve a workflow `:id` (as it appears in the route path) to the registered
115
+ * workflow, or null when unknown. The caller supplies this from the live
116
+ * registry — the router does not read the registry directly.
117
+ */
118
+ resolveWorkflow: (id: string) => ActionWorkflow | null;
119
+ /** Injectable core operations. Test seam; defaults to the real exports. */
120
+ coreDeps?: Partial<ActionsCoreDeps>;
121
+ /**
122
+ * Spawn the run-local bootstrap child. Undocumented for prod — a test seam
123
+ * only (defaults to `node`ing the compiled bootstrap), so a test can stream a
124
+ * scripted trace without spawning a real process, mirroring `fetchImpl` in
125
+ * runs.ts and the `spawnProcess` seam in task-manager.ts.
126
+ */
127
+ runLocalSpawn?: RunLocalSpawnFn;
128
+ }
129
+ /**
130
+ * Create the actions router. Mounts:
131
+ * - `POST /api/workflows/:id/deploy` — NDJSON build-status stream.
132
+ * - `POST /api/runs` — `{ executionId }` for a started prod execution.
133
+ * - `POST /api/runs/local` — NDJSON offline stub-run trace + summary.
134
+ *
135
+ * Deploy and prod-run run server-side with the held API key; run-local is fully
136
+ * offline and needs no key. None of them ever involve an AI coding agent.
137
+ */
138
+ export declare function createActionsRouter(opts: ActionsRouterOpts): Router;
139
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../../src/server/actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAOH,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAEL,YAAY,EACZ,MAAM,IAAI,UAAU,EACpB,GAAG,IAAI,OAAO,EACd,UAAU,IAAI,cAAc,EAI7B,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EACL,KAAK,cAAc,EAEpB,MAAM,6BAA6B,CAAC;AAErC;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC5E;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,MAAM,EAAE,OAAO,UAAU,CAAC;IAC1B,GAAG,EAAE,OAAO,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,cAAc,CAAC;CACnC;AASD;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC;IACpC,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC;IACrC,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC;IACrC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC;IACpE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC;CAC7D;AAED;0BAC0B;AAC1B,MAAM,MAAM,eAAe,GAAG,MAAM,oBAAoB,CAAC;AAEzD;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAItE;AA0CD,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;OAQG;IACH,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,cAAc,CAAC;IACvC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,cAAc,GAAG,IAAI,CAAC;IACvD,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IACpC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,eAAe,CAAC;CACjC;AA4FD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAuRnE"}