-
Notifications
You must be signed in to change notification settings - Fork 0
docs(plan-fleet): plan mode is a property of the dispatch, not a default #499
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| #!/usr/bin/env bash | ||
| #MISE description="PreToolUse hook body: decide the harness-connector verbs AGENTS.md already rules on, by tool-name SUFFIX, so the verdict survives the exposed server name changing (CLOUD-178)" | ||
| # | ||
| # CLOUD-178. A `.claude/settings.json` permission rule must name its server as a | ||
| # LITERAL — `mcp-allow-check` refuses a glob in that segment, correctly, because | ||
| # the CLI skips such a rule with a warning and it grants nothing. But a claude.ai | ||
| # connector's exposed name is chosen per registration episode by the HOST: | ||
| # readable at session start (`mcp__Linear__save_issue`), a UUID after a reconnect | ||
| # (`mcp__4db58e41-…__save_issue`). The literal that would match the second is an | ||
| # account-specific identifier, which non-negotiable rule 1 keeps out of committed | ||
| # config. | ||
| # | ||
| # So a permission rule cannot express these verdicts portably. CLOUD-178 concluded | ||
| # from that "no gate is possible for this one". That conclusion was wrong, and the | ||
| # counter-example was already shipping beside it: `issue-read-guard` and | ||
| # `board-move-guard` match `*save_issue` on the SUFFIX and never read the server | ||
| # segment at all. This file is that idiom applied to the verbs AGENTS.md rules on. | ||
| # | ||
| # MEASURED 2026-08-19, and it is why this exists rather than a fifth permission | ||
| # rule. `.claude/settings.json` on `main` allows `mcp__Claude_Code_Remote__*` and | ||
| # `mcp__Linear__*`; in the session that landed it the host exposed those same two | ||
| # connectors as `mcp__bf7c680d-…__*` and `mcp__cc451d34-…__*`, so | ||
| # `…__get_session` and `…__list_comments` both answered "requires approval" from | ||
| # inside an allow list that reads as covering them. A second literal spelling is | ||
| # still a literal. | ||
| # | ||
| # WHAT IT DECIDES, and every one is an existing AGENTS.md rule rather than a new | ||
| # one this file invents: | ||
| # | ||
| # unsubscribe_pr_activity ALLOW — dropping a subscription only ever narrows | ||
| # what reaches the session, and `land`'s | ||
| # `pr-unsubscribed` gate REQUIRES the drop | ||
| # before a runner is spent. An enforced path | ||
| # that prompts on every PR is a path that | ||
| # gets worked around instead of satisfied. | ||
| # subscribe_pr_activity DENY — "No heartbeats … to babysit a PR." | ||
| # send_later DENY — "No heartbeats (`send_later`/Routines/ | ||
| # create_trigger DENY timers) to babysit a PR … the ban is on | ||
| # timers." | ||
| # | ||
| # The last two already sit in `permissions.deny` under a literal server name, and | ||
| # that is precisely the shape measured inert above: a deny that under-matches | ||
| # reads as a prohibition and enforces NOTHING. An allow that under-matches fails | ||
| # the other way — into a prompt a human sees — which is why `mcp-allow-check`'s | ||
| # new predicate binds denies and not allows. | ||
| # | ||
| # THE SUBSTRING HAZARD, AND WHICH DEFENCE ACTUALLY CARRIES IT. | ||
| # `unsubscribe_pr_activity` ENDS WITH `subscribe_pr_activity`, so an unbounded | ||
| # `*subscribe_pr_activity` test matches BOTH and denies the very call this file | ||
| # exists to free — identical to the hazard `closing-key-check` records against | ||
| # its own `DO-NOT-CLOSE` marker, where "the opt-out was unusable exactly when it | ||
| # was written correctly, and it failed as the inverse of the author's intent | ||
| # rather than as a refusal, which is the silent direction." | ||
| # | ||
| # Two independent defences, and the first is the structural one: the deny arm is | ||
| # LEFT-BOUNDED on `__`, so `…__unsubscribe_pr_activity` cannot match | ||
| # `*__subscribe_pr_activity` at all — the boundary character is `u`, not `s`. | ||
| # Testing the allow arm FIRST is the second, and it is redundancy rather than the | ||
| # mechanism. Said plainly because an earlier draft of this header claimed the | ||
| # ordering was what carried it, and the mutation run disproved that: with the | ||
| # allow arm deleted the unsubscribe rows fail by going UNDECIDED, never by being | ||
| # denied. Neither defence alone is observable through the other, which is why the | ||
| # suite carries a row for the bound (`…_v2`, `resubscribe_…`) as well as the | ||
| # ordering. | ||
| # | ||
| # ALLOW IS A DECISION HERE, not an omission, and it is why this is a by-path guard | ||
| # rather than a `batten.toml` row. The engine models `Decision::Allow` as "emit | ||
| # nothing, exit 0", which leaves the ordinary permission flow to prompt. Actually | ||
| # pre-approving needs `hookSpecificOutput.permissionDecision: "allow"` on stdout, | ||
| # a variant the engine has no representation for. That is a capability gap on | ||
| # CLOUD-312's ledger — a missing decision variant, not a missing rule kind. | ||
| # | ||
| # Pointer-only (non-negotiable 4): the decision names the verb and the rule, never | ||
| # the payload. | ||
| # | ||
| # `--covers` prints the tool suffixes this file decides, one per line, so | ||
| # `mcp-allow-check` can bind a deny rule to its coverage without grepping this | ||
| # file's `case` statements. One authority per fact. | ||
| # | ||
| # Exit 0 always — a guard renders its verdict in the JSON, never in the status. | ||
| # FAILS OPEN on everything it cannot establish, and on BATTEN_CONNECTOR_VERB_BYPASS=1. | ||
| # | ||
| # The mutation drops the allow arm, so an unsubscribe falls through UNDECIDED and | ||
| # the approval prompt this file exists to remove comes back. Every deny row still | ||
| # passes under it — only the unsubscribe rows can catch it, which is what makes | ||
| # them the suite's spine and why they are written under both spellings. | ||
| #MUTANT unsubscribe-not-preapproved|s@^ALLOW_SUFFIXES=.*@ALLOW_SUFFIXES=""@|an unsubscribe must be pre-approved, not merely left to the permission flow | ||
| set -uo pipefail | ||
|
|
||
| ALLOW_SUFFIXES="unsubscribe_pr_activity" | ||
| DENY_SUFFIXES="subscribe_pr_activity send_later create_trigger" | ||
|
|
||
| if [ "${1:-}" = "--covers" ]; then | ||
| for suffix in $ALLOW_SUFFIXES $DENY_SUFFIXES; do | ||
| printf '%s\n' "$suffix" | ||
| done | ||
| exit 0 | ||
| fi | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| # CLOUD-479's pairing, declared rather than discovered: registered BY PATH, so | ||
| # mise's `"aqua:jqlang/jq"` pin does not reach it and `jq` is whatever the ambient | ||
| # PATH holds. Every read below fails open, so an absent parser would ALLOW | ||
| # silently — the one outcome a guard must never reach by accident. Say so on | ||
| # stderr and get out of the way; exit 2 is a DENY on this event and a broken | ||
| # toolchain must not take that channel. | ||
| #PIN-OK: jq | ||
| if ! command -v jq >/dev/null 2>&1; then | ||
| echo "::error:: connector-verb-guard: no jq on PATH — this guard is registered by path, so it does not get mise's pinned jq. It is checking NOTHING and allowing every call. Run: mise install" >&2 | ||
| exit 0 | ||
| fi | ||
|
|
||
| [ -n "${BATTEN_CONNECTOR_VERB_BYPASS:-}" ] && exit 0 | ||
|
|
||
| raw=$(cat) || exit 0 | ||
|
|
||
| tool=$(printf '%s' "$raw" | jq -r '.tool_name // empty' 2>/dev/null) || exit 0 | ||
| [ -n "$tool" ] || exit 0 | ||
|
|
||
| decide() { | ||
| jq -n --arg d "$1" --arg r "$2" '{ | ||
| hookSpecificOutput: { | ||
| hookEventName: "PreToolUse", | ||
| permissionDecision: $d, | ||
| permissionDecisionReason: $r | ||
| } | ||
| }' | ||
| exit 0 | ||
| } | ||
|
|
||
| # ALLOW FIRST. See the order note above: the deny suffix is a substring of this | ||
| # one, so testing the other way round denies what this exists to permit. | ||
| for suffix in $ALLOW_SUFFIXES; do | ||
| case "$tool" in | ||
| *__"$suffix" | "$suffix") | ||
| decide allow "connector-verb-guard: $suffix is pre-approved. Dropping a PR subscription only narrows what reaches this session, and \`mise run land\` refuses to land a PR whose subscription is still armed — so this must never wait on an approval. Record the drop afterwards: mise run pr-unsubscribed record <pr>." | ||
| ;; | ||
| esac | ||
| done | ||
|
|
||
| # Left-bounded on `__` so it cannot reach back across the word boundary into | ||
| # `unsubscribe_pr_activity`. The bare-name arm is for a host that exposes a verb | ||
| # with no server prefix at all. | ||
| for suffix in $DENY_SUFFIXES; do | ||
| case "$tool" in | ||
| *__"$suffix" | "$suffix") | ||
| decide deny "connector-verb-guard: $suffix is denied by AGENTS.md — no heartbeats (send_later/Routines/timers) and no babysitting a PR. Landing is driven by \`mise run land\`, which laps fetch -> rebase -> verify -> push -> ci-wait -> fast-forward by itself; a webhook's silence is not success. Bypass with BATTEN_CONNECTOR_VERB_BYPASS=1 when a human has asked for this specific call." | ||
| ;; | ||
| esac | ||
| done | ||
|
|
||
| exit 0 | ||
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Apply the approval condition to the one-PR loop.
The new condition appears in the default per-ticket loop. The one-PR loop at Line 137-139 still says
plan→buildwithout the same condition. Repeat “wait for approval only if this was a plan-mode dispatch” in that branch. Otherwise, attended plan-mode one-PR bundles have inconsistent instructions.🤖 Prompt for AI Agents