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
3 changes: 0 additions & 3 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1036,7 +1036,6 @@ export async function createHub(config: HubConfig) {
sidecarRouter,
eventCollectors,
credentialCipher,
hubPublicKey,
toolGrantsForPins,
mcpCredentialBindingsFor,
noopInferenceBaseUrl: `${config.baseUrl}/api/chat/noop-inference`,
Expand Down Expand Up @@ -1594,7 +1593,6 @@ export async function createHub(config: HubConfig) {
assetService,
sidecarRouter,
eventCollectors,
hubPublicKey,
toolGrantsForPins,
mcpCredentialBindingsFor,
cryptoProviderCache: foldedRunCryptoProviders,
Expand Down Expand Up @@ -2002,7 +2000,6 @@ export async function createHub(config: HubConfig) {
sidecarRouter,
eventCollectors,
credentialCipher,
hubPublicKey,
toolGrantsForPins,
mcpCredentialBindingsFor,
cryptoProviderCache: foldedRunCryptoProviders,
Expand Down
7 changes: 0 additions & 7 deletions apps/hub/src/routine-launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ function buildLauncher(overrides: { definition?: unknown } = {}) {
sessionService: {} as never,
assetService: {} as never,
sidecarRouter: {} as never,
hubPublicKey: "hub-key",
toolGrantsForPins: () => [],
eventCollectors: {} as never,
cryptoProviderCache: { get: async () => ({}) as never },
Expand Down Expand Up @@ -232,7 +231,6 @@ describe("createHubRoutineLauncher — delivery workbench", () => {
sessionService: {} as never,
assetService: {} as never,
sidecarRouter: {} as never,
hubPublicKey: "hub-key",
toolGrantsForPins: () => [],
eventCollectors: {} as never,
cryptoProviderCache: { get: async () => ({}) as never },
Expand All @@ -259,7 +257,6 @@ describe("createHubRoutineLauncher — recurring-task bridge", () => {
sessionService: {} as never,
assetService: {} as never,
sidecarRouter: {} as never,
hubPublicKey: "hub-key",
toolGrantsForPins: () => [],
eventCollectors: {} as never,
cryptoProviderCache: { get: async () => ({}) as never },
Expand Down Expand Up @@ -297,7 +294,6 @@ describe("createHubRoutineLauncher — recurring-task bridge", () => {
sessionService: {} as never,
assetService: {} as never,
sidecarRouter: {} as never,
hubPublicKey: "hub-key",
toolGrantsForPins: () => [],
eventCollectors: {} as never,
cryptoProviderCache: { get: async () => ({}) as never },
Expand All @@ -324,7 +320,6 @@ describe("createHubRoutineLauncher — recurring-task bridge", () => {
sessionService: {} as never,
assetService: {} as never,
sidecarRouter: {} as never,
hubPublicKey: "hub-key",
toolGrantsForPins: () => [],
eventCollectors: {} as never,
cryptoProviderCache: { get: async () => ({}) as never },
Expand Down Expand Up @@ -356,7 +351,6 @@ describe("createHubRoutineLauncher — recurring-task bridge", () => {
sessionService: {} as never,
assetService: {} as never,
sidecarRouter: {} as never,
hubPublicKey: "hub-key",
toolGrantsForPins: () => [],
eventCollectors: {} as never,
cryptoProviderCache: { get: async () => ({}) as never },
Expand All @@ -378,7 +372,6 @@ describe("createHubRoutineLauncher — recurring-task bridge", () => {
sessionService: {} as never,
assetService: {} as never,
sidecarRouter: {} as never,
hubPublicKey: "hub-key",
toolGrantsForPins: () => [],
eventCollectors: {} as never,
cryptoProviderCache: { get: async () => ({}) as never },
Expand Down
20 changes: 19 additions & 1 deletion bun.lock

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

102 changes: 100 additions & 2 deletions docs/revendor-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,11 +366,11 @@ approval bundle, migrations 0082/0083) and `hub-api` (run trigger) all move
together, and `apps/sidecar` reads the frame both sides write. Leaving any
one on the old pin leaves the frame contract split down the middle.

Open conversion sites, all blocked on that one decision:
Open conversion sites:

| Site | What it needs |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `packages/folded-runs/src/launch.ts` (`deployAtHead`), `wake.ts` | A code-sourced deploy for the folded single-step run — the root blocker. |
| ~~`packages/folded-runs/src/launch.ts` (`deployAtHead`), `wake.ts`~~ | **Done** — see "Conversion step 2" below. |
| `apps/sidecar/src/workflow-host-wiring/index.ts`, `asset-materialization.ts` | Stop writing `workflow.json` and stop reading `projection.definition`; stage the closure instead. |
| `apps/sidecar/src/workflow-substrate-factory/index.ts`, `child-runtime.ts`, `config.ts` | Drop `WORKFLOW_DEFINITION_REPO_ID`/`_REF`; in-memory child spawn; `closurePackageDir` plumbing. |
| `apps/sidecar/src/workflow-deployment-record.ts` | Drop `referencedDefinitionHashes`; carry the grant-walk snapshot. |
Expand All @@ -380,3 +380,101 @@ 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.

#### Conversion step 2: `deployAtHead` is on the seam

`deployAtHead` no longer synthesizes a definition. It renders the run's
config into a per-run `@corbits/agent-runtime` package, commits that tree
into the run's OWN `workflow`-kind definition asset on
`refs/heads/runs/<runId>`, and deploys the resulting `commitSha` through
`deployAdoptedWorkflowFromSource` — the adopting shared-capacity front,
the only one that accepts a pre-minted anchor row and threads a
`credentialCipher`. `wake.ts` takes the same path. The old
synthesize-in-memory branch is deleted, not gated.

Reuse, not a second asset: one definition asset can back many runs (a
chat's workbench host, an invited agent's every launch), so each run gets
its own ref inside that asset rather than its own asset per deploy. The
pin is the `commitSha`, so the ref is bookkeeping.

The step's input selector became the config's `mode`: `step` (with the
workbench host's optional `literalInput`, the CL-6164 pin) or `section`
with a per-turn timeout. The Phase 1.3 swap changes a caller's argument,
never a branch inside `deployAtHead`. Section mode authors
`onBodyFailure: "continue"`, which the vendored surface now carries
through the live→inert projection.

##### What still blocks EXECUTION

Deploying works at the type and call level; nothing has run it end to
end, because the two sidecar prerequisites are untouched: nothing
produces `CLOSURE_PACKAGE_DIR`, and no `WorkflowProbeExecutor` is wired,
so every probe still answers `workflow.probe.error`. The remaining
in-tree typecheck failures are exactly the sidecar rows in the table
above — `projection.definition` reads, `createWorkflowSpawnChild` /
`createWorkflowSpawnSuspendableChild`, `SpawnTimeEnv.referencedDefinitionHashes`,
and `RunWorkflowChildBindings.workflowDefinitionRepoId`.

##### Defect surfaced by the conversion

`renderAgentRuntimeSourceTree` parses the config before writing it, which
is the first time a folded run's credential bindings are validated
against the platform's `CredentialBinding` schema. `apps/hub`'s
`mcp-credential-bindings.ts` mints `handle: "mcp:<slug>"`, and
`ToolCredentialHandle` is `/^[a-z0-9][a-z0-9._-]*$/` — the colon is not
in it, so every MCP-pinned launch would now fail closed at render time.
Nothing caught this before because the in-memory definition was never
parsed. Either the handle shape changes here (and with it the
`env.credentials.resolve("mcp:<slug>")` key `@corbits/mcp-tools` uses) or
upstream widens the handle grammar; it is not fixed in this change.
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/);
});
});
Loading
Loading