/** * Actions router — backs the direct, in-app agent action macros: * POST /api/workflows/:id/deploy → deploy the linked agent (build + poll). * POST /api/runs → start a prod execution → { executionId }. * * These are the "direct" replacements for the old CLI/agent-driven macros: the * harness server calls the Sapiom backend itself (via {@link deploy} / {@link run} * from @sapiom/agent-core), so an action never spawns a subprocess agent and never * consumes the user's LLM credits. The Sapiom API key is held server-side and * never forwarded to the browser — exactly like {@link createRunsRouter}: the * SPA hits these local `/api/*` routes (no key in the request) and the router * presents the key to the backend on its behalf. * * Deploy streams its build lifecycle as NDJSON (one JSON object per line, the * same line-oriented convention the local-run stream uses) so the canvas can * show "building…" the moment the build kicks off and a terminal line when it * settles. Prod-run is a single request/response returning `{ executionId }`, * which the existing live-canvas path then polls via the runs router. * * Run-local (`POST /api/runs/local`) is the signed-out sibling: it spawns the * run-local bootstrap child, which runs the workflow in-process with Sapiom * capability calls served by stubs and streams NDJSON back — one * {@link LocalStepTrace} per line, then a terminal summary carrying * `unusedStubs`/`stubWarnings`. The runner needs no API key and makes no Sapiom * capability request. Arbitrary user step code still executes on the machine * and may use its network. */ import { Router } from "express"; import { createClient, deploy as coreDeploy, link as coreLink, run as coreRun, readConfig as coreReadConfig, writeConfig as coreWriteConfig } from "@sapiom/agent-core"; import type { PendingSecretsStore } from "../core/pending-secrets.js"; import { type VaultSecretsClient } from "../core/vault-secrets.js"; import type { WorkflowInputContractResponse } from "../shared/types.js"; import { type ApiKeyProvider } from "../core/api-key-provider.js"; /** * A registered workflow the actions router can act on — the subset of * {@link WorkflowInfo} deploy needs. Resolved by the injected * {@link ActionsRouterOpts.resolveWorkflow} so the router stays decoupled from * the registry (mirrors the rest router's `findWorkflow` seam). */ export interface ActionWorkflow { /** Absolute path to the agent project directory (deploy's `projectDir`). */ path: string; /** * Registry display name (`package.json`'s `name`, else the folder basename). * Optional so a host that only knows the path still works; used as a * mid-priority fallback when naming an agent this route has to create. */ name?: string; } /** * One line of the deploy NDJSON stream. `linking` is emitted only when the * project had no `definitionId` and the route is resolving-or-creating its * remote agent; `warning` is non-terminal and advisory — it never replaces a * terminal line and may appear alongside either outcome (e.g. linking * succeeded but caching the id in `sapiom.json` failed); `building` is emitted * once the build is triggered; exactly one terminal line (`ready` | `error`) * closes the stream. `capability`-agnostic and credential-free by construction. */ export type DeployStreamEvent = { phase: "linking"; name: string; } | { phase: "warning"; message: string; } | { phase: "building"; definitionId: string; } | { phase: "ready"; definitionId: string; buildRunId: string; status: string; } | { phase: "error"; code: string; message: string; hint?: string; }; /** * Injectable core operations. Real implementations are the @sapiom/agent-core * exports; tests substitute fakes so no route ever touches git or the network. * Undocumented for prod — a test seam only, mirroring `fetchImpl` in runs.ts. */ export interface ActionsCoreDeps { createClient: typeof createClient; deploy: typeof coreDeploy; run: typeof coreRun; readConfig: typeof coreReadConfig; /** Resolve-or-create the server-side agent for an unlinked project. */ link: typeof coreLink; /** Cache the linked id back into `sapiom.json` (merges; never clobbers). */ writeConfig: typeof coreWriteConfig; } /** * The slice of node's ChildProcess the run-local route uses — injectable so * tests drive a fake child (a scripted stdout stream) without spawning a real * `node` process. Mirrors {@link TaskProcess} in task-manager.ts. */ export interface RunLocalChildProcess { /** Where the request JSON is written; closed immediately after. */ stdin: NodeJS.WritableStream | null; /** Line-oriented NDJSON the route forwards to the HTTP response. */ stdout: NodeJS.ReadableStream | null; /** Diagnostics; a bounded tail is kept for failure reporting. */ stderr: NodeJS.ReadableStream | null; on(event: "exit", listener: (code: number | null) => void): unknown; on(event: "error", listener: (err: Error) => void): unknown; } /** Spawn the run-local bootstrap child. Test seam — defaults to `node`ing the * compiled bootstrap. * * `secrets` are the project's pending Studio credentials, merged into the * child's environment (core/pending-secrets.ts). Passed per call rather than * captured at construction because they are per-PROJECT and one server serves * many. */ export type RunLocalSpawnFn = (secrets?: Record) => RunLocalChildProcess; /** * Resolve the compiled run-local bootstrap entry. This module lives at * `dist/server/actions.js` (built) or `src/server/actions.ts` (tsx dev / * vitest) — the bootstrap is its sibling one directory over in `core/`, with * the same `.js`/`.ts` extension as this file. Reading the extension off * `import.meta.url` (rather than hard-coding `.js`) keeps a real dev-server * spawn resolvable too. Exported for unit coverage of the path math. */ export declare function resolveRunLocalBootstrapPath(moduleUrl: string): string; /** Everything needed to launch the run-local child, resolved but not yet spawned * — so the packaging-sensitive path math is unit-testable. */ export interface RunLocalChildSpec { command: string; args: string[]; cwd: string; env: NodeJS.ProcessEnv; } /** * Resolve how to launch the run-local bootstrap child: `node ` with * `cwd` at this package's root, so the bootstrap's `import "@sapiom/agent-core"` * resolves against the harness's real dependency. A `.ts` bootstrap (dev only) * goes through the `tsx` register hook; the built `.js` runs on bare node. * * Three things here exist ONLY for the packaged desktop app, and their absence * is why every local run failed there with `spawn ENOTDIR` while the CLI was * fine. `canvas-manifest-check.ts` already does all three; this call site did * none of them: * * 1. **cwd must be the unpacked twin.** `import.meta.url` reports a path inside * `app.asar`; nothing translates a child's cwd, so `chdir` fails. ENOTDIR is * not in Node's deferred-error list, so `spawn` throws SYNCHRONOUSLY and the * route answers `{"kind":"error","error":"spawn ENOTDIR"}`. * 2. **the script path must be the unpacked twin too.** The child is plain Node * with no asar support whatsoever — it cannot read a file inside the archive, * however well Electron's own `fs` copes. * 3. **`ELECTRON_RUN_AS_NODE=1` under Electron.** `process.execPath` is the * Sapiom binary there; without the flag it boots a SECOND COPY OF THE APP * instead of running the script. Only set when actually inside Electron, so * the CLI (real node) is untouched. * * `ESBUILD_BINARY_PATH` is dropped: the desktop host pins it so its in-process * bundler can exec a binary outside app.asar, but no child needs it (a child * resolves real on-disk paths itself) and a workflow step body that shells out * to the project's own toolchain would hit an esbuild version mismatch. */ export declare function resolveRunLocalChildSpec(moduleUrl: string, runtime: { execPath: string; env: NodeJS.ProcessEnv; isElectron: boolean; /** * The project's pending Studio secrets. These OVERRIDE the inherited * environment on purpose: a value the user typed into this agent's Secrets * tab is a per-agent statement, and it is what the deployed run will get * from the vault. Letting an unrelated shell export win here would make a * local run disagree with production precisely when the user was trying to * check that it agrees. */ secrets?: Record; }): RunLocalChildSpec; export interface ActionsRouterOpts { /** * Sapiom credential the deploy/prod-run actions authenticate with. Accepts * either a plain `string | null` (the boot-time key) or an * {@link ApiKeyProvider}; pass a provider — exactly like * {@link createRunsRouter} — so a rejected key can refresh + retry and so each * request reads the current key rather than a boot-time snapshot. This is the * API key (`sk_…`), NOT the local boot token. `null` (or a provider whose * `getKey()` is null) means the harness is not signed in. */ apiKey: string | null | ApiKeyProvider; /** * Backend host for @sapiom/agent-core's GatewayClient (the CORE surface — * `/v1/workflows` is appended by the client). Resolved from env by default. * Test seam. */ coreBaseUrl?: string; /** * Resolve a workflow `:id` (as it appears in the route path) to the registered * workflow, or null when unknown. The caller supplies this from the live * registry — the router does not read the registry directly. */ resolveWorkflow: (id: string) => ActionWorkflow | null; /** Resolve the entry contract for a registered workflow. Test seam; the * default reads the cached manifest extraction for `workflow.path`. */ resolveInputContract?: (workflow: ActionWorkflow) => Promise; /** * The agent's DECLARED name (`defineAgent({ name })`) for a project the route * has to link on the fly, or null when it cannot be determined. Optional: the * route falls back to `sapiom.json`'s cached name, then * {@link ActionWorkflow.name}, then the project folder's basename. * * A seam rather than a direct call so this router keeps knowing nothing about * the canvas extraction cache (see core/definition-name.ts, which * `server/index.ts` wires in here). Never expected to throw — a rejection is * treated as "could not determine". */ resolveDefinitionName?: (workflow: ActionWorkflow) => Promise; /** * Called after the route has linked a previously-unlinked project and cached * its new `definitionId`, so the host can refresh whatever it derives from * `sapiom.json`. The registry's `list()` never re-reads the file and the * workspace watcher ignores a content-only edit, so without this a * first-time deploy leaves the SPA showing "Draft" with Prod Run gated. * * A seam rather than a direct call so this router keeps knowing nothing about * the workflow registry (`server/index.ts` wires it). Never expected to * throw — a rejection must not cost the user their build. */ onLinked?: (workflow: ActionWorkflow) => Promise; /** Injectable core operations. Test seam; defaults to the real exports. */ coreDeps?: Partial; /** * Spawn the run-local bootstrap child. Undocumented for prod — a test seam * only (defaults to `node`ing the compiled bootstrap), so a test can stream a * scripted trace without spawning a real process, mirroring `fetchImpl` in * runs.ts and the `spawnProcess` seam in task-manager.ts. */ runLocalSpawn?: RunLocalSpawnFn; /** * Secret values authored in Studio before the agent was linked * (core/pending-secrets.ts). The run-local route merges the requested * project's values into its child's environment, and the deploy route pushes * them to the vault once a definition id exists. Optional: a host that does * not supply one simply has no pending secrets to apply. */ pendingSecrets?: PendingSecretsStore; /** Injectable vault client for the deploy flush. Test seam; defaults to the * real one built from the held key. */ vault?: VaultSecretsClient; } /** * Create the actions router. Mounts: * - `POST /api/workflows/:id/deploy` — NDJSON build-status stream. * - `POST /api/runs` — `{ executionId }` for a started prod execution. * - `POST /api/runs/local` — NDJSON local stub-run trace + summary. * * Deploy and prod-run run server-side with the held API key; run-local needs no * key and replaces ctx.sapiom calls with stubs, while ordinary author-code side * effects remain real. None of them ever involve an AI coding agent. */ export declare function createActionsRouter(opts: ActionsRouterOpts): Router; //# sourceMappingURL=actions.d.ts.map