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
4 changed files with 180 additions and 34 deletions
Showing only changes of commit 06046f7675 - Show all commits
+7
View File
@@ -61,6 +61,13 @@ steps:
# endpoints and ordinary commands through. Both directions are asserted —
# a guard that over-blocks gets routed around, which fails just as hard.
- bash packages/mosaic/framework/tools/git/test-wrapper-guard.sh
# Hermetic regression for mosaic-worktree.sh at fleet scale: stubs git onto
# PATH so `list` faces ~450 KB of porcelain. The defect it pins is invisible
# at small size — `git … | awk '…exit'` gives the producer SIGPIPE, which
# under `set -euo pipefail` aborts the caller silently with rc=141 and no
# output. A repo only reaches that once it has enough worktrees, so the
# stub supplies the scale instead of the host's own checkout.
- bash packages/mosaic/framework/tools/git/test-mosaic-worktree-large-repo.sh
# Blocking gate (#791): a framework upgrade must never write or delete an
# operator-owned path. The HARD GATE proves an unanticipated operator sentinel
View File
@@ -267,12 +267,34 @@ FIXTURES="$TMP/fixtures.tsv"
# and the call went through. An unreadable request is not a cleared one.
printf '2\t{"tool_input":{"command":"curl --config /tmp/req https://git.example.invalid/api/v1/repos/a/b/pulls/1/reviews"}}\tthe request in a config file is unreadable, so it is refused\t--config/-K\n'
printf '2\t{"tool_input":{"command":"curl -K /tmp/req https://git.example.invalid/api/v1/repos/a/b/issues"}}\tthe short spelling, same answer\t--config/-K\n'
# Cost, stated: the scope gate admits any https URL, so this refuses a
# --config read against a host that has nothing to do with a forge. The
# alternative is to require a forge marker in a command whose URL may itself
# be in the file, which is the hole again.
# Cost, stated: this refuses a --config read against a host that has nothing
# to do with a forge. The alternative is to require a forge marker in a
# command whose URL may itself be in the file, which is the hole again.
printf '2\t{"tool_input":{"command":"curl --config /tmp/req https://example.invalid/anything"}}\tan unrelated https URL with --config is refused too, by decision\t--config/-K\n'
printf '0\t{"tool_input":{"command":"eslint --config .eslintrc.json src/"}}\t--config outside an API-shaped command is nobody'"'"'s business\n'
printf '0\t{"tool_input":{"command":"eslint --config .eslintrc.json src/"}}\t--config on a command that is not curl is nobody'"'"'s business\n'
# ROUND TEN. The --config check above was first written INSIDE the API-shape
# gate, so it was guarded by a condition that the capability it guards against
# removes. A config file can carry the URL; delete the URL from the command and
# nothing is API-shaped, the branch is never entered, and the guard reports
# clean on precisely the call it exists to refuse. It is now asked of any curl.
printf '2\t{"tool_input":{"command":"curl --config /tmp/provider-write.cfg"}}\ta config file can own the URL, so there is nothing API-shaped left to gate on\t--config/-K\n'
printf '2\t{"tool_input":{"command":"curl -K/tmp/provider-write.cfg"}}\tcurl accepts the value attached to the short flag\t--config/-K\n'
printf '2\t{"tool_input":{"command":"curl -sK /tmp/provider-write.cfg"}}\tand inside a bundle, which a space-separated test does not see\t--config/-K\n'
printf '0\t{"tool_input":{"command":"tar -K /tmp/archive.tar"}}\t-K on a command that is not curl is not this hook'"'"'s business\n'
# Percent-encoded endpoints. Not hypothetical: /issues/1174 and /iss%%75es/1174
# both returned HTTP 200 with the same object from the live forge, so the
# encoded spelling IS the wrapped endpoint and the literal comparison below it
# sees a segment matching nothing. Refused rather than decoded — a decoder has
# to be exactly right about depth and normalization, which is the parser
# mistake this file declines everywhere else.
printf '2\t{"tool_input":{"command":"gh api -X POST repos/a/b/iss%%75es/1/comments -f body=x"}}\tan encoded path segment reaches the wrapped endpoint\tpercent-escape\n'
printf '2\t{"tool_input":{"command":"curl -X POST -d @b https://git.example.invalid/api/v1/repos/a/b/pulls/1/revi%%65ws"}}\tsame for the review endpoint, which is the one that matters most\tpercent-escape\n'
printf '2\t{"tool_input":{"command":"gh api -X POST repos/a/b/iss%%2575es/1/comments -f body=x"}}\tdouble-encoded too, which is why this refuses instead of decoding\tpercent-escape\n'
# Scoped to writes, deliberately. Reads are never blocked by this guard and a
# query string carrying %%20 is an ordinary URL, not a hazard.
printf '0\t{"tool_input":{"command":"curl -s https://git.example.invalid/api/v1/repos/a/b/issues?q=a%%20b"}}\ta percent-escape in a READ is not this hook'"'"'s business\n'
} > "$FIXTURES"
fail=0 n=0
@@ -358,6 +380,62 @@ home_case 'HOME unset does not block an ordinary command' \
home_case 'HOME unset does not stop the API arms doing their job' \
2 '@unset' 'curl -X POST -d @b https://git.example.invalid/api/v1/repos/a/b/pulls/1/reviews' 'pr-review.sh'
# ---- the guard standing on its own -----------------------------------------
# Every case above runs the guard from the directory holding its siblings, so
# `[ -x "$W/pr-review.sh" ]` succeeds and the $HOME fallback beside it never
# evaluates. That is a property of the HARNESS, not of the guard, and it hid a
# live fail-open: with the guard copied somewhere alone AND no HOME, the
# fallback expanded an unset variable under `set -u` and the script died at
# rc=1 — on EVERY arm, before any adjudication. A PreToolUse hook exiting
# nonzero-but-not-2 is a non-blocking error, so that seat ran with no guard and
# nothing reported it.
#
# The first remediation moved that expansion four lines earlier and called it
# closed. It was not closed, because the test could not reach it. So the guard
# is copied ALONE here — no siblings, no installed mosaic home — which is the
# deployment this file already claims to support ("still works from a repo
# checkout with no installed mosaic home").
LONE="$TMP/lone"; mkdir -p "$LONE"
cp "$GUARD" "$LONE/wrapper-guard.sh"; chmod +x "$LONE/wrapper-guard.sh"
lone_case() {
local why="$1" want="$2" homeval="$3" cmd="$4" needle="${5:-}"
local out got
n=$((n + 1))
if [ "$homeval" = "@unset" ]; then
out="$(printf '%s' "{\"tool_input\":{\"command\":\"$cmd\"}}" | env -u HOME "$LONE/wrapper-guard.sh" 2>&1)"
else
out="$(printf '%s' "{\"tool_input\":{\"command\":\"$cmd\"}}" | env HOME="$homeval" "$LONE/wrapper-guard.sh" 2>&1)"
fi
got=$?
if [ "$got" != "$want" ]; then
printf 'FAIL %s [standalone] (want exit %s, got %s)\n' "$why" "$want" "$got"
fail=1
return
fi
if [ -n "$needle" ] && ! printf '%s' "$out" | grep -Fq -- "$needle"; then
printf 'FAIL %s [standalone] (exit %s, but the message does not say %s)\n' "$why" "$got" "$needle"
fail=1
return
fi
printf 'ok %s [standalone]\n' "$why"
}
lone_case 'no siblings and no HOME: an ordinary command still passes, not rc=1' \
0 '@unset' 'ls -la /src'
lone_case 'no siblings and no HOME: a wrapped write is still refused' \
2 '@unset' 'curl -X POST -d @b https://git.example.invalid/api/v1/repos/a/b/pulls/1/reviews' 'pr-review.sh'
lone_case 'no siblings and no HOME: a checkout is refused, not adjudicated' \
2 '@unset' 'git clone https://example.invalid/x /src/wt' 'unset or unusable'
# Spelled without quotes on purpose. The payload is interpolated into a JSON
# string by the helper, so a fixture carrying bare double quotes produces
# malformed JSON, jq returns empty, and the guard exits 0 on an empty command —
# a PASS that measures nothing. That is what the first version of this case did.
lone_case 'no siblings and no HOME: the APPROVE trap still fires' \
2 '@unset' 'gh api -X POST repos/a/b/pulls/1/reviews -f event=APPROVE'
lone_case 'no siblings, usable HOME: ordinary commands unaffected' \
0 '/home/tester' 'ls -la /src'
printf '\n'
if [ "$fail" -eq 0 ]; then
printf 'wrapper-guard: %d/%d fixtures behaved as specified.\n' "$n" "$n"
@@ -100,8 +100,26 @@ override_prefixed && exit 0
# still works from a repo checkout with no installed mosaic home (which is how it
# is exercised in CI). $HOME remains the fallback for a guard invoked by an
# absolute path from somewhere unusual.
# $HOME is resolved HERE, once, before anything expands it. The previous attempt
# fixed the checkout arm's use of $HOME and left this one, four lines earlier,
# reading it raw — so under `set -u` a seat with no HOME still died before
# reaching the adjudication that was supposed to handle exactly that. Moving a
# fail-open earlier in the file is not closing it. There is now exactly one
# expansion of HOME in this script and it is guarded; every later use reads
# HOME_DIR / home_known instead, so a new use cannot reintroduce the abort
# without going through this block.
#
# Empty and unset are different values and neither one is a home directory. '/'
# is rejected for the same reason as '': every path is under it, so comparing
# against it stops discriminating at all.
HOME_DIR=""
home_known=0
case "${HOME-}" in
/?*) HOME_DIR="$HOME"; home_known=1 ;;
esac
W="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
[ -x "$W/pr-review.sh" ] || W="$HOME/.config/mosaic/tools/git"
[ -x "$W/pr-review.sh" ] || W="$HOME_DIR/.config/mosaic/tools/git"
# ---- 1. checkout into $HOME ------------------------------------------------
# $HOME has to be RESOLVED before anything can be compared against it, and the
@@ -125,11 +143,9 @@ W="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# was unable to ask. The blast radius of that fail-closed arm is exactly one
# command shape (clone / worktree add), not the session.
home_re='~|\$HOME'
home_known=0
case "${HOME-}" in
/?*) home_re="$home_re|$(printf '%s' "$HOME" | sed 's/[][\.*^$+?(){}|]/\\&/g')"
home_known=1 ;;
esac
if [ "$home_known" -eq 1 ]; then
home_re="$home_re|$(printf '%s' "$HOME_DIR" | sed 's/[][\.*^$+?(){}|]/\\&/g')"
fi
if printf '%s' "$CMD" | grep -Eq 'git[^|;&]*(clone|worktree[[:space:]]+add)'; then
if [ "$home_known" -eq 0 ]; then
@@ -254,30 +270,35 @@ fi
# `gh pr merge`) is NOT covered — catching that means modelling every CLI's verb
# grammar, which is the parser mistake again in a new costume. Porcelain is a
# gate-7 gap for prose and review to hold, not this hook.
API_SHAPED='https?://|/api/v[0-9]|/repos/'
API_SHAPED="$API_SHAPED"'|(^|[[:space:]|;&(])(gh|tea|glab|hub)[[:space:]]+api([[:space:]]|$)'
if printf '%s' "$CMD" | grep -Eq "$API_SHAPED"; then
# curl takes its options from a file with -K/--config, and that file may carry
# the method, the body, the headers and the URL itself. Every write test below
# reads the command TEXT, and none of those things are in it:
#
# curl --config /tmp/req .../repos/a/b/pulls/1/reviews
#
# carries no -X, no -d, no -f, so is_write stayed 0 and the call went straight
# through. This is not a spelling the write detection was missing — it is the
# write question being unaskable, which is the same situation as an endpoint
# assembled from expansions, and it gets the same answer. Refusing a READ that
# happens to use --config is the acceptable side of that trade, and the message
# says how to proceed.
if printf '%s' "$CMD" | grep -Eq -- '(^|[[:space:]])(-K|--config)([[:space:]=]|$)'; then
cat <<EOF
BLOCKED: provider API call whose options are supplied from a --config/-K file.
# ---- curl whose request lives in a file ------------------------------------
# curl takes its options from a file with -K/--config, and that file may carry
# the method, the body, the headers AND THE URL. That last one is why this test
# cannot live inside the API-shape gate below, which is where it was first put:
#
# curl --config /tmp/provider-write.cfg
#
# has no URL, no /repos/, no `gh api` — nothing API-shaped in the text at all —
# so it never entered the branch that was supposed to refuse it, and the guard
# reported clean on the exact capability the check exists to deny. The check was
# guarded by a condition the thing it guards against defeats. It is now asked of
# any curl, because "is this a provider call" is not answerable about a command
# whose URL is in a file, and a question that cannot be asked is not a question
# that came back clean.
#
# The spelling is deliberately loose. curl accepts the value attached to the
# short flag (`-K/tmp/req`, verified) and inside a bundle (`-sK /tmp/req`), and a
# guard that recognizes only the space- and equals-separated forms is defeated by
# deleting one character. Scoped to curl so that `eslint --config .eslintrc.json`
# and every other tool with a --config flag are untouched.
if printf '%s' "$CMD" | grep -Eq '(^|[[:space:]|;&(])curl([[:space:]]|$)' \
&& printf '%s' "$CMD" | grep -Eq -- '(^|[[:space:]])(-[A-Za-z]*K([[:space:]=]|$|[^[:space:]])|--config([[:space:]=]|$))'; then
cat <<EOF
BLOCKED: curl invocation whose request is supplied from a --config/-K file.
curl reads the request method, body, headers and even the URL from that file.
None of them appear in the command, so this guard cannot tell whether the call
is a read or a write, or what endpoint it reaches. An unreadable request is not
a cleared one.
is a read or a write, what endpoint it reaches, or whether it is a provider call
at all. An unreadable request is not a cleared one.
$W/ <- the wrappers; use the one for the endpoint you are calling
@@ -286,8 +307,12 @@ is a write to a wrapped endpoint, use the wrapper. For a genuine gap no wrapper
can express, prefix MOSAIC_WRAPPER_OVERRIDE=1 (as the first thing in the
command — it is read positionally, not matched as text).
EOF
exit 2
fi
exit 2
fi
API_SHAPED='https?://|/api/v[0-9]|/repos/'
API_SHAPED="$API_SHAPED"'|(^|[[:space:]|;&(])(gh|tea|glab|hub)[[:space:]]+api([[:space:]]|$)'
if printf '%s' "$CMD" | grep -Eq "$API_SHAPED"; then
# Write detection, now client-agnostic. Every spelling curl accepts, because
# the guard is defeated by the one spelling it does not know: `-d@body` (no
@@ -325,6 +350,42 @@ EOF
'\.(post|put|patch|delete)\(' && is_write=1
if [ "$is_write" -eq 1 ]; then
# A percent-escape makes the endpoint unreadable HERE and perfectly readable
# to the PROVIDER, which is the whole hazard. Measured against the live
# forge, read-only: GET /repos/mosaicstack/stack/issues/1174 and
# GET /repos/mosaicstack/stack/iss%75es/1174 both returned HTTP 200 with the
# same object. So `iss%75es` IS the wrapped endpoint by the only authority
# that gets a vote, while every literal comparison below sees a segment that
# matches nothing and clears the write.
#
# This refuses rather than decodes. A decoder has to be exactly right about
# depth (%2569 -> %69 -> i) and about which characters the provider
# normalizes, and being exactly right about someone else's parser is the
# mistake this file declines everywhere else. Refusing is correct at every
# depth at once.
#
# Scoped to WRITES. Reads are never blocked by this guard, and a query
# string carrying %20 is not a hazard — it is an ordinary URL. Putting this
# test on the whole API-shaped branch would have refused those too, which is
# how a guard earns being routed around.
if printf '%s' "$CMD" | grep -Eq '%[0-9A-Fa-f][0-9A-Fa-f]'; then
cat <<EOF
BLOCKED: provider API WRITE containing a percent-escape.
The provider decodes the path before routing; this guard compares it literally.
An encoded segment therefore reaches a wrapped endpoint while reading, here, as
a segment that matches nothing — /iss%75es/ and /issues/ were measured returning
the same object from the same repository.
$W/ <- the wrappers; use the one for the endpoint you are calling
Spell the path literally and re-run. If the escape is genuinely required and no
wrapper can express the call, prefix MOSAIC_WRAPPER_OVERRIDE=1 (as the first
thing in the command — it is read positionally, not matched as text).
EOF
exit 2
fi
# The endpoint map, and the rule that keeps it honest: an arm exists here
# ONLY because a wrapper in this directory owns that call. It is an
# inventory, not a model — read off `ls tools/git/*.sh` and the flags each