@agent-compose/sdk 0.5.6 → 0.5.7
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/README.md +4 -4
- package/dist/agent/agent-loop-contract.test.d.ts +1 -0
- package/dist/agent/agent-loop.d.ts +1 -1
- package/dist/client.d.ts +65 -23
- package/dist/index.d.ts +4 -2
- package/dist/index.js +335 -75
- package/dist/runtimes/claude.d.ts +9 -1
- package/dist/runtimes/openai-desktop.js +333 -75
- package/dist/sandbox-errors.d.ts +49 -0
- package/dist/sandbox.d.ts +68 -13
- package/dist/step-invocation/protocol.d.ts +6 -0
- package/dist/types/sandbox-environment.d.ts +1 -10
- package/dist/types/sandbox.d.ts +27 -3
- package/dist/types/workflow-metadata.d.ts +69 -12
- package/dist/types/workflow.d.ts +38 -10
- package/dist/utils/bundler.d.ts +38 -9
- package/dist/workflow-steps/workflow.d.ts +1 -3
- package/package.json +2 -2
- package/src/agent/agent-loop.ts +56 -7
- package/src/client.ts +144 -25
- package/src/index.ts +3 -2
- package/src/runtimes/claude.ts +66 -8
- package/src/sandbox-errors.ts +53 -0
- package/src/sandbox.ts +378 -56
- package/src/step-invocation/invoker.ts +66 -8
- package/src/step-invocation/protocol.ts +9 -0
- package/src/step-invocation/server.ts +27 -4
- package/src/types/sandbox-environment.ts +1 -11
- package/src/types/sandbox.ts +28 -3
- package/src/types/workflow-metadata.ts +77 -15
- package/src/types/workflow.ts +38 -11
- package/src/utils/bundler.ts +43 -13
- package/src/workflow-steps/workflow.ts +1 -3
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
|
|
24
24
|
import { randomBytes } from "node:crypto";
|
|
25
25
|
import { z } from "zod";
|
|
26
|
+
import { SandboxUnavailableError } from "../sandbox-errors.js";
|
|
26
27
|
import type { SandboxProvider } from "../types/sandbox.js";
|
|
27
|
-
import { RUNNER_COMMAND, STEP_ENV, stepResultLinePrefix, stepPauseLinePrefix, requestContextPath, stepInputPath } from "./protocol.js";
|
|
28
|
+
import { RUNNER_COMMAND, STEP_ENV, stepResultLinePrefix, stepPauseLinePrefix, requestContextPath, stepInputPath, stepResultFilePath } from "./protocol.js";
|
|
28
29
|
import { StepPauseRequestSchema } from "./types.js";
|
|
29
30
|
import type { StepRequest, StepResult } from "./types.js";
|
|
30
31
|
import type { StepObservability } from "../workflow-steps/observability.js";
|
|
@@ -113,6 +114,20 @@ export function parseStepResult<TOutput = unknown>(
|
|
|
113
114
|
if (pauseLine && resultLine) break;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// Fallback: a concurrent writer whose chunk ended WITHOUT a newline can
|
|
118
|
+
// glue noise onto the front of the sentinel ("…noise__AC_STEP_…"),
|
|
119
|
+
// defeating the line-anchored scan. The per-invocation token is
|
|
120
|
+
// unguessable, so matching the prefix mid-line is equally sound —
|
|
121
|
+
// extract from the prefix to that line's end.
|
|
122
|
+
const midLine = (prefix: string): string | undefined => {
|
|
123
|
+
const at = stdout.indexOf(prefix);
|
|
124
|
+
if (at === -1) return undefined;
|
|
125
|
+
const end = stdout.indexOf("\n", at);
|
|
126
|
+
return stdout.slice(at, end === -1 ? undefined : end);
|
|
127
|
+
};
|
|
128
|
+
if (!pauseLine) pauseLine = midLine(pausePrefix);
|
|
129
|
+
if (!resultLine) resultLine = midLine(resultPrefix);
|
|
130
|
+
|
|
116
131
|
if (pauseLine) {
|
|
117
132
|
try {
|
|
118
133
|
const raw = JSON.parse(pauseLine.slice(pausePrefix.length));
|
|
@@ -244,18 +259,54 @@ export async function invokeStep<TOutput = unknown>(
|
|
|
244
259
|
const stdoutSplitter = makeLineSplitter(opts?.onStdout, true);
|
|
245
260
|
const stderrSplitter = makeLineSplitter(opts?.onStderr, false);
|
|
246
261
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
262
|
+
let result: { stdout: string; stderr: string; exitCode: number };
|
|
263
|
+
try {
|
|
264
|
+
result = await sandbox.commands.run(RUNNER_COMMAND, {
|
|
265
|
+
envs,
|
|
266
|
+
timeoutMs: 0,
|
|
267
|
+
...(stdoutSplitter.onChunk ? { onStdout: stdoutSplitter.onChunk } : {}),
|
|
268
|
+
...(stderrSplitter.onChunk ? { onStderr: stderrSplitter.onChunk } : {}),
|
|
269
|
+
});
|
|
270
|
+
} catch (e) {
|
|
271
|
+
// Typed sandbox-infrastructure failure: the engine's recovery contract
|
|
272
|
+
// (re-provision when retryable, honest terminal classification otherwise)
|
|
273
|
+
// keys on this error propagating INTACT — its `[sandbox-unavailable:*]`
|
|
274
|
+
// message prefix must reach the workflow as the failure leaf, not ride an
|
|
275
|
+
// embedded stderr tail that a later slice(-2000) can drop. Rethrow before
|
|
276
|
+
// the CommandExitError downgrade below.
|
|
277
|
+
if (e instanceof SandboxUnavailableError) throw e;
|
|
278
|
+
// Some providers (E2B) throw a CommandExitError on a non-zero exit instead of
|
|
279
|
+
// returning it. Recover stdout/stderr/exitCode from the error so we can STILL
|
|
280
|
+
// parse the runner's structured `__AC_STEP_RESULT__` payload — otherwise the
|
|
281
|
+
// real failure reason is lost behind a generic "exit status 1".
|
|
282
|
+
const ce = e as { stdout?: string; stderr?: string; exitCode?: number; result?: { stdout?: string; stderr?: string; exitCode?: number } };
|
|
283
|
+
result = {
|
|
284
|
+
stdout: ce.stdout ?? ce.result?.stdout ?? "",
|
|
285
|
+
stderr: ce.stderr ?? ce.result?.stderr ?? (e instanceof Error ? e.message : String(e)),
|
|
286
|
+
exitCode: ce.exitCode ?? ce.result?.exitCode ?? 1,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
253
289
|
stdoutSplitter.flush();
|
|
254
290
|
stderrSplitter.flush();
|
|
255
291
|
|
|
256
292
|
const parsed = parseStepResult<TOutput>(result.stdout, resultToken);
|
|
257
293
|
if (parsed) return parsed;
|
|
258
294
|
|
|
295
|
+
// No sentinel on stdout. The runner also persists the sentinel line to a
|
|
296
|
+
// token-keyed file before emitting — providers drop the tail of heavy
|
|
297
|
+
// stdout streams, so read the durable copy back. Tried before exit-code
|
|
298
|
+
// classification: a runner that exited 1 after a user-step throw still
|
|
299
|
+
// wrote the file, and its `{ok:false, kind:"user-step"}` beats a generic
|
|
300
|
+
// runner-exit error. A runner killed before emitting wrote no file and
|
|
301
|
+
// falls through.
|
|
302
|
+
const fileRead = await sandbox.commands.run(
|
|
303
|
+
`cat ${stepResultFilePath(resultToken)} 2>/dev/null || true`,
|
|
304
|
+
).catch(() => null);
|
|
305
|
+
if (fileRead?.stdout) {
|
|
306
|
+
const fromFile = parseStepResult<TOutput>(fileRead.stdout, resultToken);
|
|
307
|
+
if (fromFile) return fromFile;
|
|
308
|
+
}
|
|
309
|
+
|
|
259
310
|
// No tokenised sentinel on stdout. Distinguish "runner exited badly
|
|
260
311
|
// before emitting" (runner-exit) from "runner exited cleanly but didn't
|
|
261
312
|
// speak the protocol" (protocol) — the exit code is the evidence.
|
|
@@ -275,8 +326,15 @@ export async function invokeStep<TOutput = unknown>(
|
|
|
275
326
|
},
|
|
276
327
|
};
|
|
277
328
|
}
|
|
329
|
+
// Carry the stdout tail: "no tokenised step result" alone is useless to
|
|
330
|
+
// an operator — the tail usually shows whether the runner finished its
|
|
331
|
+
// work (output truncated by the provider) or never got there.
|
|
332
|
+
const tail = result.stdout.trim().slice(-500);
|
|
278
333
|
return {
|
|
279
334
|
ok: false,
|
|
280
|
-
error: {
|
|
335
|
+
error: {
|
|
336
|
+
kind: "protocol",
|
|
337
|
+
message: `no tokenised step result on stdout or in the result file (runner exited 0 without emitting)${tail ? `\nstdout tail:\n${tail}` : ""}`,
|
|
338
|
+
},
|
|
281
339
|
};
|
|
282
340
|
}
|
|
@@ -71,3 +71,12 @@ export function stepInputPath(stepIndex: number): string {
|
|
|
71
71
|
export function requestContextPath(stepIndex: number): string {
|
|
72
72
|
return `/tmp/wf/request-context-${stepIndex}.json`;
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/** Sandbox-side path where the runner persists the sentinel line as a
|
|
76
|
+
* FILE, keyed by the per-invocation token (unique even across resumes
|
|
77
|
+
* of the same step index). stdout is the fast path, but providers can
|
|
78
|
+
* drop the tail of a heavy stdout stream — the invoker falls back to
|
|
79
|
+
* reading this file when no sentinel is found on stdout. */
|
|
80
|
+
export function stepResultFilePath(token: string): string {
|
|
81
|
+
return `/tmp/wf/step-result-${token}.json`;
|
|
82
|
+
}
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
* trust nothing runs after.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { readFileSync } from "node:fs";
|
|
24
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
25
|
import type { RequestContextWire } from "../request-context/request-context.js";
|
|
26
26
|
import { RequestContextWireSchema } from "../request-context/request-context.js";
|
|
27
27
|
import type { StepObservability } from "../workflow-steps/observability.js";
|
|
28
|
-
import { STEP_ENV, STEP_PAUSE_PREFIX, STEP_RESULT_PREFIX } from "./protocol.js";
|
|
28
|
+
import { STEP_ENV, STEP_PAUSE_PREFIX, STEP_RESULT_PREFIX, stepResultFilePath } from "./protocol.js";
|
|
29
29
|
import { PauseSignal, isPauseSignal } from "../pause/pause-core.js";
|
|
30
30
|
|
|
31
31
|
/** What the handler receives. The serveStep already parsed the input
|
|
@@ -76,8 +76,28 @@ function writeSentinelLine(line: string): Promise<void> {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** Persist the sentinel line as a token-keyed FILE before the stdout
|
|
80
|
+
* emit. stdout is the fast path, but sandbox providers can drop the
|
|
81
|
+
* tail of a heavy stdout stream — the invoker reads this file back as
|
|
82
|
+
* the durable fallback. Best-effort: a write failure must never mask
|
|
83
|
+
* the stdout emit. */
|
|
84
|
+
function persistSentinelFile(token: string, line: string): void {
|
|
85
|
+
try {
|
|
86
|
+
mkdirSync("/tmp/wf", { recursive: true });
|
|
87
|
+
writeFileSync(stepResultFilePath(token), line);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
process.stderr.write(`[step-server] result-file persist failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
function emitResult(token: string, payload: SentinelPayload): Promise<void> {
|
|
80
|
-
|
|
94
|
+
const line = `${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}\n`;
|
|
95
|
+
persistSentinelFile(token, line);
|
|
96
|
+
// Leading \n: stdout is shared with user/agent output, and a prior write
|
|
97
|
+
// that ended WITHOUT a trailing newline would otherwise glue onto this
|
|
98
|
+
// line — the invoker's line-anchored scan then misses the sentinel
|
|
99
|
+
// ("no tokenised step result" on heavy-output runs). A blank line is free.
|
|
100
|
+
return writeSentinelLine(`\n${line}`);
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
/** Emit the tokenised pause sentinel — `__AC_STEP_PAUSE__<token>:{pauseId,
|
|
@@ -91,7 +111,10 @@ function emitPause(token: string, signal: PauseSignal): Promise<void> {
|
|
|
91
111
|
pauseRequest: signal.pauseRequest,
|
|
92
112
|
...(signal.observability ? { observability: signal.observability } : {}),
|
|
93
113
|
});
|
|
94
|
-
|
|
114
|
+
const line = `${STEP_PAUSE_PREFIX}${token}:${body}\n`;
|
|
115
|
+
persistSentinelFile(token, line);
|
|
116
|
+
// Leading \n for the same partial-line immunity as emitResult.
|
|
117
|
+
return writeSentinelLine(`\n${line}`);
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
/** Delete step-invocation transport envs so the user's workflow code
|
|
@@ -48,20 +48,11 @@ export interface SandboxEnvironmentDefinition {
|
|
|
48
48
|
* useful — an env with no snapshot can't be referenced as a
|
|
49
49
|
* `bootFrom` on another workflow). */
|
|
50
50
|
snapshots?: SnapshotConfig;
|
|
51
|
-
/** Override the sugar's `memory: false` default. Setup workflows
|
|
52
|
-
* don't typically benefit from memory extraction; opt in explicitly
|
|
53
|
-
* when they do. */
|
|
54
|
-
memory?: boolean;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
/** Sugar over `defineWorkflow` for setup-only workflows that exist to
|
|
58
54
|
* capture a snapshot. The workflow takes no meaningful input and returns
|
|
59
|
-
* nothing — its value is the side effect on the sandbox VM.
|
|
60
|
-
*
|
|
61
|
-
* Defaults `memory: false` because sandbox environments emit setup
|
|
62
|
-
* output (npm installs, command exit codes) rather than agent traces
|
|
63
|
-
* worth memorising. Authors can opt in explicitly via `env.memory:
|
|
64
|
-
* true` if their environment somehow does want extraction. */
|
|
55
|
+
* nothing — its value is the side effect on the sandbox VM. */
|
|
65
56
|
export function defineSandboxEnvironment(
|
|
66
57
|
env: SandboxEnvironmentDefinition,
|
|
67
58
|
): Workflow<Record<string, unknown>, void> {
|
|
@@ -71,7 +62,6 @@ export function defineSandboxEnvironment(
|
|
|
71
62
|
return defineWorkflow<void, Record<string, unknown>>({
|
|
72
63
|
...(env.description !== undefined ? { description: env.description } : {}),
|
|
73
64
|
snapshots: env.snapshots ?? { saveLatest: true },
|
|
74
|
-
memory: env.memory ?? false,
|
|
75
65
|
run: async (_ctx, sandbox) => env.setup(sandbox),
|
|
76
66
|
});
|
|
77
67
|
}
|
package/src/types/sandbox.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Provider-agnostic: E2B, Vercel, Docker, or any other backend implements this.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
7
|
+
|
|
6
8
|
/** Base compute interface — pure I/O, no filesystem path or git concerns. */
|
|
7
9
|
export interface SandboxCommandRunOptions {
|
|
8
10
|
cwd?: string;
|
|
@@ -23,6 +25,22 @@ export interface SandboxCommandResult {
|
|
|
23
25
|
stderr: string;
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A sandbox provider implements the RAW provider operations only. It does NOT
|
|
30
|
+
* implement transient-failure retry/backoff: reconnecting and snapshotting both
|
|
31
|
+
* pause/resume/transition the sandbox and race with it being reclaimed/stopped
|
|
32
|
+
* under load (E2B "Paused sandbox not found", Vercel stopping-sandbox calls,
|
|
33
|
+
* 5xx/429, dropped connections). That resilience is supplied centrally —
|
|
34
|
+
* `reconnectSandbox` runs every provider's `reconnect` through
|
|
35
|
+
* `withSandboxRetry` (generic p-retry backoff, gated by
|
|
36
|
+
* `isTransientSandboxError`), and `createSandbox`/`reconnectSandbox` wrap the
|
|
37
|
+
* returned provider's `snapshot` the same way (via `withSnapshotRetry`).
|
|
38
|
+
* `create` is deliberately NOT retried: a create whose response is lost would
|
|
39
|
+
* mint a second billed sandbox and orphan the first; provision failures are
|
|
40
|
+
* re-attempted at the workflow layer instead. So: a NEW provider only writes
|
|
41
|
+
* the happy-path call; the abstraction guarantees the robust pattern, and no
|
|
42
|
+
* provider re-implements it.
|
|
43
|
+
*/
|
|
26
44
|
export interface SandboxProvider {
|
|
27
45
|
sandboxId: string;
|
|
28
46
|
/** Working directory for the agent process. Set by onStart after environment setup. */
|
|
@@ -38,15 +56,22 @@ export interface SandboxProvider {
|
|
|
38
56
|
write(path: string, content: string): Promise<void>;
|
|
39
57
|
};
|
|
40
58
|
kill(): Promise<void>;
|
|
41
|
-
/** Capture the running sandbox's state as a reusable snapshot. Vercel
|
|
42
|
-
*
|
|
43
|
-
*
|
|
59
|
+
/** Capture the running sandbox's state as a reusable snapshot. Vercel and E2B
|
|
60
|
+
* both support it natively (E2B via `createSnapshot()`); providers without a
|
|
61
|
+
* live-snapshot primitive return `undefined`. Used by the server's
|
|
44
62
|
* `--build` flow to stamp the snapshot id on the workflow row.
|
|
45
63
|
*
|
|
46
64
|
* `sizeBytes` is the on-disk footprint reported by the provider. May
|
|
47
65
|
* be omitted when the provider doesn't expose it; the server stores
|
|
48
66
|
* `null` for missing values rather than estimating. */
|
|
49
67
|
snapshot?(): Promise<{ snapshotId: string; sizeBytes?: number }>;
|
|
68
|
+
/** Replace the live sandbox's egress policy in place. Vercel implements
|
|
69
|
+
* it via `sandbox.update({ networkPolicy })` (2.x) so the server can
|
|
70
|
+
* push a freshly resolved policy — with re-minted connector access
|
|
71
|
+
* tokens — before each step instead of relying on the policy baked at
|
|
72
|
+
* create. Providers whose enforcement lives inside the VM (E2B
|
|
73
|
+
* iron-proxy) leave it undefined. */
|
|
74
|
+
updateNetworkPolicy?(policy: SandboxNetworkPolicy): Promise<void>;
|
|
50
75
|
}
|
|
51
76
|
|
|
52
77
|
/** Stateless provider-level snapshot deletion — no live sandbox needed,
|
|
@@ -20,11 +20,6 @@ import type { Processor } from "../processors/processor.js";
|
|
|
20
20
|
* The bundler reads these from the default export at registration time
|
|
21
21
|
* and forwards them to the server's POST /api/v1/templates payload.
|
|
22
22
|
*/
|
|
23
|
-
/** Whether the built-in Workflow Memory extractor should run after this
|
|
24
|
-
* workflow completes. Boolean toggle — custom post-run workflows live
|
|
25
|
-
* in the separate `postRunHooks` array on `WorkflowMetadata`. */
|
|
26
|
-
export type WorkflowMemoryConfig = boolean;
|
|
27
|
-
|
|
28
23
|
/** Boot from a specific captured snapshot, addressed by its id. Operators
|
|
29
24
|
* pick one from the dashboard snapshot list (or `agentc snapshot list`) and
|
|
30
25
|
* paste the id here. */
|
|
@@ -82,6 +77,70 @@ export interface IOSchema {
|
|
|
82
77
|
* output-only release. New code should prefer `IOSchema`. */
|
|
83
78
|
export type OutputSchema = IOSchema;
|
|
84
79
|
|
|
80
|
+
/** Per-connector HTTP request matcher (Tier-2 capability narrowing). The
|
|
81
|
+
* iron-proxy / firewall consults these when deciding whether to attach the
|
|
82
|
+
* brokered Authorization header to an outbound request. A request to the
|
|
83
|
+
* connector host whose method is not in `methods`, or whose path matches no
|
|
84
|
+
* entry in `pathPrefixes`, is refused (403) and the token is WITHHELD. */
|
|
85
|
+
export interface ConnectorRequestRules {
|
|
86
|
+
/** Allowed HTTP methods (upper-case). Omit = any method (subject to
|
|
87
|
+
* `access`). */
|
|
88
|
+
methods?: string[];
|
|
89
|
+
/** Allowed path prefixes (matched against the request path, case-
|
|
90
|
+
* sensitive). Omit = any path. */
|
|
91
|
+
pathPrefixes?: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** One provider entry in a workflow's `connectors` declaration (ADR-0007).
|
|
95
|
+
* Scopes are the provider-side OAuth scopes the workflow's API calls
|
|
96
|
+
* need; registration warns when no installed grant covers them. */
|
|
97
|
+
export interface ConnectorRequirement {
|
|
98
|
+
scopes?: string[];
|
|
99
|
+
/** Coarse capability bound on the brokered token. `"read"` defaults the
|
|
100
|
+
* allowed methods to GET/HEAD/OPTIONS (unless `request.methods` overrides)
|
|
101
|
+
* and, for the GitHub App provider, narrows the minted installation token
|
|
102
|
+
* to `contents: read`. `"write"` permits all methods. Omit = `"write"`
|
|
103
|
+
* (unchanged broad behaviour). */
|
|
104
|
+
access?: "read" | "write";
|
|
105
|
+
/** Fine-grained per-request matcher enforced at the egress proxy. */
|
|
106
|
+
request?: ConnectorRequestRules;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Map of provider key (`github`, `slack`, …) → requirement. Declaring a
|
|
110
|
+
* provider here makes the server resolve an authorized grant at dispatch
|
|
111
|
+
* and inject a fresh access token at the network layer — workflow code
|
|
112
|
+
* calls the provider API with plain fetch/SDKs and never sees the token. */
|
|
113
|
+
export type ConnectorRequirements = Record<string, ConnectorRequirement>;
|
|
114
|
+
|
|
115
|
+
/** Marks this workflow as a catalogue OPERATION of a connector — e.g. the
|
|
116
|
+
* `create-issue` operation of the `github` connector. Operations are
|
|
117
|
+
* ordinary workflows (deterministic or agent-driven) that declare the
|
|
118
|
+
* matching `connectors` requirement; the tag is what groups them under
|
|
119
|
+
* the connector in the dashboard catalogue and the agent-facing
|
|
120
|
+
* operation listing. Together with `description` + `input`/`output`
|
|
121
|
+
* schemas this gives every operation MCP-tool-like self-description:
|
|
122
|
+
* what it does, what it takes, what it produces. */
|
|
123
|
+
export interface ConnectorOperationTag {
|
|
124
|
+
provider: string;
|
|
125
|
+
operation: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Tier-1 invoke ACL. When a workflow brokers a connector credential AND
|
|
129
|
+
* declares an `invokePolicy`, dispatch evaluates the calling principal
|
|
130
|
+
* against it BEFORE binding any grant. A caller matching ANY provided
|
|
131
|
+
* clause passes; matching none → HTTP 403. Omit = whole team (unchanged).
|
|
132
|
+
*
|
|
133
|
+
* Each clause is OR-combined:
|
|
134
|
+
* - `users`: `"owner"` (only the registering user) or an array of user
|
|
135
|
+
* ids / emails the caller must be one of.
|
|
136
|
+
* - `apiKeys`: api-key ids permitted to invoke.
|
|
137
|
+
* - `workflows`: parent workflow names permitted to child-invoke this one. */
|
|
138
|
+
export interface InvokePolicy {
|
|
139
|
+
users?: "owner" | string[];
|
|
140
|
+
apiKeys?: string[];
|
|
141
|
+
workflows?: string[];
|
|
142
|
+
}
|
|
143
|
+
|
|
85
144
|
export interface WorkflowMetadata {
|
|
86
145
|
/** One-line, human-readable description of what the workflow does.
|
|
87
146
|
* Surfaced on the dashboard template tile + run page header. Authors
|
|
@@ -103,14 +162,16 @@ export interface WorkflowMetadata {
|
|
|
103
162
|
/** All snapshot config — boot source + capture mode. */
|
|
104
163
|
snapshots?: SnapshotConfig;
|
|
105
164
|
processors?: readonly Processor[];
|
|
106
|
-
/**
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
165
|
+
/** Connector requirements — providers whose APIs this workflow calls.
|
|
166
|
+
* Dispatch resolves an authorized grant per provider and injects a
|
|
167
|
+
* fresh access token at the network layer (ADR-0007). */
|
|
168
|
+
connectors?: ConnectorRequirements;
|
|
169
|
+
/** Catalogue tag marking this workflow as an operation of a connector.
|
|
170
|
+
* See `ConnectorOperationTag`. */
|
|
171
|
+
connectorOperation?: ConnectorOperationTag;
|
|
172
|
+
/** Tier-1 invoke ACL — who may dispatch this connector-brokering workflow.
|
|
173
|
+
* See `InvokePolicy`. */
|
|
174
|
+
invokePolicy?: InvokePolicy;
|
|
114
175
|
}
|
|
115
176
|
|
|
116
177
|
/**
|
|
@@ -138,8 +199,9 @@ export function extractMetadata(source: Partial<WorkflowMetadata>): WorkflowMeta
|
|
|
138
199
|
if (source.outputSchema !== undefined) out.outputSchema = freezeMetadataValue(source.outputSchema);
|
|
139
200
|
if (source.snapshots !== undefined) out.snapshots = Object.freeze({ ...source.snapshots });
|
|
140
201
|
if (source.processors !== undefined) out.processors = Object.freeze([...source.processors]);
|
|
141
|
-
if (source.
|
|
142
|
-
if (source.
|
|
202
|
+
if (source.connectors !== undefined) out.connectors = freezeMetadataValue(source.connectors);
|
|
203
|
+
if (source.connectorOperation !== undefined) out.connectorOperation = Object.freeze({ ...source.connectorOperation });
|
|
204
|
+
if (source.invokePolicy !== undefined) out.invokePolicy = freezeMetadataValue(source.invokePolicy);
|
|
143
205
|
return Object.freeze(out);
|
|
144
206
|
}
|
|
145
207
|
|
package/src/types/workflow.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type { StepWorkflowDefinition } from "../workflow-steps/workflow.js";
|
|
|
24
24
|
import type { Step, Workflow } from "../workflow-steps/types.js";
|
|
25
25
|
import { WORKFLOW_BRAND } from "../workflow-steps/types.js";
|
|
26
26
|
import type { BaseExecutionContext, WorkflowRun } from "./execution-context.js";
|
|
27
|
-
import { extractMetadata, type WorkflowMetadata } from "./workflow-metadata.js";
|
|
27
|
+
import { extractMetadata, type WorkflowMetadata, type ConnectorRequirements, type ConnectorOperationTag, type InvokePolicy } from "./workflow-metadata.js";
|
|
28
28
|
|
|
29
29
|
export type { WorkflowRun, InvokeChild, BaseExecutionContext } from "./execution-context.js";
|
|
30
30
|
// Re-export so consumers can keep importing `WorkflowMetadata` from
|
|
@@ -37,8 +37,8 @@ export interface AgentEventSink {
|
|
|
37
37
|
emit(event: AgentLifecycleEvent): void | Promise<void>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
import type {
|
|
41
|
-
export type {
|
|
40
|
+
import type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema } from "./workflow-metadata.js";
|
|
41
|
+
export type { SnapshotConfig, BootSnapshot, ReuseSnapshot, IOSchema, OutputSchema };
|
|
42
42
|
|
|
43
43
|
/** Turn/iteration budget for `agent(opts)`. Re-exported here so authors
|
|
44
44
|
* can type per-invoke budget overrides they pass as workflow input. */
|
|
@@ -156,14 +156,41 @@ export interface WorkflowDefinition<
|
|
|
156
156
|
* }
|
|
157
157
|
*/
|
|
158
158
|
placeholders?: Record<string, string>;
|
|
159
|
-
/**
|
|
160
|
-
*
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Connector requirements (ADR-0007). Declaring a provider makes the
|
|
161
|
+
* server resolve an authorized OAuth grant for the calling principal at
|
|
162
|
+
* dispatch time and inject a fresh access token at the network layer —
|
|
163
|
+
* workflow code talks to the provider API with plain fetch/SDKs and
|
|
164
|
+
* never holds the credential.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* connectors: { github: { scopes: ["repo"] } }
|
|
168
|
+
*/
|
|
169
|
+
connectors?: ConnectorRequirements;
|
|
170
|
+
/**
|
|
171
|
+
* Marks this workflow as a catalogue OPERATION of a connector — e.g.
|
|
172
|
+
* the `create-issue` operation of the `github` connector. Pair with
|
|
173
|
+
* `description` + `input`/`output` schemas so the operation is fully
|
|
174
|
+
* self-describing (MCP-tool-like) to humans and agents browsing the
|
|
175
|
+
* connector catalogue.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* connectorOperation: { provider: "github", operation: "create-issue" }
|
|
179
|
+
*/
|
|
180
|
+
connectorOperation?: ConnectorOperationTag;
|
|
181
|
+
/**
|
|
182
|
+
* Tier-1 invoke ACL (connector credential boundary). When this workflow
|
|
183
|
+
* declares `connectors` (it brokers a credential) AND an `invokePolicy`,
|
|
184
|
+
* the server evaluates the calling principal against the policy BEFORE
|
|
185
|
+
* binding any grant — a caller matching no clause is refused with HTTP
|
|
186
|
+
* 403. Has no effect on workflows that declare no connectors. Omit to
|
|
187
|
+
* leave the workflow invokable by the whole team.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* connectors: { github: { access: "read" } },
|
|
191
|
+
* invokePolicy: { users: "owner", workflows: ["nightly-orchestrator"] }
|
|
192
|
+
*/
|
|
193
|
+
invokePolicy?: InvokePolicy;
|
|
167
194
|
}
|
|
168
195
|
|
|
169
196
|
/**
|
package/src/utils/bundler.ts
CHANGED
|
@@ -24,7 +24,8 @@ import { createHash } from "node:crypto";
|
|
|
24
24
|
import { parse as babelParse } from "@babel/parser";
|
|
25
25
|
import type { File, ExportDefaultDeclaration, CallExpression, Expression, Statement } from "@babel/types";
|
|
26
26
|
import { importSourceModule } from "./source-loader.js";
|
|
27
|
-
import type {
|
|
27
|
+
import type { SnapshotConfig } from "../types/workflow.js";
|
|
28
|
+
import type { IOSchema, OutputSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy } from "../types/workflow-metadata.js";
|
|
28
29
|
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
29
30
|
import { isWorkflow } from "../workflow-steps/workflow.js";
|
|
30
31
|
import type { Workflow } from "../workflow-steps/types.js";
|
|
@@ -33,8 +34,33 @@ import { workflowPlan, type WorkflowPlan } from "../types/workflow-plan.js";
|
|
|
33
34
|
/** Bumped when the manifest contract changes in a way the server should
|
|
34
35
|
* notice. The server pins its manifest schema to this exact value (no
|
|
35
36
|
* `>=` accepted) so a manifest forged with a future version is refused
|
|
36
|
-
* by zod before any other validation runs.
|
|
37
|
-
|
|
37
|
+
* by zod before any other validation runs. NOTE: this gates NEW
|
|
38
|
+
* registrations only — nothing re-validates stored `workflows.metadata`
|
|
39
|
+
* rows, so a non-additive metadata change ALSO needs a server-side
|
|
40
|
+
* warning for already-registered workflows (see `warnRetiredMetadataKeys`
|
|
41
|
+
* in server/src/engine/dispatch.ts).
|
|
42
|
+
*
|
|
43
|
+
* v2: `memory` / `postRunHooks` registration options removed — bundles
|
|
44
|
+
* from CLIs that still emit them are refused at register.
|
|
45
|
+
*
|
|
46
|
+
* TODO(registration-versioning): turn this constant into a real migration
|
|
47
|
+
* policy so breaking changes start with a number, not an investigation:
|
|
48
|
+
* 1. Server keeps a MIN_SUPPORTED_BUNDLER_VERSION alongside the exact
|
|
49
|
+
* pin — versions in [min, current] keep dispatching; below min gets
|
|
50
|
+
* an ACTIONABLE 409 naming the agentc version to re-register with
|
|
51
|
+
* (today, pre-enforcement templates with no manifest at all 409 with
|
|
52
|
+
* no migration path — see dispatch.ts).
|
|
53
|
+
* 2. Stamp the SDK version into the manifest at registration, so
|
|
54
|
+
* runtime-compat breaks (not just bundler-format breaks) are also
|
|
55
|
+
* queryable per stored workflow.
|
|
56
|
+
* 3. Operator audit = one query: workflows whose
|
|
57
|
+
* metadata.manifest.bundlerVersion < current (or missing), grouped
|
|
58
|
+
* by team/owner — the definitive "who needs migration" list before
|
|
59
|
+
* any breaking release.
|
|
60
|
+
* 4. Dashboard badge on stale templates: "registered with an older
|
|
61
|
+
* toolchain — re-register to update" (pairs with the template
|
|
62
|
+
* connector/metadata card work). */
|
|
63
|
+
export const BUNDLER_VERSION = 2;
|
|
38
64
|
|
|
39
65
|
/**
|
|
40
66
|
* Manifest emitted alongside the bundled source. Fully serialisable JSON;
|
|
@@ -73,16 +99,19 @@ export interface BundledWorkflow {
|
|
|
73
99
|
* restore at run start), `save`, `retain`. */
|
|
74
100
|
snapshots?: SnapshotConfig;
|
|
75
101
|
workflowPlan: WorkflowPlan;
|
|
76
|
-
/** Workflow Memory extractor config. */
|
|
77
|
-
memory?: WorkflowMemoryConfig;
|
|
78
|
-
/** Ordered post-hook workflow names declared on the workflow. */
|
|
79
|
-
postRunHooks?: readonly string[];
|
|
80
102
|
/** Compact JSON-Schema-shaped description of the workflow's input
|
|
81
103
|
* type. Extracted from the workflow's declared `input` zod schema
|
|
82
104
|
* at bundle time; undefined when the schema is `z.unknown()`. */
|
|
83
|
-
inputSchema?:
|
|
105
|
+
inputSchema?: IOSchema;
|
|
84
106
|
/** Same for the workflow's output zod schema. */
|
|
85
|
-
outputSchema?:
|
|
107
|
+
outputSchema?: IOSchema;
|
|
108
|
+
/** Connector requirements declared on the workflow (ADR-0007). */
|
|
109
|
+
connectors?: ConnectorRequirements;
|
|
110
|
+
/** Connector-catalogue operation tag, when this workflow is one. */
|
|
111
|
+
connectorOperation?: ConnectorOperationTag;
|
|
112
|
+
/** Tier-1 invoke ACL — who may dispatch this connector-brokering
|
|
113
|
+
* workflow (`defineWorkflow({ invokePolicy })`). */
|
|
114
|
+
invokePolicy?: InvokePolicy;
|
|
86
115
|
}
|
|
87
116
|
|
|
88
117
|
async function bundle(path: string, label: string): Promise<string> {
|
|
@@ -305,8 +334,9 @@ export async function bundleWorkflow(
|
|
|
305
334
|
...(inputSchema !== undefined ? { inputSchema } : {}),
|
|
306
335
|
...(outputSchema !== undefined ? { outputSchema } : {}),
|
|
307
336
|
...(metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {}),
|
|
308
|
-
...(metadata.
|
|
309
|
-
...(metadata.
|
|
337
|
+
...(metadata.connectors !== undefined ? { connectors: metadata.connectors } : {}),
|
|
338
|
+
...(metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {}),
|
|
339
|
+
...(metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {}),
|
|
310
340
|
};
|
|
311
341
|
}
|
|
312
342
|
|
|
@@ -322,7 +352,7 @@ export async function bundleWorkflow(
|
|
|
322
352
|
* no meaningful contract to render). */
|
|
323
353
|
function extractIOSchema(
|
|
324
354
|
zodSchema: { _zod?: unknown } | unknown,
|
|
325
|
-
):
|
|
355
|
+
): IOSchema | undefined {
|
|
326
356
|
// Late require to keep this module tree-shakeable for callers that
|
|
327
357
|
// don't bundle. `toJSONSchema` lives on the top-level Zod export in v4.
|
|
328
358
|
let json: Record<string, unknown> | undefined;
|
|
@@ -340,7 +370,7 @@ function extractIOSchema(
|
|
|
340
370
|
// — there's nothing useful to show.
|
|
341
371
|
if (Object.keys(json).filter((k) => k !== "$schema").length === 0) return undefined;
|
|
342
372
|
|
|
343
|
-
const out:
|
|
373
|
+
const out: OutputSchema = {};
|
|
344
374
|
if (json.type !== undefined) out.type = json.type as string | string[];
|
|
345
375
|
if (json.description !== undefined) out.description = String(json.description);
|
|
346
376
|
if (json.properties && typeof json.properties === "object") {
|
|
@@ -23,7 +23,7 @@ import type { z } from "zod";
|
|
|
23
23
|
import type { Step, Workflow } from "./types.js";
|
|
24
24
|
import { WORKFLOW_BRAND } from "./types.js";
|
|
25
25
|
import { extractMetadata } from "../types/workflow-metadata.js";
|
|
26
|
-
import type { SnapshotConfig
|
|
26
|
+
import type { SnapshotConfig } from "../types/workflow-metadata.js";
|
|
27
27
|
import type { SandboxNetworkPolicy } from "../sandbox.js";
|
|
28
28
|
import type { Processor } from "../processors/processor.js";
|
|
29
29
|
|
|
@@ -51,8 +51,6 @@ export interface StepWorkflowDefinition<TInput, TOutput> {
|
|
|
51
51
|
networkPolicy?: SandboxNetworkPolicy;
|
|
52
52
|
placeholders?: Record<string, string>;
|
|
53
53
|
snapshots?: SnapshotConfig;
|
|
54
|
-
memory?: WorkflowMemoryConfig;
|
|
55
|
-
postRunHooks?: readonly string[];
|
|
56
54
|
processors?: readonly Processor[];
|
|
57
55
|
}
|
|
58
56
|
|