Skip to content

feat(sdk): flows build — immutable content-addressed bundle (#298) - #316

Merged
kjgbot merged 3 commits into
mainfrom
feat/flows-build-immutable-bundle
Sep 11, 2026
Merged

kjgbot merged 3 commits into
mainfrom
feat/flows-build-immutable-bundle

Conversation

@kjgbot

@kjgbot kjgbot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #298.

Summary

Adds flows build <flow> — canonical spec JSON + compiled TS with pinned deps + preflight declaration + identity signature, sealed into dist/flows/<name>@sha256:<hex>/ with a manifest linking every file to its sha256. Top-level digest is sha256(manifest.json), making the bundle content-addressed and tamper-evident.

Design decision

TS authored flows: because flow() bodies execute at runtime and cannot be canonicalized without side effects, this slice ships an exported-spec pattern — authors export both the flow() body and a validated spec declaration. flows build compiles the spec, not the body. The limitation is documented in the PR body and README.

Not in scope

  • flows deploy at digest
  • flows run flow@sha256:... from bucket
  • Remote push / registry

Written by codex agent spec-A-flows-build-v4 on finn-mini; head at 97aed52.

Test plan

  • linux-x64-artifact green
  • packed-consumer green

🤖 Generated with Claude Code


Note

Medium Risk
Introduces bundle signing, executable/asset bundling, and strict verification logic that will underpin digest-based deployment; mistakes could weaken integrity guarantees or ship wrong binaries, though run/deploy-by-digest is not wired yet.

Overview
Adds flows build and flows build --verify to seal flows into content-addressed directories under dist/flows/<name>@sha256:<digest>/, and documents the contract in docs/SURFACE.md.

Build pipeline: YAML flows compile and preflight at build time (credentials/workers/MCP probes deferred); relative ./ CLIs and deterministic command words are copied into assets/ and rewritten in the canonical spec. TypeScript flows require Bun, a matching package-lock.json / npm ci tree, and either a default-exported declarative spec or flow() plus an exported spec (body not executed at build); Bun also emits a compiled flow binary and embeds the lockfile.

Bundle format: sealBundle writes manifest + Ed25519 identity.json (from FLOWS_BUILD_KEY or repo .flows/build.key, else ephemeral with a stderr warning), reuses an existing valid digest directory, and verifyBundle fails closed on tampering, extras, symlinks, or digest mismatch (CLI exit 2). .flows/build.key is gitignored.

Vitest coverage exercises sealing, verification, asset capture/dedup, YAML/TS CLI paths, and invalid args.

Reviewed by Cursor Bugbot for commit 2e3a1bf. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 786a0e1a-91b1-4b85-8f52-806943a99fe9


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/sdk/src/cli/build.ts
Comment thread packages/sdk/src/cli/build.ts Outdated
@kjgbot

kjgbot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Review swarm: maintainability

No fresh transcript was produced for run be9190fe-335a-46b4-9231-702948e3a9ff (MISSING).

@kjgbot

kjgbot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Review swarm: history

No fresh transcript was produced for run be9190fe-335a-46b4-9231-702948e3a9ff (MISSING).

@kjgbot

kjgbot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Review swarm: structure

No fresh transcript was produced for run be9190fe-335a-46b4-9231-702948e3a9ff (MISSING).

@kjgbot

kjgbot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

🎯 review-swarm: FAILED (M:pass H:pass S:missing)

Lens transcripts posted as sibling comments above.

kjgbot pushed a commit that referenced this pull request Sep 11, 2026
…d-agent CLIs (#316)

Two HIGH Bugbot findings on flows#316 (Cursor Bugbot):

1. `packages/sdk/src/cli/build.ts:76` — build's preflight ran with no
   `modelRegistryPath` or `models` allowlist, so every declared model
   surfaced as `model_unknown` and refused the whole build. Load the
   nearest `flows.json` through the existing `readProjectConfig` helper
   and forward `models` + `modelRegistryPath` + `projectCli` +
   `projectSearchStart` into the preflight call. Live model probes stay
   deferred because build-time preflight cannot honestly claim a live
   model is reachable — but model existence, which is a build-provable
   fact, is now honored.

2. `packages/sdk/src/cli/build.ts:148` — captureFiles only walked the
   steps, so a flow whose CLI was inherited from `flow.cli` or from an
   entry in `agents:` never had that binary copied into the bundle. Add
   two capture passes before the step walk: `flow.cli` (top-level
   default) and each `flow.agents[name].cli`. Rewrite them to the
   `./assets/...` bundled path so the sealed spec never points outside
   the bundle. Step-level captures still run and still win when both
   are set.

The 20 bundle tests still pass end-to-end (including the two-run
byte-for-byte determinism, TS fixture build, and CLI-argument refusals).

Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82
@kjgbot
kjgbot force-pushed the feat/flows-build-immutable-bundle branch from 97aed52 to 08b9e03 Compare September 11, 2026 09:58

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/sdk/src/cli/build.ts
miyaontherelay and others added 3 commits September 11, 2026 14:47
Session-Id: 01a08f7d-7049-7f30-ad8d-447f6cee21a2

Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82

Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82
…d-agent CLIs (#316)

Two HIGH Bugbot findings on flows#316 (Cursor Bugbot):

1. `packages/sdk/src/cli/build.ts:76` — build's preflight ran with no
   `modelRegistryPath` or `models` allowlist, so every declared model
   surfaced as `model_unknown` and refused the whole build. Load the
   nearest `flows.json` through the existing `readProjectConfig` helper
   and forward `models` + `modelRegistryPath` + `projectCli` +
   `projectSearchStart` into the preflight call. Live model probes stay
   deferred because build-time preflight cannot honestly claim a live
   model is reachable — but model existence, which is a build-provable
   fact, is now honored.

2. `packages/sdk/src/cli/build.ts:148` — captureFiles only walked the
   steps, so a flow whose CLI was inherited from `flow.cli` or from an
   entry in `agents:` never had that binary copied into the bundle. Add
   two capture passes before the step walk: `flow.cli` (top-level
   default) and each `flow.agents[name].cli`. Rewrite them to the
   `./assets/...` bundled path so the sealed spec never points outside
   the bundle. Step-level captures still run and still win when both
   are set.

The 20 bundle tests still pass end-to-end (including the two-run
byte-for-byte determinism, TS fixture build, and CLI-argument refusals).

Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82

Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82
captureFiles wrapped every named-agent CLI capture in Promise.all, but
the map guard held resolved strings that were only written AFTER await.
Two agents sharing the same ./cli path both passed the has() check and
both pushed the same assets/... entry — then sealBundle refused the
whole flow as a duplicate manifest path.

Store the in-flight promise instead, so concurrent captures of the same
path return the same resolved target and the file is pushed exactly
once. Adds a regression test with two named agents sharing a CLI path;
the manifest has one asset entry and both lowered steps reference it.

Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82

Session-Id: efeda5df-9b7c-48d4-b2ce-957f5bef0a82
@kjgbot
kjgbot force-pushed the feat/flows-build-immutable-bundle branch from 5a913ac to 2e3a1bf Compare September 11, 2026 12:50
@github-actions

Copy link
Copy Markdown

Review swarm: FAILED

  • maintainability: MISSING
  • history: MISSING
  • structure: MISSING

Cloud run: be9190fe-335a-46b4-9231-702948e3a9ff

@kjgbot
kjgbot merged commit 3a078ab into main Sep 11, 2026
6 of 7 checks passed
@kjgbot
kjgbot deleted the feat/flows-build-immutable-bundle branch September 11, 2026 12:58

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2e3a1bf. Configure here.

});
const refusals = report.diagnostics.filter(d => d.severity === 'refusal' && d.kind !== 'probe_failed');
if (refusals.length > 0) throw new Error(refusals.map(d => `[${d.kind}] ${d.message}`).join('\n'));
authoring = await captureFiles(authoring, directory, files);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Project CLI omitted from sealed bundle

High Severity

flows build feeds flows.json cli into preflight so those flows pass, but captureFiles never records or copies that CLI. toKernelSpec also does not lower project CLI onto steps. The sealed spec can therefore omit the CLI that preflight accepted, and a relative project CLI never enters assets/.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2e3a1bf. Configure here.

if (!path.includes('/')) return Promise.resolve(path);
if (!path.startsWith('./') || path.split('/').includes('..') || path.includes('\\')) {
return Promise.reject(new Error(`${path}: bundle file references must start with ./ and stay inside the flow directory`));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unprefixed relative CLIs cannot bundle

Medium Severity

capture treats any CLI containing / that does not start with ./ as invalid. flows check already resolves those paths against the spec directory, and first-party flows such as testdata/hn-monitor.flow.yaml declare cli: preflight/analyze-story-claude-cli. Building those valid specs now fails.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2e3a1bf. Configure here.

const ref = (match[1] ?? match[2] ?? match[3])!;
const start = match.index! + match[0].indexOf(ref);
command = command.slice(0, start) + await capture(ref) + command.slice(start + ref.length);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redirection targets captured as required assets

Medium Severity

The command scanner treats every ./ word after >, <, or whitespace as a bundle input. A deterministic step that redirects onto ./out.txt therefore lstats an output path and refuses the build when that file does not already exist.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2e3a1bf. Configure here.

command = command.slice(0, start) + await capture(ref) + command.slice(start + ref.length);
}
if (/^\s*["']?\//.test(command)) throw new Error(`${step.id}: absolute command paths cannot be bundled`);
steps.push({ ...step, command });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parent-relative commands escape the bundle

Medium Severity

Absolute command paths are refused, but ../ words never match the ./ capture pattern and are not rejected. The sealed spec can keep a parent-relative command that walks out of the digest directory at run time.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2e3a1bf. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

flows: content-addressed immutable bundle (flows build) — SURFACE §4

2 participants