Repository navigation
Catch, block and revert knowledge changes that bypassed the gate (design v8) - #230
Conversation
…ign v8)
Without GitHub-side enforcement, a person can still merge or push a
knowledge change the gate never saw. The service now catches it on the next
tick, keeps it from being served, and opens a revert.
- audit (provenance) runs every scheduler tick, no longer daily. Each
first-parent commit on main that touches knowledge/ is classified as:
- trusted: a recorded merge SHA, or a two-parent merge of a recorded head
carrying only that head's change;
- pending: a merge of a head whose merge item still waits for its receipt;
looked at again next tick;
- disposed: an exact revert of it was merged;
- unknown: anything else. An inexact revert merge is itself unknown.
- For an unknown commit the service:
- pauses the repositories it touched (everything outside a repository
scope);
- records unknown:<sha> and alerts people;
- sends its content back to intake as a new candidate (event source
"unrecorded");
- opens a revert PR that restores the governed paths to their first-parent
content. The publisher builds it and it is NEVER merged automatically;
open_revert_pr is always executable, since it is issued because the
repository was paused.
A path changed again since (the revert would conflict), or a non-governed
path, goes to people instead. Without the lease the revert is recorded as
revert_pending and issued by the next pass that holds it.
- A merged revert disposes of the unknown commit only if it is exact: the PR
merged the revert commit the publisher built, and on governed paths the
merge changed exactly the reverted paths back to their pre-images.
disposed:<sha> records the pair. A closed revert leaves it undisposed and
alerts people.
- Exactness compares tree entries (mode and content). A revert made by hand
is recognised the same way. The set is every undisposed unknown commit
sharing a knowledge path with the new commit, closed transitively (an
earlier partial revert is itself one of them). The new commit disposes of
the set when it changes only the set's paths and, after it, EVERY path the
set touched, governed or not, holds its tree entry from before the earliest
commit (ordered by ancestry): together they changed nothing. The unknown
record keeps the complete path list and the entries of all its knowledge
paths, uncapped, so conflict detection and exactness cover every path.
A disposed commit is trusted only while its disposing revert is part of
the history being checked, so main rewound past the revert brings it back.
A revert PR or intake candidate is created only when EVERY repository
scope the commit touches may publish; a commit touching a private upstream
goes to people. A merged
revert PR is settled only after fetching, so a fresh merge is never
mistaken for another unknown change.
- activate() itself, and so `kb activate`, refuses any SHA with an unknown or
pending knowledge commit between the active snapshot and it (the first
activation has no baseline). The scheduler checks the same before
activating and records activation_blocked, and nothing is activated while
an unknown commit is undisposed (the G5 exception in the design).
- The intake event for an unknown commit carries its diff, not just its
message and file names.
- The signed control record now carries provenance: the active snapshot and
the trusted list (merge SHAs plus disposal pairs). Before merging, the
publisher checks that every governed knowledge commit on main since the
active snapshot is trusted or one of its own merges; otherwise it refuses
(transient: the service pauses and reverts).
Not implemented yet: kb accept-unknown (accepting the unknown content instead
of reverting). For now a merged exact revert, the service's or one made by
hand, lifts the block.
Tests: an unknown change is paused, blocks activation, is reverted and sent
back to intake, and is handled once; an exact revert disposes of it and both
commits become trusted; a revert merged with other changes disposes of
nothing and is caught itself; a conflicting revert goes to people; nothing is
activated while an unknown change is open, and activation resumes after the
revert; the publisher refuses to merge on top of an unknown change; our own
merge waiting for its receipt is not unknown; a revert merged with a mode
change is not exact; a hand-made revert disposes of what it undoes;
activate() refuses a commit that passed no gate; the intake event carries
the diff; a revert is settled without the test pre-fetching; a revert must
restore non-governed paths too (a partial revert keeps the block, and a
second completes it); a hand revert of an AGENTS-only change clears it; a
later edit to the 54th of 55 paths makes the revert conflict; rewinding
main past a revert brings the unknown change back; a commit touching a
private scope is never reverted in public.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 09303ec4e2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| commits = self._git("rev-list", "--first-parent", f"{active}..{main_sha}", "--", | ||
| "knowledge/repos", "knowledge/general").split() | ||
| return [sha for sha in commits if sha not in trusted] |
There was a problem hiding this comment.
Reject main when it no longer descends from active
If an unknown commit U is exactly reverted by R and the active snapshot advances to R, an administrator can reset main back to U; in that case git rev-list R..U is empty because U is an ancestor of R, so this reports no provenance problem and permits the publisher to merge on top of the untrusted tree even though the service-side check rejects it. Verify that active is an ancestor of main_sha before evaluating the trusted commit list.
Useful? React with 👍 / 👎.
| # they only ever stop or undo: executable while a repository is paused (a | ||
| # revert is issued BECAUSE the repository was paused for an unknown change) | ||
| ALWAYS_EXECUTABLE = frozenset({"pause", "close", "open_revert_pr"}) |
There was a problem hiding this comment.
Keep revert PRs subject to invalidation checks
Adding open_revert_pr to ALWAYS_EXECUTABLE makes check_item return before checking expiry, generation changes, current mode, or publication visibility. If an operator handles the unknown change manually and resumes or switches the repository to shadow/disabled before the publisher consumes the item, the stale signed item can still push a branch and open a revert PR despite the generation bump that is intended to invalidate all paused-era work; allow this kind while paused, but still apply the other invalidation checks.
Useful? React with 👍 / 👎.
| rt.ledger.record_event(lifecycle.repo, "unrecorded", sha, { | ||
| "source_reference": f"commit {sha}", "title": message.splitlines()[0] if message else sha, | ||
| "body": message[:3000], "changed_files": paths[:50], | ||
| "diff_excerpt": rt.knowledge.diff_text(parents[0] if parents else "", sha, paths)[:DIFF_LIMIT]}) |
There was a problem hiding this comment.
Enqueue bypass evidence for every touched owner
When one bypassed commit touches two public auto-merge owner scopes, revert_repo selects only one lifecycle and this single event sends all evidence to that owner's intake. run_intake processes events per repository and rejects generated writes outside that lifecycle's knowledge_dir, so conclusions belonging to the other owner are never reconsidered after the revert; enqueue an owner-scoped event for every touched lifecycle instead of collapsing the commit into the first one.
AGENTS.md reference: AGENTS.md:L12-L17
Useful? React with 👍 / 👎.
PR 4 of the v8 knowledge-service change: a knowledge change that bypassed the gate (merged or pushed by hand) is caught on the next scheduler tick, never served, and reverted.
knowledge/is trusted, pending (our merge awaiting its receipt), disposed, or unknown.activate()itself, so alsokb activate, refuses any SHA past an unknown or pending commit. A disposed commit is trusted only while its revert is in the checked history.kb accept-unknown.The commit hook approved it after several rounds of fixes; full suite and SPEC freshness pass locally.
🤖 Generated with Claude Code