Skip to content

attribution binds forward: Cubits11 signs this log, no agent names itself - #25

Open
Cubits11 wants to merge 5 commits into
mainfrom
claude/friendly-babbage-5hro0x
Open

Cubits11 wants to merge 5 commits into
mainfrom
claude/friendly-babbage-5hro0x

Conversation

@Cubits11

@Cubits11 Cubits11 commented Sep 22, 2026

Copy link
Copy Markdown
Owner

An agent that signs a commit makes a claim about who did the work, on the one surface a reader trusts without checking. This repository prosecutes its claims and cannot leave that one unprosecuted.

What this adds

  • .githooks/commit-msg strips machine-attribution trailers before a message is written: Co-Authored-By naming a model or vendor no-reply address, the session backlink, the "Generated with" footer. A Co-Authored-By naming a person is left alone — co-authorship by a human is a fact, and this hook is not in the business of deleting facts.
  • scripts/verify_attribution.py is the gate behind the hook. Every commit after the recorded baseline must carry a Cubits11 author and no such trailer. --history reports the pre-baseline count; --test runs 19 fixtures in memory.
  • Both enter CHECKS, so a hook someone forgot to install fails CI rather than passing silently. The trunk goes 83 → 85 checks.
  • metrics/attribution_baseline.json records where the rule starts and who decided it.

The trade, stated

The rule binds forward and not backward. 43 of the 129 pre-baseline commits carry a model trailer. Rewriting them would change every SHA on the default branch, and those SHAs are pinned by published permalinks in distribution/traction/events.json, distribution/dispatch-log.yaml, distribution/dossiers/bells-misuse-2025.md and missing-column/index.html.

A binding that no longer resolves is a worse failure than a trailer that accurately records who drafted a sentence. So the history stands, the baseline records where the rule starts, and the count is reported rather than hidden.

Reversing this judgement is a one-command rewrite plus a re-pin of those permalinks. It is the owner's call, not this branch's.

The web-flow exemption, and how it was found

On a merge GitHub writes, the author stays the person and the committer becomes GitHub <noreply@github.com>. The committer slot alone is exempt, and only on a merge. The author is always checked, so a merge still names a person; a single-parent commit made as GitHub fails; a merge authored by a model fails on its author.

This was not designed in. The gate failed CI three times on its own pull request before it was right:

  1. The rule demanded a Cubits11 committer, which would have failed on every merge into main forever, starting with this one.
  2. The first fix demanded that a merge's author and committer both be GitHub. GitHub sets only the committer, so the exemption never fired and the same failure repeated.
  3. Fixed by exempting the committer slot alone.

The fixtures share the blame: they re-implemented the condition inline instead of calling the gate, so they agreed with the bug and reported green while CI was red. The decision now lives in one function, identities_to_check(), which both the gate and its fixtures call, and the shapes were verified against fabricated commits rather than against a description of them.

Residual, stated rather than hidden: a merge can carry conflict-resolution content present in no parent, attributed only by the merge's author. Closing that means checking merge-diff provenance, which is a different gate.

CI

Three checks green on 8732e64. Trusted evidence boundary is red and will stay red until this is merged.

scripts/verification_manifest.py is in security/evidence-policy.json:trust_root, and evidence_guard.py:68 refuses any diff to a trust-root file unconditionally — no bypass flag — because adding checks widens the verification surface. Its own comment: "An upgrade needs an explicit admin migration." Every past manifest change crossed the same gate (c9dc338, 5a39b06, 48e98e4, 6728545, f3a6a1e, 0aad901).

There is no push that clears it. That is the design: the merge is a deliberate human act, which is the right shape for a PR whose subject is constraining what an agent may claim about its own work.

Also fixed here: docs/graph/repo-graph.json's experiments/e2.first_result_commit_epoch. The true first add (dd36c1a) is an ancestor of HEAD, but history simplification through a later merge hides it from git log --diff-filter=A on some git builds. The committed value is the correct one. The underlying nondeterminism is not fixed — that field feeds the D-series post-hoc-edit detectors, so a value that varies with whose git computes it is a weakness in a detector whose job is catching forbidden rescues. Worth its own change.


Generated by Claude Code

…self

An agent that signs a commit makes a claim about who did the work on the one
surface a reader trusts without checking. This repository prosecutes its
claims and cannot leave that one unprosecuted.

.githooks/commit-msg strips machine-attribution trailers before a message is
written. scripts/verify_attribution.py is the gate behind it: every commit
after the recorded baseline must carry a Cubits11 author and committer and no
such trailer. Both enter the manifest, so a hook someone forgot to install
fails CI rather than passing silently. A Co-Authored-By naming a person is
left alone; co-authorship by a human is a fact.

