Skip to content

Verify original bundle seals before schema upgrades - #1928

Open
abrichr wants to merge 4 commits into
mainfrom
agent/1899
Open

abrichr wants to merge 4 commits into
mainfrom
agent/1899

Conversation

@abrichr

@abrichr abrichr commented Nov 5, 2026

Copy link
Copy Markdown
Member

Closes #1899.

Verify a sealed bundle’s original canonical payload before schema upgrades, and retain that verified identity for admission and runtime authorization. Unknown versions refuse; upgrades advance one explicit version at a time. Later workflow, asset or manifest changes still invalidate the seal.

Adds frozen linear and program bundles generated and self-verified by pinned Flow2.2.0,2.5.0 and2.6.0 source serializers, with CI load/seal/replay checks. The fixture README records the serializer environment and lockfile limits.

Validation:473 tests passed, including six headless Chromium replay cases, with zero skips. Ruff, whole-package mypy, generality, release consistency, source boundary and Flow wheel/sdist validation passed. A repeated six-case run retained runtime reports and screenshots.

The separate root archive scanner, outside Flow’s canonical archive job, reports four matches in unchanged policy/discussion files; its baseline archive was not reproduced. Three standard transport-excluded source files were not inspected. Historical release-wheel environments, live signing and the whole repository suite were not tested.

@abrichr

abrichr commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Independent adversarial review: APPROVE

Head: e38299f2f5d24cd67a1df270ac319a7440426cec. No P1 or P2 findings.

What I ran

  • tests/test_bundle_original_format.py and tests/test_bundle_release_compatibility.py through the CPU slot runner: 60 passed, 0 skipped.
  • ruff check on all changed Python files: clean. ruff format --check on ir.py and bundle_validation.py: clean.
  • A tamper script against the frozen 2.2.0 and 2.6.0 linear fixtures:
    • Rejected before migration: an edited step value, an added non-default field_label, an unknown top-level key, schema_version rewritten to 1, and a manifest digest swap.
    • Rejected with BundleSchemaError: schema_version set to 3 or true.
    • Still load and reseal correctly: a clean bundle, and default-valued omissions (field_label: null, risk_review_required: 0).
  • A type walk over the IR models. Every position that can contain an omission owner (Workflow, Step, Anchor, StructuralLocator, Effect, and so on) is covered by _SOURCE_V2_CHILDREN. No owner is reachable through an unmapped field, so a stripped default cannot hide inside a free-form mapping.

What I didn't run: the full repository suite, mypy, or CI. CI is held by the draft guard. The "Fail on a draft pull request" jobs fire as intended, so the draft guards are intact.

Merge resolution: The PR's flow hunks are identical before and after the merge, apart from one sidecar read in Workflow.load (ir.py:3720). That read now uses main's read_text(encoding="utf-8"), so both sides' intent is kept.

Findings

  • P3, ir.py:3727-3730 and ir.py:3554-3562: Workflow.load(verify_integrity=False) records an unverified original payload as _loaded_seal, and save() then reseals it. I reproduced this: a tampered bundle loaded this way becomes a validly sealed bundle. content_identity_digest already refuses that object, so the authorization paths stay closed. The only production caller, sanitized_artifact.py:797, verifies the source first at line 858, so I don't see a way to exploit it today. Suggested hardening: refuse to reseal when sha256(original_payload) != content_digest, unless the caller opts in explicitly.
  • P3, qualification_proposal.py:447: still uses compute_content_digest rather than content_identity_digest. For a historical bundle whose current-model digest differs from its seal, local-dev admission records a digest that run_gate will later refuse. This fails closed, but it is inconsistent with the other call sites.
  • P3, informational, not introduced here: a plaintext bundle with both its embedded and sidecar manifests stripped loads as unsealed and gets a fresh manifest. The seal is a hash, not a signature, so authenticity still rests on the admission and authorization digest binding.
  • P3: authorization_summary.py:118 and :195 still call model_validate_json directly, without migration or verification. Both are display or hash-only, and both fail closed.

This branch has not been deployed

No deployments
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.

Bundle schema versioning: old sealed bundles keep working after upgrades

1 participant