Skip to content

Catch, block and revert knowledge changes that bypassed the gate (design v8) - #230

Merged
tzhouam merged 1 commit into
mainfrom
kb/local-provenance
Sep 29, 2026
Merged

tzhouam merged 1 commit into
mainfrom
kb/local-provenance

Conversation

@tzhouam

@tzhouam tzhouam commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

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.

  • Provenance, every tick: each first-parent commit touching knowledge/ is trusted, pending (our merge awaiting its receipt), disposed, or unknown.
  • An unknown commit:
    • pauses its repositories and alerts you;
    • goes back to intake with its diff;
    • gets a revert PR restoring the tree entries (mode and content). The revert is never auto-merged. Conflicts, non-governed paths and private scopes go to people.
  • Disposal: only an exact revert disposes of an unknown commit, whether the service's PR or one made by hand, including a partial revert followed by the rest. It must restore every knowledge path the commit changed.
  • Activation: activate() itself, so also kb activate, refuses any SHA past an unknown or pending commit. A disposed commit is trusted only while its revert is in the checked history.
  • Publisher: the signed control record carries the active snapshot and the trusted list, and the publisher refuses to merge on top of an untrusted commit.
  • Not yet: kb accept-unknown.

The commit hook approved it after several rounds of fixes; full suite and SPEC freshness pass locally.

🤖 Generated with Claude Code

…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>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T06:36:20.608432Z 09303ec PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tzhouam
tzhouam merged commit 86f152c into main Sep 29, 2026
5 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +489 to +491
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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +42 to +44
# 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"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +248 to +251
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]})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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