From deb4760b115d0848657bd5fe6725ddf2512603b7 Mon Sep 17 00:00:00 2001 From: veronica Date: Mon, 24 Aug 2026 20:44:09 -0500 Subject: [PATCH] feat(tools/git): add grant-reviewer.sh org-team reviewer grant with fail-closed read-back (#1415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the operator-tree grant-reviewer tool into the framework suite. grant-reviewer.sh -u [-r /] [-t ] grants a reviewer code-read plus issues/pulls-write on an org-owned Gitea repository via an org team (default fleet-reviewers). Gitea only; on a GitHub remote it refuses with a clear error. Idempotent: exact-name team lookup before create (permission read, units_map: code read, issues write, pulls write); member and repo additions are idempotent puts. Fail-closed read-back: after the writes, the single member resource and the single team-repo resource are both fetched back and must confirm the grant or the run fails — a success status is an exit code, not evidence of a durable write (#865 defect class). HTTP 403 on any step surfaces as 'org admin required on ', never a silent partial grant. Identity follows issue-comment.sh: GITEA_LOGIN names a tea login whose host-matched token MUST resolve (fail closed, no downgrade to the host default); otherwise the detect-platform.sh identity ladder applies. The bearer token travels via a curl config file, never argv. Documented limitation (field finding, usc/infrastructure PR 273): under branch protection with required approvals, reviews from a read-permission team are official=false and do not count toward the required total; whitelisting the team on the protected branch is an operator review-policy decision this script deliberately does not automate, and official is computed at review submission. Hermetic suite test-grant-reviewer.sh models a real server with persistent on-disk state, exact-payload validation, a substring-named decoy team, write-without-persist sabotage modes, identity-per-request assertions, and token-not-in-argv plus temp-leak checks; enumerated in test:framework-shell. --- .../framework/tools/git/grant-reviewer.sh | 343 ++++++++++ .../tools/git/test-grant-reviewer.sh | 616 ++++++++++++++++++ packages/mosaic/package.json | 2 +- 3 files changed, 960 insertions(+), 1 deletion(-) create mode 100755 packages/mosaic/framework/tools/git/grant-reviewer.sh create mode 100755 packages/mosaic/framework/tools/git/test-grant-reviewer.sh diff --git a/packages/mosaic/framework/tools/git/grant-reviewer.sh b/packages/mosaic/framework/tools/git/grant-reviewer.sh new file mode 100755 index 00000000..81995578 --- /dev/null +++ b/packages/mosaic/framework/tools/git/grant-reviewer.sh @@ -0,0 +1,343 @@ +#!/bin/bash +# grant-reviewer.sh - Grant a reviewer read + review access to an org-owned +# Gitea repository via an org team (default: fleet-reviewers). +# +# Usage: grant-reviewer.sh -u [-r /] [-t ] +# +# The team carries `permission: read` with per-unit overrides +# {repo.code: read, repo.issues: write, repo.pulls: write}: the reviewer can +# read code and write issues/PR reviews, but cannot push. The grant is +# idempotent — the team is looked up before it is created, and member/repo +# additions are PUTs. +# +# KNOWN LIMITATION — branch protection counts these reviews as UNOFFICIAL. +# Gitea computes a review's `official` flag at SUBMISSION time, from write +# permission on the repo or from membership in the protected branch's +# approvals whitelist (disabled by default). A team granted through this +# script has read permission on code, so under branch protection with +# required_approvals the reviewer's approval shows but does NOT count toward +# the required total — the merge still fails with "not enough approvals". +# Enabling the approvals whitelist and adding this team to it is review +# policy (who counts as an official approver), an operator decision made in +# the repo's branch-protection settings, deliberately NOT automated here. +# Because `official` is fixed at submission, whitelisting after the fact +# requires the review to be re-submitted before it counts. +# +# Platform: Gitea only. On a GitHub-remoted repo this script refuses to run — +# GitHub review access is granted through collaborator/team facilities that +# have no equivalent to Gitea's org-team unit map. +# +# Identity: the acting credential resolves exactly as in issue-comment.sh — +# GITEA_LOGIN (when set) names a tea login whose token MUST resolve for the +# remote host (fail closed, never downgrade to the host default identity); +# otherwise the per-seat identity ladder in detect-platform.sh applies +# (MOSAIC_GIT_IDENTITY / git config mosaic.gitIdentity → per-slot token, +# fail-loud on fleet hosts). Managing org teams requires org owner/admin: +# an HTTP 403 from any step is reported as "org admin required on ", +# never as a silent partial grant. +# +# Verification is fail-closed: after the member and repo PUTs, the script +# GETs the single resources back (GET /teams/{id}/members/{user} and +# GET /teams/{id}/repos/{owner}/{repo}) and refuses to report success unless +# both confirm the grant. A PUT that returns success without persisting +# (the #865 defect class: an exit code is not evidence of a durable write) +# therefore fails the run instead of reporting a grant that does not exist. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/detect-platform.sh" + +usage() { + echo "Usage: grant-reviewer.sh -u [-r /] [-t ]" + echo "" + echo "Options:" + echo " -u, --user Gitea username to grant reviewer access (required)" + echo " -r, --repo Target repository as /; defaults to the" + echo " current repository's origin. The owner must be an" + echo " organization." + echo " -t, --team Org team to use/create (default: fleet-reviewers)" + echo " -h, --help Show this help" + echo "" + echo "Environment:" + echo " GITEA_LOGIN Override the acting identity with a named tea login" + echo " (must resolve for the remote host; fails closed)." + echo "" + echo "Grants: code read + issues/pulls write via an org team. Gitea only." + echo "" + echo "LIMITATION: under branch protection with required approvals, reviews" + echo "from a read-permission team are official=false and do not count" + echo "toward the required total. Making them count means enabling the" + echo "protected branch's approvals whitelist and adding the team — an" + echo "operator review-policy decision this script does not automate. The" + echo "official flag is computed at review submission, so a review made" + echo "before whitelisting must be re-submitted afterwards." +} + +REVIEWER="" +REPO_OVERRIDE="" +TEAM="fleet-reviewers" + +while [[ $# -gt 0 ]]; do + case $1 in + -u|--user) + REVIEWER="$2" + shift 2 + ;; + -r|--repo) + REPO_OVERRIDE="$2" + shift 2 + ;; + -t|--team) + TEAM="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +if [[ -z "$REVIEWER" ]]; then + echo "Error: reviewer username is required (-u)" >&2 + exit 1 +fi + +# Gitea usernames and team names are AlphaDashDot. Validating here keeps the +# values safe to interpolate into API paths without URL-encoding. +NAME_RE='^[A-Za-z0-9][A-Za-z0-9._-]*$' +if ! [[ "$REVIEWER" =~ $NAME_RE ]]; then + echo "Error: invalid reviewer username '$REVIEWER'" >&2 + exit 1 +fi +if ! [[ "$TEAM" =~ $NAME_RE ]]; then + echo "Error: invalid team name '$TEAM'" >&2 + exit 1 +fi +if [[ -n "$REPO_OVERRIDE" ]] && ! [[ "$REPO_OVERRIDE" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + echo "Error: -r expects /, got '$REPO_OVERRIDE'" >&2 + exit 1 +fi + +detect_platform >/dev/null + +if [[ "$PLATFORM" != "gitea" ]]; then + echo "Error: grant-reviewer.sh is Gitea only (detected platform: $PLATFORM)." >&2 + echo " On GitHub, grant review access via repository collaborators or org teams in the GitHub UI/CLI." >&2 + exit 1 +fi + +HOST=$(get_remote_host) || { + echo "Error: could not resolve the remote host from origin" >&2 + exit 1 +} + +# Acting credential: GITEA_LOGIN (explicit, fail closed) or the identity +# ladder. Same ordering contract as issue-comment.sh — an explicit override is +# never silently downgraded to the host default identity. +if [[ -n "${GITEA_LOGIN:-}" ]]; then + GITEA_API_TOKEN=$(get_gitea_token_for_login "$GITEA_LOGIN" "$HOST") || { + echo "Error: could not resolve a host-matched Gitea token for GITEA_LOGIN '$GITEA_LOGIN' on host '$HOST'; refusing to fall back to the host default identity (reviewer grant)" >&2 + exit 1 + } +else + GITEA_API_TOKEN=$(get_gitea_token "$HOST") || { + echo "Error: no Gitea credential resolved for the acting identity on host '$HOST' (reviewer grant). Set MOSAIC_GIT_IDENTITY=, or set GITEA_LOGIN= to use a named tea credential." >&2 + exit 1 + } +fi + +CONFIGURED_URL=$(get_gitea_url_for_host "$HOST") || { + echo "Error: configured Gitea URL not found for host '$HOST'" >&2 + exit 1 +} +GITEA_API_ROOT="${CONFIGURED_URL%/}/api/v1" + +if [[ -n "$REPO_OVERRIDE" ]]; then + REPO_SLUG="$REPO_OVERRIDE" +else + REPO_SLUG=$(get_gitea_repo_slug_for_url "$CONFIGURED_URL") || { + echo "Error: could not resolve / from origin; pass -r /" >&2 + exit 1 + } +fi +ORG="${REPO_SLUG%%/*}" +REPO_NAME="${REPO_SLUG#*/}" + +RESPONSE_FILE=$(mktemp "${TMPDIR:-/tmp}/mosaic-grant-reviewer-resp.XXXXXX") +AUTH_CONFIG=$(gitea_write_auth_config "$GITEA_API_TOKEN") || { + rm -f "$RESPONSE_FILE" + echo "Error: could not stage Gitea credential for reviewer grant" >&2 + exit 1 +} +trap 'rm -f "$RESPONSE_FILE" "$AUTH_CONFIG"' EXIT + +# gitea_api [json-payload] +# Runs one API call with the staged credential (token never in argv). Sets +# GITEA_API_STATUS and leaves the body in $RESPONSE_FILE. Transport failure +# and HTTP 403 are terminal here: 403 on ANY step means the acting identity +# cannot manage org teams, and the run must stop rather than continue into a +# partial grant. +gitea_api() { + local step="$1" method="$2" path="$3" payload="${4:-}" + local -a payload_args=() + if [[ -n "$payload" ]]; then + payload_args=(-H 'Content-Type: application/json' -d "$payload") + fi + if ! GITEA_API_STATUS=$(curl -sS -o "$RESPONSE_FILE" -w '%{http_code}' \ + -X "$method" \ + --config "$AUTH_CONFIG" \ + "${payload_args[@]}" \ + "$GITEA_API_ROOT$path"); then + echo "Error: Gitea transport failed during $step" >&2 + return 1 + fi + if [[ "$GITEA_API_STATUS" == "403" ]]; then + echo "Error: HTTP 403 during $step: org admin required on '$ORG' — managing org teams needs owner/admin on the organization. No grant was completed." >&2 + return 1 + fi + return 0 +} + +# json_field — print a top-level scalar field or fail. +json_field() { + python3 - "$1" "$2" <<'PY' +import json +import sys + +try: + with open(sys.argv[1], encoding="utf-8") as response: + data = json.load(response) + value = data.get(sys.argv[2]) if isinstance(data, dict) else None + if value is None or isinstance(value, (dict, list, bool)): + raise ValueError(f"missing or non-scalar field {sys.argv[2]!r}") +except (OSError, json.JSONDecodeError, ValueError) as error: + print(f"Error: unusable Gitea response: {error}", file=sys.stderr) + raise SystemExit(1) +print(value) +PY +} + +# 1. The owner must be an organization: teams are an org facility, and a +# user-owned repo would fail later with a misleading team error. +gitea_api "organization check" GET "/orgs/$ORG" +if [[ "$GITEA_API_STATUS" == "404" ]]; then + echo "Error: owner '$ORG' is not an organization on '$HOST'; grant-reviewer requires an org-owned repository" >&2 + exit 1 +fi +if [[ "$GITEA_API_STATUS" != "200" ]]; then + echo "Error: organization check for '$ORG' failed with HTTP $GITEA_API_STATUS" >&2 + exit 1 +fi + +# 2. Idempotent team resolution: exact-name lookup first, create only on miss. +# The search endpoint substring-matches, so the exact-name filter is done +# on the response, not trusted to the query. +gitea_api "team lookup" GET "/orgs/$ORG/teams/search?q=$TEAM" +if [[ "$GITEA_API_STATUS" != "200" ]]; then + echo "Error: team lookup for '$TEAM' on '$ORG' failed with HTTP $GITEA_API_STATUS" >&2 + exit 1 +fi +TEAM_ID=$(TEAM_NAME="$TEAM" python3 - "$RESPONSE_FILE" <<'PY' +import json +import os +import sys + +wanted = os.environ["TEAM_NAME"] +try: + with open(sys.argv[1], encoding="utf-8") as response: + result = json.load(response) + teams = result.get("data") if isinstance(result, dict) else None + if not isinstance(teams, list): + raise ValueError("team search response carried no data list") +except (OSError, json.JSONDecodeError, ValueError) as error: + print(f"Error: unusable team search response: {error}", file=sys.stderr) + raise SystemExit(1) +for team in teams: + if isinstance(team, dict) and team.get("name") == wanted: + team_id = team.get("id") + if not isinstance(team_id, int) or team_id <= 0: + print("Error: matched team carried no positive id", file=sys.stderr) + raise SystemExit(1) + print(team_id) + raise SystemExit(0) +print("") +PY +) + +if [[ -z "$TEAM_ID" ]]; then + CREATE_PAYLOAD=$(TEAM_NAME="$TEAM" python3 -c ' +import json +import os + +print(json.dumps({ + "name": os.environ["TEAM_NAME"], + "description": "review seats: code read + issues/pulls write", + "permission": "read", + "includes_all_repositories": False, + "can_create_org_repo": False, + "units_map": { + "repo.code": "read", + "repo.issues": "write", + "repo.pulls": "write", + }, +})) +') + gitea_api "team create" POST "/orgs/$ORG/teams" "$CREATE_PAYLOAD" + if [[ "$GITEA_API_STATUS" != "201" ]]; then + echo "Error: team create for '$TEAM' on '$ORG' failed with HTTP $GITEA_API_STATUS" >&2 + exit 1 + fi + TEAM_ID=$(json_field "$RESPONSE_FILE" id) || { + echo "Error: team create returned no usable team id" >&2 + exit 1 + } + echo "Created team '$TEAM' (id $TEAM_ID) on org '$ORG'" +else + echo "Found existing team '$TEAM' (id $TEAM_ID) on org '$ORG'" +fi + +# 3. Membership and repo attachment — both PUTs, both idempotent in Gitea. +gitea_api "member add" PUT "/teams/$TEAM_ID/members/$REVIEWER" +if [[ "$GITEA_API_STATUS" != "204" ]]; then + echo "Error: adding '$REVIEWER' to team '$TEAM' failed with HTTP $GITEA_API_STATUS" >&2 + exit 1 +fi + +gitea_api "repo add" PUT "/teams/$TEAM_ID/repos/$ORG/$REPO_NAME" +if [[ "$GITEA_API_STATUS" != "204" ]]; then + echo "Error: adding repo '$REPO_SLUG' to team '$TEAM' failed with HTTP $GITEA_API_STATUS" >&2 + exit 1 +fi + +# 4. Fail-closed read-back: a 204 from a PUT is an exit code, not evidence the +# grant persisted. GET the single resources back and require both. +gitea_api "member read-back" GET "/teams/$TEAM_ID/members/$REVIEWER" +if [[ "$GITEA_API_STATUS" != "200" ]]; then + echo "Error: reviewer grant NOT verified — GET /teams/$TEAM_ID/members/$REVIEWER returned HTTP $GITEA_API_STATUS after a successful PUT. Treat the grant as not made." >&2 + exit 1 +fi +READBACK_LOGIN=$(json_field "$RESPONSE_FILE" login) || exit 1 +if [[ "${READBACK_LOGIN,,}" != "${REVIEWER,,}" ]]; then + echo "Error: reviewer grant NOT verified — member read-back returned login '$READBACK_LOGIN', expected '$REVIEWER'" >&2 + exit 1 +fi + +gitea_api "repo read-back" GET "/teams/$TEAM_ID/repos/$ORG/$REPO_NAME" +if [[ "$GITEA_API_STATUS" != "200" ]]; then + echo "Error: reviewer grant NOT verified — GET /teams/$TEAM_ID/repos/$ORG/$REPO_NAME returned HTTP $GITEA_API_STATUS after a successful PUT. Treat the grant as not made." >&2 + exit 1 +fi +READBACK_FULL_NAME=$(json_field "$RESPONSE_FILE" full_name) || exit 1 +if [[ "${READBACK_FULL_NAME,,}" != "${REPO_SLUG,,}" ]]; then + echo "Error: reviewer grant NOT verified — repo read-back returned '$READBACK_FULL_NAME', expected '$REPO_SLUG'" >&2 + exit 1 +fi + +echo "Granted: '$REVIEWER' is a member of team '$TEAM' (id $TEAM_ID) with access to '$REPO_SLUG' (code read, issues/pulls write) — verified by read-back" +echo "Note: under branch protection with required approvals this reviewer's approvals are official=false unless the branch's approvals whitelist includes the team (operator decision; reviews submitted before whitelisting must be re-submitted)." diff --git a/packages/mosaic/framework/tools/git/test-grant-reviewer.sh b/packages/mosaic/framework/tools/git/test-grant-reviewer.sh new file mode 100755 index 00000000..7a11a712 --- /dev/null +++ b/packages/mosaic/framework/tools/git/test-grant-reviewer.sh @@ -0,0 +1,616 @@ +#!/usr/bin/env bash +# Regression harness for grant-reviewer.sh (#1415): org-team reviewer grant +# with fail-closed read-back verification. +# +# This harness models a REAL server: the curl stub keeps persistent team/ +# member/repo state on disk, the POST actually CREATES and PERSISTS the team, +# the member/repo PUTs persist (except in the sabotage modes), and the +# read-back GETs answer from that same state. There is no fabricated record +# for the wrapper to "find" — verification passes only if the PUTs genuinely +# persisted what the read-back retrieves. It proves the wrapper: +# 1. creates the team with the EXACT reviewer payload (permission: read, +# units_map {repo.code: read, repo.issues: write, repo.pulls: write}) — +# the stub rejects any other payload; +# 2. is idempotent: an existing team is found by EXACT name (a decoy team +# whose name merely CONTAINS the wanted name is listed first and must +# not be matched) and no create POST is issued; +# 3. refuses to run against a GitHub-remoted repo (Gitea only); +# 4. refuses when the owner is not an organization; +# 5. maps HTTP 403 to "org admin required on " and stops before any +# partial grant; +# 6. fails closed when the member PUT returns 204 without persisting (the +# #865 defect class: an exit code is not evidence of a durable write); +# 7. fails closed when the repo PUT returns 204 without persisting; +# 8. with GITEA_LOGIN set, performs EVERY request under that login's token +# (never the host default), and with an UNRESOLVABLE GITEA_LOGIN fails +# closed with ZERO API calls instead of downgrading; +# 9. never lets the bearer token ride in curl argv (curl --config only); +# 10. leaves no temp files behind on success or failure paths. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/grant-reviewer}" +REPO_DIR="$WORK_DIR/repo" +GH_REPO_DIR="$WORK_DIR/gh-repo" +BIN_DIR="$WORK_DIR/bin" +XDG_DIR="$WORK_DIR/xdg" +TEA_LOG="$WORK_DIR/tea.log" +CURL_LOG="$WORK_DIR/curl.log" +# Full curl argv per invocation — proves the bearer token never rides in argv. +CURL_ARGV_LOG="$WORK_DIR/curl-argv.log" +AUTH_LOG="$WORK_DIR/auth.log" +OUTPUT_FILE="$WORK_DIR/output.log" +CREDENTIALS_FILE="$WORK_DIR/credentials.json" +STATE_FILE="$WORK_DIR/grants.json" +PAYLOAD_VIOLATION_FILE="$WORK_DIR/payload-violation" +TMP_SCRATCH="$WORK_DIR/scratch" +HOME_DIR="$WORK_DIR/home" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +mkdir -p "$REPO_DIR" "$GH_REPO_DIR" "$BIN_DIR" "$XDG_DIR" "$TMP_SCRATCH" "$HOME_DIR" +git -C "$REPO_DIR" init -q +git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git +git -C "$GH_REPO_DIR" init -q +git -C "$GH_REPO_DIR" remote add origin https://github.com/someorg/somerepo.git +# HERMETICITY (#1007): get_gitea_token() step 0 resolves a per-agent identity +# from `git config --get mosaic.gitIdentity`, which on a provisioned seat is +# set GLOBALLY and leaks into this fresh repo, after which a REAL per-slot +# token is read from $HOME and the fixture credential is silently ignored. An +# empty repo-local value shadows the global one and reads back empty at rc=0. +# (The env-var route does NOT neutralize step 0's git-config read — but the +# run env below still pins MOSAIC_GIT_IDENTITY= empty so the ENV rung of the +# ladder cannot resolve either: `${MOSAIC_GIT_IDENTITY:-}` treats set-but-empty +# as unset.) +git -C "$REPO_DIR" config mosaic.gitIdentity "" +git -C "$GH_REPO_DIR" config mosaic.gitIdentity "" + +ORG="mosaicstack" +REPO_SLUG="mosaicstack/stack" +API_ROOT="https://git.mosaicstack.dev/api/v1" +REVIEWER="rev-user" +TEAM_NAME="fleet-reviewers" +TEAM_ID=42 +DECOY_TEAM_ID=99 +DEFAULT_TOKEN="test-only-placeholder" +DEFAULT_IDENTITY="seat-default" +OVERRIDE_LOGIN="granter" +OVERRIDE_TOKEN="override-token-placeholder" + +# tea config: the GITEA_LOGIN override login has its own host-bound token here. +mkdir -p "$XDG_DIR/tea" +OVERRIDE_LOGIN="$OVERRIDE_LOGIN" OVERRIDE_TOKEN="$OVERRIDE_TOKEN" \ + python3 - "$XDG_DIR/tea/config.yml" <<'PY' +import os +import sys + +with open(sys.argv[1], "w", encoding="utf-8") as handle: + handle.write("logins:\n") + handle.write(f" - name: {os.environ['OVERRIDE_LOGIN']}\n") + handle.write(" url: https://git.mosaicstack.dev\n") + handle.write(f" token: {os.environ['OVERRIDE_TOKEN']}\n") +PY + +CONFIGURED_GITEA_URL="https://git.mosaicstack.dev" python3 - "$CREDENTIALS_FILE" <<'PY' +import json +import os +import sys + +with open(sys.argv[1], "w", encoding="utf-8") as credentials: + json.dump({ + "gitea": { + "mosaicstack": { + "url": os.environ["CONFIGURED_GITEA_URL"], + "token": "test-only-placeholder", + } + } + }, credentials) +PY + +# tea stub: grant-reviewer.sh must never shell out to tea at all. +cat > "$BIN_DIR/tea" <<'SH' +#!/usr/bin/env bash +set -euo pipefail +printf '%s\n' "$*" >> "$GRANT_REVIEWER_TEA_LOG" +echo "Unexpected tea command (grant-reviewer must not use tea): $*" >&2 +exit 92 +SH +chmod +x "$BIN_DIR/tea" + +# curl stub: a small REST server backed by persistent on-disk grant state. +# GET /orgs/{org} -> org existence (404 in not-an-org mode) +# GET /orgs/{org}/teams/search -> teams from state (decoy always listed FIRST) +# POST /orgs/{org}/teams -> validate EXACT payload, CREATE + PERSIST +# PUT /teams/{id}/members/{user} -> 204; persists unless member-put-noop +# PUT /teams/{id}/repos/{org}/{repo} -> 204; persists unless repo-put-noop +# GET /teams/{id}/members/{user} -> answers from persisted state only +# GET /teams/{id}/repos/{org}/{repo} -> answers from persisted state only +cat > "$BIN_DIR/curl" <<'SH' +#!/usr/bin/env bash +set -euo pipefail + +# Record the FULL argv exactly as spawned, before consumption. The bearer token +# must NOT appear here — it is delivered via a curl --config file, so only the +# config file PATH may show up. +printf '%s\n' "$*" >> "$GRANT_REVIEWER_CURL_ARGV_LOG" + +output_file="" +method="GET" +url="" +data="" +auth_token="" +config_file="" +while [[ $# -gt 0 ]]; do + case "$1" in + -o) output_file="$2"; shift 2 ;; + -H) + [[ "$2" == Authorization:* ]] && auth_token="${2##* }" + shift 2 ;; + -K|--config) config_file="$2"; shift 2 ;; + -w) shift 2 ;; + -X) method="$2"; shift 2 ;; + -d|--data) data="$2"; shift 2 ;; + -s|-S|-sS) shift ;; + http://*|https://*) url="$1"; shift ;; + *) shift ;; + esac +done + +# Resolve the bearer token from the curl --config file (its real, secure +# source). The config line is `header = "Authorization: token "`. +if [[ -z "$auth_token" && -n "$config_file" && -f "$config_file" ]]; then + config_hdr="$(grep -i 'Authorization' "$config_file" 2>/dev/null || true)" + if [[ "$config_hdr" == *"token "* ]]; then + auth_token="${config_hdr##*token }" + auth_token="${auth_token%\"}" + fi +fi + +path="${url%%\?*}" +printf '%s %s\n' "$method" "$url" >> "$GRANT_REVIEWER_CURL_LOG" + +# Map the presented bearer token to the identity it authenticates as. Every +# request the wrapper makes must carry the SAME credential, so the identity +# recorded here reveals which credential actually performed each request. +acting_identity="" +case "$auth_token" in + "$GRANT_REVIEWER_DEFAULT_TOKEN") acting_identity="$GRANT_REVIEWER_DEFAULT_IDENTITY" ;; + "$GRANT_REVIEWER_OVERRIDE_TOKEN") acting_identity="$GRANT_REVIEWER_OVERRIDE_LOGIN" ;; +esac +printf '%s %s %s\n' "$method" "$path" "${acting_identity:-}" >> "$GRANT_REVIEWER_AUTH_LOG" + +write_response() { + local status="$1" body="$2" + [[ -n "$output_file" ]] || exit 96 + printf '%s' "$body" > "$output_file" + printf '%s' "$status" +} + +[[ -n "$acting_identity" ]] || { write_response 401 '{"message":"unauthenticated"}'; exit 0; } + +mode="$GRANT_REVIEWER_TEST_MODE" +org="$GRANT_REVIEWER_ORG" +api="$GRANT_REVIEWER_API_ROOT" + +if [[ "$method" == "GET" && "$path" == "$api/orgs/$org" ]]; then + if [[ "$mode" == "not-an-org" ]]; then + write_response 404 '{"message":"not found"}' + else + write_response 200 "{\"username\":\"$org\"}" + fi +elif [[ "$method" == "GET" && "$path" == "$api/orgs/$org/teams/search" ]]; then + result=$(python3 - "$GRANT_REVIEWER_STATE" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as handle: + state = json.load(handle) +print(json.dumps({"ok": True, "data": state["teams"]})) +PY +) + write_response 200 "$result" +elif [[ "$method" == "POST" && "$path" == "$api/orgs/$org/teams" ]]; then + if [[ "$mode" == "create-403" ]]; then + write_response 403 '{"message":"forbidden"}' + exit 0 + fi + result=$(GRANT_REVIEWER_DATA="$data" python3 - "$GRANT_REVIEWER_STATE" <<'PY' +import json +import os +import sys + +payload = json.loads(os.environ["GRANT_REVIEWER_DATA"]) +expected = { + "name": os.environ["GRANT_REVIEWER_TEAM_NAME"], + "description": "review seats: code read + issues/pulls write", + "permission": "read", + "includes_all_repositories": False, + "can_create_org_repo": False, + "units_map": { + "repo.code": "read", + "repo.issues": "write", + "repo.pulls": "write", + }, +} +if payload != expected: + with open(os.environ["GRANT_REVIEWER_PAYLOAD_VIOLATION"], "w", encoding="utf-8") as handle: + json.dump({"got": payload, "expected": expected}, handle, indent=2) + print("422") + print(json.dumps({"message": "payload mismatch"})) + raise SystemExit(0) + +state_path = sys.argv[1] +with open(state_path, encoding="utf-8") as handle: + state = json.load(handle) +team = {"id": int(os.environ["GRANT_REVIEWER_TEAM_ID"]), "name": payload["name"]} +state["teams"].append(team) +with open(state_path, "w", encoding="utf-8") as handle: + json.dump(state, handle) +print("201") +print(json.dumps(team)) +PY +) + response_status="${result%%$'\n'*}" + response_body="${result#*$'\n'}" + write_response "$response_status" "$response_body" +elif [[ "$method" == "PUT" && "$path" == "$api/teams/$GRANT_REVIEWER_TEAM_ID/members/$GRANT_REVIEWER_REVIEWER" ]]; then + # Sabotage mode member-put-noop: 204 WITHOUT persisting — the exit-code lie. + if [[ "$mode" != "member-put-noop" ]]; then + python3 - "$GRANT_REVIEWER_STATE" <<'PY' +import json +import os +import sys + +state_path = sys.argv[1] +with open(state_path, encoding="utf-8") as handle: + state = json.load(handle) +member = os.environ["GRANT_REVIEWER_REVIEWER"] +if member not in state["members"]: + state["members"].append(member) +with open(state_path, "w", encoding="utf-8") as handle: + json.dump(state, handle) +PY + fi + write_response 204 '' +elif [[ "$method" == "PUT" && "$path" == "$api/teams/$GRANT_REVIEWER_TEAM_ID/repos/$GRANT_REVIEWER_REPO_SLUG" ]]; then + # Sabotage mode repo-put-noop: 204 WITHOUT persisting. + if [[ "$mode" != "repo-put-noop" ]]; then + python3 - "$GRANT_REVIEWER_STATE" <<'PY' +import json +import os +import sys + +state_path = sys.argv[1] +with open(state_path, encoding="utf-8") as handle: + state = json.load(handle) +slug = os.environ["GRANT_REVIEWER_REPO_SLUG"] +if slug not in state["repos"]: + state["repos"].append(slug) +with open(state_path, "w", encoding="utf-8") as handle: + json.dump(state, handle) +PY + fi + write_response 204 '' +elif [[ "$method" == "GET" && "$path" == "$api/teams/$GRANT_REVIEWER_TEAM_ID/members/$GRANT_REVIEWER_REVIEWER" ]]; then + if python3 - "$GRANT_REVIEWER_STATE" <<'PY' +import json +import os +import sys + +with open(sys.argv[1], encoding="utf-8") as handle: + state = json.load(handle) +raise SystemExit(0 if os.environ["GRANT_REVIEWER_REVIEWER"] in state["members"] else 1) +PY + then + write_response 200 "{\"login\":\"$GRANT_REVIEWER_REVIEWER\"}" + else + write_response 404 '{"message":"not a member"}' + fi +elif [[ "$method" == "GET" && "$path" == "$api/teams/$GRANT_REVIEWER_TEAM_ID/repos/$GRANT_REVIEWER_REPO_SLUG" ]]; then + if python3 - "$GRANT_REVIEWER_STATE" <<'PY' +import json +import os +import sys + +with open(sys.argv[1], encoding="utf-8") as handle: + state = json.load(handle) +raise SystemExit(0 if os.environ["GRANT_REVIEWER_REPO_SLUG"] in state["repos"] else 1) +PY + then + write_response 200 "{\"full_name\":\"$GRANT_REVIEWER_REPO_SLUG\"}" + else + write_response 404 '{"message":"repo not on team"}' + fi +else + echo "Unexpected curl request: $method $url" >&2 + exit 97 +fi +SH +chmod +x "$BIN_DIR/curl" + +# Seed persistent server state for a mode: fresh (no team yet) or a pre-seeded +# team. The DECOY team — whose name CONTAINS the wanted name — is always listed +# FIRST, so a first-result or substring match would grab the wrong team. +seed_state() { + local seeded_team="$1" + GRANT_REVIEWER_SEEDED_TEAM="$seeded_team" GRANT_REVIEWER_TEAM_NAME="$TEAM_NAME" \ + GRANT_REVIEWER_TEAM_ID="$TEAM_ID" GRANT_REVIEWER_DECOY_TEAM_ID="$DECOY_TEAM_ID" \ + python3 - "$STATE_FILE" <<'PY' +import json +import os +import sys + +wanted = os.environ["GRANT_REVIEWER_TEAM_NAME"] +teams = [{"id": int(os.environ["GRANT_REVIEWER_DECOY_TEAM_ID"]), "name": wanted + "-archive"}] +if os.environ["GRANT_REVIEWER_SEEDED_TEAM"] == "yes": + teams.append({"id": int(os.environ["GRANT_REVIEWER_TEAM_ID"]), "name": wanted}) +with open(sys.argv[1], "w", encoding="utf-8") as handle: + json.dump({"teams": teams, "members": [], "repos": []}, handle) +PY +} + +# run_grant [extra env VAR=value ...] -- [wrapper args ...] +run_grant() { + local mode="$1" seeded="$2" + shift 2 + local -a extra_env=() + while [[ $# -gt 0 && "$1" != "--" ]]; do + extra_env+=("$1") + shift + done + [[ $# -gt 0 ]] && shift + : > "$TEA_LOG" + : > "$CURL_LOG" + : > "$CURL_ARGV_LOG" + : > "$AUTH_LOG" + : > "$OUTPUT_FILE" + rm -f "$PAYLOAD_VIOLATION_FILE" + seed_state "$seeded" + ( + cd "$RUN_REPO_DIR" + env \ + PATH="$BIN_DIR:$PATH" \ + TMPDIR="$TMP_SCRATCH" \ + HOME="$HOME_DIR" \ + XDG_CONFIG_HOME="$XDG_DIR" \ + MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \ + MOSAIC_BRAIN_HOME="$HOME_DIR/.mosaic" \ + MOSAIC_GIT_IDENTITY= \ + GITEA_LOGIN= \ + GITEA_TOKEN= \ + GITEA_URL= \ + GRANT_REVIEWER_TEA_LOG="$TEA_LOG" \ + GRANT_REVIEWER_CURL_LOG="$CURL_LOG" \ + GRANT_REVIEWER_CURL_ARGV_LOG="$CURL_ARGV_LOG" \ + GRANT_REVIEWER_AUTH_LOG="$AUTH_LOG" \ + GRANT_REVIEWER_STATE="$STATE_FILE" \ + GRANT_REVIEWER_TEST_MODE="$mode" \ + GRANT_REVIEWER_ORG="$ORG" \ + GRANT_REVIEWER_API_ROOT="$API_ROOT" \ + GRANT_REVIEWER_TEAM_NAME="$TEAM_NAME" \ + GRANT_REVIEWER_TEAM_ID="$TEAM_ID" \ + GRANT_REVIEWER_REVIEWER="$REVIEWER" \ + GRANT_REVIEWER_REPO_SLUG="$REPO_SLUG" \ + GRANT_REVIEWER_DEFAULT_TOKEN="$DEFAULT_TOKEN" \ + GRANT_REVIEWER_DEFAULT_IDENTITY="$DEFAULT_IDENTITY" \ + GRANT_REVIEWER_OVERRIDE_LOGIN="$OVERRIDE_LOGIN" \ + GRANT_REVIEWER_OVERRIDE_TOKEN="$OVERRIDE_TOKEN" \ + GRANT_REVIEWER_PAYLOAD_VIOLATION="$PAYLOAD_VIOLATION_FILE" \ + "${extra_env[@]}" \ + "$SCRIPT_DIR/grant-reviewer.sh" -u "$REVIEWER" "$@" + ) > "$OUTPUT_FILE" 2>&1 +} + +assert_no_temp_leak() { + local context="$1" leaked + # Includes the curl auth-config files (mosaic-gitea-auth-*), which carry the + # bearer token and must be unlinked on every exit path. + leaked=$(find "$TMP_SCRATCH" -type f \( -name 'mosaic-grant-reviewer-*' -o -name 'mosaic-gitea-auth-*' \) 2>/dev/null || true) + if [[ -n "$leaked" ]]; then + echo "FAIL: grant-reviewer temp files leaked ($context):" >&2 + printf '%s\n' "$leaked" >&2 + exit 1 + fi +} + +assert_token_not_in_argv() { + local context="$1" + if grep -qF -e "$DEFAULT_TOKEN" -e "$OVERRIDE_TOKEN" "$CURL_ARGV_LOG"; then + echo "FAIL: a Gitea bearer token leaked into curl argv ($context)" >&2 + exit 1 + fi + if ! grep -q -- '--config' "$CURL_ARGV_LOG"; then + echo "FAIL: curl was not invoked with --config file auth ($context)" >&2 + exit 1 + fi +} + +assert_no_payload_violation() { + local context="$1" + if [[ -f "$PAYLOAD_VIOLATION_FILE" ]]; then + echo "FAIL: team create payload deviated from the reviewer contract ($context):" >&2 + cat "$PAYLOAD_VIOLATION_FILE" >&2 + exit 1 + fi +} + +RUN_REPO_DIR="$REPO_DIR" + +# Case 1: fresh grant — team absent, created with the exact reviewer payload, +# member + repo PUTs persist, both read-backs verify against server state. +run_grant normal no -- || { + echo "FAIL: fresh grant exited nonzero" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +grep -q "Created team '$TEAM_NAME' (id $TEAM_ID) on org '$ORG'" "$OUTPUT_FILE" || { + echo "FAIL: fresh grant did not create the team" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +grep -q "Granted: '$REVIEWER' is a member of team '$TEAM_NAME' (id $TEAM_ID) with access to '$REPO_SLUG'" "$OUTPUT_FILE" || { + echo "FAIL: fresh grant did not report a verified grant" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +assert_no_payload_violation "fresh" +assert_token_not_in_argv "fresh" +assert_no_temp_leak "fresh" +# The default path must have acted as the host-default identity on EVERY request. +if grep -qv " $DEFAULT_IDENTITY\$" "$AUTH_LOG"; then + echo "FAIL: fresh grant made a request under an unexpected identity" >&2 + cat "$AUTH_LOG" >&2 + exit 1 +fi +# grant-reviewer must never shell out to tea. +if [[ -s "$TEA_LOG" ]]; then + echo "FAIL: grant-reviewer invoked tea" >&2 + cat "$TEA_LOG" >&2 + exit 1 +fi + +# Case 2: idempotent — the team already exists. It must be found by EXACT name +# (the decoy is listed first), no create POST issued, and the decoy team must +# never be touched. +run_grant normal yes -- || { + echo "FAIL: idempotent grant exited nonzero" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +grep -q "Found existing team '$TEAM_NAME' (id $TEAM_ID) on org '$ORG'" "$OUTPUT_FILE" || { + echo "FAIL: idempotent grant did not find the existing team" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +grep -q "Granted: '$REVIEWER'" "$OUTPUT_FILE" || { + echo "FAIL: idempotent grant did not report a verified grant" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +if grep -q "^POST " "$CURL_LOG"; then + echo "FAIL: idempotent grant issued a create POST for an existing team" >&2 + cat "$CURL_LOG" >&2 + exit 1 +fi +if grep -q "/teams/$DECOY_TEAM_ID/" "$CURL_LOG"; then + echo "FAIL: substring-named decoy team was operated on" >&2 + cat "$CURL_LOG" >&2 + exit 1 +fi +assert_no_temp_leak "idempotent" + +# Case 3: GITEA_LOGIN override — every request must carry the override login's +# token, never the host default credential. +run_grant normal no GITEA_LOGIN="$OVERRIDE_LOGIN" -- || { + echo "FAIL: GITEA_LOGIN override grant exited nonzero" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +grep -q "Granted: '$REVIEWER'" "$OUTPUT_FILE" || { + echo "FAIL: GITEA_LOGIN override grant did not succeed" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +if grep -qv " $OVERRIDE_LOGIN\$" "$AUTH_LOG"; then + echo "FAIL: GITEA_LOGIN override made a request under a different identity" >&2 + cat "$AUTH_LOG" >&2 + exit 1 +fi +assert_token_not_in_argv "override" +assert_no_temp_leak "override" + +# Case 4: unresolvable GITEA_LOGIN — fail closed BEFORE any API call; no +# downgrade to the host default identity. +if run_grant normal no GITEA_LOGIN="no-such-login" --; then + echo "FAIL: unresolvable GITEA_LOGIN did not fail" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +fi +grep -q "refusing to fall back to the host default identity" "$OUTPUT_FILE" || { + echo "FAIL: unresolvable GITEA_LOGIN missing the fail-closed message" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +if [[ -s "$CURL_LOG" ]]; then + echo "FAIL: unresolvable GITEA_LOGIN still made API calls" >&2 + cat "$CURL_LOG" >&2 + exit 1 +fi +assert_no_temp_leak "unresolvable-login" + +# Case 5: GitHub-remoted repo — refuse before any API call. +RUN_REPO_DIR="$GH_REPO_DIR" +if run_grant normal no --; then + echo "FAIL: GitHub repo was not refused" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +fi +grep -q "Gitea only" "$OUTPUT_FILE" || { + echo "FAIL: GitHub refusal missing the 'Gitea only' message" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +if [[ -s "$CURL_LOG" ]]; then + echo "FAIL: GitHub refusal still made API calls" >&2 + cat "$CURL_LOG" >&2 + exit 1 +fi +RUN_REPO_DIR="$REPO_DIR" + +# Case 6: owner is not an organization — clear refusal. +if run_grant not-an-org no --; then + echo "FAIL: non-org owner was not refused" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +fi +grep -q "is not an organization" "$OUTPUT_FILE" || { + echo "FAIL: non-org refusal missing its message" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +assert_no_temp_leak "not-an-org" + +# Case 7: HTTP 403 on team create — reported as an org-admin requirement, and +# the run stops before any member/repo PUT (no partial grant). +if run_grant create-403 no --; then + echo "FAIL: 403 on team create did not fail the run" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +fi +grep -q "org admin required on '$ORG'" "$OUTPUT_FILE" || { + echo "FAIL: 403 was not mapped to the org-admin message" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 +} +if grep -q "^PUT " "$CURL_LOG"; then + echo "FAIL: run continued into PUTs after a 403 (partial grant)" >&2 + cat "$CURL_LOG" >&2 + exit 1 +fi +assert_no_temp_leak "create-403" + +# Cases 8-9: the exit-code lie — a PUT answers 204 without persisting. The +# read-back must fail closed; no success line may appear. +for noop_mode in member-put-noop repo-put-noop; do + if run_grant "$noop_mode" no --; then + echo "FAIL: $noop_mode was reported as success" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 + fi + grep -q "NOT verified" "$OUTPUT_FILE" || { + echo "FAIL: $noop_mode missing the fail-closed verification message" >&2 + cat "$OUTPUT_FILE" >&2 + exit 1 + } + if grep -q "^Granted:" "$OUTPUT_FILE"; then + echo "FAIL: $noop_mode still printed the success line" >&2 + exit 1 + fi + assert_no_temp_leak "$noop_mode" +done + +echo "grant-reviewer.sh org-team grant + fail-closed read-back regression passed" diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index 13da161a..a6753269 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -25,7 +25,7 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests && pnpm run test:framework-shell", - "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh" + "test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-create-fallback-default-base.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-ci-queue-wait-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-no-ci-expected.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/_scripts/test-structure-anchor-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh && bash framework/tools/fleet/test-agent-session-legacy-socket-guard.sh && bash framework/tools/git/test-grant-reviewer.sh" }, "dependencies": { "@mosaicstack/brain": "workspace:*",