Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions docs/revendor-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
26 changes: 26 additions & 0 deletions packages/agent-runtime/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@corbits/agent-runtime",
"private": true,
"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",
"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:"
}
}
52 changes: 52 additions & 0 deletions packages/agent-runtime/src/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, test } from "bun:test";

import { parseAgentRuntimeConfig, 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/);
});
});
84 changes: 84 additions & 0 deletions packages/agent-runtime/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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.
//
// 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";

const InferencePreference = type({
provider: "string > 0",
model: "string > 0",
"parameters?": "Record<string, unknown>",
});

/**
* 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;
}
Loading
Loading