Spec section (SURFACE.md §4)
flows build seals a flow into a content-addressed, immutable bundle: canonical spec JSON, compiled TS with pinned deps, helper/plugin lockfile, assets, preflight declaration, identity signature — flow@sha256:…, pushed to a bucket/registry. flows deploy points a trigger at a digest; flows run flow@sha256:… executes from the bucket on any cell, no checkout. Preflight runs at build time for everything build-provable and again at deploy time for environment facts (credentials, workers, MCP servers). The working tree is for authoring; production only ever runs digests.
This slice ships the flows build verb only — the local, content-addressed, immutable bundle. flows deploy, remote push to a bucket/registry, and flows run flow@sha256:… are separate follow-up slices.
Scope
Add a new CLI verb flows build:
flows build [--out <dir>] <flow.yaml|flow.ts>
Produces an output directory:
<out>/<name>@sha256:<hex>/
├── manifest.json # ordered list of {path, sha256, bytes}; the bundle digest = sha256(canonical(manifest.json))
├── spec.canonical.json # canonical-form spec (packages/sdk/src/canonical.ts result)
├── flow # for TS flows: single-file executable (bun build --compile output); omitted for YAML flows
├── preflight.json # preflight declaration produced by packages/sdk/src/preflight.ts
├── lockfile.json # pinned dep versions: for TS, resolved package-lock.json subset for the flow's dependency closure; for YAML, an explicit `{version: 1, adapters: []}` (adapters land with the helper-namespace slice)
├── identity.json # {algorithm, keyid, signature_hex} covering sha256(manifest.json)
└── assets/ # (present only if referenced by the flow)
--out defaults to dist/flows/.
- The final path segment
<name>@sha256:<hex> uses the canonical spec name (name field on the flow) and the manifest digest.
- If a bundle with the same digest already exists at that path,
flows build prints its path and exits 0 — bundles are immutable, so recomputing the same one is a no-op.
Determinism requirements
- Same input → same digest byte-for-byte, on the same platform.
- Cross-platform digest equality is NOT a gate for this slice (bun's single-file compile outputs differ per host arch); the manifest records
platform: {os, arch} inside spec.canonical.json sibling metadata to make cross-platform divergence explicit rather than surprising. TS flows built on Linux x64 must be reproducible on Linux x64.
- No timestamps in bundle files. No absolute paths in bundle files. No environment leakage (
HOME, PWD, etc.).
manifest.json lists files in a stable order (sorted by path). Each entry is {path, sha256, bytes}. The manifest is itself canonicalized before its sha256 is taken.
- The top-level digest is
sha256(canonicalize(manifest.json)).
Identity signature
- Algorithm: ed25519 by default. Key discovery order:
FLOWS_BUILD_KEY env var (base64-encoded 32-byte seed), OR
<repo>/.flows/build.key (base64-encoded 32-byte seed; gitignored), OR
- If neither present, generate an ephemeral keypair per build and print a warning:
identity_ephemeral: bundle can be verified but not attributed.
identity.json: {algorithm: "ed25519", keyid: <sha256(pubkey)[:16]>, pubkey_b64, signature_hex} where signature_hex is over sha256(manifest.json).
- The signature covers the bundle digest, not the source tree, so the bundle is self-verifiable without the working tree.
Verification
Provide a small verify helper flows build --verify <bundle-dir>:
- Recomputes each file's sha256 and compares to
manifest.json.
- Recomputes
sha256(manifest.json), confirms it matches the directory name suffix.
- Verifies
identity.json signature against the recomputed digest.
- Any mismatch: exit 2, print which file, which check failed. Do not exit 0 on any mismatch.
What is NOT in scope this slice
flows deploy — points a trigger at a digest. Separate slice.
- Remote push (bucket/registry upload). Bundles stay local this slice.
flows run flow@sha256:… — running a bundle. Separate slice.
- Signing key management beyond env/local file. No KMS, no keyring, no rotation.
- Cross-platform reproducibility of
bun build --compile — different host arch will produce different flow files by design.
- Any change to canonical.ts semantics or preflight.ts semantics — consume both as-is.
- YAML asset resolution beyond files already referenced by the compiled spec.
- Backwards compatibility shims — this is a new verb.
Acceptance evidence
The PR must include:
- Bundle output structure verified for
testdata/hello-deterministic.flow.yaml (YAML path) AND for a small TS flow (place it under testdata/ next to existing fixtures).
- Determinism test: build the same input twice → identical directory names, identical
manifest.json, identical every-file sha256.
- Tamper test: after building, mutate any one file in the bundle (a byte flip in
spec.canonical.json) → flows build --verify exits 2 with a diagnostic naming the tampered file.
- Ephemeral-key path: unset
FLOWS_BUILD_KEY, remove .flows/build.key, run build → emits the identity_ephemeral warning to stderr; signature still verifies against the pubkey in identity.json.
- New SDK test file at
packages/sdk/tests/bundle.test.ts exercising (2), (3), (4).
Files to touch
packages/sdk/src/cli/build.ts (new) — verb handler.
packages/sdk/src/bundle.ts (new) — bundle assembly, manifest computation, signing.
packages/sdk/src/cli.ts (edit) — wire the build verb.
packages/sdk/src/cli-executable.ts (edit if it registers the verb list) — wire.
packages/sdk/tests/bundle.test.ts (new).
docs/SURFACE.md (edit) — remove flows build from the "not yet implemented" language if any; keep §4 wording intact.
Not-in-scope guard
Do NOT modify packages/sdk/src/canonical.ts or packages/sdk/src/preflight.ts semantics. Consume them via their exported APIs. If either exposes an API that is insufficient for this slice (e.g., preflight lacks a JSON-serializable output), add a new exported helper in a new file rather than reshaping the existing modules.
Testing expectations
- All existing kernel tests continue to pass:
cd kernel && cargo test --workspace.
- All existing SDK tests continue to pass:
cd packages/sdk && npm test.
- New tests land in
packages/sdk/tests/bundle.test.ts.
- The bundle assembly logic (manifest computation, canonicalization, signing) is unit-tested; the CLI verb has one end-to-end integration test invoking the compiled CLI against a fixture.
Prior art in this repo
packages/sdk/src/canonical.ts — the canonicalization already used by spec.canonical.json snapshot fixtures under testdata/.
testdata/hello-deterministic.spec.canonical.json and testdata/hello-deterministic.spec.sha256 — how canonical form + sha256 are already used.
packages/sdk/src/preflight.ts — preflight already produces a report shape.
Related
- Later slices:
flows deploy (points a trigger at a digest), flows run flow@sha256:… (execute from a bundle), plugin/helper lockfile expansion once f.slack and friends land.
Spec section (SURFACE.md §4)
This slice ships the
flows buildverb only — the local, content-addressed, immutable bundle.flows deploy, remote push to a bucket/registry, andflows run flow@sha256:…are separate follow-up slices.Scope
Add a new CLI verb
flows build:Produces an output directory:
--outdefaults todist/flows/.<name>@sha256:<hex>uses the canonical spec name (namefield on the flow) and the manifest digest.flows buildprints its path and exits 0 — bundles are immutable, so recomputing the same one is a no-op.Determinism requirements
platform: {os, arch}insidespec.canonical.jsonsibling metadata to make cross-platform divergence explicit rather than surprising. TS flows built on Linux x64 must be reproducible on Linux x64.HOME,PWD, etc.).manifest.jsonlists files in a stable order (sorted by path). Each entry is{path, sha256, bytes}. The manifest is itself canonicalized before its sha256 is taken.sha256(canonicalize(manifest.json)).Identity signature
FLOWS_BUILD_KEYenv var (base64-encoded 32-byte seed), OR<repo>/.flows/build.key(base64-encoded 32-byte seed; gitignored), ORidentity_ephemeral: bundle can be verified but not attributed.identity.json:{algorithm: "ed25519", keyid: <sha256(pubkey)[:16]>, pubkey_b64, signature_hex}wheresignature_hexis oversha256(manifest.json).Verification
Provide a small verify helper
flows build --verify <bundle-dir>:manifest.json.sha256(manifest.json), confirms it matches the directory name suffix.identity.jsonsignature against the recomputed digest.What is NOT in scope this slice
flows deploy— points a trigger at a digest. Separate slice.flows run flow@sha256:…— running a bundle. Separate slice.bun build --compile— different host arch will produce differentflowfiles by design.Acceptance evidence
The PR must include:
testdata/hello-deterministic.flow.yaml(YAML path) AND for a small TS flow (place it undertestdata/next to existing fixtures).manifest.json, identical every-file sha256.spec.canonical.json) →flows build --verifyexits 2 with a diagnostic naming the tampered file.FLOWS_BUILD_KEY, remove.flows/build.key, run build → emits theidentity_ephemeralwarning to stderr; signature still verifies against the pubkey inidentity.json.packages/sdk/tests/bundle.test.tsexercising (2), (3), (4).Files to touch
packages/sdk/src/cli/build.ts(new) — verb handler.packages/sdk/src/bundle.ts(new) — bundle assembly, manifest computation, signing.packages/sdk/src/cli.ts(edit) — wire thebuildverb.packages/sdk/src/cli-executable.ts(edit if it registers the verb list) — wire.packages/sdk/tests/bundle.test.ts(new).docs/SURFACE.md(edit) — removeflows buildfrom the "not yet implemented" language if any; keep §4 wording intact.Not-in-scope guard
Do NOT modify
packages/sdk/src/canonical.tsorpackages/sdk/src/preflight.tssemantics. Consume them via their exported APIs. If either exposes an API that is insufficient for this slice (e.g., preflight lacks a JSON-serializable output), add a new exported helper in a new file rather than reshaping the existing modules.Testing expectations
cd kernel && cargo test --workspace.cd packages/sdk && npm test.packages/sdk/tests/bundle.test.ts.Prior art in this repo
packages/sdk/src/canonical.ts— the canonicalization already used byspec.canonical.jsonsnapshot fixtures undertestdata/.testdata/hello-deterministic.spec.canonical.jsonandtestdata/hello-deterministic.spec.sha256— how canonical form + sha256 are already used.packages/sdk/src/preflight.ts— preflight already produces a report shape.Related
flows deploy(points a trigger at a digest),flows run flow@sha256:…(execute from a bundle), plugin/helper lockfile expansion oncef.slackand friends land.