framework: make tool discoverability, workspace placement and model tiering mechanical #1174

Merged
Mos merged 24 commits from feat/workspace-hygiene-tool-enforcement into main 2026-08-13 14:21:23 +00:00
2 changed files with 68 additions and 16 deletions
Showing only changes of commit 51746f44eb - Show all commits
@@ -180,6 +180,23 @@ FIXTURES="$TMP/fixtures.tsv"
# Requesting a reviewer is not submitting one; pr-review.sh files verdicts and
# nothing in the tree adds a requested reviewer.
printf '0\t{"tool_input":{"command":"gh api -X POST repos/a/b/pulls/1/requested_reviewers -f reviewers[]=u"}}\tno wrapper requests a reviewer, so it is not refused with pr-review.sh\n'
# SPAN, applied to the guard's OWN fail-closed rule rather than to a wrapper.
# The scope gate admits three shapes; the unreadable-endpoint rule asked only
# for `https?://`, so a split endpoint in the other two was in scope to block,
# produced no readable endpoint, and fell through to allow. Same defect class
# as the milestone arm, one layer up. Each shape gets its own fixture, because
# a single one would have passed on the arm that already worked.
printf '2\t{"tool_input":{"command":"p=repos/a/b/iss; q=ues; gh api -X POST ${p}${q} -f title=x"}}\ta split endpoint in a provider-CLI api call is unreadable, not absent\n'
printf '2\t{"tool_input":{"command":"p=repos/a/b/issues/1/comm; q=ents; gh api -X POST ${p}${q} -f body=x"}}\tsame, comments\n'
printf '2\t{"tool_input":{"command":"p=repos/a/b/pulls/1/rev; q=iews; gh api -X POST ${p}${q} -f event=APPROVED"}}\tsame, and a verdict is the costliest one to lose\n'
printf '2\t{"tool_input":{"command":"p=repos/a/b/iss; q=ues; tea api -X POST ${p}${q} -f title=x"}}\tevery CLI the scope gate admits, not just gh\n'
printf '2\t{"tool_input":{"command":"p=/api/v1/repos/a/b/iss; q=ues; curl -X POST -d x git.example.invalid${p}${q}"}}\ta schemeless forge host with a split path is unreadable too\n'
printf '2\t{"tool_input":{"command":"h=git.example.invalid; q=ues; curl -X POST -d x ${h}/api/v1/repos/a/b/iss${q}"}}\tthe expansion may come first; the token is what matters\n'
# And the reason this is not "any variable blocks a write": a payload in a
# variable is the SAFE way to pass one and leaves the endpoint fully legible.
printf '0\t{"tool_input":{"command":"gh api repos/a/b/git/refs -f sha=$SHA"}}\tan expansion in a body value leaves the endpoint readable\n'
printf '0\t{"tool_input":{"command":"curl -X POST -d \\"$BODY\\" https://git.example.invalid/api/v1/repos/a/b/git/refs"}}\tsame for a quoted body on an unwrapped endpoint\n'
printf '0\t{"tool_input":{"command":"gh api repos/${OWNER}/${REPO}/git/refs"}}\ta read with a split endpoint is still a read\n'
printf '0\t{"tool_input":{"command":"curl -X PATCH -d @b https://git.example.invalid/api/v1/repos/a/b/issues/comments/5"}}\tediting a comment has no wrapper; only creating one does\n'
printf '0\t{"tool_input":{"command":"curl -X POST -d @b https://git.example.invalid/api/v1/repos/a/b/issues/1/stopwatch/start"}}\tno wrapper owns a stopwatch, and none is invented for it\n'
printf '0\t{"tool_input":{"command":"gh api -X POST repos/a/b/issues/1/times -f time=60"}}\tnor time tracking\n'
@@ -308,7 +308,7 @@ edit — that one is a real wrapper gap, and the override exists for it." ;;
fi ;;
esac
# A URL the guard cannot READ is a URL the guard must not CLEAR.
# An endpoint the guard cannot READ is an endpoint the guard must not CLEAR.
#
# Round one fixed one spelling of this and review immediately produced the
# general form: split the endpoint token itself across two variables —
@@ -318,25 +318,58 @@ edit — that one is a real wrapper gap, and the override exists for it." ;;
# fragments is unwinnable: the endpoint does not exist until the shell
# expands it, and this hook runs before that.
#
# So stop pretending to read it. If a write's URL contains an expansion,
# So stop pretending to read it. If a write's endpoint contains an expansion,
# the guard has no endpoint to judge, and "no endpoint" must not mean
# "allowed" — that is the same absence-driven allow as the missing-wrapper
# case, wearing different clothes.
#
# Scoped to commands that are visibly forge-shaped, so an opaque webhook or
# artifact POST is untouched. A caller who splits `/api/` and the hostname
# as well does get through; that is no longer a mistake anyone makes by
# accident, and this hook stops mistakes. It is not a sandbox, and pretending
# otherwise is how you get a control nobody can trust the boundaries of.
if [ -z "$endpoint" ] \
&& printf '%s' "$CMD" | grep -Eq 'https?://[^[:space:]"'"'"'|;&)]*[$`]' \
&& printf '%s' "$CMD" | grep -Eq '/api/v[0-9]|/repos/|git\.|gitea|github\.com|gitlab|forgejo'; then
cat <<EOF
BLOCKED: raw provider API write whose URL this guard cannot read.
# SPAN, and the defect review found here: a fail-closed rule must cover the
# same surface as the block it guards. This test asked only for `https?://`
# while the scope gate above had already been widened to three shapes, so
# p=repos/a/b/iss; q=ues; gh api -X POST ${p}${q} -f title=x
# p=/api/v1/repos/a/b/iss; q=ues; curl -X POST -d x git.example.invalid${p}${q}
# were in scope to be blocked, produced no readable endpoint, and then fell
# through to ALLOW — while the identical split behind a literal `https://`
# blocked. Same shape as the milestone arm one round earlier: the correct
# treatment already existed and was applied to one of the surfaces it
# covered. A control is only as wide as its narrowest arm.
#
# Three arms, one per shape the scope gate admits:
# A a scheme-bearing URL token carrying an expansion
# B a schemeless token carrying BOTH a forge fragment and an expansion
# C the endpoint argument of a provider-CLI `api` call carrying one
#
# Stated limits, because a control may not claim more than it measures. B
# requires the fragment and the expansion in the SAME shell token, so a
# caller who splits the hostname and `/api/` as well gets through. C reads
# the endpoint positionally — the first bare token after `api` and its option
# run — so an endpoint pushed past an option whose value itself contains
# whitespace is not seen. Both are deliberate: this hook stops mistakes, it
# is not a sandbox, and pretending otherwise is how you get a control nobody
# can trust the boundaries of.
#
# Note what is NOT unreadable: an expansion in a BODY (`-d "$BODY"`,
# `-f sha=$SHA`) leaves the endpoint perfectly legible, and blocking it would
# punish the safest way to pass a payload. Only the endpoint region counts.
URLTOK='[^[:space:]"'"'"'|;&)]*'
FORGE='(/api/v[0-9]|/repos/|git\.|gitea|github\.com|gitlab|forgejo)'
unreadable=0
if printf '%s' "$CMD" | grep -Eq "https?://$URLTOK"'[$`]' \
&& printf '%s' "$CMD" | grep -Eq "$FORGE"; then unreadable=1; fi
printf '%s' "$CMD" | grep -Eq \
"$URLTOK($FORGE$URLTOK"'[$`]'"|"'[$`]'"$URLTOK$FORGE)" && unreadable=1
printf '%s' "$CMD" | grep -Eq \
'(^|[[:space:]|;&(])(gh|tea|glab|hub)[[:space:]]+api([[:space:]]+--?[A-Za-z][A-Za-z-]*([[:space:]]+[^-[:space:]][^[:space:]]*)?)*[[:space:]]+[^-[:space:]][^[:space:]]*[$`]' \
&& unreadable=1
The URL is assembled from shell expansions, so the endpoint it names does not
if [ -z "$endpoint" ] && [ "$unreadable" -eq 1 ]; then
cat <<EOF
BLOCKED: raw provider API write whose endpoint this guard cannot read.
The endpoint is assembled from shell expansions, so the path it names does not
exist until the shell builds it — after this check runs. The guard cannot tell
whether it is a wrapped endpoint, and an unreadable URL is not a cleared one.
whether it is a wrapped endpoint, and an unreadable endpoint is not a cleared
one.
$W/ <- the wrappers; use the one for the endpoint you are calling
@@ -344,8 +377,10 @@ If you are calling a wrapped endpoint (reviews, merges, comments, pulls,
issues, milestones), use the wrapper — it also resolves identity explicitly,
which matters on a host whose default provider login is an admin account.
If this is genuinely not a provider endpoint, either write the URL literally so
the guard can see what it is, or prefix MOSAIC_WRAPPER_OVERRIDE=1.
If this is genuinely not a provider endpoint, either write the endpoint
literally so the guard can see what it is, or prefix MOSAIC_WRAPPER_OVERRIDE=1.
A variable in the BODY is fine and does not trigger this; only the endpoint
itself has to be legible.
EOF
exit 2
fi