Skip to content

Kernel: a change to the Factory's own rules or a Lens's inputs needs a person #113

Closed

Description

@don-petry

Part of the epic "Epic: slice 0, foundations. The repo holds the line, its context is tested, and the platform facts are recorded".

Why. The security pass on "A Lens run by GitHub Actions is read only from its own workflow file" (#97) found that nothing the Kernel reads stops a pull request that changes the Factory's own rules, or the inputs of a Lens, from being approved and enqueued with no person:

  • The facts carry no list of the paths a pull request changes, so the Kernel cannot see that .github/, lifecycle/, config/, the hooks, kernel/gate.py, kernel/brakes.py or a Lens's inputs (Makefile, tools/, tests/, .gitleaks.toml) were touched.
  • A same-repo pull request can edit .github/workflows/verify.yml in place: GitHub runs the edited job and reports it from that path, so the Lens pin (A Lens run by GitHub Actions is read only from its own workflow file #97) holds.
  • Dependabot's security updates are enabled and edit workflow pins. Such a pull request is class dependency, skips the model review, and is judged by the deterministic Lenses alone, one of which is the file it changes.
  • The main ruleset requires no review and no code-owner review, so GitHub accepts whatever the Kernel enqueues ("The Factory repo's rulesets hold the Release tags and each required check's App", The Factory repo's rulesets hold the Release tags and each required check's App #62).

Design §5.1 makes the Factory's own rules the Maintainer's. Today that is held only by the session hooks, locally.

Done when

  • The facts carry the paths the pull request changes (read in full, with a complete flag), and the configuration names the protected paths and each Lens's inputs, from one list shared with config/hooks.json.
  • A pull request that changes a protected path or a Lens's inputs converges only once every model Lens has reviewed its current head, whatever its class; the machine user then approves it as usual (amended by the Maintainer, 2026-10-16: no person; see the rulings below). A queued one whose head moves leaves the queue. A dependency update is no exception. An unreadable list of paths holds for the Maintainer (needs_you).
  • A boundary test and a mutant for each; the simulator makes such changes, by a Role, by Dependabot and by a person.
  • Decision 0018's row on a change to a Lens's own workflow file is given its check.

Activity

  1. don-petry commented on Oct 16, 2026

    @don-petry
    ContributorAuthor

    Working rulings (overturnable in review)

    Solution Architect, delegated; rule 3 and the load decided by the Maintainer. Builds on #114 (decision 0018, merged) and #33's dequeue.

    1. The paths are a fact. New in schemas/facts.schema.json: changed_paths, every path the pull request changes, each once, from the top of the repo. A rename or copy gives both its old and new path; a deletion gives its old one. New flag complete.changed_paths, added to READ_IN_FULL in kernel/terms.py. A flag that is not exactly true gives facts_incomplete, and a queued item leaves the queue (#33). A list read in full can still be unreadable: not a list of strings, or a path that is empty, has a leading or trailing /, or a . or .. name. That gives the new paths_unreadable, in NEEDS_YOU_REASONS, read where review_unreadable is. Why: the Kernel cannot hold what it cannot see, and a misread list must stop the item.

    2. One list, in config/hooks.json. protected gains config/apps.json. A new key, lens_inputs, gives {lens, path, is}:

    • verify: Makefile, plus CHECKS from tools/check_context.py;
    • the secret scan: .gitleaks.toml.

    The workflow files are already under .github/, and CodeQL has no input in the repo. The syntax is what schemas/hooks.schema.json already allows: a final / means everything below it, and * matches part of one name. Matching ignores case, so it can hold more, never less. One new function beside load() in tools/hooks/protected.py gives the Kernel config protected_paths, the patterns without is, which is prose. tests/support.py and sim/world.py both call it. The session hooks treat lens_inputs as reported. New tests in tests/test_tools.py hold this:

    • the Kernel's matcher and protected.entries() agree on one corpus;
    • the config carries every entry;
    • each lens is a required deterministic Lens, and verify's inputs cover CHECKS;
    • each 0018 workflow is protected.

    Why: with one file, only the two matchers can drift, and a test pins them.

    3. What lets such a change converge: pr-review's review of this head. Decided by the Maintainer, 2026-10-16. No person is needed. A change that touches the list converges only once every model Lens has reviewed its current head, as any change is reviewed, and the approval is then submitted by the machine user (donpetry-bot, the configured approver), as it is for every item. In required_model_lenses() in kernel/derive.py, a change that touches the list needs every model Lens whatever its class: model_not_required_for does not apply. A session pushing as the Maintainer can never approve its own change, since the approval comes from another account, and the machine user never pushes (§5.1, I-26). No new approval fact and no self-push exception. Why: the gap is a change judged by nothing but the Lens it edits. A review by another model, bound to the content, closes it without stopping each change for a person.

    4. Without it. No new Hold: an item whose model review of this head is missing waits for it as any item does (waiting on the review), and a queued one whose head changes already leaves the queue (#33). Only an unreadable list stops the item for a person (rule 1, paths_unreadable, needs_you). The decision names the matched configured patterns, never a changed path. Why: the review is the gate, so the gate's own machinery is enough.

    5. No class is excused. A Dependabot pin update to .github/workflows/verify.yml is reviewed by pr-review before it converges. model_not_required_for still excuses a dependency update that touches nothing on the list. Why: the Lens it edits is otherwise its only judge.

    6. Records. The next free number is 0020 (0019 is the session hooks' bypass, decided the same day): docs/decisions/0020-a-change-to-the-factorys-own-rules-or-a-lenss-inputs-is-reviewed-by-a-model-lens.md, new, replacing nothing. In 0018's workflow-file row, only the Check cell changes, as tools/check_decisions.py allows. It names test_a_lens_input_counts_like_a_protected_path, and the refusal stays waiting on harness/. Design §4.4 gains I-45.

    7. Simulator. Touched paths draw from random.Random("paths %d" % seed), so the fixed seeds stay as they were. Three actors make such changes: a Role's fix Run touches tests/, Dependabot edits .github/workflows/verify.yml, and the Maintainer pushes config/defaults.json. World.invariants() and approve() assert I-45: nothing touching the list is approved, enqueued or merged without every model Lens's review of that head, whatever its class. tests/test_simulator.py checks the seeds reach every case.

    8. Boundary tests (tests/test_decide.py) and mutants.

    Test Mutant it kills
    test_a_change_to_a_protected_path_needs_the_model_review (README.md on a dependency item converges without it) the list never read
    test_a_lens_input_counts_like_a_protected_path a final / matches only the directory
    test_a_pattern_matches_one_name_per_star_in_any_case * crosses /; case compared
    test_either_name_of_a_rename_counts only the first path read
    test_a_dependency_update_to_a_workflow_pin_needs_the_model_review dependency excused on the list
    test_a_queued_change_to_the_list_whose_head_moves_leaves_the_queue review of an old head read as this one's
    test_changed_paths_read_short_defer changed_paths taken out of READ_IN_FULL
    test_an_unreadable_changed_path_defers_for_the_maintainer guard removed; .. read
    test_a_decision_names_the_pattern_never_the_path evidence is the paths

    Counter-examples: facts.no-completeness-for-changed-paths.json, hooks.a-lens-input-with-no-lens.json.

    9. Follow-ups.

    • "Port: the paths a pull request changes, and both names of a rename". Waits on adapters/; until then only fixtures produce the fact.
    • With The Factory repo's rulesets hold the Release tags and each required check's App #62: "Ruleset: a change to a protected path needs the machine user's approval of the latest push".
    • "Harness: apply refuses a Role's push to a protected path or a Lens's input that no model Lens has reviewed".
    • "Run each deterministic Lens from the base's inputs, so a change cannot weaken the Lens that judges it".
    • "A model Lens's inputs: its manifest and prompt" (waits on roles/).
    • "Consumer entry: its protected_paths in this syntax".

    Decided by the Maintainer, 2026-10-16.

    • Rule 3: no person and no self-push exception. pr-review's review, approved by donpetry-bot, is the gate; a session pushing as the Maintainer cannot approve itself.
    • The load: accepted. With rule 3 as decided, it is small: every change but a dependency update already needs the model review, so only those touching the list wait for it. "Run each deterministic Lens from the base's inputs" comes straight after this story.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions