From fbc41f862ad19abb65506823694f2673b64fb12c Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 19 Aug 2026 23:07:43 -0700 Subject: [PATCH 1/4] Add tests for the agent-runtime workflow source package Covers the deploy-time config contract (arktype-parsed, env-delivered) and the two definition shapes its mode selects: the folded unbounded step and the per-turn onTrigger section. --- bun.lock | 19 +- packages/agent-runtime/package.json | 29 +++ packages/agent-runtime/src/config.test.ts | 98 ++++++++++ packages/agent-runtime/src/definition.test.ts | 170 ++++++++++++++++++ packages/agent-runtime/tsconfig.json | 7 + 5 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 packages/agent-runtime/package.json create mode 100644 packages/agent-runtime/src/config.test.ts create mode 100644 packages/agent-runtime/src/definition.test.ts create mode 100644 packages/agent-runtime/tsconfig.json diff --git a/bun.lock b/bun.lock index 977cc66a2..15917d83e 100644 --- a/bun.lock +++ b/bun.lock @@ -229,6 +229,21 @@ "typescript": "catalog:", }, }, + "packages/agent-runtime": { + "name": "@corbits/agent-runtime", + "version": "0.0.1", + "dependencies": { + "@intx/agent": "workspace:*", + "@intx/types": "workspace:*", + "@intx/workflow": "workspace:*", + "@intx/workflow-deploy": "workspace:*", + "arktype": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:", + }, + }, "packages/api-query": { "name": "@corbits/api-query", "version": "0.0.1", @@ -527,7 +542,7 @@ }, "packages/connections-tools": { "name": "@corbits/connections-tools", - "version": "0.0.3", + "version": "0.0.4", "dependencies": { "@intx/agent": "workspace:*", "@intx/types": "workspace:*", @@ -1842,6 +1857,8 @@ "@corbits/agent-lifecycle": ["@corbits/agent-lifecycle@workspace:packages/agent-lifecycle"], + "@corbits/agent-runtime": ["@corbits/agent-runtime@workspace:packages/agent-runtime"], + "@corbits/api-query": ["@corbits/api-query@workspace:packages/api-query"], "@corbits/approvals": ["@corbits/approvals@workspace:packages/approvals"], diff --git a/packages/agent-runtime/package.json b/packages/agent-runtime/package.json new file mode 100644 index 000000000..a415b6e52 --- /dev/null +++ b/packages/agent-runtime/package.json @@ -0,0 +1,29 @@ +{ + "name": "@corbits/agent-runtime", + "private": true, + "description": "The single versioned workflow source package every workbench agent run deploys from; its entry module builds the definition from deploy-time config", + "version": "0.0.1", + "license": "LGPL-2.1-or-later", + "type": "module", + "interchange": { + "workflow": "./src/workflow.ts" + }, + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "bun test" + }, + "dependencies": { + "@intx/agent": "workspace:*", + "@intx/types": "workspace:*", + "@intx/workflow": "workspace:*", + "@intx/workflow-deploy": "workspace:*", + "arktype": "catalog:" + }, + "devDependencies": { + "@types/bun": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/agent-runtime/src/config.test.ts b/packages/agent-runtime/src/config.test.ts new file mode 100644 index 000000000..6070555d0 --- /dev/null +++ b/packages/agent-runtime/src/config.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, test } from "bun:test"; + +import { + AGENT_RUNTIME_CONFIG_ENV, + encodeAgentRuntimeConfig, + parseAgentRuntimeConfig, + readAgentRuntimeConfig, + type AgentRuntimeConfig, +} from "./config"; + +const stepConfig: AgentRuntimeConfig = { + workflowId: "wf_run_a", + agentId: "run_a", + triggerAddress: "run_a@bench.example", + systemPrompt: "You are helpful.", + inferencePreferences: [{ provider: "acme", model: "acme-1" }], + toolPackagePins: [], + credentialBindings: [], + mode: { kind: "step" }, +}; + +describe("parseAgentRuntimeConfig", () => { + test("accepts a step-mode config", () => { + expect(parseAgentRuntimeConfig(stepConfig)).toEqual(stepConfig); + }); + + test("accepts a section-mode config with its turn timeout", () => { + const sectionConfig: AgentRuntimeConfig = { + ...stepConfig, + mode: { kind: "section", turnTimeoutMs: 60_000 }, + }; + expect(parseAgentRuntimeConfig(sectionConfig)).toEqual(sectionConfig); + }); + + test("rejects an empty inference chain rather than building a modelless agent", () => { + expect(() => + parseAgentRuntimeConfig({ ...stepConfig, inferencePreferences: [] }), + ).toThrow(/invalid agent-runtime config/); + }); + + test("rejects a section mode with no turn timeout", () => { + expect(() => + parseAgentRuntimeConfig({ ...stepConfig, mode: { kind: "section" } }), + ).toThrow(/invalid agent-runtime config/); + }); + + test("rejects an unknown mode", () => { + expect(() => + parseAgentRuntimeConfig({ ...stepConfig, mode: { kind: "swarm" } }), + ).toThrow(/invalid agent-runtime config/); + }); + + test("rejects an empty trigger address", () => { + expect(() => + parseAgentRuntimeConfig({ ...stepConfig, triggerAddress: "" }), + ).toThrow(/invalid agent-runtime config/); + }); +}); + +describe("readAgentRuntimeConfig", () => { + test("round-trips an encoded config out of the environment", () => { + const env = { + [AGENT_RUNTIME_CONFIG_ENV]: encodeAgentRuntimeConfig(stepConfig), + }; + expect(readAgentRuntimeConfig(env)).toEqual(stepConfig); + }); + + test("throws when the config variable is absent", () => { + expect(() => readAgentRuntimeConfig({})).toThrow( + new RegExp(AGENT_RUNTIME_CONFIG_ENV), + ); + }); + + test("throws when the config variable is not JSON", () => { + expect(() => + readAgentRuntimeConfig({ [AGENT_RUNTIME_CONFIG_ENV]: "not json" }), + ).toThrow(/valid JSON/); + }); + + test("throws when the encoded config does not parse as a config", () => { + expect(() => + readAgentRuntimeConfig({ + [AGENT_RUNTIME_CONFIG_ENV]: JSON.stringify({ workflowId: "wf" }), + }), + ).toThrow(/invalid agent-runtime config/); + }); +}); + +describe("encodeAgentRuntimeConfig", () => { + test("refuses to encode a config the child would reject", () => { + expect(() => + encodeAgentRuntimeConfig({ + ...stepConfig, + inferencePreferences: [], + }), + ).toThrow(/invalid agent-runtime config/); + }); +}); diff --git a/packages/agent-runtime/src/definition.test.ts b/packages/agent-runtime/src/definition.test.ts new file mode 100644 index 000000000..795f5cbb1 --- /dev/null +++ b/packages/agent-runtime/src/definition.test.ts @@ -0,0 +1,170 @@ +import { describe, expect, test } from "bun:test"; + +import type { AgentRuntimeConfig } from "./config"; +import { + AGENT_RUNTIME_SECTION_ID, + AGENT_RUNTIME_STEP_ID, + AGENT_RUNTIME_TURN_STEP_ID, + agentRuntimeTurnRunId, + buildAgentRuntimeWorkflow, +} from "./definition"; + +const baseConfig: AgentRuntimeConfig = { + workflowId: "wf_run_a", + agentId: "run_a", + triggerAddress: "run_a@bench.example", + systemPrompt: "You are helpful.", + inferencePreferences: [ + { provider: "acme", model: "acme-1" }, + { provider: "acme", model: "acme-2" }, + ], + toolPackagePins: [], + credentialBindings: [], + mode: { kind: "step" }, +}; + +describe("buildAgentRuntimeWorkflow — step mode", () => { + test("builds one unbounded agent step on the config's own address", () => { + const definition = buildAgentRuntimeWorkflow(baseConfig); + + expect(definition.id).toBe("wf_run_a"); + expect(definition.stepOrder).toEqual([AGENT_RUNTIME_STEP_ID]); + const stepPrimitive = definition.steps[AGENT_RUNTIME_STEP_ID]; + expect(stepPrimitive?.kind).toBe("step"); + expect(definition.triggers).toEqual([ + { type: "mail", to: "run_a@bench.example" }, + ]); + }); + + test("the step's trigger budget is unbounded, so a run never goes silent after one reply", () => { + const definition = buildAgentRuntimeWorkflow(baseConfig); + const stepPrimitive = definition.steps[AGENT_RUNTIME_STEP_ID]; + + expect(stepPrimitive).toMatchObject({ triggers: "unbounded" }); + }); + + test("carries the config's system prompt and inference chain onto the step agent", () => { + const definition = buildAgentRuntimeWorkflow(baseConfig); + + expect(definition.steps[AGENT_RUNTIME_STEP_ID]).toMatchObject({ + agent: { + id: "run_a", + systemPrompt: "You are helpful.", + inference: { sources: baseConfig.inferencePreferences }, + }, + }); + }); + + test("carries the config's tool package pins onto the step agent", () => { + const definition = buildAgentRuntimeWorkflow({ + ...baseConfig, + toolPackagePins: [{ name: "@corbits/mcp-tools", version: "0.0.1" }], + }); + + expect(definition.steps[AGENT_RUNTIME_STEP_ID]).toMatchObject({ + agent: { + toolPackagePins: [{ name: "@corbits/mcp-tools", version: "0.0.1" }], + }, + }); + }); + + test("declares the config's credential bindings on the definition itself", () => { + const credentialBindings = [ + { + package: "@corbits/mcp-tools", + handle: "mcp:notion", + provider: "notion", + locator: "tenant" as const, + }, + ]; + const definition = buildAgentRuntimeWorkflow({ + ...baseConfig, + credentialBindings, + }); + + expect(definition.credentialBindings).toEqual(credentialBindings); + }); + + test("omits credentialBindings entirely when the config declares none", () => { + expect( + buildAgentRuntimeWorkflow(baseConfig).credentialBindings, + ).toBeUndefined(); + }); + + test("pins the step's input to a literal when the config supplies one", () => { + const definition = buildAgentRuntimeWorkflow({ + ...baseConfig, + mode: { kind: "step", literalInput: "wake up" }, + }); + + expect(definition.steps[AGENT_RUNTIME_STEP_ID]).toMatchObject({ + input: { literal: "wake up" }, + }); + }); + + test("leaves the step reading its real trigger payload by default", () => { + const stepPrimitive = + buildAgentRuntimeWorkflow(baseConfig).steps[AGENT_RUNTIME_STEP_ID]; + + expect(stepPrimitive).not.toMatchObject({ input: { literal: undefined } }); + }); +}); + +describe("buildAgentRuntimeWorkflow — section mode", () => { + const sectionConfig: AgentRuntimeConfig = { + ...baseConfig, + mode: { kind: "section", turnTimeoutMs: 45_000 }, + }; + + test("builds an onTrigger section on the config's address, not a plain step", () => { + const definition = buildAgentRuntimeWorkflow(sectionConfig); + + expect(definition.stepOrder).toEqual([AGENT_RUNTIME_SECTION_ID]); + expect(definition.steps[AGENT_RUNTIME_SECTION_ID]).toMatchObject({ + kind: "onTrigger", + on: { type: "mail", to: "run_a@bench.example" }, + }); + }); + + test("the section's body is one agent step carrying the per-turn timeout", () => { + const section = buildAgentRuntimeWorkflow(sectionConfig).steps[ + AGENT_RUNTIME_SECTION_ID + ]; + + expect(section).toMatchObject({ + body: { + inline: { + id: "wf_run_a_body", + steps: { + [AGENT_RUNTIME_TURN_STEP_ID]: { + kind: "step", + timeout: 45_000, + agent: { systemPrompt: "You are helpful." }, + }, + }, + }, + }, + }); + }); + + test("the mode alone selects the shape — same config fields, different definition", () => { + const asStep = buildAgentRuntimeWorkflow(baseConfig); + const asSection = buildAgentRuntimeWorkflow(sectionConfig); + + expect(asStep.steps[AGENT_RUNTIME_STEP_ID]?.kind).toBe("step"); + expect(asSection.steps[AGENT_RUNTIME_SECTION_ID]?.kind).toBe("onTrigger"); + expect(asStep.id).toBe(asSection.id); + }); +}); + +describe("agentRuntimeTurnRunId", () => { + test("matches the runtime's __ child-run scheme", () => { + expect(agentRuntimeTurnRunId(0)).toBe(`${AGENT_RUNTIME_SECTION_ID}__0`); + expect(agentRuntimeTurnRunId(7)).toBe(`${AGENT_RUNTIME_SECTION_ID}__7`); + }); + + test("rejects a non-integer or negative occurrence", () => { + expect(() => agentRuntimeTurnRunId(-1)).toThrow(); + expect(() => agentRuntimeTurnRunId(1.5)).toThrow(); + }); +}); diff --git a/packages/agent-runtime/tsconfig.json b/packages/agent-runtime/tsconfig.json new file mode 100644 index 000000000..e956ddd88 --- /dev/null +++ b/packages/agent-runtime/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["bun"] + }, + "include": ["src", "test"] +} From 1656eada8526d961caafbba1f86ba79a1a61f014 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 19 Aug 2026 23:07:44 -0700 Subject: [PATCH 2/4] Agent runtime: one versioned workflow source package, configured per deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflow.json retirement makes source-ref the only deploy lineage: a deployment's definition is evaluated from its own pinned code closure and re-verified against the hub-approved wire hash. Workbench had no code-sourced package to deploy, so this adds the one every agent run will share. The bytes are static and versioned; everything per-run — mailbox, system prompt, inference chain, tool package pins, credential bindings — arrives as deploy-time config in the child's environment and is parsed at the entry module's boundary. The config's mode selects the shape, so the deploy front keeps one parameter set and never branches on step-vs-section. --- packages/agent-runtime/src/config.ts | 129 ++++++++++++++++++++ packages/agent-runtime/src/definition.ts | 145 +++++++++++++++++++++++ packages/agent-runtime/src/index.ts | 15 +++ packages/agent-runtime/src/pin.ts | 13 ++ packages/agent-runtime/src/workflow.ts | 11 ++ 5 files changed, 313 insertions(+) create mode 100644 packages/agent-runtime/src/config.ts create mode 100644 packages/agent-runtime/src/definition.ts create mode 100644 packages/agent-runtime/src/index.ts create mode 100644 packages/agent-runtime/src/pin.ts create mode 100644 packages/agent-runtime/src/workflow.ts diff --git a/packages/agent-runtime/src/config.ts b/packages/agent-runtime/src/config.ts new file mode 100644 index 000000000..150406400 --- /dev/null +++ b/packages/agent-runtime/src/config.ts @@ -0,0 +1,129 @@ +// The deploy-time config contract for the agent-runtime source package. +// +// A code-sourced deploy evaluates this package's `interchange.workflow` +// entry module twice — once in the approval probe, once in the run child +// — and refuses to run unless both evaluations project to the same wire +// hash. The package bytes are therefore identical for every run: one +// published version, deployed over and over. Everything that differs per +// run (which mailbox it answers on, what it is told to be, which models +// it may use, which tool packages and credentials it carries) arrives as +// this config, parsed at the module's trust boundary before a definition +// is built from it. +// +// The config travels out of band from the bytes, in the child's +// environment under `AGENT_RUNTIME_CONFIG_ENV`. That is the only channel +// available: mutating the closure would change the bytes that the SRI +// pin and the content cache are keyed on, and no deploy frame field +// reaches the entry module's evaluation. +import { type } from "arktype"; +import { CredentialBinding } from "@intx/types"; +import { ToolPackagePin } from "@intx/types/tool-packages"; + +/** + * The environment variable the entry module reads its config from. The + * host that applies the frozen closure sets it identically for the + * approval probe and for every run child, so both evaluations of the + * same package produce the same definition and the same wire hash. + */ +export const AGENT_RUNTIME_CONFIG_ENV = "CORBITS_AGENT_RUNTIME_CONFIG"; + +const InferencePreference = type({ + provider: "string > 0", + model: "string > 0", + "parameters?": "Record", +}); + +/** + * One unbounded agent step on the run's own address: the folded + * conversational shape. Every inbound mail is another turn of the same + * step, and the run never completes on its own. + */ +const StepMode = type({ + kind: "'step'", + /** + * When present, the step reads this fixed value instead of the + * triggering mail's `trigger.payload`. A run whose system prompt + * forbids acting on what it receives (the workbench host) pins a + * literal here so attachments-only mail — whose `content` is + * legitimately empty — cannot crash the step before it opens. + */ + "literalInput?": "unknown", +}); + +/** + * One long-lived `onTrigger` section on the run's own address: each + * inbound mail is one occurrence, run as its own child run with its own + * id and event log, so a reply is traceable. + */ +const SectionMode = type({ + kind: "'section'", + /** Per-occurrence timeout, enforced on the body's one step. */ + turnTimeoutMs: "number.integer > 0", +}); + +export const AgentRuntimeConfig = type({ + /** Definition id; also the base of the section body's id. */ + workflowId: "string > 0", + /** The step agent's id — the folded run's instance id. */ + agentId: "string > 0", + /** The mailbox this deployment answers on. */ + triggerAddress: "string > 0", + systemPrompt: "string", + /** Resolved catalog chain, in deploy order; the gate approves these. */ + inferencePreferences: InferencePreference.array().atLeastLength(1), + /** Tool packages the step agent carries; no inline tool factories. */ + toolPackagePins: ToolPackagePin.array(), + /** Definition-level bindings the host's per-step snapshot derives from. */ + credentialBindings: CredentialBinding.array(), + mode: StepMode.or(SectionMode), +}); +export type AgentRuntimeConfig = typeof AgentRuntimeConfig.infer; + +/** + * Parse `raw` as an `AgentRuntimeConfig`, throwing on any malformed + * shape. A deploy whose config does not parse must fail before a + * definition exists, not build a half-configured agent. + */ +export function parseAgentRuntimeConfig(raw: unknown): AgentRuntimeConfig { + const parsed = AgentRuntimeConfig(raw); + if (parsed instanceof type.errors) { + throw new Error(`invalid agent-runtime config: ${parsed.summary}`); + } + return parsed; +} + +/** + * Read and parse the deploy-time config out of an environment map. The + * entry module calls this with `process.env`; a missing or unparseable + * value throws, because a workflow package with no config has no + * definition to export. + */ +export function readAgentRuntimeConfig( + env: Record, +): AgentRuntimeConfig { + const encoded = env[AGENT_RUNTIME_CONFIG_ENV]; + if (encoded === undefined || encoded === "") { + throw new Error( + `the agent-runtime workflow package requires its deploy-time config in ${AGENT_RUNTIME_CONFIG_ENV}`, + ); + } + let decoded: unknown; + try { + decoded = JSON.parse(encoded); + } catch (cause) { + throw new Error( + `${AGENT_RUNTIME_CONFIG_ENV} does not hold valid JSON`, + { cause }, + ); + } + return parseAgentRuntimeConfig(decoded); +} + +/** + * Serialize a config for delivery in `AGENT_RUNTIME_CONFIG_ENV`. The + * deploying host validates before it encodes, so a config that would + * fail inside the child fails loud at the deploy call instead. + */ +export function encodeAgentRuntimeConfig(config: AgentRuntimeConfig): string { + return JSON.stringify(parseAgentRuntimeConfig(config)); +} diff --git a/packages/agent-runtime/src/definition.ts b/packages/agent-runtime/src/definition.ts new file mode 100644 index 000000000..0ccabbd7a --- /dev/null +++ b/packages/agent-runtime/src/definition.ts @@ -0,0 +1,145 @@ +// Builds the workflow definition a workbench agent run executes, from +// the run's deploy-time config alone. +// +// Two shapes, selected by `config.mode`, never by a branch in the deploy +// API: the deploy front takes one parameter set and the shape is purely +// whatever this module evaluates to. +// +// `step` is the folded conversational run — one unbounded agent step +// servicing every inbound mail as another turn. Its `triggers: +// "unbounded"` budget is the whole reason this is authored rather than +// wrapped: the platform's default budget of 1 makes a run go silent +// after its first reply. +// +// `section` is the per-turn shape (CL-6329) — an `onTrigger` section +// whose body is one agent step, so every message becomes an occurrence +// with its own child run id and event log. +// +// [Intx gap] CL-6329's `onBodyFailure: "continue"` policy — the failure +// edge that keeps a section subscribed after a failed turn — does not +// exist at the vendored pin `4ed8baf4`: `OnTriggerOpts` carries no such +// field and the inert projector's onTrigger whitelist +// (`vendor/intx/workflow/src/live-inert-projector.ts`) has no slot for +// it. Section mode is therefore authored without it here rather than +// with a workbench-local reimplementation of the primitive. When +// upstream lands the field, it is authored HERE — the projection drops +// it, so it survives only because the run child re-evaluates this +// module from the closure, and nothing may ever treat the projection as +// the executable definition. +import { buildSingleStepAgentDefinition } from "@intx/workflow-deploy"; +import { defineWorkflow, onTrigger, step } from "@intx/workflow"; +import type { WorkflowDefinition } from "@intx/workflow"; + +import type { AgentRuntimeConfig } from "./config"; + +/** The step id of the folded conversational run's one step. */ +export const AGENT_RUNTIME_STEP_ID = "default"; + +/** The section's step id in `section` mode. */ +export const AGENT_RUNTIME_SECTION_ID = "turn"; + +/** The body step inside one section occurrence — the agent that answers. */ +export const AGENT_RUNTIME_TURN_STEP_ID = "reply"; + +/** + * The child run id occurrence `occurrence` runs under. The runtime names + * an occurrence `__` (see `onTriggerBodyRef` in + * `@intx/workflow`), so the section id plus a zero-based occurrence + * index is the whole derivation. + */ +export function agentRuntimeTurnRunId(occurrence: number): string { + if (!Number.isInteger(occurrence) || occurrence < 0) { + throw new Error( + "agentRuntimeTurnRunId requires a non-negative integer occurrence", + ); + } + return `${AGENT_RUNTIME_SECTION_ID}__${String(occurrence)}`; +} + +function buildTurnAgent(config: AgentRuntimeConfig, id: string) { + return buildSingleStepAgentDefinition({ + id, + systemPrompt: config.systemPrompt, + inferencePreferences: config.inferencePreferences, + toolFactories: [], + toolPackagePins: config.toolPackagePins, + }); +} + +function buildFoldedStepWorkflow( + config: AgentRuntimeConfig, + literalInput: unknown, + hasLiteralInput: boolean, +): WorkflowDefinition { + const steps = { + [AGENT_RUNTIME_STEP_ID]: step({ + agent: buildTurnAgent(config, config.agentId), + triggers: "unbounded" as const, + ...(hasLiteralInput ? { input: { literal: literalInput } } : {}), + }), + }; + return config.credentialBindings.length > 0 + ? defineWorkflow({ + id: config.workflowId, + trigger: { type: "mail", to: config.triggerAddress }, + credentialBindings: config.credentialBindings, + steps, + }) + : defineWorkflow({ + id: config.workflowId, + trigger: { type: "mail", to: config.triggerAddress }, + steps, + }); +} + +function buildSectionWorkflow( + config: AgentRuntimeConfig, + turnTimeoutMs: number, +): WorkflowDefinition { + const body = defineWorkflow({ + id: `${config.workflowId}_body`, + trigger: { type: "mail", to: config.triggerAddress }, + steps: { + [AGENT_RUNTIME_TURN_STEP_ID]: step({ + agent: buildTurnAgent(config, AGENT_RUNTIME_TURN_STEP_ID), + timeout: turnTimeoutMs, + }), + }, + }); + const steps = { + [AGENT_RUNTIME_SECTION_ID]: onTrigger({ + on: { type: "mail" as const, to: config.triggerAddress }, + body, + }), + }; + return config.credentialBindings.length > 0 + ? defineWorkflow({ + id: config.workflowId, + trigger: { type: "mail", to: config.triggerAddress }, + credentialBindings: config.credentialBindings, + steps, + }) + : defineWorkflow({ + id: config.workflowId, + trigger: { type: "mail", to: config.triggerAddress }, + steps, + }); +} + +/** + * Build the run's definition from its deploy-time config. The config's + * `mode` selects the shape; every other field is the same per-run data + * either shape needs. + */ +export function buildAgentRuntimeWorkflow( + config: AgentRuntimeConfig, +): WorkflowDefinition { + if (config.mode.kind === "section") { + return buildSectionWorkflow(config, config.mode.turnTimeoutMs); + } + return buildFoldedStepWorkflow( + config, + config.mode.literalInput, + "literalInput" in config.mode, + ); +} diff --git a/packages/agent-runtime/src/index.ts b/packages/agent-runtime/src/index.ts new file mode 100644 index 000000000..351694e68 --- /dev/null +++ b/packages/agent-runtime/src/index.ts @@ -0,0 +1,15 @@ +export { + AGENT_RUNTIME_CONFIG_ENV, + AgentRuntimeConfig, + encodeAgentRuntimeConfig, + parseAgentRuntimeConfig, + readAgentRuntimeConfig, +} from "./config"; +export { + AGENT_RUNTIME_SECTION_ID, + AGENT_RUNTIME_STEP_ID, + AGENT_RUNTIME_TURN_STEP_ID, + agentRuntimeTurnRunId, + buildAgentRuntimeWorkflow, +} from "./definition"; +export { AGENT_RUNTIME_PACKAGE_NAME, AGENT_RUNTIME_WORKFLOW_ENTRY } from "./pin"; diff --git a/packages/agent-runtime/src/pin.ts b/packages/agent-runtime/src/pin.ts new file mode 100644 index 000000000..5b38420a6 --- /dev/null +++ b/packages/agent-runtime/src/pin.ts @@ -0,0 +1,13 @@ +// How a deploy names this package. A code-sourced deploy carries a +// `name@range` pin plus the `interchange.workflow` entry path; both are +// properties of this package, so they are declared next to it rather +// than re-typed at each deploying call site. + +/** The published package name a deploy's `name@range` pin selects. */ +export const AGENT_RUNTIME_PACKAGE_NAME = "@corbits/agent-runtime"; + +/** + * The `interchange.workflow` entry path the sidecar evaluates, matching + * this package's own `package.json`. + */ +export const AGENT_RUNTIME_WORKFLOW_ENTRY = "./src/workflow.ts"; diff --git a/packages/agent-runtime/src/workflow.ts b/packages/agent-runtime/src/workflow.ts new file mode 100644 index 000000000..81ba6f26f --- /dev/null +++ b/packages/agent-runtime/src/workflow.ts @@ -0,0 +1,11 @@ +// The `interchange.workflow` entry the code-sourced deploy evaluates. +// +// Nothing imports this module statically. The approval probe and the run +// child each import it out of the materialized closure, read the same +// deploy-time config out of the environment, and must arrive at the same +// definition — the child refuses to run one whose recomputed wire hash +// differs from the approved one. +import { readAgentRuntimeConfig } from "./config"; +import { buildAgentRuntimeWorkflow } from "./definition"; + +export default buildAgentRuntimeWorkflow(readAgentRuntimeConfig(process.env)); From 3be67477a45b451c317711360cc73537d35ccac0 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 19 Aug 2026 23:15:07 -0700 Subject: [PATCH 3/4] Agent runtime: render the per-run config into the deployed bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first shape here read the config from the child's environment. That cannot work: the approval probe and the run child each evaluate the entry module independently, and the hashed projection covers the trigger address, the system prompt, the (provider, model) pairs, the tool package pins, and the credential bindings — every field of the config. A config read from outside the closure diverges between the two evaluations and fails the re-verify barrier closed. There is also nowhere to read one from: no source variant carries an overlay, the deploy frame carries no config bag, and the probe frame carries no environment at all. So the config becomes the bytes. renderAgentRuntimeSourceTree emits a thin per-run package that pins this versioned one and calls the builder with the run's config as a literal, ready to commit into a workflow-kind asset and deploy as source at a commitSha — the only source variant cheap enough to mint per run. --- packages/agent-runtime/package.json | 5 +- packages/agent-runtime/src/config.test.ts | 48 +--------- packages/agent-runtime/src/config.ts | 75 ++++------------ packages/agent-runtime/src/definition.test.ts | 5 +- packages/agent-runtime/src/index.ts | 16 ++-- packages/agent-runtime/src/pin.ts | 15 +--- .../agent-runtime/src/source-tree.test.ts | 89 +++++++++++++++++++ packages/agent-runtime/src/source-tree.ts | 66 ++++++++++++++ packages/agent-runtime/src/workflow.ts | 11 --- 9 files changed, 186 insertions(+), 144 deletions(-) create mode 100644 packages/agent-runtime/src/source-tree.test.ts create mode 100644 packages/agent-runtime/src/source-tree.ts delete mode 100644 packages/agent-runtime/src/workflow.ts diff --git a/packages/agent-runtime/package.json b/packages/agent-runtime/package.json index a415b6e52..e1a35d50b 100644 --- a/packages/agent-runtime/package.json +++ b/packages/agent-runtime/package.json @@ -1,13 +1,10 @@ { "name": "@corbits/agent-runtime", "private": true, - "description": "The single versioned workflow source package every workbench agent run deploys from; its entry module builds the definition from deploy-time config", + "description": "The versioned definition builder every workbench agent run deploys, plus the renderer that pins it into a per-run code-sourced workflow package", "version": "0.0.1", "license": "LGPL-2.1-or-later", "type": "module", - "interchange": { - "workflow": "./src/workflow.ts" - }, "exports": { ".": "./src/index.ts" }, diff --git a/packages/agent-runtime/src/config.test.ts b/packages/agent-runtime/src/config.test.ts index 6070555d0..cbaad13f3 100644 --- a/packages/agent-runtime/src/config.test.ts +++ b/packages/agent-runtime/src/config.test.ts @@ -1,12 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { - AGENT_RUNTIME_CONFIG_ENV, - encodeAgentRuntimeConfig, - parseAgentRuntimeConfig, - readAgentRuntimeConfig, - type AgentRuntimeConfig, -} from "./config"; +import { parseAgentRuntimeConfig, type AgentRuntimeConfig } from "./config"; const stepConfig: AgentRuntimeConfig = { workflowId: "wf_run_a", @@ -56,43 +50,3 @@ describe("parseAgentRuntimeConfig", () => { ).toThrow(/invalid agent-runtime config/); }); }); - -describe("readAgentRuntimeConfig", () => { - test("round-trips an encoded config out of the environment", () => { - const env = { - [AGENT_RUNTIME_CONFIG_ENV]: encodeAgentRuntimeConfig(stepConfig), - }; - expect(readAgentRuntimeConfig(env)).toEqual(stepConfig); - }); - - test("throws when the config variable is absent", () => { - expect(() => readAgentRuntimeConfig({})).toThrow( - new RegExp(AGENT_RUNTIME_CONFIG_ENV), - ); - }); - - test("throws when the config variable is not JSON", () => { - expect(() => - readAgentRuntimeConfig({ [AGENT_RUNTIME_CONFIG_ENV]: "not json" }), - ).toThrow(/valid JSON/); - }); - - test("throws when the encoded config does not parse as a config", () => { - expect(() => - readAgentRuntimeConfig({ - [AGENT_RUNTIME_CONFIG_ENV]: JSON.stringify({ workflowId: "wf" }), - }), - ).toThrow(/invalid agent-runtime config/); - }); -}); - -describe("encodeAgentRuntimeConfig", () => { - test("refuses to encode a config the child would reject", () => { - expect(() => - encodeAgentRuntimeConfig({ - ...stepConfig, - inferencePreferences: [], - }), - ).toThrow(/invalid agent-runtime config/); - }); -}); diff --git a/packages/agent-runtime/src/config.ts b/packages/agent-runtime/src/config.ts index 150406400..664345636 100644 --- a/packages/agent-runtime/src/config.ts +++ b/packages/agent-runtime/src/config.ts @@ -1,32 +1,23 @@ -// The deploy-time config contract for the agent-runtime source package. +// Everything about an agent run that its deployed definition must know: +// which mailbox it answers on, what it is told to be, which models it +// may use, which tool packages and credentials it carries, and which of +// the two shapes it takes. // -// A code-sourced deploy evaluates this package's `interchange.workflow` -// entry module twice — once in the approval probe, once in the run child -// — and refuses to run unless both evaluations project to the same wire -// hash. The package bytes are therefore identical for every run: one -// published version, deployed over and over. Everything that differs per -// run (which mailbox it answers on, what it is told to be, which models -// it may use, which tool packages and credentials it carries) arrives as -// this config, parsed at the module's trust boundary before a definition -// is built from it. -// -// The config travels out of band from the bytes, in the child's -// environment under `AGENT_RUNTIME_CONFIG_ENV`. That is the only channel -// available: mutating the closure would change the bytes that the SRI -// pin and the content cache are keyed on, and no deploy frame field -// reaches the entry module's evaluation. +// This config is DEPLOY-TIME data, and under the workflow.json +// retirement it has to live inside the deployed package's own bytes. +// The approval probe and the run child each evaluate the deployment's +// entry module independently and the child refuses to run a definition +// whose recomputed wire hash differs from the approved one; the hashed +// projection covers the system prompt, the trigger address, the model +// pairs, the tool package pins, and the credential bindings — every +// field here. So a config delivered out of band (an env var, a file the +// sidecar drops next to the entry) diverges between the two evaluations +// and fails closed. `./source-tree.ts` renders it into the bytes +// instead. import { type } from "arktype"; import { CredentialBinding } from "@intx/types"; import { ToolPackagePin } from "@intx/types/tool-packages"; -/** - * The environment variable the entry module reads its config from. The - * host that applies the frozen closure sets it identically for the - * approval probe and for every run child, so both evaluations of the - * same package produce the same definition and the same wire hash. - */ -export const AGENT_RUNTIME_CONFIG_ENV = "CORBITS_AGENT_RUNTIME_CONFIG"; - const InferencePreference = type({ provider: "string > 0", model: "string > 0", @@ -91,39 +82,3 @@ export function parseAgentRuntimeConfig(raw: unknown): AgentRuntimeConfig { } return parsed; } - -/** - * Read and parse the deploy-time config out of an environment map. The - * entry module calls this with `process.env`; a missing or unparseable - * value throws, because a workflow package with no config has no - * definition to export. - */ -export function readAgentRuntimeConfig( - env: Record, -): AgentRuntimeConfig { - const encoded = env[AGENT_RUNTIME_CONFIG_ENV]; - if (encoded === undefined || encoded === "") { - throw new Error( - `the agent-runtime workflow package requires its deploy-time config in ${AGENT_RUNTIME_CONFIG_ENV}`, - ); - } - let decoded: unknown; - try { - decoded = JSON.parse(encoded); - } catch (cause) { - throw new Error( - `${AGENT_RUNTIME_CONFIG_ENV} does not hold valid JSON`, - { cause }, - ); - } - return parseAgentRuntimeConfig(decoded); -} - -/** - * Serialize a config for delivery in `AGENT_RUNTIME_CONFIG_ENV`. The - * deploying host validates before it encodes, so a config that would - * fail inside the child fails loud at the deploy call instead. - */ -export function encodeAgentRuntimeConfig(config: AgentRuntimeConfig): string { - return JSON.stringify(parseAgentRuntimeConfig(config)); -} diff --git a/packages/agent-runtime/src/definition.test.ts b/packages/agent-runtime/src/definition.test.ts index 795f5cbb1..be2f29202 100644 --- a/packages/agent-runtime/src/definition.test.ts +++ b/packages/agent-runtime/src/definition.test.ts @@ -127,9 +127,8 @@ describe("buildAgentRuntimeWorkflow — section mode", () => { }); test("the section's body is one agent step carrying the per-turn timeout", () => { - const section = buildAgentRuntimeWorkflow(sectionConfig).steps[ - AGENT_RUNTIME_SECTION_ID - ]; + const section = + buildAgentRuntimeWorkflow(sectionConfig).steps[AGENT_RUNTIME_SECTION_ID]; expect(section).toMatchObject({ body: { diff --git a/packages/agent-runtime/src/index.ts b/packages/agent-runtime/src/index.ts index 351694e68..17e9e4e8b 100644 --- a/packages/agent-runtime/src/index.ts +++ b/packages/agent-runtime/src/index.ts @@ -1,10 +1,4 @@ -export { - AGENT_RUNTIME_CONFIG_ENV, - AgentRuntimeConfig, - encodeAgentRuntimeConfig, - parseAgentRuntimeConfig, - readAgentRuntimeConfig, -} from "./config"; +export { AgentRuntimeConfig, parseAgentRuntimeConfig } from "./config"; export { AGENT_RUNTIME_SECTION_ID, AGENT_RUNTIME_STEP_ID, @@ -12,4 +6,10 @@ export { agentRuntimeTurnRunId, buildAgentRuntimeWorkflow, } from "./definition"; -export { AGENT_RUNTIME_PACKAGE_NAME, AGENT_RUNTIME_WORKFLOW_ENTRY } from "./pin"; +export { AGENT_RUNTIME_PACKAGE_NAME } from "./pin"; +export { + AGENT_RUNTIME_ENTRY_PATH, + renderAgentRuntimeSourceTree, + type AgentRuntimeSourceTree, + type RenderAgentRuntimeSourceTreeInput, +} from "./source-tree"; diff --git a/packages/agent-runtime/src/pin.ts b/packages/agent-runtime/src/pin.ts index 5b38420a6..1bfb22dac 100644 --- a/packages/agent-runtime/src/pin.ts +++ b/packages/agent-runtime/src/pin.ts @@ -1,13 +1,6 @@ -// How a deploy names this package. A code-sourced deploy carries a -// `name@range` pin plus the `interchange.workflow` entry path; both are -// properties of this package, so they are declared next to it rather -// than re-typed at each deploying call site. - -/** The published package name a deploy's `name@range` pin selects. */ -export const AGENT_RUNTIME_PACKAGE_NAME = "@corbits/agent-runtime"; - /** - * The `interchange.workflow` entry path the sidecar evaluates, matching - * this package's own `package.json`. + * The package name a rendered per-run workflow tree depends on and + * imports its builder from. Declared next to the package rather than + * re-typed in the renderer's template. */ -export const AGENT_RUNTIME_WORKFLOW_ENTRY = "./src/workflow.ts"; +export const AGENT_RUNTIME_PACKAGE_NAME = "@corbits/agent-runtime"; diff --git a/packages/agent-runtime/src/source-tree.test.ts b/packages/agent-runtime/src/source-tree.test.ts new file mode 100644 index 000000000..6a13c179e --- /dev/null +++ b/packages/agent-runtime/src/source-tree.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, test } from "bun:test"; + +import type { AgentRuntimeConfig } from "./config"; +import { AGENT_RUNTIME_PACKAGE_NAME } from "./pin"; +import { + AGENT_RUNTIME_ENTRY_PATH, + renderAgentRuntimeSourceTree, +} from "./source-tree"; + +const config: AgentRuntimeConfig = { + workflowId: "wf_run_a", + agentId: "run_a", + triggerAddress: "run_a@bench.example", + systemPrompt: "You are helpful.", + inferencePreferences: [{ provider: "acme", model: "acme-1" }], + toolPackagePins: [], + credentialBindings: [], + mode: { kind: "step" }, +}; + +function render(overrides: Partial = {}) { + return renderAgentRuntimeSourceTree({ + packageName: "run-a-workflow", + runtimeVersion: "0.0.1", + config: { ...config, ...overrides }, + }); +} + +describe("renderAgentRuntimeSourceTree", () => { + test("declares the entry the sidecar evaluates", () => { + const pkg = JSON.parse(render()["package.json"] ?? ""); + + expect(pkg.interchange).toEqual({ workflow: AGENT_RUNTIME_ENTRY_PATH }); + expect(Object.keys(render())).toContain("workflow.js"); + }); + + test("pins the versioned runtime package as the tree's one dependency", () => { + const pkg = JSON.parse(render()["package.json"] ?? ""); + + expect(pkg.dependencies).toEqual({ [AGENT_RUNTIME_PACKAGE_NAME]: "0.0.1" }); + }); + + test("renders the config into the entry module's own bytes", () => { + const entry = render()["workflow.js"] ?? ""; + + expect(entry).toContain(`from "${AGENT_RUNTIME_PACKAGE_NAME}"`); + expect(entry).toContain("buildAgentRuntimeWorkflow("); + expect(entry).toContain('"run_a@bench.example"'); + expect(entry).toContain('"You are helpful."'); + }); + + test("a differing per-run field produces differing bytes — the hash barrier's whole premise", () => { + const a = render()["workflow.js"]; + const b = render({ systemPrompt: "You are terse." })["workflow.js"]; + + expect(a).not.toBe(b); + }); + + test("the same config renders byte-identically, so probe and run agree", () => { + expect(render()).toEqual(render()); + }); + + test("the rendered entry's config round-trips back to the config it was given", () => { + const entry = render()["workflow.js"] ?? ""; + const literal = entry.slice( + entry.indexOf("buildAgentRuntimeWorkflow(") + + "buildAgentRuntimeWorkflow(".length, + entry.lastIndexOf(");"), + ); + + expect(JSON.parse(literal)).toEqual(config); + }); + + test("renders the section mode's turn timeout into the bytes too", () => { + const entry = + render({ mode: { kind: "section", turnTimeoutMs: 45_000 } })[ + "workflow.js" + ] ?? ""; + + expect(entry).toContain('"kind": "section"'); + expect(entry).toContain('"turnTimeoutMs": 45000'); + }); + + test("refuses to render a config the run child would reject", () => { + expect(() => render({ inferencePreferences: [] })).toThrow( + /invalid agent-runtime config/, + ); + }); +}); diff --git a/packages/agent-runtime/src/source-tree.ts b/packages/agent-runtime/src/source-tree.ts new file mode 100644 index 000000000..490e895f2 --- /dev/null +++ b/packages/agent-runtime/src/source-tree.ts @@ -0,0 +1,66 @@ +// Renders the source tree a code-sourced deploy actually deploys. +// +// Under the workflow.json retirement a deployment's definition is +// whatever its own pinned code closure evaluates to, and the approved +// wire hash covers every field that differs per run. So the per-run +// config cannot ride beside the bytes — it has to BE the bytes. +// +// The tree this renders is deliberately thin: a `package.json` and a +// four-line entry module that pins `@corbits/agent-runtime` and calls +// `buildAgentRuntimeWorkflow` with the run's config as a literal. All +// the behaviour stays in this one versioned package, reviewed and +// upgraded in one place; what varies per run is a JSON literal. A host +// commits the tree into a `workflow`-kind asset and deploys it with +// `source.kind: "asset"`, `package.format: "source"`, `commitSha` — the +// only source variant whose pin is cheap enough to mint per run (the +// registry and tarball variants would each need a publish). +import { parseAgentRuntimeConfig, type AgentRuntimeConfig } from "./config"; +import { AGENT_RUNTIME_PACKAGE_NAME } from "./pin"; + +/** The entry path the rendered `package.json` declares and the sidecar evaluates. */ +export const AGENT_RUNTIME_ENTRY_PATH = "./workflow.js"; + +export interface RenderAgentRuntimeSourceTreeInput { + /** + * The rendered package's own name. It never leaves the asset, so it + * only has to be a valid package name and stable for a given run. + */ + readonly packageName: string; + /** The `@corbits/agent-runtime` range the rendered package depends on. */ + readonly runtimeVersion: string; + /** The run's deploy-time config, rendered into the entry module. */ + readonly config: AgentRuntimeConfig; +} + +/** File contents keyed by path relative to the tree root. */ +export type AgentRuntimeSourceTree = Readonly>; + +/** + * Render the per-run workflow package. The config is validated before + * it is written, so a config the run child would reject fails at the + * deploying call site instead of inside the approval probe. + */ +export function renderAgentRuntimeSourceTree( + input: RenderAgentRuntimeSourceTreeInput, +): AgentRuntimeSourceTree { + const config = parseAgentRuntimeConfig(input.config); + const packageJson = { + name: input.packageName, + version: "0.0.0", + private: true, + type: "module", + interchange: { workflow: AGENT_RUNTIME_ENTRY_PATH }, + dependencies: { [AGENT_RUNTIME_PACKAGE_NAME]: input.runtimeVersion }, + }; + const entry = [ + `import { buildAgentRuntimeWorkflow } from ${JSON.stringify(AGENT_RUNTIME_PACKAGE_NAME)};`, + "", + `export default buildAgentRuntimeWorkflow(${JSON.stringify(config, null, 2)});`, + "", + ].join("\n"); + + return { + "package.json": `${JSON.stringify(packageJson, null, 2)}\n`, + "workflow.js": entry, + }; +} diff --git a/packages/agent-runtime/src/workflow.ts b/packages/agent-runtime/src/workflow.ts deleted file mode 100644 index 81ba6f26f..000000000 --- a/packages/agent-runtime/src/workflow.ts +++ /dev/null @@ -1,11 +0,0 @@ -// The `interchange.workflow` entry the code-sourced deploy evaluates. -// -// Nothing imports this module statically. The approval probe and the run -// child each import it out of the materialized closure, read the same -// deploy-time config out of the environment, and must arrive at the same -// definition — the child refuses to run one whose recomputed wire hash -// differs from the approved one. -import { readAgentRuntimeConfig } from "./config"; -import { buildAgentRuntimeWorkflow } from "./definition"; - -export default buildAgentRuntimeWorkflow(readAgentRuntimeConfig(process.env)); From c1061b9ef6efbed1c93caa03f084bc9284112b11 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 19 Aug 2026 23:15:08 -0700 Subject: [PATCH 4/4] Update docs: the agent-runtime package and what still blocks deployAtHead --- docs/revendor-inventory.md | 77 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/docs/revendor-inventory.md b/docs/revendor-inventory.md index 3c8cd099e..bdb45ce80 100644 --- a/docs/revendor-inventory.md +++ b/docs/revendor-inventory.md @@ -380,3 +380,80 @@ Upstream's own diff over the same span is the reference implementation: `workflow-host-wiring.ts` at `4ed8baf4` show every one of these conversions against the same contracts, and `apps/sidecar`'s `VENDORED.md` row stays at `59f5e7b9` until workbench's fork is reconciled with them. + +### Conversion step 1: `packages/agent-runtime` + +`packages/agent-runtime` holds the definition builder every workbench +agent run deploys. `AgentRuntimeConfig` is the arktype contract for +everything that differs per run — the mailbox it answers on, its system +prompt, its resolved inference chain, its tool-package pins, its +credential bindings — and the config's `mode` selects the shape: +`buildAgentRuntimeWorkflow` returns either the folded unbounded step or +the per-turn `onTrigger` section. Because the mode lives in the config, +the deploy front keeps one parameter set and no call site ever branches +on which shape it wants; `deployCodeSourcedWorkflow` already enumerates +inert `onTrigger` bodies on every deploy, so the section shape needs +nothing extra from the API. + +#### There is no out-of-band config channel — the config IS the bytes + +The obvious design, one static published package whose entry reads a +per-run config from its environment, does not work at this pin and +cannot be made to work by the sidecar. + +The approval probe and the run child each evaluate the entry module +independently, and the child refuses to run a definition whose recomputed +wire hash differs from the approved one +(`workflow-host/src/child/verified-definition-loader.ts`). The hashed +preimage (`workflow/src/live-inert-projector.ts`) covers the trigger +address, the agent's system prompt, its `(provider, model)` pairs, its +tool-package pins, and the definition's credential bindings — every field +of the config. So a config read from anywhere outside the closure's bytes +diverges between the two evaluations and fails closed. And there is +nowhere to read it from anyway: `WorkflowDefinitionSource` has no overlay +or params on any variant, `AgentDeployWorkflow` carries no config bag +(the code-sourced route builds `HarnessConfig` with an empty +`systemPrompt`, empty `tools`, empty `grants`), `SpawnTimeEnv` has no +config field, and `WorkflowProbeRequestFrame` carries no env at all — so +even a sidecar willing to inject one could not make the probe see it. + +`renderAgentRuntimeSourceTree` is the consequence: it renders a thin +per-run package — a `package.json` plus a four-line entry module that +pins `@corbits/agent-runtime` and calls `buildAgentRuntimeWorkflow` with +the run's config as a literal. All the behaviour stays in the one +versioned package; what varies per run is a JSON literal inside the +hashed bytes. A host commits that tree into a `workflow`-kind asset and +deploys `source.kind: "asset"`, `package.format: "source"`, `commitSha` — +the only source variant whose pin is cheap enough to mint per run, since +the registry and tarball variants each need a publish. + +Two in-tree prerequisites remain for any of this to execute, both already +on the conversion table above: nothing produces `CLOSURE_PACKAGE_DIR`, and +no `WorkflowProbeExecutor` is wired on the sidecar, so every probe +currently answers `workflow.probe.error`. Both are conversion step 2. + +#### What still blocks `deployAtHead` + +A folded run pre-mints its own anchor `workflow_run` row (`mintFoldedRun`, +carrying the `principalId` its `agent_session` join needs) and it commonly +carries credential bindings (every `@corbits/mcp-tools` launch). Neither +code-sourced deploy front accepts that combination: + +| Front | Anchor row | Credential cipher | Capacity | +| ----------------------------------- | ---------- | ----------------- | ----------------------------------------------------- | +| `deployWorkflowFromSource` | INSERTs | not threaded | shared | +| `deployPreparedCodeSourcedWorkflow` | UPDATEs | threaded | exclusive allocation only (`requireAllocationRouter`) | + +`deployWorkflowFromSource` collides on the primary key of the row the +folded run already owns, and its `commonDeploy` passes no +`credentialCipher`, so a definition with bindings throws inside +`deployCodeSourcedWorkflow`. The prepared front does both correctly but +hard-requires an `allocationTarget`, and exclusive placement is dormant +in-tree. Composing the halves is not open either: `emitSourceRefDeployFrame` +and `buildInertProjectionStepSources` are module-private in `hub-sessions`. + +[Intx gap] The missing capability is a SHARED-capacity code-sourced deploy +that ADOPTS a pre-existing anchor run and threads a `credentialCipher` — +`deployPreparedCodeSourcedWorkflow` minus the allocation lock. Until it +exists upstream, `deployAtHead` cannot cut over without either forking the +front or dismantling the folded run's own anchor-row ownership.