2a2a87251a6d9cba6fe6a4563d4ea69b52ebb35d
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2a2a87251a |
wrapper-guard: read the command the shell will run, and stop losing the client behind option values
ci/woodpecker/pr/ci Pipeline failed
Round-four review, three more absence-driven allows. 1. The guard read the command as TYPED. A backslash before a newline is removed before anything else happens, so an endpoint token split across the join (`.../iss\` + newline + `ues/1/comments`) executed the comments endpoint while the literal token never appeared in the text. Continuations are now joined before every check, because the joined form IS the command. This is the same defect as the split-across-variables case, minus the excuse: there the token genuinely does not exist until the shell expands it, here it was sitting in the input the whole time and the guard chose the wrong reading of it. 2. Transparent prefixes take option VALUES. `sudo -u root curl` hid a live write because `root` was a word the prefix list did not know. Enumerating option grammars per prefix is the wrong game, so what is skipped is an option and at most one value for it, plus a bare duration for `timeout` — never an arbitrary word. `xargs echo curl ...` therefore stays ALLOWED, because there the command is echo and the client is its argument. 3. `find -exec` runs the client. It opens command position the same way an operator does, and now reads that way. All seven reviewer repros are fixtures, each with its counter-case in the allowed direction: a continuation inside a heredoc document stays a document, `xargs echo curl` stays allowed, `sudo apt-get install curl` stays allowed, a prefixed READ stays allowed. 48/48, and the 18-command ordinary sweep still blocks none. Gates: sanitization, resident budget, test enumeration, tools-index (self-test 4/4, git suite 100%), prettier. |
||
|
|
e6a881a795 |
wrapper-guard: judge command position on prefixes and on what a shell will execute
ci/woodpecker/pr/ci Pipeline failed
Round-three review found two more absence-driven allows, both in the skeleton introduced by round two, and fixing them exposed a third the reviewer had not reached yet. 1. A word in front of a command does not displace the command. `env VAR=v curl`, `command curl`, `timeout 10 curl` and `/usr/bin/curl` were all real writes at execution position that a bare-name match could not see. The `env` form is the one that matters: it is what an agent reaches for to keep a credential out of the global environment, so the careful spelling was the invisible one. 2. Quoted data stops being data when a shell is about to execute it, and the first version knew only `bash -c`, `sh <<` and `eval`. It did not know the pipe, which is the form people actually use: `printf ... | sh`, `cat <<EOF | sh`, `sh -s <<EOF` each made a live call vanish from the skeleton while still running. 3. Found while testing the fix: that decision was made for the WHOLE command, so a single unrelated `docker run ... sh -c 'echo hi'` promoted every other quoted span on every other line to code. It blocked its own author for the second time in a day. A shell on one line does not execute a string on another line, and over-blocking is not the safe direction — a guard that blocks ordinary work gets switched off, and a guard that is off permits everything. The skeleton is now built per line, and a heredoc body is code only when the line that opened it fed a shell. All seven reviewer repros are pinned as fixtures, each with a counter-fixture in the allowed direction: `echo timeout 10 curl ...` is not a call, a pipe to `wc` is not execution, an unrelated shell on another line changes nothing. Fixtures 40/40, and a sweep of 18 ordinary commands blocks none of them. Gates: sanitization, resident budget, test enumeration, tools-index (self-test 4/4, git suite 100%), prettier. |
||
|
|
7962e4302f |
guard: judge command position on the code, not on the text
ci/woodpecker/pr/ci Pipeline was canceled
The position test added an hour ago blocked its own author. The message being sent quoted one of the fixtures, so the quoted text contained an operator followed by a client, and an operator inside a string is not an operator. That is the reported over-blocking defect one level in, and it landed within an hour of shipping the fix for the reported one — which is the argument for pinning both directions as fixtures rather than reasoning about them. Position is now judged against a SKELETON: the command with its data spans (quoted strings, heredoc bodies) removed. Endpoint, URL and body detection keep running against the full text, because real calls quote their URLs and a skeleton would be blind to them. The exception is what makes quotes data in the first place. If something is about to EXECUTE the quoted text — `bash -c`, `sh <<EOF`, `eval` — the quotes hold code, and the skeleton keeps them as command separators so the client inside is still at command position, one interpreter down. Three fixtures: an operator inside a quoted string, a heredoc body, and `bash -c` making the same text code again. 30/30. |
||
|
|
8a901cc19a |
guard: read command position, refuse unreadable URLs, survive pipefail
ci/woodpecker/pr/ci Pipeline was canceled
Round two of the same independent review. Three findings, all real, and the first two share a root cause: the guard was reading command TEXT as though it were a command. 1. Splitting the endpoint token itself defeats fragment matching outright — `a=/api/v1/repos/o/r/iss; b=ues/1/comments` leaves no fragment contiguous. Round one fixed one spelling of this and the reviewer produced the general form immediately. It is not winnable by more fragments: the endpoint does not exist until the shell expands it, and this hook runs first. So the guard stops pretending to read it. A write whose URL contains an expansion, on a visibly forge-shaped command, is now BLOCKED as unreadable — because "I could not find an endpoint" must not mean "there is no endpoint". Opaque URLs that are not forge-shaped (webhooks, artifact stores) still pass. 2. The broadened body detection false-blocked ordinary work: `grep -R "curl -d https://.../issues" docs/`, `echo "curl -d ..." > note.txt`, printing an example from python. Talking about a call is not making one, and this is the direction that actually kills a control — an over-blocking hook gets turned off, and an off hook permits everything. The client must now appear at COMMAND POSITION: line start or after a shell operator, optionally behind VAR=value. In every false positive it sat behind a quote instead. Quotes are deliberately NOT stripped before matching; real calls quote their URLs. 3. `wt_precious()` aborted `cmd_rm` under `set -euo pipefail`: `grep -v` exits 1 when it filters everything out, which is exactly the disposable-only case, so a SAFE worktree failed to remove with no message. Fixed, and the same defect was latent one step upstream in `wt_dirty()`, where `head -200` SIGPIPEs git on any worktree with 201 changed files. The cap is gone — counting is cheap and the cap only ever truncated output that is no longer printed. Seven new fixtures pin all of it, in both directions. 27/27. |
||
|
|
8b7ac5b51e |
guard: close four fail-open holes found by independent review
ci/woodpecker/pr/ci Pipeline was canceled
An independent reviewer broke all three new controls before they shipped. Every finding is reproduced as a fixture or a repro, because the class is recurring rather than incidental: each hole was a case where the answer was "allow" because something was ABSENT rather than because it was CHECKED. 1. wrapper-guard read only the spellings it knew. `curl -d@body` (no space), `--request=POST` (equals form), and a URL path assembled from shell variables each carried a real provider write straight through. Write detection now covers every body and method form curl accepts, and the endpoint match no longer anchors on a literal host path that a variable can dissolve. 2. wrapper-guard blocked only when the wrapper FILE existed. A host with a broken or partial install therefore permitted exactly the raw writes the guard exists to stop. Blocking is now on the endpoint; a missing wrapper changes the remedy text, not the verdict — a broken install is not permission to bypass gate 7. 3. mosaic-worktree read a worktree's safety from two questions, and a clean, fully-pushed tree holding a gitignored `local.secret` answered both with zero. `git worktree remove` then deleted the one copy in existence. A file is gitignored precisely so nothing else holds it, so ignored-but-not- disposable files are now a third evidence question. Build junk (node_modules, .venv, dist, caches, *.pyc) stays disposable, so the common case still reads SAFE. 4. check-tools-index counted a documented tool as discoverable at mode 0644. Every caller tests `[ -x ]`, so a non-executable tool is a missing tool; it now fails the gate with its own message. Local gates green: sanitization, resident budget, test enumeration, tools-index (4/4 self-test, 100% on the enforced git suite), and wrapper-guard 20/20. |
||
|
|
96609bdade |
framework: prove the wrapper guard both ways, and resolve its wrappers relatively
ci/woodpecker/pr/ci Pipeline failed
Two defects found by running the guard rather than reading it. 1. The guard resolved its sibling wrappers through a hardcoded $HOME/.config/mosaic/tools/git. On a host with no installed mosaic home — a CI container, a bare checkout — every wrapper lookup missed, `[ -x ]` failed, and the guard fell through allowing the raw API write it exists to block. It failed OPEN, silently, in exactly the environment least likely to notice. It now resolves relative to its own path, so it names the wrappers from the install it was launched from, with $HOME as the fallback. 2. There was no test. Adding one surfaced the guard's other sharp edge immediately: it matches the literal text of the Bash command, so a harness that embeds a blocked pattern inline trips the guard on itself rather than on the fixture. That is the correct fail-closed posture and it is now recorded in the test's own comments, because the next person will hit it too. test-wrapper-guard.sh asserts twelve fixtures and asserts the ALLOWED cases as hard as the blocked ones. A guard that over-blocks gets routed around and a guard that under-blocks is decoration; only pinning both edges keeps it useful. It is hermetic — no network, no credentials, no repository — so it joins the CI sanitization step directly rather than the exclusions file. |
||
|
|
e3a0ee87b3 |
framework: make tool discoverability, workspace placement and model tiering mechanical
An undocumented tool is, from inside an agent session, indistinguishable from a tool that was never written. The framework shipped 26 git wrappers and named 6 of them in its resident index docs — 23% discoverability, with pr-review.sh among the missing. The observable consequence was an agent obeying Constitution gate 7 as best it could see it, reaching for raw curl, sending GitHub's APPROVE to a Gitea host, and getting HTTP 200 with the review silently filed PENDING. Three times. That is not a discipline failure and no amount of prose fixes it. Four changes, each converting a rule that decayed into a mechanism that cannot: - check-tools-index.sh (new, CI-blocking): every tool in an enforced suite must be named in a resident index doc, and every tool an index names must exist. The git suite is enforced now; other suites report coverage without failing, so the ratchet tightens one reviewed PR at a time instead of landing as one sweep. The enforced list is framework-owned rather than a marker inside operator-owned TOOLS.md — a doc marker would let an operator silence the gate on exactly the host where it matters most. Carries --self-test, because a checker that only ever passes is indistinguishable from one that is not running. - TOOLS-REFERENCE.md: complete 28-entry git index, plus the APPROVED/APPROVE dialect note that explains why pr-review.sh is not a formality. - mosaic-worktree.sh + wrapper-guard.sh (upstreamed): the rule "big work goes on a work filesystem" already existed in prose, and 255 GB accumulated in $HOME across 842 directories anyway, under five simultaneous placement conventions on one host. The helper therefore exposes no placement decision — given a branch name, every path is derived from `git worktree list --porcelain`. Worktrees rather than clones because enumerability is the only thing that makes reclaim safe, and reclaim is by evidence (clean tree + no unpushed commits), never by size or age. The guard blocks three mechanically-detectable mistakes and nothing else: a checkout into $HOME, a raw provider-API write to an endpoint that has a wrapper, and the literal APPROVE event. Reads pass untouched. - STANDARDS.md: model tiering as a standard, named by capability class so it survives a model generation. Start cheapest, escalate on evidence, benchmark before demoting a task class, and keep the class->model binding in operator config with the DB-backed config service as the end state. Registering the guard in runtime/claude/settings.json is the point of upstreaming it: ~/.claude/settings.json is a framework-managed copy, so a hand-added hook there is destroyed by the next upgrade. In the template it survives, and it reaches every host instead of one. |
||
|
|
880c28b191 |
docs(glpi-skills): genericize operator-specific content per review
ci/woodpecker/pr/ci Pipeline was successful
|