feat(loop): gate the loop's reading list on author association, in CI - #285
Merged
Conversation
The loop could not tell a teammate's issue from a stranger's, and this repository is public: 12 of its 25 open issues are authored by accounts with no association to it. `gh issue list` reports no author at all, so the information never reached the model — it did not ignore the distinction, it never had it. The gate belongs at the boundary rather than in the tick. A workflow labels each issue by `author_association` when it is opened, edited, reopened or transferred, and loop.sh reads only issues carrying `loop:trusted` (the team) or `loop:cleared` (a human read it and said so). Four properties, in order of how much they matter: - The decision is metadata, not judgement. Author association cannot be spoofed by the issue text, which is the one thing an attacker controls. - It fails closed. Anything that is not OWNER, MEMBER or COLLABORATOR is untrusted, including an empty value, a lowercase spelling, or a role GitHub adds later. CONTRIBUTOR is the trap: it reads like membership and means one merged pull request. - An edit withdraws clearance. Otherwise the gate is defeated by opening something harmless, waiting for a human to clear it, then editing the text. - It runs once per issue instead of once per tick. No model in that workflow, deliberately. An LLM in a job that reads attacker-controlled text while holding a token and `issues: write` is the classic pwn-request shape, and it would buy little: the deterministic gate already stops a stranger's issue being acted on unsupervised, whatever it says. The model-assisted injection scan stays in triage.md, where the text is read anyway and the blast radius is a throwaway worktree. The classification is a security boundary, so it lives in bin/ with its own offline test, following bin/prises-check.sh. The test asserts the fail-closed cases and that only an edit withdraws clearance; inverting one branch of the classification makes it fail. workflow_dispatch backfills, because the event triggers only see new activity and every already-open issue would otherwise stay invisible to the loop for ever. Until that runs the loop reads no issues at all, which is the correct direction to fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #284. The loop it added could not tell a teammate's issue from a stranger's.
The problem
The agentic loop reads open issues to decide what to work on. This repository is public, and
gh issue listreports no author:Number, state, title, labels, date. A stranger's issue and the owner's arrived at the conductor as the same thing — the model was not ignoring the distinction, it never had it.
This is not hypothetical: 12 of the 25 currently open issues carry
author_association: NONE.The gate
A CI workflow labels every issue by
author_associationonopened,edited,reopenedandtransferred;loop.shthen reads only issues carryingloop:trustedorloop:cleared.OWNER,MEMBER,COLLABORATORloop:trustedCONTRIBUTOR,FIRST_TIME_CONTRIBUTOR,NONE, anything elseloop:untrustedloop:clearedFour properties, in order of how much they matter:
CONTRIBUTORis the trap the test guards: it reads like membership and means one merged pull request.Why there is no model in that workflow
An LLM in a job that reads attacker-controlled text while holding a token and
issues: writeis the classic pwn-request shape. It would also buy little: the deterministic gate already stops a stranger's issue from being acted on unsupervised, whatever the text says. The model-assisted injection scan stays inloop/triage.md, where the text has to be read anyway and the blast radius is a throwaway worktree.If you want the model layer in CI as well, say so and I will add it — but I would not put the API key there for what it adds.
Tested
The classification is a security boundary, so it lives in
bin/with an offline test, following the existingbin/prises-check.sh+bin/prises-check-test.shpattern. The workflow runs that test on every change to itself.Inverting one branch of the classification (moving
CONTRIBUTORto trusted) makes the suite fail — verified, so the test is load-bearing rather than decorative.After merging
Run the workflow once via workflow_dispatch to backfill. The event triggers only see new activity, so every already-open issue would otherwise stay invisible to the loop for ever. Until that backfill runs the loop reads no issues at all — which is the correct direction to fail, and was confirmed against the live repository.
Then the 12
NONE-authored issues need a decision: read and labelloop:clearedone by one, or add the author as a collaborator, which flips them toloop:trustedon their next event.Not covered
The loop currently reads issue titles only, so comments are not yet in its context. The moment bodies or comments enter the reading list, a trusted issue becomes a place a stranger can write. Noted in WA007 as the thing to revisit before that happens.