Skip to content

feat(bag): production readiness Phases 0–1.5 + partial 2 (security hard-close; removes banned nix) - #11

Merged
hyperpolymath merged 13 commits into
mainfrom
feat/prod-readiness-phase0
Jun 29, 2026
Merged

hyperpolymath merged 13 commits into
mainfrom
feat/prod-readiness-phase0

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

bag-of-actions → production readiness: Phases 0–1.5 + partial Phase 2

Lands the prod-readiness work that had accumulated, unpushed, on feat/prod-readiness-phase0 (rebased onto current main, commit series preserved, linear history). Closes the live security holes from the production-readiness audit and removes the banned nix capability that main currently ships.

What's in it

  • Phase 0 — foundations: src/crypto.zig + src/store.zig extracted from main.zig (pure refactor); mix release + fail-closed Bag.Config (gated to prod boot — test/dev stay green); de-templated Justfile (real build/test/release + estate-sync rosters guard); docs reconciled honestly (anti-overclaim).
  • Phase 1 — security hard-close (the live exploit): removed the hardcoded dev-HMAC-key fallback → BAG_ATTEST_KEY mandatory (≥32B) or refuse (exit 78); BAG-CHECK-BATON v2 envelope binding mode + key_id; BAG_MODE (prod-default, fail-closed); mandatory ed25519 signing + verification (no HMAC-only / unsigned acceptance).
  • Phase 1.5 — bridge greening gate: Bag.GitHubBridge refuses to post a green status unless the verdict is mode=prod + ed25519:verified — a dev/laptop node can never green a required check (+ 138-line test).
  • Phase 2 (partial): guix/manifest.scm + guix/bootstrap.sh (reproducible node toolchain), verified on a real mesh-server-1.
  • Fixes a live estate-policy violation: removes the banned nix capability from src/estate.zig / Protocol.idr / Estate.idr (Guix is canon). main currently ships it via the PR feat(ci): report Baton verdicts to GitHub (commit-status bridge) + nix capability #7 bridge merge; this lands the removal.

Verified green (rebased tree, run not asserted)

  • zig build ✓ (+ zig-out/lib/zig_fmt.wasm installed)
  • idris2 --typecheck verification/proofs/bag.ipkg ✓ (4 modules)
  • cd bag && mix test → 1 doctest, 35 tests, 0 failures
  • ./scripts/ci-baton-demo.sh → step-0 fail-closed (no key → exit 78), pass/fail/suspended/pass, tamper rejected two ways (hmac + unsigned)
  • just estate-sync → "all three estate manifests agree"

Not in this PR (forward roadmap)

Phase 2-finish (per-node Guix configs, laptop as 2nd node, pilot branch-protection cut-over), Phase 3 (durable store / claim-lease-commit registry / hash-chained ledger + custody chain / SSH-mailbox transport / dynamic roster / conformance gate), Phase 4 (zero-GHA webhook + observability), Phase 5 (Wasm linear-memory snapshot migration), Phase 6 (estate rollout + OSS scaffolding). Plan: ~/.claude/plans/woolly-zooming-gizmo.md.

🤖 Generated with Claude Code

hyperpolymath and others added 13 commits June 29, 2026 13:57
…eck on default build

The verdict runner and GitHubBridge were already correct (exit!=0 -> verdict=fail
-> non-green status; only :pass is green). The dogfood's 4/32 failures + the demo's
'honest fail' showing pass were a FIXTURE bug: src/estate.zig was used as the
'badly-formatted' negative fixture and had since been formatted clean, so every
'should fail' case silently passed. Generate the dirty fixture at runtime instead,
so it cannot rot to clean. Also install zig_fmt.wasm to zig-out/lib/ on the default
build (was only built by 'zig build wasm'), so the wasm:// dogfood check is runnable
from a fresh build. mix test now 32/32; demo step (2) now an honest VERDICT=fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t wire-ready owned-CI trigger

A 'nix' capability (tag 13) was added 2026-06-14, before the 2026-06-22 estate
Nix ban; 'guix' (tag 4) already existed and mesh-server-1 already carried it, so nix
was a redundant, now-banned duplicate. Removed it across all three layers
(Protocol.idr / Estate.idr / estate.zig / mesh.ex) + the snifs-report test + the doc.
The formal ABI still typechecks (idris2, 4 modules), zig builds, and a check that
requests the removed cap now fails safe ('Unknown capability', no silent pass).
Adds .github/workflows/owned-ci.yml: Option A thin-GHA dispatcher that runs the
dogfood manifest on an owned guix node at 0 further GitHub minutes and reports
verdicts back. It is a no-op until the owner sets BAG_NODE_HOST + BAG_NODE_SSH_KEY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pure)

Factor the duplicated leaf crypto/key helpers out of the 1309-line main.zig into src/crypto.zig (bytesToHex, hexDecode, readEd25519Seed, hashFileSha256) and add a new crash-safe src/store.zig (atomicWrite: write-tmp+fsync+rename+dir-fsync). CheckBaton method signatures and the BAG-CHECK-BATON envelope/HMAC/ed25519 bytes are unchanged; the methods now call crypto.* internally. store.atomicWrite is provided+tested but not yet wired into Baton.save/freeze. A refAllDecls test block pulls both modules' tests into zig build test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…laim)

Align README.md milestone list with README.adoc and the code: M3 Verified ABI and M4 Guix are Complete (M3 with an honest 'proofs typecheck, not invoked at runtime' caveat). Fix README.adoc broken links (CONTRIBUTING.md/SECURITY.md do not exist) to point at GOVERNANCE/MAINTAINERS/docs/.github. Soften 'Powered by Ephapax' and 'Mobile Wasm Contexts' to match what the code does; reframe Trust Attestation from a future Milestone 3 line to the shipped HMAC-SHA256 + optional ed25519 reality. Add an EXPLAINME Trust-Attestation claim entry, document the closed gaps (negative-fixture rot now generated; duplicate nix capability removed under the Nix ban), and tighten the standing Known-gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…README.adoc

Follow-up to 994b9c5 per review brief. EXPLAINME: add the still-open gaps that were missing (static estate manifest; cross-machine Baton transport not yet built), frame the Idris gap as a planned-but-unbuilt build-time conformance gate, and record the fail-closed verdict path as resolved (runner exit!=0 => fail; GitHubBridge maps only :pass->success). README.adoc status table: drop the 'via Zig FFIs' overclaim for M4 (the host shells out to the guix CLI; no FFI exists in src/) and add the 'proofs typecheck, not runtime-invoked' caveat to M3 so the canonical doc matches EXPLAINME. No SPDX/licence lines touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add bag/config/{config,dev,test,prod,runtime,releases}.exs and Bag.Config (bag/lib/bag/config.ex). Bag.Config.validate!/0 reads + validates the required prod env (BAG_ATTEST_KEY, GH_TOKEN, BAG_STATE_DIR, plus the Zig host binary; optional BAG_SIGN_KEY_PATH must exist if set) and FAILS CLOSED with a clear message listing every missing item. TRAP 1: validate!/0 is invoked ONLY from config/runtime.exs behind a `config_env() == :prod` guard, so it never runs under :test/:dev — `mix test` and `mix run` work with no env vars set (verified 32/32 with all bag env vars unset). Add a releases: block to mix.exs; `MIX_ENV=prod mix release --overwrite` assembles. Bag.Executor now resolves its binary via Bag.Config.executor_path/0 so BAG_BIN can override.

De-template the 784-line Justfile in place (NOT a rewrite): replace rsr-template project metadata + the ~30 placeholder TODO recipes with real Zig+Elixir+Idris recipes (build/test/fmt/lint/run/release/typecheck/estate-sync/version). TRAP 2: PRESERVE all estate machinery — the import? lines (contractile, init, groove, assess, validate, proofs), assail/maint-assault panic-attack hooks, STATE.a2ml state recipes, git-cliff changelog, session bindings, CRG/cookbook/man. `clean` no longer deletes build/ (estate machinery lives there). Removed the nix-shell recipe (estate Nix ban); kept guix-shell/guix-build. estate-sync diffs the node roster across Estate.idr/estate.zig/nodes.scm and exits non-zero on drift; version is read-only documentation of the four version-bearing files (Justfile, bag/mix.exs, build.zig.zon, .machine_readable/6a2/STATE.a2ml) — version number left at 0.1.0. SPDX/licence header untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, mandatory ed25519

Closes the forgeable-verdict hole in the Zig host (src/main.zig):

- attestKey dev-key fallback DELETED. activeAttestKey requires BAG_ATTEST_KEY
  present and >= 32 bytes, else exit 78 (no integrity-only default).
- ed25519 signing is now MANDATORY on freeze (signOrExit aborts if the signing
  key is absent/unreadable) — replaces the old fail-open HMAC-only path.
- thaw uses checkAttestation: a verdict is accepted ONLY if it is v2, HMAC-valid,
  carries an ed25519 signature, AND that signature verifies. An unsigned/HMAC-only
  envelope (the exploit) now returns VERDICT=tampered, exit 3.
- BAG-CHECK-BATON bumped v1 -> v2; `mode` (BAG_MODE, default prod) and `key_id`
  (BAG_ATTEST_KEY_ID, default default) are bound into canonicalString, so a v1
  envelope can never produce a matching digest — v1 acceptance dropped
  cryptographically, not just by policy.
- key_id keyring: thaw resolves the verifying key from
  ~/.config/bag/attest-keys/<key_id> (overlap-window rotation), falling back to
  BAG_ATTEST_KEY when its id matches. validKeyId rejects path traversal.

Verified: zig build + zig build test green (8 new security tests); demo shows
unset/short key -> exit 78, tamper/HMAC-only/key_id-traversal -> exit 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Zig host now refuses to freeze an unsigned / non-prod verdict; this closes
the Elixir end so such a verdict can never be POSTed as a green required check.

- Bag.GitHubBridge.report_check refuses to post `success` unless the caller
  proves the verdict was attested mode=prod + ed25519:verified via the new
  `:attestation` option, returning {:refused, {:unverified_attestation, _}}
  WITHOUT calling the runner. Non-green verdicts post as before.
- verdict_attestation/1 parses the authoritative Zig `thaw` output
  (mode=… + ed25519:verified) into the gate value — the real verification
  primitive the mesh threads through in Phase 3 (thaw-before-report).
- report_sweep threads per-result :attestation; absent ⇒ green refused.
- bag.report surfaces REFUSED posts and exits non-zero (GREEN REFUSED) rather
  than crashing on the new return shape.
- Bag.Config min key length 16 → 32, in lock-step with the Zig host so a key one
  side accepts and the other rejects can't cause a boot/runtime split-brain.
- test_helper provides a deterministic >=32-byte BAG_ATTEST_KEY + an ephemeral
  ed25519 signing key so the integration tests can exercise the fail-closed host.

Verified: cd bag && mix test → 35 tests + 1 doctest, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… store

- ci-check-baton.adoc: envelope v1 → v2; mode/key_id fields; attestation is now
  fail-closed and ed25519-mandatory (freeze exit 78, thaw exit 3); key_id keyring
  rotation; bridge greens only mode=prod + ed25519:verified.
- EXPLAINME.adoc: replace the stale "dev placeholder / signing best-effort"
  caveat with the fail-closed reality.
- docs/secret-store.adoc (new): Phase 1.6 ops procedure — distribute the shared
  BAG_ATTEST_KEY via age, per-node ed25519 signing key, key_id keyring layout, and
  zero-downtime overlap-window rotation. (age install is a node prerequisite.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-server-1)

guix/manifest.scm: the bag-pilot toolchain, pinned to what base Guix 1.5.0
actually ships (zig 0.15.2, elixir 1.19.3, just, age, jq) — verified installed on
the Hetzner node. idris2 + wasmtime are NOT in Guix (Idris 1 only; wasmtime
unpackaged) → documented as out-of-band acquisition, not needed for the first
$0 green check. echidna caps (rust/julia/GNAT-SPARK/z3) deferred to Phase 6.

guix/bootstrap.sh: idempotent staged bring-up for Debian 13 + Guix-as-pkgmgr.
Corrected against the real bring-up: Debian 13 dropped the `guix` apt package, so
it uses the official installer (needs `uidmap`/newgidmap, driven with newlines).
Adds a swapfile first (guix pull OOMs on 4GB). `all` skips `pull` since base 1.5.0
suffices. `extra` stubs the idris2/wasmtime acquisition.

Verified end-to-end on mesh-server-1 (Debian 13, Hetzner CPX22): Guix 1.5.0
installed, toolchain in, `zig build` produces the host, and ci-baton-demo.sh
passes — including the fail-closed exit-78 and the tamper/HMAC-only exit-3
rejections — on real owned compute.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hyperpolymath
hyperpolymath enabled auto-merge (rebase) June 29, 2026 13:00
@hyperpolymath
hyperpolymath disabled auto-merge June 29, 2026 13:01
@hyperpolymath
hyperpolymath merged commit c08eff4 into main Jun 29, 2026
3 of 4 checks passed
@hyperpolymath
hyperpolymath deleted the feat/prod-readiness-phase0 branch June 29, 2026 13:03
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.

1 participant