@agent-compose/sdk 0.5.5 → 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.
Files changed (40) hide show
  1. package/README.md +4 -4
  2. package/dist/agent/agent-loop-contract.test.d.ts +1 -0
  3. package/dist/agent/agent-loop.d.ts +1 -1
  4. package/dist/client.d.ts +65 -23
  5. package/dist/index.d.ts +4 -2
  6. package/dist/index.js +389 -109
  7. package/dist/runtimes/_cli-agent.d.ts +25 -8
  8. package/dist/runtimes/amp.d.ts +7 -6
  9. package/dist/runtimes/claude.d.ts +9 -1
  10. package/dist/runtimes/cli-agent.test.d.ts +9 -0
  11. package/dist/runtimes/codex.d.ts +6 -5
  12. package/dist/runtimes/openai-desktop.js +387 -109
  13. package/dist/sandbox-errors.d.ts +49 -0
  14. package/dist/sandbox.d.ts +68 -13
  15. package/dist/step-invocation/protocol.d.ts +6 -0
  16. package/dist/types/sandbox-environment.d.ts +1 -10
  17. package/dist/types/sandbox.d.ts +27 -3
  18. package/dist/types/workflow-metadata.d.ts +88 -23
  19. package/dist/types/workflow.d.ts +38 -10
  20. package/dist/utils/bundler.d.ts +38 -9
  21. package/dist/workflow-steps/workflow.d.ts +1 -3
  22. package/package.json +2 -2
  23. package/src/agent/agent-loop.ts +56 -7
  24. package/src/client.ts +144 -25
  25. package/src/index.ts +4 -2
  26. package/src/runtimes/_cli-agent.ts +75 -42
  27. package/src/runtimes/amp.ts +11 -6
  28. package/src/runtimes/claude.ts +66 -8
  29. package/src/runtimes/codex.ts +10 -5
  30. package/src/sandbox-errors.ts +53 -0
  31. package/src/sandbox.ts +378 -56
  32. package/src/step-invocation/invoker.ts +66 -8
  33. package/src/step-invocation/protocol.ts +9 -0
  34. package/src/step-invocation/server.ts +27 -4
  35. package/src/types/sandbox-environment.ts +1 -11
  36. package/src/types/sandbox.ts +28 -3
  37. package/src/types/workflow-metadata.ts +97 -26
  38. package/src/types/workflow.ts +38 -11
  39. package/src/utils/bundler.ts +43 -13
  40. 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
- const result = await sandbox.commands.run(RUNNER_COMMAND, {
248
- envs,
249
- timeoutMs: 0,
250
- ...(stdoutSplitter.onChunk ? { onStdout: stdoutSplitter.onChunk } : {}),
251
- ...(stderrSplitter.onChunk ? { onStderr: stderrSplitter.onChunk } : {}),
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: { kind: "protocol", message: "no tokenised step result found on stdout" },
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
- return writeSentinelLine(`${STEP_RESULT_PREFIX}${token}:${JSON.stringify(payload)}\n`);
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
- return writeSentinelLine(`${STEP_PAUSE_PREFIX}${token}:${body}\n`);
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
  }
@@ -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
- * supports it natively; E2B's model is Dockerfile-based and doesn't map
43
- * cleanly — `undefined` on providers that don't. Used by the server's
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,28 +20,32 @@ 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
- /** Where a run boots from. The snapshot id is the unit of identity —
29
- * each captured snapshot already records the workflow + version it
30
- * came from on the snapshot row, so there's no separate "latest of
31
- * workflow X" resolution at dispatch time. Operators pick a snapshot
32
- * from the dashboard snapshot list (or `agentc snapshot list`) and
33
- * paste the id here.
34
- *
35
- * Omit `bootFrom` entirely to boot a fresh base sandbox. */
23
+ /** Boot from a specific captured snapshot, addressed by its id. Operators
24
+ * pick one from the dashboard snapshot list (or `agentc snapshot list`) and
25
+ * paste the id here. */
36
26
  export type BootSnapshot = { snapshotId: string };
37
27
 
28
+ /** Boot from this workflow's OWN most recent snapshot, scoped to its content
29
+ * hash. The first run — and the first after a re-register changes the source
30
+ * — finds none and boots a fresh base sandbox; `"reuse"` also implies
31
+ * `saveLatest`, so that run captures a snapshot and every run after it boots
32
+ * from it. This lets a runtime install its tooling once (e.g. a CLI-agent
33
+ * runtime `npm i -g`'ing its CLI) and skip the install on every later run,
34
+ * with no hardcoded snapshot id to manage. Re-registering with changed
35
+ * source rolls the content hash, which transparently invalidates the cache
36
+ * and re-installs on the next run. */
37
+ export type ReuseSnapshot = "reuse";
38
+
38
39
  /** Snapshot configuration — boot source plus capture knobs. One object
39
40
  * per workflow / per invocation; collapsing boot + capture under a
40
41
  * single key reads as "all snapshot config lives here." */
41
42
  export interface SnapshotConfig {
42
- /** Where the runner restores from at run start. Structured (workflow
43
- * ref or snapshot id) so the intent is explicit at the call site. */
44
- bootFrom?: BootSnapshot;
43
+ /** Where the runner restores from at run start:
44
+ * - `{ snapshotId }` — a specific captured snapshot.
45
+ * - `"reuse"` — this workflow's own latest snapshot (content-hash scoped);
46
+ * fresh on the first run / after a re-register. Implies `saveLatest`.
47
+ * - omitted — a fresh base sandbox. */
48
+ bootFrom?: BootSnapshot | ReuseSnapshot;
45
49
  /** Capture the sandbox state on terminal success. The latest pointer
46
50
  * on `workflow_runs.vercel_snapshot_id` always tracks the most
47
51
  * recent capture; without `retainSteps`, prior captures are deleted
@@ -73,6 +77,70 @@ export interface IOSchema {
73
77
  * output-only release. New code should prefer `IOSchema`. */
74
78
  export type OutputSchema = IOSchema;
75
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
+
76
144
  export interface WorkflowMetadata {
77
145
  /** One-line, human-readable description of what the workflow does.
78
146
  * Surfaced on the dashboard template tile + run page header. Authors
@@ -94,14 +162,16 @@ export interface WorkflowMetadata {
94
162
  /** All snapshot config — boot source + capture mode. */
95
163
  snapshots?: SnapshotConfig;
96
164
  processors?: readonly Processor[];
97
- /** Run the built-in memory extractor after this workflow completes.
98
- * Opt-in; defaults to false when omitted. */
99
- memory?: boolean;
100
- /** Ordered list of workflow names that run after this workflow
101
- * completes. The runtime dispatches them in declaration order; the
102
- * built-in memory extractor (when `memory: true`) runs as a separate
103
- * hook alongside whatever's declared here. */
104
- postRunHooks?: readonly string[];
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;
105
175
  }
106
176
 
107
177
  /**
@@ -129,8 +199,9 @@ export function extractMetadata(source: Partial<WorkflowMetadata>): WorkflowMeta
129
199
  if (source.outputSchema !== undefined) out.outputSchema = freezeMetadataValue(source.outputSchema);
130
200
  if (source.snapshots !== undefined) out.snapshots = Object.freeze({ ...source.snapshots });
131
201
  if (source.processors !== undefined) out.processors = Object.freeze([...source.processors]);
132
- if (source.memory !== undefined) out.memory = source.memory;
133
- if (source.postRunHooks !== undefined) out.postRunHooks = Object.freeze([...source.postRunHooks]);
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);
134
205
  return Object.freeze(out);
135
206
  }
136
207
 
@@ -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 { WorkflowMemoryConfig, SnapshotConfig, BootSnapshot, IOSchema, OutputSchema } from "./workflow-metadata.js";
41
- export type { WorkflowMemoryConfig, SnapshotConfig, BootSnapshot, IOSchema, OutputSchema };
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
- /** Run the built-in memory extractor after this workflow completes.
160
- * Opt-in; defaults to false when omitted. */
161
- memory?: WorkflowMemoryConfig;
162
- /** Ordered list of workflow names to dispatch as post-hooks after
163
- * this workflow completes. Each hook receives the source run's
164
- * context. The memory extractor (when `memory: true`) runs as an
165
- * additional hook alongside these. */
166
- postRunHooks?: readonly string[];
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
  /**
@@ -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 { WorkflowMemoryConfig, SnapshotConfig } from "../types/workflow.js";
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
- export const BUNDLER_VERSION = 1;
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?: import("../types/workflow-metadata.js").IOSchema;
105
+ inputSchema?: IOSchema;
84
106
  /** Same for the workflow's output zod schema. */
85
- outputSchema?: import("../types/workflow-metadata.js").IOSchema;
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.memory !== undefined ? { memory: metadata.memory } : {}),
309
- ...(metadata.postRunHooks !== undefined ? { postRunHooks: metadata.postRunHooks } : {}),
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
- ): import("../types/workflow-metadata.js").IOSchema | undefined {
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: import("../types/workflow-metadata.js").OutputSchema = {};
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, WorkflowMemoryConfig } from "../types/workflow-metadata.js";
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