The rule binds forward and not backward, and metrics/attribution_baseline.json
records where it starts. Rewriting the 43 pre-baseline commits that carry a
model trailer would change every SHA on the default branch, and those SHAs are
pinned by published permalinks in distribution/traction/events.json,
distribution/dispatch-log.yaml, distribution/dossiers/ and
missing-column/index.html. A binding that no longer resolves is a worse
failure than a trailer that accurately records who drafted a sentence.
--history reports that count rather than hiding it.

Copy link
Copy Markdown
Owner Author

Trusted evidence boundary is red, and it is supposed to be.

FAIL: trust-root change requires separate migration: scripts/verification_manifest.py

scripts/verification_manifest.py is listed in security/evidence-policy.json:trust_root. evidence_guard.py:68 refuses any diff to a trust-root file under pull_request_target, unconditionally — there is no bypass flag, and the comment in the source says why: "An upgrade needs an explicit admin migration."

So this is not a failure to fix in the branch. Adding the two checks to CHECKS is a widening of the verification surface, and the guard is built so a human sees and accepts that widening rather than letting a change vouch for itself. Every past manifest change has crossed the same gate: c9dc338, 5a39b06, 48e98e4, 6728545, f3a6a1e, 0aad901.

What makes it worth naming here rather than merging past it quietly: the subject of this PR is a rule constraining what an agent may claim about its own work, and the check that stops it is the rule that no change may certify its own trust root. The same principle, one level down. An agent clearing that gate with its own hand to install a rule about agent restraint is the exact move the gate exists to catch — so the merge is the owner's, not this branch's.

The other three checks are unaffected by the trust-root refusal and run on their own.


Generated by Claude Code

scripts/repo_graph.py:139 reads the first add of each result file with
`git log --diff-filter=A`, and takes the oldest. For experiments/e2 the true
first add is dd36c1a (1788395285, 2026-09-02) and it is an ancestor of HEAD,
but history simplification through the later merge hides it from that command
on this container's git, which reports 4fa9a22 (1788656837, 2026-09-05)
instead. Regenerating here therefore wrote a value three days late, and CI's
git 2.55 recomputed the earlier one and failed the drift gate.

The committed value goes back to 1788395285, which is the correct answer and
the one CI derives. Everything else in this regeneration stands: the two
attribution checks and their static-reference edges.

The nondeterminism itself is not fixed here. first_result_commit_epoch feeds
D-series detectors that decide whether a governing file was edited after a
result existed, so a value that varies with the traversing git is a real
weakness in a post-hoc-edit detector. Pinning it to the oldest add over
--full-history, or to the commit recorded in the experiment's own freeze,
would remove the dependence. That is a change to a detector and does not
belong in a commit about attribution.
The gate failed on its own pull request. CI checks out the synthetic commit
GitHub builds to test a PR, whose author and committer are
`GitHub <noreply@github.com>`, and the identity rule rejected it. The same
would have happened to every merge the button writes, so the rule as shipped
would have failed on main permanently from the first merge onward.

A merge GitHub wrote is plumbing, not authorship: nobody is claiming credit
for a merge, and the content entering through it is in the parents, which are
each checked on their own. So a commit with two or more parents carrying
exactly that identity is exempt from the identity check. It stays subject to
the trailer check, and the exemption requires the merge shape as well as the
identity — a single-parent commit authored as GitHub is someone committing as
GitHub, and still fails.

Four fixtures cover the distinction a live run found before they did: a
GitHub merge, a GitHub single-parent commit, a model-identity merge, an owner
merge.

The residual is stated in the docstring rather than papered over: a merge can
carry conflict-resolution content present in no parent, and that content is
unattributed under this exemption. Closing it means checking merge-diff
provenance, which is a different gate.
The previous commit's exemption never fired. GitHub's merge keeps the person
as author and replaces only the committer, but the exemption demanded that
both slots be GitHub's, so the third CI run failed exactly as the first two
did. The exemption now applies to the committer slot alone, on a merge, which
is both correct and narrower: the author is always checked, so a merge still
names a person, a single-parent commit made as GitHub still fails, and a merge
authored by a model fails on its author.

The fixtures deserve the larger share of the blame. They re-implemented the
condition inline rather than calling the gate, so they agreed with the bug and
reported 17 passing while CI failed on the same commit. The decision now lives
in identities_to_check(), and the fixtures call it, so the gate and its tests
cannot disagree again. Five cases replace the four that were testing a copy.

Verified against fabricated commits of the exact shapes rather than against a
description of them: author Cubits11 / committer GitHub / two parents is
exempt; committing as GitHub on one parent gives 4 violations; a
model-authored merge gives 2.

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.

1 participant