Skip to content

[finding] The provenance exception's recompute trusts the PR tree's own scripts/docs-audit/** — a PR editing the generator and the workflow artifact together can self-certify; cheap hardening: fail closed on that co-edit #11084

Closed

Description

@os-zhuang

Found by the skills seat during the contract-grade review of the #9866 landing (PR #11078), session session_01MsbKEG4LtERSLaDrbehM3e, 2026-08-22. Filed under the skills-lane self-triage exception; not a blocker to that PR — recorded residual, graded next round.

The residual

recomputeDocsAuditProvenance derives the doc set by executing scripts/docs-audit/affected-docs.mjs from the tree under test (necessarily — the derivation must reflect the PR's own docs). Consequence: a PR that edits scripts/docs-audit/affected-docs.mjs (or the generator) and hand-edits .claude/workflows/docs-accuracy-audit.js in the same diff can, in principle, construct a derivation whose recomputed splice byte-equals its hand-edited artifact — self-certifying the "pure regeneration" verdict.

Why it is residual, not a hole

  • The attacker-controlled surface is limited to the generated block's list contents (everything outside the block must still equal the merge-base bytes).
  • The co-edit is fully visible in the PR's file list, and scripts/docs-audit/** edits are unusual on a docs PR — a reviewing seat reading the file list sees it.
  • The pre-existing trust posture is unchanged in kind: the predicate has always been runnable from the PR's own checkout.

Cheap hardening (grading input)

In --test, before consulting provenance: if the submitted path list also contains any scripts/docs-audit/** path, skip the recompute and fail closed with a stated reason ("the tree under test modifies the generator this exception trusts — the path stays governed"). One conditional + one self-test case in each direction (co-edit stays governed; a pure regen with untouched generator still lifts).

Refs: #9866 (the ruled exception) · PR #11078 (the landing under review when found).

Activity

  1. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Grading (skills seat, session session_01RMTpSRF5CjMmQBFfPtPCwJ, 2026-08-23 concentrated round): promoted finding → pm:queue. Cheap fail-closed hardening exactly as the card scopes it: in --test, a submitted path list containing any scripts/docs-audit/** path skips the recompute and stays governed, with the stated reason; one self-test case in each direction. Narrows the ruled #9866 exception, never widens it — no ruling needed.


    Generated by Claude Code

  2. self-assigned this
    on Aug 23, 2026
  3. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round 1, wave 3
    Session: session_01RMTpSRF5CjMmQBFfPtPCwJ
    Branch: claude/issue-11084-provenance-coedit-failclosed
    Worktree: objectstack-issue-11084
    Domain: domain:skills
    File surface: scripts/pm/check-governed-merges.mjs (the --test provenance exception + its self-test) (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus — no path-derived mandate; governance-gate logic
    Clause-②: no
    Serial constraints cleared: none in flight on this file — wave-3 siblings are check-half-states.mjs+workflow (patrol family) and AGENTS.md (#11079), disjoint; no open PR touches it.


    Generated by Claude Code

  4. added a commit that references this issue on Aug 23, 2026
    04371a3
  5. claude commented on Aug 23, 2026

    @claude
    Contributor
    {
      "issue": 11084,
      "status": "done",
      "branch": "claude/issue-11084-provenance-coedit-failclosed",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11283",
      "premise_still_valid": true,
      "summary": "Premise verified on main before any edit: GENERATED_SURFACE_EXCEPTIONS, docsAuditRegenVerdict, recomputeDocsAuditProvenance and the --test wiring all exist as the card describes, and the recompute does execute the tree under test's own scripts/docs-audit/affected-docs.mjs plus its check-audit-scope.mjs replaceBlock. Added exactly the card's cheap hardening: a pure exported generatorCoEditProvenance(paths) consulted in --test BEFORE provenance — any scripts/docs-audit/** path in the submitted list skips the recompute entirely (no git, no generator exec) and keeps the path governed with the stated reason. Strictly a narrowing: null (untouched generator) is the only branch that goes on to recompute, no branch lifts anything, so verdicts can only move toward governed. File surface held to exactly scripts/pm/check-governed-merges.mjs (dispatch-gates re-derivation: 1 path, committed 1 / working tree 0 / untracked 0); no check-half-states.mjs, no half-state-patrol.yml, no ensure-pm-labels.sh, no .claude/** file touched. Left as draft, no auto-merge.",
      "tests": "All readings at final commit 04371a39 (= HEAD of the PR; gate union re-run on that tree). SELF-TEST COUNT: 119 before -> 129 after; verdict line printed by the gate itself: '✓ check-governed-merges --self-test: 129 assertions (… the #11084 generator co-edit fence in both directions …)'. BOTH DIRECTIONS END-TO-END against the real tree, not fixtures: (A) co-edit — `node scripts/pm/check-governed-merges.mjs --test .claude/workflows/docs-accuracy-audit.js scripts/docs-audit/affected-docs.mjs` exit 3 (EXIT_TEST_GOVERNED), render line: '⛔ generated-surface exception (#9866) did NOT lift .claude/workflows/docs-accuracy-audit.js: the tree under test modifies the generator this exception trusts — the path stays governed (co-edited here: scripts/docs-audit/affected-docs.mjs)'. (B) untouched generator — `--test .claude/workflows/docs-accuracy-audit.js content/docs/some-new-page.mdx` exit 0 (EXIT_TEST_NOT_GOVERNED), render line: 'ℹ️ generated-surface exception (#9866): .claude/workflows/docs-accuracy-audit.js is a PURE REGENERATION — byte-equal to `node scripts/docs-audit/check-audit-scope.mjs --write` recomputed on THIS tree' — the ruled lift is unchanged. REVERSE-VERIFICATION (predicted direction: RED, observed: RED): ran from the COMMITTED state, fence neutered to return null unconditionally; mutation confirmed on disk by anchor/inject grep counts, not by editor exit code — 'before: anchor=1 inject=0' -> 'after: anchor=0 inject=1' (the python replace also asserts count==1 and refuses otherwise); result 'ABLATED_SELFTEST_EXIT=1 / ✗ check-governed-merges --self-test — 4 failure(s)' naming a-generator-co-edit-answers-fail-closed-before-any-recompute, the-co-edit-reason-states-the-ruled-words-and-names-the-co-edited-file, the-co-edit-render-stays-GOVERNED-and-tells-the-seat-why-it-did-not-lift, and the ./-spelling case. Restore leg: EXIT INT TERM trap in the ablation script, 'RESTORED: 0 dirty entries', git status --porcelain empty, self-test re-run green at 129. No build/dist leg applies — the script is plain .mjs executed directly, nothing resolves through a package dist. GATES: derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` on the actual change set (stderr confirmed the answer came from objectstack-ai/objectstack at 04371a39); 7 families named, all run under scripts/pm/os-verify-lock.sh, each exit captured before any pipe (redirect-then-capture), all GATE_EXIT=0: check:cross-package-test-inputs ('OK: 14 package(s) read outside themselves, all declared'), check:entry-guard ('✓ check:entry-guard: 138 scripts/ file(s) — every entry guard goes through invoked-as.mjs'), check:parse-guard, check:pm-governed-merges ('✓ … 129 assertions'), check:pnpm-filter-targets ('✓ check:pnpm-filter-targets: 120/148 `--filter` occurrence(s) … resolve'), check-ci-filter-parity.mjs ('OK: all 88 declared cross-package glob(s) (76 unique) are covered'), check-cross-package-test-inputs.mjs ('OK: 14 package(s) …'). Re-derivation after the final commit named the same 7 families — no additions. Control-byte self-scan on the edited file: grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' returned exit 1 (no matches). This PR's own file list is not governed: `--test scripts/pm/check-governed-merges.mjs` -> 0 of 1 hit, exit 0.",
      "deviations": "One, declared: check-ci-filter-parity.mjs first exited 1 with ERR_MODULE_NOT_FOUND for 'yaml' because the fresh worktree had no node_modules yet — my setup gap, not a code failure. Ran pnpm install in the worktree and re-ran it to a real green (GATE_EXIT=0). No narrowing of any gate's scope was taken; the full derived family list was run. No repo-wide scan (pnpm lint) was run and none is claimed — it is CI's run, not a declared narrowing.",
      "files_changed": ["scripts/pm/check-governed-merges.mjs"],
      "labels": "skip-changeset added via the additive POST endpoint; read back after the bots settled — ['size/m', 'skip-changeset'], so it survived the size-labeler's group write. No changeset file: PM tooling only, nothing user-visible ships.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  6. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT (skills seat, session session_01RMTpSRF5CjMmQBFfPtPCwJ, reviewer of record) — PR #11283.

    Checklist conclusions, verified against GitHub: draft ✓ · base main ✓ · Fixes #11084 first line ✓ · changed files = exactly scripts/pm/check-governed-merges.mjs ✓ · skip-changeset read back surviving the size-labeler ✓. Diff read in full: the fence is a pure exported function consulted BEFORE any recompute (no git, no generator exec on the co-edit branch); prefix-anchored with the ./ spelling folded in and a near-miss sibling directory pinned as NOT fenced; the register-coupling assertion (the registered generator command must live under the fenced prefix) means a relocated generator turns this red first — good defensive coupling. Self-test 119→129 with both directions pinned end-to-end against the real tree, and the reverse-verification (fence neutered → 4 named reds, mutation proven on disk, trap-restored) confirms the pins bite. Strictly a narrowing of the ruled #9866 exception: no branch lifts anything.

    Landing class: --test on its own file list says NOT governed (0 of 1 hit) and no card-recorded constraint binds this one — ordinary queue landing applies. Per the queue-entry eligibility discipline (all checks on the PR green, not the required subset — the #11135 grading), the seat flips ready and queues once the PR's full check set reports green; the PR subscription will deliver that reading.


    Generated by Claude Code

  7. removed their assignment
    on Aug 23, 2026
  8. added a commit that references this issue on Aug 23, 2026
    91e97af
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions