Pre-fill the alert permissions and test the plugin manifest parsing

Two of the three permissions treated as manual are in fact pre-fillable. The
earlier check scraped the rendered docs page, whose table splits those rows in
a way the parse missed; the docs source lists secret_scanning_alerts and
vulnerability_alerts as supported query parameters. Only checks is genuinely
absent, so the manual list shrinks to that one entry.

That entry now names what breaks without it. Checks: Read governs the status
rollup behind gh pr checks and the annotations behind gh run view, and both
degrade to empty results rather than permission errors, so an unticked box
reads as a broken CI integration rather than a missing scope.

The marketplace and enabled-plugin extraction move out of the install function
into host_marketplaces and host_enabled_plugins so they can be exercised
directly. The tests cover the pipe separator that keeps an absent repo from
shifting a url leftwards, rejection of marketplace sources that are neither
github nor git, disabled plugins being excluded, and the allowlist refusing to
carry credentials, transcripts or history.
This commit is contained in:
2026-07-31 08:42:28 -05:00
parent dbe6c2e34b
commit b8bf9f9eff
4 changed files with 158 additions and 28 deletions
+31 -22
View File
@@ -36,14 +36,17 @@ TOKEN_URL_PERMISSIONS=(
actions=write
statuses=read
security_events=write
secret_scanning_alerts=read
vulnerability_alerts=read
)
# GitHub omits these from the pre-fill parameters, so they can only be ticked
# on the form itself.
# "checks" is the one permission the agent needs that GitHub omits from the
# pre-fill parameters, so it has to be ticked by hand. Each entry names what
# breaks without it, because an unticked box fails later as an empty result or
# a 403 rather than as a permission error.
TOKEN_MANUAL_PERMISSIONS=(
"Checks: Read"
"Dependabot alerts: Read"
"Secret scanning alerts: Read"
"Checks: Read - without it 'gh pr checks' reports no status rollup and"
" 'gh run view' returns no annotations"
)
usage() {
@@ -202,7 +205,8 @@ Create a fine-grained token for $REPOSITORY.
Everything except the repository is pre-filled. On the page:
1. Repository access -> Only select repositories -> ${REPOSITORY#*/}
2. Tick the permissions the form cannot pre-fill:
2. Under Permissions -> Repository permissions, tick the one the form
cannot pre-fill:
EOF
local permission
@@ -557,6 +561,25 @@ EOF
done
}
# Not @tsv: tab is an IFS whitespace character, so read collapses the empty
# field an entry without a repo produces and shifts the URL into it.
host_marketplaces() {
jq -r '
to_entries[]
| [
.key,
(.value.source.source // ""),
(.value.source.repo // ""),
(.value.source.url // "")
]
| join("|")
' "$1"
}
host_enabled_plugins() {
jq -r '(.enabledPlugins // {}) | to_entries[] | select(.value) | .key' "$1"
}
marketplace_url() {
local source_kind="$1"
local repo="$2"
@@ -683,18 +706,7 @@ install_sandbox_claude_plugins() {
</dev/null >/dev/null 2>&1 ||
printf 'Could not add marketplace %s (%s).\n' \
"$name" "$marketplace" >&2
done < <(
jq -r '
to_entries[]
| [
.key,
(.value.source.source // ""),
(.value.source.repo // ""),
(.value.source.url // "")
]
| join("|")
' "$known"
)
done < <(host_marketplaces "$known")
local plugin
while read -r plugin; do
@@ -706,10 +718,7 @@ install_sandbox_claude_plugins() {
else
printf 'Could not install plugin %s\n' "$plugin" >&2
fi
done < <(
jq -r '(.enabledPlugins // {}) | to_entries[] | select(.value) | .key' \
"$settings"
)
done < <(host_enabled_plugins "$settings")
}
install_sandbox_mise() {