Skip to content

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

Description

@kjgbot

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:
    1. FLOWS_BUILD_KEY env var (base64-encoded 32-byte seed), OR
    2. <repo>/.flows/build.key (base64-encoded 32-byte seed; gitignored), OR
    3. 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:

  1. 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).
  2. Determinism test: build the same input twice → identical directory names, identical manifest.json, identical every-file sha256.
  3. 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.
  4. 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.
  5. 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.

No activity

Activity on this issue will appear here.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions