@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.
- package/CHANGELOG.md +61 -0
- package/dist/cli/bin.js +24 -7
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/consent.js +1 -1
- package/dist/core/adapters/adapter.d.ts +2 -2
- package/dist/core/api-key-provider.d.ts +79 -0
- package/dist/core/api-key-provider.d.ts.map +1 -0
- package/dist/core/api-key-provider.js +91 -0
- package/dist/core/api-key-provider.js.map +1 -0
- package/dist/core/canvas-run-state.d.ts +9 -5
- package/dist/core/canvas-run-state.d.ts.map +1 -1
- package/dist/core/canvas-run-state.js +42 -13
- package/dist/core/canvas-run-state.js.map +1 -1
- package/dist/core/definition-slug-resolver.d.ts +13 -0
- package/dist/core/definition-slug-resolver.d.ts.map +1 -1
- package/dist/core/definition-slug-resolver.js +29 -0
- package/dist/core/definition-slug-resolver.js.map +1 -1
- package/dist/core/render-local-run.d.ts +82 -0
- package/dist/core/render-local-run.d.ts.map +1 -0
- package/dist/core/render-local-run.js +105 -0
- package/dist/core/render-local-run.js.map +1 -0
- package/dist/core/render-log-slice.d.ts +30 -0
- package/dist/core/render-log-slice.d.ts.map +1 -0
- package/dist/core/render-log-slice.js +49 -0
- package/dist/core/render-log-slice.js.map +1 -0
- package/dist/core/render-run-state.d.ts.map +1 -1
- package/dist/core/render-run-state.js +30 -49
- package/dist/core/render-run-state.js.map +1 -1
- package/dist/core/run-local-bootstrap.d.ts +93 -0
- package/dist/core/run-local-bootstrap.d.ts.map +1 -0
- package/dist/core/run-local-bootstrap.js +155 -0
- package/dist/core/run-local-bootstrap.js.map +1 -0
- package/dist/core/run-state.d.ts +16 -1
- package/dist/core/run-state.d.ts.map +1 -1
- package/dist/core/run-state.js +75 -25
- package/dist/core/run-state.js.map +1 -1
- package/dist/core/stub-feedback.d.ts +52 -0
- package/dist/core/stub-feedback.d.ts.map +1 -0
- package/dist/core/stub-feedback.js +33 -0
- package/dist/core/stub-feedback.js.map +1 -0
- package/dist/server/actions.d.ts +139 -0
- package/dist/server/actions.d.ts.map +1 -0
- package/dist/server/actions.js +430 -0
- package/dist/server/actions.js.map +1 -0
- package/dist/server/auth-routes.d.ts +92 -0
- package/dist/server/auth-routes.d.ts.map +1 -0
- package/dist/server/auth-routes.js +151 -0
- package/dist/server/auth-routes.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +0 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/rest.d.ts.map +1 -1
- package/dist/server/rest.js +0 -2
- package/dist/server/rest.js.map +1 -1
- package/dist/server/runs.d.ts +16 -15
- package/dist/server/runs.d.ts.map +1 -1
- package/dist/server/runs.js +15 -80
- package/dist/server/runs.js.map +1 -1
- package/dist/server/static.d.ts +15 -1
- package/dist/server/static.d.ts.map +1 -1
- package/dist/server/static.js +54 -4
- package/dist/server/static.js.map +1 -1
- package/dist/shared/types.d.ts +79 -35
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/web/assets/index-DCtyI4VW.css +32 -0
- package/dist/web/assets/index-WSw5RvYR.js +445 -0
- package/dist/web/canvas/sess-boot/index.html +305 -0
- package/dist/web/index.html +4 -9
- package/dist/web/welcome-hero-dark.png +0 -0
- package/dist/web/welcome-hero-light.png +0 -0
- package/package.json +6 -3
- package/dist/core/run-spend.d.ts +0 -55
- package/dist/core/run-spend.d.ts.map +0 -1
- package/dist/core/run-spend.js +0 -109
- package/dist/core/run-spend.js.map +0 -1
- package/dist/core/run-transactions.d.ts +0 -60
- package/dist/core/run-transactions.d.ts.map +0 -1
- package/dist/core/run-transactions.js +0 -129
- package/dist/core/run-transactions.js.map +0 -1
- package/dist/server/skills.d.ts +0 -51
- package/dist/server/skills.d.ts.map +0 -1
- package/dist/server/skills.js +0 -312
- package/dist/server/skills.js.map +0 -1
- package/dist/web/assets/index-DM84J83Y.js +0 -234
- package/dist/web/assets/index-DemtWW4L.css +0 -32
|
@@ -0,0 +1,430 @@
|
|
|
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 { spawn as spawnChildProcess } from "node:child_process";
|
|
27
|
+
import { createInterface } from "node:readline";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { Router } from "express";
|
|
31
|
+
import { AgentOperationError, createClient, deploy as coreDeploy, run as coreRun, readConfig as coreReadConfig, } from "@sapiom/agent-core";
|
|
32
|
+
import { resolveCoreBaseUrl } from "../core/definition-slug-resolver.js";
|
|
33
|
+
import { staticApiKeyProvider, } from "../core/api-key-provider.js";
|
|
34
|
+
const DEFAULT_CORE_DEPS = {
|
|
35
|
+
createClient,
|
|
36
|
+
deploy: coreDeploy,
|
|
37
|
+
run: coreRun,
|
|
38
|
+
readConfig: coreReadConfig,
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the compiled run-local bootstrap entry. This module lives at
|
|
42
|
+
* `dist/server/actions.js` (built) or `src/server/actions.ts` (tsx dev /
|
|
43
|
+
* vitest) — the bootstrap is its sibling one directory over in `core/`, with
|
|
44
|
+
* the same `.js`/`.ts` extension as this file. Reading the extension off
|
|
45
|
+
* `import.meta.url` (rather than hard-coding `.js`) keeps a real dev-server
|
|
46
|
+
* spawn resolvable too. Exported for unit coverage of the path math.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveRunLocalBootstrapPath(moduleUrl) {
|
|
49
|
+
const here = fileURLToPath(moduleUrl);
|
|
50
|
+
const ext = here.endsWith(".ts") ? ".ts" : ".js";
|
|
51
|
+
return join(dirname(here), "..", "core", `run-local-bootstrap${ext}`);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The default spawn: `node <bootstrap>` with `cwd` set to this package's root
|
|
55
|
+
* so the bootstrap's `import "@sapiom/agent-core"` resolves against the
|
|
56
|
+
* harness's real dependency (same technique as canvas-manifest-check). A `.ts`
|
|
57
|
+
* bootstrap (dev only) is loaded through the `tsx` register hook; the built
|
|
58
|
+
* `.js` runs on bare node. stdin is piped so the route can write the request.
|
|
59
|
+
*/
|
|
60
|
+
function defaultRunLocalSpawn() {
|
|
61
|
+
const bootstrap = resolveRunLocalBootstrapPath(import.meta.url);
|
|
62
|
+
const nodeArgs = bootstrap.endsWith(".ts")
|
|
63
|
+
? ["--import", "tsx", bootstrap]
|
|
64
|
+
: [bootstrap];
|
|
65
|
+
return spawnChildProcess(process.execPath, nodeArgs, {
|
|
66
|
+
cwd: join(dirname(fileURLToPath(import.meta.url)), "..", ".."),
|
|
67
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** Bounded stderr tail kept per run-local child for failure display. */
|
|
71
|
+
const RUN_LOCAL_STDERR_TAIL_CHARS = 2000;
|
|
72
|
+
/**
|
|
73
|
+
* The run-local request body the SPA POSTs. `sourceDir` is required; the rest
|
|
74
|
+
* mirror {@link RunLocalRequest} and are forwarded to the bootstrap as-is.
|
|
75
|
+
*/
|
|
76
|
+
function parseRunLocalBody(body) {
|
|
77
|
+
if (typeof body !== "object" || body === null)
|
|
78
|
+
return null;
|
|
79
|
+
const b = body;
|
|
80
|
+
if (typeof b.sourceDir !== "string" || b.sourceDir.trim() === "")
|
|
81
|
+
return null;
|
|
82
|
+
return {
|
|
83
|
+
sourceDir: b.sourceDir,
|
|
84
|
+
input: b.input,
|
|
85
|
+
stubs: b.stubs,
|
|
86
|
+
maxAttemptsPerStep: typeof b.maxAttemptsPerStep === "number"
|
|
87
|
+
? b.maxAttemptsPerStep
|
|
88
|
+
: undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Extract the linked `definitionId` from a project's `sapiom.json`. The config
|
|
93
|
+
* file — not the registry — is the source of truth for a project's server-side
|
|
94
|
+
* definition id, so both deploy and (a future) resolve path read it here. Never
|
|
95
|
+
* throws: an unlinked/unreadable project returns null and the route maps that to
|
|
96
|
+
* a 409.
|
|
97
|
+
*/
|
|
98
|
+
function readDefinitionId(readConfig, projectDir) {
|
|
99
|
+
let config;
|
|
100
|
+
try {
|
|
101
|
+
config = readConfig(projectDir);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// BAD_CONFIG (unparseable sapiom.json) — treat as "not linked" for the route.
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return config?.definitionId ?? null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Map an {@link AgentOperationError} (or any thrown value) to a terminal deploy
|
|
111
|
+
* stream event. The hint is forwarded — it is safe to do so because git errors
|
|
112
|
+
* are redacted at source (credentials stripped before they reach the hint field),
|
|
113
|
+
* so no key material can reach the browser via this path.
|
|
114
|
+
*/
|
|
115
|
+
function toDeployErrorEvent(err) {
|
|
116
|
+
if (err instanceof AgentOperationError) {
|
|
117
|
+
return {
|
|
118
|
+
phase: "error",
|
|
119
|
+
code: err.code,
|
|
120
|
+
message: err.message,
|
|
121
|
+
...(err.hint !== undefined ? { hint: err.hint } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
phase: "error",
|
|
126
|
+
code: "UNKNOWN",
|
|
127
|
+
message: err instanceof Error ? err.message : String(err),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Whether a thrown value is an upstream "the API key was rejected" — worth one
|
|
132
|
+
* refresh + retry before giving up. agent-core maps a 401/403 to an
|
|
133
|
+
* {@link AgentOperationError} with `code: "HTTP_401" | "HTTP_403"` (see
|
|
134
|
+
* client.ts), so we match on that code rather than an HTTP status here — the
|
|
135
|
+
* error-code analogue of run-state.ts's `isAuthRejection(status)`.
|
|
136
|
+
*/
|
|
137
|
+
function isAuthRejectionError(err) {
|
|
138
|
+
return (err instanceof AgentOperationError &&
|
|
139
|
+
(err.code === "HTTP_401" || err.code === "HTTP_403"));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Run a core operation with the current API key and, when it fails with an auth
|
|
143
|
+
* rejection, refresh the shared credential store once and retry with the newer
|
|
144
|
+
* key — the deploy/prod-run analogue of run-state.ts's refresh-on-401 recovery.
|
|
145
|
+
*
|
|
146
|
+
* `invoke` is handed a freshly-minted client for the key in force (the boot key
|
|
147
|
+
* on the first attempt, the refreshed key on the retry) so a rotated/re-logged-in
|
|
148
|
+
* credential recovers in place instead of every action locking on the stale key.
|
|
149
|
+
* Retries only when refresh actually yields a *different, non-null* key —
|
|
150
|
+
* otherwise the original error is re-thrown unchanged (no wasted call, and the
|
|
151
|
+
* caller's error mapping sees the real auth failure).
|
|
152
|
+
*
|
|
153
|
+
* The caller has already checked `provider.getKey()` is non-null, so `apiKey!`
|
|
154
|
+
* here is safe; a concurrent sign-out would surface as a normal auth error.
|
|
155
|
+
*/
|
|
156
|
+
async function withKeyRefreshRetry(provider, createClientFor, invoke) {
|
|
157
|
+
const apiKey = provider.getKey();
|
|
158
|
+
try {
|
|
159
|
+
return await invoke(createClientFor(apiKey));
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (!isAuthRejectionError(err))
|
|
163
|
+
throw err;
|
|
164
|
+
const refreshed = await provider.refresh();
|
|
165
|
+
if (!refreshed || refreshed === apiKey)
|
|
166
|
+
throw err;
|
|
167
|
+
return invoke(createClientFor(refreshed));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Create the actions router. Mounts:
|
|
172
|
+
* - `POST /api/workflows/:id/deploy` — NDJSON build-status stream.
|
|
173
|
+
* - `POST /api/runs` — `{ executionId }` for a started prod execution.
|
|
174
|
+
* - `POST /api/runs/local` — NDJSON offline stub-run trace + summary.
|
|
175
|
+
*
|
|
176
|
+
* Deploy and prod-run run server-side with the held API key; run-local is fully
|
|
177
|
+
* offline and needs no key. None of them ever involve an AI coding agent.
|
|
178
|
+
*/
|
|
179
|
+
export function createActionsRouter(opts) {
|
|
180
|
+
const router = Router();
|
|
181
|
+
const deps = { ...DEFAULT_CORE_DEPS, ...opts.coreDeps };
|
|
182
|
+
const baseUrl = opts.coreBaseUrl ?? resolveCoreBaseUrl();
|
|
183
|
+
const runLocalSpawn = opts.runLocalSpawn ?? defaultRunLocalSpawn;
|
|
184
|
+
// Normalize to a provider so deploy/prod-run always authenticate with the
|
|
185
|
+
// held API key and can refresh + retry when that key is rejected — a plain
|
|
186
|
+
// string|null becomes a no-op static provider (no refresh). Mirrors the runs
|
|
187
|
+
// router; keeps both action surfaces on the one credential contract.
|
|
188
|
+
const provider = opts.apiKey !== null && typeof opts.apiKey === "object"
|
|
189
|
+
? opts.apiKey
|
|
190
|
+
: staticApiKeyProvider(opts.apiKey);
|
|
191
|
+
/** Mint a core client for a specific key against the resolved core host. */
|
|
192
|
+
const clientFor = (apiKey) => deps.createClient({ host: baseUrl, apiKey });
|
|
193
|
+
/**
|
|
194
|
+
* POST /api/workflows/:id/deploy
|
|
195
|
+
*
|
|
196
|
+
* Deploys the linked agent for the given workflow id: mints push credentials,
|
|
197
|
+
* pushes the synthesized tree, triggers a build, and polls to a terminal
|
|
198
|
+
* status — all inside @sapiom/agent-core's {@link deploy}. Streams NDJSON: a
|
|
199
|
+
* `building` line up front, then exactly one terminal `ready`/`error` line.
|
|
200
|
+
*
|
|
201
|
+
* 200 NDJSON stream (even a build failure is a 200 with a terminal `error`
|
|
202
|
+
* line — the request itself succeeded; the build outcome is in-band).
|
|
203
|
+
* 400 id missing/empty
|
|
204
|
+
* 404 workflow id not registered
|
|
205
|
+
* 409 workflow is not linked to a Sapiom agent (no definitionId)
|
|
206
|
+
* 503 harness is not signed in to Sapiom
|
|
207
|
+
*/
|
|
208
|
+
router.post("/api/workflows/:id/deploy", async (req, res) => {
|
|
209
|
+
const id = req.params.id;
|
|
210
|
+
if (!id || typeof id !== "string" || id.trim() === "") {
|
|
211
|
+
res.status(400).json({ error: "workflow id is required" });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!provider.getKey()) {
|
|
215
|
+
res.status(503).json({ error: "harness is not signed in to Sapiom" });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const workflow = opts.resolveWorkflow(id);
|
|
219
|
+
if (!workflow) {
|
|
220
|
+
res.status(404).json({ error: "workflow not found" });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const definitionId = readDefinitionId(deps.readConfig, workflow.path);
|
|
224
|
+
if (!definitionId) {
|
|
225
|
+
res
|
|
226
|
+
.status(409)
|
|
227
|
+
.json({ error: "workflow is not linked to a Sapiom agent" });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// From here the outcome is streamed in-band as NDJSON — status is 200 and
|
|
231
|
+
// headers are committed before the (potentially long) build runs.
|
|
232
|
+
res.status(200);
|
|
233
|
+
res.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
234
|
+
res.setHeader("Cache-Control", "no-store");
|
|
235
|
+
const write = (event) => {
|
|
236
|
+
res.write(JSON.stringify(event) + "\n");
|
|
237
|
+
};
|
|
238
|
+
write({ phase: "building", definitionId });
|
|
239
|
+
try {
|
|
240
|
+
// Auth against the live key, refreshing + retrying once on a rejected key
|
|
241
|
+
// (same recovery the runs router gets). The building/terminal streaming
|
|
242
|
+
// shape is unchanged — the retry is transparent to the NDJSON stream.
|
|
243
|
+
const result = await withKeyRefreshRetry(provider, clientFor, (client) => deps.deploy({ projectDir: workflow.path, definitionId }, client));
|
|
244
|
+
write({
|
|
245
|
+
phase: "ready",
|
|
246
|
+
definitionId: result.definitionId,
|
|
247
|
+
buildRunId: result.buildRunId,
|
|
248
|
+
status: result.status,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
write(toDeployErrorEvent(err));
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
res.end();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
/**
|
|
259
|
+
* POST /api/runs { definitionId, input }
|
|
260
|
+
*
|
|
261
|
+
* Starts a prod execution of the given definition and returns
|
|
262
|
+
* `{ executionId }` — the live-canvas path then polls the runs router for its
|
|
263
|
+
* per-step state. `input` is optional (defaults to an empty object for
|
|
264
|
+
* no-input agents). The key stays server-side.
|
|
265
|
+
*
|
|
266
|
+
* 200 { executionId } — execution created
|
|
267
|
+
* 400 definitionId missing/empty
|
|
268
|
+
* 502 gateway error (network or non-2xx from the backend)
|
|
269
|
+
* 503 harness is not signed in to Sapiom
|
|
270
|
+
*/
|
|
271
|
+
router.post("/api/runs", async (req, res) => {
|
|
272
|
+
if (!provider.getKey()) {
|
|
273
|
+
res.status(503).json({ error: "harness is not signed in to Sapiom" });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const body = (req.body ?? {});
|
|
277
|
+
const definitionId = body.definitionId;
|
|
278
|
+
if (typeof definitionId !== "string" || definitionId.trim() === "") {
|
|
279
|
+
res.status(400).json({ error: "definitionId is required" });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
// Auth against the live key, refreshing + retrying once on a rejected key
|
|
284
|
+
// (same recovery the runs router gets), then return { executionId }.
|
|
285
|
+
const result = await withKeyRefreshRetry(provider, clientFor, (client) => deps.run({ definitionId, input: body.input }, client));
|
|
286
|
+
res.json({ executionId: result.executionId });
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
if (err instanceof AgentOperationError) {
|
|
290
|
+
// The gateway/network failed — surface a 502 with the machine code, no
|
|
291
|
+
// credential hint (that hint names the login flow, not for the browser).
|
|
292
|
+
res.status(502).json({ error: err.message, code: err.code });
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
res
|
|
296
|
+
.status(502)
|
|
297
|
+
.json({ error: err instanceof Error ? err.message : String(err) });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
/**
|
|
302
|
+
* POST /api/runs/local { sourceDir, input?, stubs?, maxAttemptsPerStep? }
|
|
303
|
+
*
|
|
304
|
+
* Runs the workflow at `sourceDir` entirely offline against stub
|
|
305
|
+
* capabilities, in a child process (the run-local bootstrap), and streams the
|
|
306
|
+
* result back as NDJSON: one {@link LocalStepTrace} per line, then a terminal
|
|
307
|
+
* summary line `{ kind: "summary", outcome, output, error, unusedStubs,
|
|
308
|
+
* stubWarnings }`. A run that could not be invoked at all (bad project, bad
|
|
309
|
+
* stub file) yields a terminal `{ kind: "error", outcome: "failed", error }`
|
|
310
|
+
* line instead. Needs no API key and makes no network call — zero cost.
|
|
311
|
+
*
|
|
312
|
+
* The child owns the wire shapes; this handler validates the request, pipes
|
|
313
|
+
* it to the child's stdin, forwards each stdout line unchanged, and (only if
|
|
314
|
+
* the child dies without a terminal line) synthesizes one from its stderr so
|
|
315
|
+
* the stream always ends well-formed.
|
|
316
|
+
*
|
|
317
|
+
* 200 NDJSON stream (a failed *run* is still a 200 — the request succeeded;
|
|
318
|
+
* the outcome is in-band on the terminal line).
|
|
319
|
+
* 400 sourceDir missing/empty
|
|
320
|
+
*/
|
|
321
|
+
router.post("/api/runs/local", (req, res) => {
|
|
322
|
+
const request = parseRunLocalBody(req.body);
|
|
323
|
+
if (!request) {
|
|
324
|
+
res.status(400).json({ error: "sourceDir is required" });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
let child;
|
|
328
|
+
try {
|
|
329
|
+
child = runLocalSpawn();
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
// Never launched (e.g. the node binary or bootstrap is missing) — the
|
|
333
|
+
// request itself is fine, so answer in-band with a terminal error line.
|
|
334
|
+
res.status(200);
|
|
335
|
+
res.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
336
|
+
res.setHeader("Cache-Control", "no-store");
|
|
337
|
+
res.write(JSON.stringify({
|
|
338
|
+
kind: "error",
|
|
339
|
+
outcome: "failed",
|
|
340
|
+
error: err instanceof Error ? err.message : String(err),
|
|
341
|
+
}) + "\n");
|
|
342
|
+
res.end();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Headers are committed before the (potentially long) run streams.
|
|
346
|
+
res.status(200);
|
|
347
|
+
res.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
348
|
+
res.setHeader("Cache-Control", "no-store");
|
|
349
|
+
// Hand the request to the child, then close its stdin so the bootstrap's
|
|
350
|
+
// read-to-EOF completes.
|
|
351
|
+
child.stdin?.end(JSON.stringify(request));
|
|
352
|
+
// Forward each well-formed JSON line straight through — the bootstrap emits
|
|
353
|
+
// exactly the wire shapes, so no re-shaping happens here. A line that isn't
|
|
354
|
+
// JSON is stray stdout noise (an esbuild banner, a dependency's console
|
|
355
|
+
// write) and is dropped rather than corrupting the NDJSON stream — the same
|
|
356
|
+
// "degrade, never throw" stance as core/task-stream.ts.
|
|
357
|
+
let sawTerminalLine = false;
|
|
358
|
+
const onLine = (line) => {
|
|
359
|
+
const trimmed = line.trim();
|
|
360
|
+
if (trimmed === "")
|
|
361
|
+
return;
|
|
362
|
+
let parsed;
|
|
363
|
+
try {
|
|
364
|
+
parsed = JSON.parse(trimmed);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return; // non-JSON noise — not part of the contract.
|
|
368
|
+
}
|
|
369
|
+
// A `summary`/`error` line is terminal — track it so a crash after the
|
|
370
|
+
// summary isn't double-reported as a failure.
|
|
371
|
+
const kind = parsed.kind;
|
|
372
|
+
if (kind === "summary" || kind === "error")
|
|
373
|
+
sawTerminalLine = true;
|
|
374
|
+
res.write(trimmed + "\n");
|
|
375
|
+
};
|
|
376
|
+
// Keep a bounded stderr tail for the crash path (never forwarded inline —
|
|
377
|
+
// stderr is diagnostics, not part of the NDJSON contract).
|
|
378
|
+
let stderrTail = "";
|
|
379
|
+
child.stderr?.on("data", (chunk) => {
|
|
380
|
+
stderrTail = (stderrTail + String(chunk)).slice(-RUN_LOCAL_STDERR_TAIL_CHARS);
|
|
381
|
+
});
|
|
382
|
+
// End the response exactly once (mirrors task-manager's finish() guard).
|
|
383
|
+
// `crashReason` is captured from `exit`/`error` but the terminal decision is
|
|
384
|
+
// deferred to `settle()` so a still-buffered summary line is never clobbered.
|
|
385
|
+
let settled = false;
|
|
386
|
+
let crashReason = null;
|
|
387
|
+
const settle = () => {
|
|
388
|
+
if (settled)
|
|
389
|
+
return;
|
|
390
|
+
settled = true;
|
|
391
|
+
// Only synthesize a terminal line when the child produced none — otherwise
|
|
392
|
+
// the stream already ended well-formed with the bootstrap's own summary.
|
|
393
|
+
if (!sawTerminalLine) {
|
|
394
|
+
res.write(JSON.stringify({
|
|
395
|
+
kind: "error",
|
|
396
|
+
outcome: "failed",
|
|
397
|
+
error: stderrTail.trim() || crashReason || "run-local produced no output",
|
|
398
|
+
}) + "\n");
|
|
399
|
+
}
|
|
400
|
+
res.end();
|
|
401
|
+
};
|
|
402
|
+
// Drive the terminal decision off stdout's close (readline "close" fires
|
|
403
|
+
// only after every line has been emitted), so a summary line buffered when
|
|
404
|
+
// `exit` arrives is still forwarded first. If there's no stdout at all, fall
|
|
405
|
+
// back to `exit`/`error` directly.
|
|
406
|
+
if (child.stdout) {
|
|
407
|
+
const lines = createInterface({ input: child.stdout });
|
|
408
|
+
lines.on("line", onLine);
|
|
409
|
+
lines.on("close", settle);
|
|
410
|
+
child.on("error", (err) => {
|
|
411
|
+
crashReason = err.message;
|
|
412
|
+
});
|
|
413
|
+
child.on("exit", (code) => {
|
|
414
|
+
crashReason ?? (crashReason = `run-local process exited with code ${code ?? "null"}`);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
child.on("error", (err) => {
|
|
419
|
+
crashReason = err.message;
|
|
420
|
+
settle();
|
|
421
|
+
});
|
|
422
|
+
child.on("exit", (code) => {
|
|
423
|
+
crashReason ?? (crashReason = `run-local process exited with code ${code ?? "null"}`);
|
|
424
|
+
settle();
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
return router;
|
|
429
|
+
}
|
|
430
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../../src/server/actions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-app auth routes — expose the CLI's browser OAuth flow over HTTP so the
|
|
3
|
+
* Studio web app can trigger sign-in/sign-out without restarting the server.
|
|
4
|
+
*
|
|
5
|
+
* Reuses `performBrowserAuth` from `@sapiom/mcp/auth` (the SAME function the
|
|
6
|
+
* CLI's `ensureAuthenticated` calls at boot) — no custom OAuth implementation,
|
|
7
|
+
* no changes to any Sapiom cloud endpoint. The flow:
|
|
8
|
+
*
|
|
9
|
+
* POST /api/auth/start
|
|
10
|
+
* ↳ performBrowserAuth(appURL, apiURL) ← existing CLI flow, unchanged
|
|
11
|
+
* opens browser → localhost callback → token exchange
|
|
12
|
+
* ↳ writeCredentials(...) ← writes ~/.sapiom/credentials.json
|
|
13
|
+
* ↳ apiKeyProvider.refresh() ← adopts the new key in-process
|
|
14
|
+
* ↳ bus.publish({ type: "auth.changed" }) ← notifies open WS connections
|
|
15
|
+
*
|
|
16
|
+
* POST /api/auth/disconnect
|
|
17
|
+
* ↳ clearCredentials(...)
|
|
18
|
+
* ↳ provider.clear()
|
|
19
|
+
* ↳ bus.publish({ type: "auth.changed", authenticated: false })
|
|
20
|
+
*
|
|
21
|
+
* GET /api/auth/status
|
|
22
|
+
* ↳ { authenticated, organizationName }
|
|
23
|
+
*
|
|
24
|
+
* All three routes sit behind the existing `X-Harness-Token` middleware applied
|
|
25
|
+
* at the /api level in server/index.ts — no extra auth layer needed here.
|
|
26
|
+
*/
|
|
27
|
+
import { Router } from "express";
|
|
28
|
+
import { performBrowserAuth } from "@sapiom/mcp/auth";
|
|
29
|
+
import type { EventBus } from "../core/event-bus.js";
|
|
30
|
+
export interface AuthState {
|
|
31
|
+
authenticated: boolean;
|
|
32
|
+
organizationName: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A mutable holder for the harness's current auth identity — updated on
|
|
36
|
+
* sign-in/sign-out and queried by `GET /api/auth/status`. Shared between the
|
|
37
|
+
* auth router and any other consumer (e.g. `GET /api/state` in rest.ts) that
|
|
38
|
+
* wants live auth state rather than the boot-time snapshot.
|
|
39
|
+
*/
|
|
40
|
+
export interface MutableAuthState {
|
|
41
|
+
get(): AuthState;
|
|
42
|
+
set(state: AuthState): void;
|
|
43
|
+
}
|
|
44
|
+
export declare function createMutableAuthState(initial: AuthState): MutableAuthState;
|
|
45
|
+
/**
|
|
46
|
+
* The minimal slice of {@link ApiKeyProvider} the auth router needs: a way
|
|
47
|
+
* to push a new key in after a successful browser auth (or clear it on
|
|
48
|
+
* disconnect), without importing the full provider shape.
|
|
49
|
+
*/
|
|
50
|
+
export interface AuthKeyTarget {
|
|
51
|
+
/**
|
|
52
|
+
* Re-reads `~/.sapiom/credentials.json` and adopts the key found there.
|
|
53
|
+
* Called after `performBrowserAuth` + `writeCredentials` complete so the
|
|
54
|
+
* provider's in-memory key matches the freshly written credential.
|
|
55
|
+
*/
|
|
56
|
+
refresh(): Promise<string | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Unconditionally zero the in-memory key (sets it to null). Called on
|
|
59
|
+
* disconnect so that the provider returns null immediately — distinct from
|
|
60
|
+
* {@link refresh}, whose guard deliberately keeps the cached key when the
|
|
61
|
+
* store has nothing to offer.
|
|
62
|
+
*/
|
|
63
|
+
clear(): void;
|
|
64
|
+
}
|
|
65
|
+
export interface AuthRoutesOptions {
|
|
66
|
+
/** Mutable auth state shared with `GET /api/state` and `GET /api/auth/status`. */
|
|
67
|
+
authState: MutableAuthState;
|
|
68
|
+
/**
|
|
69
|
+
* The live API key provider — `refresh()` is called after sign-in writes
|
|
70
|
+
* `~/.sapiom/credentials.json` so the in-process key adopts the new value.
|
|
71
|
+
*/
|
|
72
|
+
apiKeyProvider: AuthKeyTarget;
|
|
73
|
+
/** The server event bus — broadcasts `auth.changed` after every state transition. */
|
|
74
|
+
bus: EventBus;
|
|
75
|
+
/**
|
|
76
|
+
* Overrides SAPIOM_ENVIRONMENT for the OAuth flow. Defaults to
|
|
77
|
+
* `process.env.SAPIOM_ENVIRONMENT` (the same override the CLI uses).
|
|
78
|
+
*/
|
|
79
|
+
environment?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Injectable seam for `performBrowserAuth` — lets tests mock the full OAuth
|
|
82
|
+
* round-trip without opening a real browser or binding a local port.
|
|
83
|
+
*/
|
|
84
|
+
performBrowserAuthImpl?: typeof performBrowserAuth;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Mount the in-app auth routes. The caller is responsible for gating these
|
|
88
|
+
* behind the boot-token middleware (applied at the /api level in
|
|
89
|
+
* server/index.ts before any router) — these routes do NOT add a second layer.
|
|
90
|
+
*/
|
|
91
|
+
export declare function createAuthRouter(opts: AuthRoutesOptions): Router;
|
|
92
|
+
//# sourceMappingURL=auth-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-routes.d.ts","sourceRoot":"","sources":["../../src/server/auth-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAEL,kBAAkB,EAGnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAMrD,MAAM,WAAW,SAAS;IACxB,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,IAAI,SAAS,CAAC;IACjB,GAAG,CAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,SAAS,GAAG,gBAAgB,CAQ3E;AAMD;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAClC;;;;;OAKG;IACH,KAAK,IAAI,IAAI,CAAC;CACf;AAMD,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,SAAS,EAAE,gBAAgB,CAAC;IAC5B;;;OAGG;IACH,cAAc,EAAE,aAAa,CAAC;IAC9B,qFAAqF;IACrF,GAAG,EAAE,QAAQ,CAAC;IACd;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,OAAO,kBAAkB,CAAC;CACpD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAuIhE"}
|