#!/usr/bin/env bash # Regression harness for `git-credential-mosaic` — per-agent Gitea identity # resolution (Gate-16 author≠reviewer separation) and fail-closed refusal. # # Covers: # 1. Identity resolution priority: MOSAIC_GIT_IDENTITY env > git config # mosaic.gitIdentity (per-worktree) > git-supplied username. # 2. Correct token file path chosen per host # (gitea-usc-.token vs gitea-mosaicstack-.token). # 3. Credential store selection: an identity with a directory under # /fleet/agents/ is a SEAT and is read ONLY from its own secrets/ # slot; any other identity is a SERVICE and is read from the framework # store. No precedence between them and NO fallback from one to the other. # 4. Fail-closed: an identity that resolves but has no credential is REFUSED — # no output, nonzero exit, a stderr diagnostic, and a durable spool record. # The shared account is never emitted in its place. # 5. Fail-closed: no identity resolvable on a host that runs a fleet is also # REFUSED, because records made there must name the agent that made them. # 6. Backward compatibility, the one surviving fallback: no identity AND no # fleet -> shared account, unchanged. On such a host the shared account is # the operator's own and there is no attribution to lose. # 7. Unknown/unrelated host -> exits 0 with no output (passthrough). # 8. Non-"get" verb -> exits 0 with no output. # # Uses stubbed token files under a fake HOME + a real (throwaway) git repo. # NEVER reads real secrets or touches the real ~/.config/mosaic/secrets. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/git-credential-mosaic}" FAKE_HOME="$WORK_DIR/home" REPO_DIR="$WORK_DIR/repo" BRAIN_DIR="$WORK_DIR/brain" SPOOL_DIR="$WORK_DIR/spool" SVC_STORE="$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" # Mirror the real deployed layout (~/.mosaic/tools/{git,_lib}/) under the # fake HOME: git-credential-mosaic resolves its credentials.sh sibling via a # script-relative path (BASH_SOURCE), so the copy must live next to a stubbed # _lib/credentials.sh, not the real one, to keep this test hermetic. HELPER="$FAKE_HOME/.mosaic/tools/git/git-credential-mosaic" IMPL="$FAKE_HOME/.mosaic/tools/git/git-credential-mosaic.impl" rm -rf "$WORK_DIR" mkdir -p "$SVC_STORE" \ "$FAKE_HOME/.mosaic/tools/git" \ "$FAKE_HOME/.mosaic/tools/_lib" \ "$REPO_DIR" "$BRAIN_DIR" cp "$SCRIPT_DIR/git-credential-mosaic" "$HELPER" chmod +x "$HELPER" cp "$SCRIPT_DIR/git-credential-mosaic.impl" "$IMPL" chmod +x "$IMPL" git -C "$REPO_DIR" init -q git -C "$REPO_DIR" config user.email "test@example.invalid" git -C "$REPO_DIR" config user.name "Test" # Fake shared-account credential loader — stands in for # tools/_lib/credentials.sh's load_credentials(), scoped to this test only. cat > "$FAKE_HOME/.mosaic/tools/_lib/credentials.sh" <<'SH' load_credentials() { case "$1" in gitea-mosaicstack) GITEA_URL="https://git.mosaicstack.dev"; GITEA_TOKEN="shared-mosaicstack-token"; export GITEA_URL GITEA_TOKEN; return 0 ;; gitea-usc) GITEA_URL="https://git.uscllc.com"; GITEA_TOKEN="shared-usc-token"; export GITEA_URL GITEA_TOKEN; return 0 ;; *) return 1 ;; esac } SH fail=0 assert_eq() { local desc="$1" expected="$2" actual="$3" if [[ "$expected" != "$actual" ]]; then echo "FAIL: $desc — expected '$expected', got '$actual'" >&2 fail=1 fi } # Feed "host=\nusername=\n\n" on stdin (mirrors git's credential protocol) # and run the helper with the fake HOME, inside REPO_DIR (so `git config # mosaic.gitIdentity` resolves per-worktree), plus any extra env passed in $@. run_helper() { local host="$1" username_in="$2"; shift 2 ( cd "$REPO_DIR" env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$SPOOL_DIR" "$@" \ "$HELPER" get < frozen at exec; # forks the helper (pid D) with a fully controlled env. # # The helper's walk then sees exactly: self -> C(caller) or D-direct -> # A(fence, empty name -> stop). EXTRA assignments ride pid D's environment # (that is where a rewrite would live — which is the point of the F1 arms). cat > "$WORK_DIR/lineage-root.sh" <<'LINROOT' #!/usr/bin/env bash # pid A — lineage root. Args: # [extra KEY=VALUE...] set -u caller="$1"; carrier="$2"; helper="$3"; spool="$4"; brain="$5"; repo="$6"; shift 6 if [ -n "$caller" ]; then env MOSAIC_AGENT_NAME="$caller" PATH="$PATH" HOME="$HOME" \ bash "$carrier" "$helper" "$spool" "$brain" "$repo" "$@" else env -i HOME="$HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$spool" \ MOSAIC_BRAIN_HOME="$brain" "$@" "$helper" get fi LINROOT cat > "$WORK_DIR/lineage-carrier.sh" <<'LINCARR' #!/usr/bin/env bash # pid C — the caller's frozen environment. Forks the helper (pid D). set -u helper="$1"; spool="$2"; brain="$3"; repo="$4"; shift 4 cd "$repo" env -i HOME="$HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$spool" \ MOSAIC_BRAIN_HOME="$brain" "$@" "$helper" get LINCARR chmod +x "$WORK_DIR/lineage-root.sh" "$WORK_DIR/lineage-carrier.sh" run_lineage() { # run_lineage [helper-env KEY=VALUE...] local caller="$1"; shift printf 'host=git.mosaicstack.dev\nusername=probe\n\n' | \ env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_LINEAGE_FENCE=1 \ bash "$WORK_DIR/lineage-root.sh" "$caller" "$WORK_DIR/lineage-carrier.sh" \ "$HELPER" "$SPOOL_DIR" "$BRAIN_DIR" "$REPO_DIR" "$@" } assert_refused_lineage() { # assert_refused_lineage [helper-env...] local desc="$1" caller="$2" want="$3"; shift 3 local stderr_file="$WORK_DIR/stderr-lin.tmp" rc stdout : > "$stderr_file" set +e stdout=$(run_lineage "$caller" "$@" 2>"$stderr_file") rc=$? set -e local stderr; stderr=$(cat "$stderr_file") if [[ "$rc" -eq 0 ]]; then echo "FAIL: $desc — expected nonzero exit, got 0 (stdout='$stdout')" >&2; fail=1 fi if [[ -n "$stdout" ]]; then echo "FAIL: $desc — expected empty stdout, got '$stdout'" >&2; fail=1 fi if [[ -n "$want" && "$stderr" != *"$want"* ]]; then echo "FAIL: $desc — stderr lacks '$want':" >&2; echo "$stderr" >&2; fail=1 fi if [[ "$stdout$stderr" == *"seatG-slot-token"* || "$stdout$stderr" == *"seatE-slot-token"* \ || "$stdout$stderr" == *"shared-mosaicstack-token"* || "$stdout$stderr" == *"shared-usc-token"* ]]; then echo "FAIL: $desc — a slot or shared token VALUE appeared in output" >&2; fail=1 fi } # A refusal must be observable in four independent ways: nonzero exit, EMPTY # stdout, a stderr diagnostic naming the identity and host, and — the assertion # that actually catches a regression to the old behavior — NO shared token value # anywhere in the output. Checking only the exit code would pass against a helper # that emitted the shared credential and then exited 1. assert_fail_closed() { local desc="$1" host="$2" username_in="$3" want_in_stderr="$4"; shift 4 local stderr_file="$WORK_DIR/stderr.tmp" : > "$stderr_file" set +e local stdout stdout=$(run_helper "$host" "$username_in" "$@" 2>"$stderr_file") local rc=$? set -e local stderr stderr=$(cat "$stderr_file") if [[ "$rc" -eq 0 ]]; then echo "FAIL: $desc — expected nonzero exit, got 0 (stdout='$stdout')" >&2 fail=1 fi if [[ -n "$stdout" ]]; then echo "FAIL: $desc — expected empty stdout (nothing emitted), got '$stdout'" >&2 fail=1 fi if [[ "$stdout$stderr" == *"shared-mosaicstack-token"* || "$stdout$stderr" == *"shared-usc-token"* ]]; then echo "FAIL: $desc — a SHARED token value appeared in the output. The shared-account fallback must be gone:" >&2 echo "$stdout$stderr" >&2 fail=1 fi if [[ -n "$want_in_stderr" && "$stderr" != *"$want_in_stderr"* ]]; then echo "FAIL: $desc — stderr does not contain '$want_in_stderr':" >&2 echo "$stderr" >&2 fail=1 fi if [[ "$stderr" != *"$host"* ]]; then echo "FAIL: $desc — stderr does not name the host '$host':" >&2 echo "$stderr" >&2 fail=1 fi } # --------------------------------------------------------------------------- # 1. Backward compatibility: nothing resolvable, and NO fleet on this host -> # shared account, unchanged. This is the only surviving fallback. # --------------------------------------------------------------------------- git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true out=$(run_helper "git.mosaicstack.dev" "") assert_eq "no identity + no fleet: username" "username=git" "$(echo "$out" | grep '^username=')" assert_eq "no identity + no fleet: password" "password=shared-mosaicstack-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 2. git-supplied username resolves to a SERVICE identity WITH a token in the # framework store -> that identity + token wins over the shared account. # --------------------------------------------------------------------------- echo -n "agentA-mosaicstack-token" > "$SVC_STORE/gitea-mosaicstack-agentA.token" out=$(run_helper "git.mosaicstack.dev" "agentA") assert_eq "username-resolved identity: username" "username=agentA" "$(echo "$out" | grep '^username=')" assert_eq "username-resolved identity: password" "password=agentA-mosaicstack-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 3. git config mosaic.gitIdentity (per-worktree) beats git-supplied username. # --------------------------------------------------------------------------- echo -n "agentB-mosaicstack-token" > "$SVC_STORE/gitea-mosaicstack-agentB.token" git -C "$REPO_DIR" config mosaic.gitIdentity agentB out=$(run_helper "git.mosaicstack.dev" "agentA") assert_eq "git-config beats username: username" "username=agentB" "$(echo "$out" | grep '^username=')" assert_eq "git-config beats username: password" "password=agentB-mosaicstack-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 4. MOSAIC_GIT_IDENTITY env beats git config mosaic.gitIdentity. # --------------------------------------------------------------------------- echo -n "agentC-mosaicstack-token" > "$SVC_STORE/gitea-mosaicstack-agentC.token" out=$(run_helper "git.mosaicstack.dev" "agentA" MOSAIC_GIT_IDENTITY=agentC) assert_eq "env beats git-config: username" "username=agentC" "$(echo "$out" | grep '^username=')" assert_eq "env beats git-config: password" "password=agentC-mosaicstack-token" "$(echo "$out" | grep '^password=')" git -C "$REPO_DIR" config --unset mosaic.gitIdentity # --------------------------------------------------------------------------- # 5. Correct token PATH is chosen per host: same agent id, different host # prefix (gitea-usc- vs gitea-mosaicstack-). # --------------------------------------------------------------------------- echo -n "agentD-usc-token" > "$SVC_STORE/gitea-usc-agentD.token" out=$(run_helper "git.uscllc.com" "agentD") assert_eq "host-scoped token path (usc): username" "username=agentD" "$(echo "$out" | grep '^username=')" assert_eq "host-scoped token path (usc): password" "password=agentD-usc-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 6. FAIL CLOSED — identity resolves, no credential for it on this host. Must # NOT borrow the shared account, and must NOT leak the same agent's token # for a DIFFERENT host (agentD holds a usc token and no mosaicstack one). # --------------------------------------------------------------------------- assert_fail_closed "cross-host absence refuses (no shared fallback, no cross-host leak)" \ "git.mosaicstack.dev" "agentD" "gitea-mosaicstack-agentD.token" # The agent's own usc token must not appear either. : > "$WORK_DIR/stderr.tmp" set +e leak_out=$(run_helper "git.mosaicstack.dev" "agentD" 2>"$WORK_DIR/stderr.tmp") set -e if [[ "$leak_out$(cat "$WORK_DIR/stderr.tmp")" == *"agentD-usc-token"* ]]; then echo "FAIL: cross-host leak — the usc token value appeared on a mosaicstack request" >&2 fail=1 fi assert_fail_closed "unknown identity refuses (shared account never substituted)" \ "git.mosaicstack.dev" "no-such-agent" "no-token-for-identity" # --------------------------------------------------------------------------- # 7. A refusal leaves a durable spool record, and that record contains no token. # The stderr diagnostic is transient; the record is what an operator reads # afterwards, so it must exist independently of anyone watching the terminal. # --------------------------------------------------------------------------- spool_file=$(find "$SPOOL_DIR" -maxdepth 1 -name '*.jsonl' | head -n 1) if [[ -z "$spool_file" ]]; then echo "FAIL: fail-closed left no spool record under $SPOOL_DIR" >&2 fail=1 else spool_body=$(cat "$spool_file") assert_eq "spool record names the refused identity" "1" \ "$(grep -c '"identity":"no-such-agent"' "$spool_file" | head -n 1)" if [[ "$spool_body" == *"shared-"*"-token"* || "$spool_body" == *"agentD-usc-token"* ]]; then echo "FAIL: spool record contains a token value:" >&2 echo "$spool_body" >&2 fail=1 fi fi # --------------------------------------------------------------------------- # 8. SEAT identity: an id with a directory under /fleet/agents/ is read # from its OWN secrets/ slot, not from the framework store. # --------------------------------------------------------------------------- mkdir -p "$BRAIN_DIR/fleet/agents/seatE/secrets" echo -n "seatE-slot-token" > "$BRAIN_DIR/fleet/agents/seatE/secrets/gitea-mosaicstack-seatE.token" chmod 600 "$BRAIN_DIR/fleet/agents/seatE/secrets/gitea-mosaicstack-seatE.token" # Seat arms run through the lineage harness below (rev-code-02 F1/F2 rework): # a seat caller must be established by ancestry, not by the helper's own env. out=$(run_lineage seatE MOSAIC_AGENT_NAME=seatE MOSAIC_GIT_IDENTITY=seatE) assert_eq "seat reads its own slot: username" "username=seatE" "$(echo "$out" | grep '^username=')" assert_eq "seat reads its own slot: password" "password=seatE-slot-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 9. NO CROSS-STORE FALLBACK — the assertion this whole store-selection design # exists for. seatF is a seat (it has a directory) with an EMPTY slot, while # a framework-store token of the identical name is present and readable. # The helper must refuse rather than read it: one credential, one location, # and a seat that reads a same-named service credential is exactly the # silent-substitution failure the fail-closed rule removes. # --------------------------------------------------------------------------- mkdir -p "$BRAIN_DIR/fleet/agents/seatF/secrets" echo -n "seatF-SERVICE-STORE-token" > "$SVC_STORE/gitea-mosaicstack-seatF.token" assert_refused_lineage "seat with empty slot does NOT fall back to the framework store" \ seatF no-token-for-identity MOSAIC_AGENT_NAME=seatF MOSAIC_GIT_IDENTITY=seatF : > "$WORK_DIR/stderr.tmp" set +e xstore_out=$(run_helper "git.mosaicstack.dev" "seatF" MOSAIC_BRAIN_HOME="$BRAIN_DIR" 2>"$WORK_DIR/stderr.tmp") set -e if [[ "$xstore_out$(cat "$WORK_DIR/stderr.tmp")" == *"seatF-SERVICE-STORE-token"* ]]; then echo "FAIL: cross-store fallback — a seat read the framework store's same-named token" >&2 fail=1 fi # Control: that framework-store token IS readable, so the refusal above is the # store rule firing and not an unreadable file. A non-seat identity pointed at # the same file gets it. out=$(run_helper "git.mosaicstack.dev" "seatF" MOSAIC_BRAIN_HOME="$WORK_DIR/no-such-brain") assert_eq "control — same file IS readable for a non-seat identity" \ "password=seatF-SERVICE-STORE-token" "$(echo "$out" | grep '^password=')" # --------------------------------------------------------------------------- # 10. FAIL CLOSED — no identity resolvable, but this host runs a fleet. Where # seats exist, an unattributable request is refused instead of receiving # the shared account. Contrast with case 1, which is the same request on a # host with no fleet and still returns the shared account. # --------------------------------------------------------------------------- git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true assert_fail_closed "no identity on a fleet host refuses" \ "git.mosaicstack.dev" "" "no-identity" MOSAIC_BRAIN_HOME="$BRAIN_DIR" # --------------------------------------------------------------------------- # 11. The brain home defaults to ~/.mosaic when MOSAIC_BRAIN_HOME is unset — # the fleet gate must fire on the default path too, not only on an # explicitly injected one. Case 1 ran before this directory existed; the # same call now refuses, which also proves case 1 was measuring the # no-fleet branch rather than passing for an unrelated reason. # --------------------------------------------------------------------------- mkdir -p "$FAKE_HOME/.mosaic/fleet/agents" assert_fail_closed "fleet gate fires on the default ~/.mosaic brain home" \ "git.mosaicstack.dev" "" "no-identity" # --------------------------------------------------------------------------- # 12. Unrelated/unknown host -> exit 0, no output (passthrough for non-Gitea # remotes, e.g. github.com via a different credential helper). A fleet host # must not refuse a host this helper does not own. # --------------------------------------------------------------------------- out=$(run_helper "github.com" "agentA") assert_eq "unknown host: no output" "" "$out" out=$(run_helper "github.com" "" MOSAIC_BRAIN_HOME="$BRAIN_DIR") assert_eq "unknown host on a fleet host: still passthrough, not a refusal" "" "$out" # --------------------------------------------------------------------------- # 13. Non-"get" verb (store/erase) -> exit 0, no output (git-credential # protocol: this helper only implements get). # --------------------------------------------------------------------------- store_out=$(cd "$REPO_DIR" && env -i HOME="$FAKE_HOME" PATH="$PATH" "$HELPER" store </dev/null 2>&1 host=git.mosaicstack.dev username=no-such-agent EOF ) || true # Deliberately not `ls ... | head -1`: under `set -o pipefail` a missed glob # makes ls exit 2, the pipeline inherits it, and `set -e` kills this suite with # zero output — the same silent-nonzero failure this file exists to catch. record_file="" for candidate in "$hostile_spool"/*.jsonl; do if [[ -e "$candidate" ]]; then record_file="$candidate" break fi done if [[ -z "$record_file" ]]; then echo "FAIL: hostile cwd — no escalation record was written at all" >&2 fail=1 elif ! python3 -c 'import json,sys for line in open(sys.argv[1]): line = line.strip() if line: json.loads(line)' "$record_file" 2>/dev/null; then echo "FAIL: hostile cwd — escalation record is not parseable JSONL:" >&2 cat "$record_file" >&2 fail=1 fi # --------------------------------------------------------------------------- # 15. When the spool cannot be created, the diagnostic must NOT name a record # path. Naming a file that was never written sends the operator to an # empty path on exactly the hosts where the escalation was lost. # --------------------------------------------------------------------------- unwritable_spool="/proc/mosaic-credential-spool-cannot-exist" nospool_err=$( cd "$REPO_DIR" env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$unwritable_spool" \ MOSAIC_GIT_IDENTITY=no-such-agent \ "$HELPER" get <&1 >/dev/null host=git.mosaicstack.dev username=no-such-agent EOF ) || true if [[ "$nospool_err" == *"record: $unwritable_spool/"* ]]; then echo "FAIL: unwritable spool — diagnostic names a record file that was never written" >&2 fail=1 fi if [[ "$nospool_err" != *"NOT WRITTEN"* ]]; then echo "FAIL: unwritable spool — diagnostic does not say the record was not written" >&2 echo "$nospool_err" >&2 fail=1 fi # --------------------------------------------------------------------------- # 11. P5-RM-006 (+r1 rework) — caller identity from exec-frozen ancestry. # A seat caller is established by lineage, not by the helper's own env; # disagreement anywhere in the lineage is a rewrite and refuses; an # anonymous caller resolves NOTHING on a fleet host (seat or service); # a loose seat-slot mode refuses. Enforcement-removal red control at 11h. # --------------------------------------------------------------------------- mkdir -p "$BRAIN_DIR/fleet/agents/seatG/secrets" echo -n "seatG-slot-token" > "$BRAIN_DIR/fleet/agents/seatG/secrets/gitea-mosaicstack-seatG.token" chmod 600 "$BRAIN_DIR/fleet/agents/seatG/secrets/gitea-mosaicstack-seatG.token" # 11a. Cross-seat negative (lineage seatE, env ident=seatG): still refused. assert_refused_lineage "seat cannot override identity to another seat's slot" \ seatE cross-seat-identity-refused MOSAIC_GIT_IDENTITY=seatG # 11b. Seat asking for a SERVICE identity: cross-seat territory. assert_refused_lineage "seat cannot resolve a service identity either" \ seatE cross-seat-identity-refused MOSAIC_GIT_IDENTITY=agentA # 11c. Anonymous caller asking for a seat slot is refused. assert_refused_lineage "anonymous caller cannot resolve a seat slot on a fleet host" \ "" anonymous-credential-refused MOSAIC_GIT_IDENTITY=seatG # 11d. Anonymous caller asking for a SERVICE identity: ALSO refused # (rev-code-02 F2 — credentialed services are seats; the legacy store is # not anonymously reachable on fleet hosts). assert_refused_lineage "anonymous caller cannot resolve a legacy service credential either" \ "" anonymous-credential-refused MOSAIC_GIT_IDENTITY=agentA # 11e. Slot permissions: a group-readable slot is refused; mode restored -> serves. chmod 644 "$BRAIN_DIR/fleet/agents/seatG/secrets/gitea-mosaicstack-seatG.token" assert_refused_lineage "loose slot mode is refused" \ seatG slot-permission-violation MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG chmod 600 "$BRAIN_DIR/fleet/agents/seatG/secrets/gitea-mosaicstack-seatG.token" out=$(run_lineage seatG MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG) assert_eq "mode restored to 600: seatG serves again" "password=seatG-slot-token" "$(echo "$out" | grep '^password=')" # 11f. rev-code-02 F1 repro: dual-variable override (caller lineage seatE, # helper env carrying MOSAIC_AGENT_NAME=seatG AND MOSAIC_GIT_IDENTITY=seatG). assert_refused_lineage "F1: dual MOSAIC_AGENT_NAME+MOSAIC_GIT_IDENTITY override refused" \ seatE caller-identity-spoof-refused MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG # 11g. Stripped lineage still serves the rightful seat: caller frozen at the # ancestor, helper env clean (self empty), own ident. out=$(run_lineage seatE MOSAIC_GIT_IDENTITY=seatE) assert_eq "lineage consensus with stripped self still serves the owning seat" \ "password=seatE-slot-token" "$(echo "$out" | grep '^password=')" # 11h. RED CONTROL: delete the ancestry binding between markers from a copy # of the IMPLEMENTATION (run directly with the clean marker — a red control # measures the binding itself, deliberately not through the wrapper); # the F1 dual-override request must then RESOLVE seatG's token — the # exact measured failure — proving the binding is the enforcement. RED_HELPER="$WORK_DIR/red/git-credential-mosaic.impl" mkdir -p "$WORK_DIR/red" sed '/P5-RM-006r1 ancestry binding begin/,/P5-RM-006r1 ancestry binding end/d' "$IMPL" > "$RED_HELPER" chmod +x "$RED_HELPER" if cmp -s "$IMPL" "$RED_HELPER"; then echo "FAIL: red control is vacuous — marker deletion removed nothing" >&2 fail=1 fi set +e red_out=$(printf 'host=git.mosaicstack.dev\nusername=probe\n\n' | \ env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_LINEAGE_FENCE=1 \ bash "$WORK_DIR/lineage-root.sh" seatE "$WORK_DIR/lineage-carrier.sh" \ "$RED_HELPER" "$SPOOL_DIR" "$BRAIN_DIR" "$REPO_DIR" \ _MOSAIC_HELPER_CLEAN=1 MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG 2>/dev/null) set -e if [[ "$(echo "$red_out" | grep '^password=')" != "password=seatG-slot-token" ]]; then echo "FAIL: red control — with the binding removed, the F1 dual-override should have resolved seatG's token, got: $red_out" >&2 fail=1 else echo "ok: red control — binding removed -> F1 dual-override resolves the victim token (the binding is the enforcement)" fi # 11h2. rev-code-02 R1 F1: PATH-shadowed tr/sed/head/grep must not forge the # ancestry. The dual-override request runs with a hostile PATH whose # utilities claim the victim name for every /proc read; the walker uses # only bash builtins, so the shadows never execute and the refusal holds. HOSTILE_BIN="$WORK_DIR/hostile-bin" mkdir -p "$HOSTILE_BIN" for tool in tr sed head grep cat stat; do printf '#!/usr/bin/env bash\ncat >/dev/null\necho "MOSAIC_AGENT_NAME=seatG"\nexit 0\n' > "$HOSTILE_BIN/$tool" chmod +x "$HOSTILE_BIN/$tool" done assert_refused_lineage "F1-R1: hostile PATH utilities cannot forge ancestry (dual override still refused)" \ seatE caller-identity-spoof-refused \ MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG PATH="$HOSTILE_BIN:$PATH" # 11i. Service automation integration arm (rev-code-02 R2 bar): a SERVICE # seat bound by lineage resolves its own slot — the brain-git-sync shape. mkdir -p "$BRAIN_DIR/fleet/agents/svc-fixture/secrets" echo -n "svc-fixture-slot-token" > "$BRAIN_DIR/fleet/agents/svc-fixture/secrets/gitea-mosaicstack-svc-fixture.token" chmod 600 "$BRAIN_DIR/fleet/agents/svc-fixture/secrets/gitea-mosaicstack-svc-fixture.token" out=$(run_lineage svc-fixture MOSAIC_AGENT_NAME=svc-fixture MOSAIC_GIT_IDENTITY=svc-fixture) assert_eq "service automation with bound seat lineage resolves its own slot" \ "password=svc-fixture-slot-token" "$(echo "$out" | grep '^password=')" # 11j. rev-code-02 R3 B1: BASH_ENV shaping. A read() shadow defined through # BASH_ENV must be refused before any resolution — the guard scrubs and # refuses with bash-environment-injection-refused. INJ_SH="$WORK_DIR/inj-read.sh" printf 'read() { builtin read -r _x || return 0; printf "MOSAIC_AGENT_NAME=seatG\\n"; return 0; }\n' > "$INJ_SH" assert_refused_lineage "F1-R3: BASH_ENV read() shadow is dropped at the wrapper boundary (identity gate governs)" \ seatE caller-identity-spoof-refused \ MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG BASH_ENV="$INJ_SH" # 11k. rev-code-02 R3 B1: exported functions (BASH_FUNC_* import) refused too. assert_refused_lineage "F1-R3: exported BASH_FUNC_* import never crosses the wrapper boundary (identity gate governs)" \ seatE caller-identity-spoof-refused \ MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG \ 'BASH_FUNC_read%%=() { builtin read -r _x || return 0; printf "MOSAIC_AGENT_NAME=seatG\\n"; return 0; }' # 11l. rev-code-02 R3 B2: hostile stat cannot launder a loose slot. Own-slot # lineage (legit caller), 0644 slot, PATH-shadowed stat reporting 600 — # mode inspection must come from the trusted PATH and still refuse. chmod 644 "$BRAIN_DIR/fleet/agents/svc-fixture/secrets/gitea-mosaicstack-svc-fixture.token" printf '#!/usr/bin/env bash\necho 600\n' > "$HOSTILE_BIN/stat" assert_refused_lineage "F2-R3: hostile stat cannot make a 0644 slot pass as 600 (own-slot path)" \ svc-fixture slot-permission-violation \ MOSAIC_AGENT_NAME=svc-fixture MOSAIC_GIT_IDENTITY=svc-fixture PATH="$HOSTILE_BIN:$PATH" chmod 600 "$BRAIN_DIR/fleet/agents/svc-fixture/secrets/gitea-mosaicstack-svc-fixture.token" # 11m. rev-code-02 R3 probe 1 through the PRODUCTION ENTRYPOINT: BASH_ENV # defines unset()/exit() no-ops (defeating in-bash scrub/termination). # The python wrapper never passes BASH_ENV across the boundary, so the # implementation cannot be shaped and the dual override still refuses. INJ_P1="$WORK_DIR/inj-probe1.sh" cat > "$INJ_P1" <<'P1' unset() { return 0; } exit() { return 0; } read() { builtin read -r _x || return 0; printf 'MOSAIC_AGENT_NAME=seatG\n'; return 0; } P1 assert_refused_lineage "F1-R4 probe1: BASH_ENV unset/exit no-ops cannot shape the helper (wrapper boundary)" \ seatE caller-identity-spoof-refused \ MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG BASH_ENV="$INJ_P1" # 11n. rev-code-02 R3 probe 2 through the PRODUCTION ENTRYPOINT: declare() # hides imported functions, unsets the marker, printf() forges the # ancestry. Dropped at the wrapper boundary; refusal holds. INJ_P2="$WORK_DIR/inj-probe2.sh" cat > "$INJ_P2" <<'P2' declare() { return 0; } printf() { builtin printf '%s' "MOSAIC_AGENT_NAME=seatG"; return 0; } read() { builtin read -r _x || return 0; printf 'MOSAIC_AGENT_NAME=seatG\n'; return 0; } P2 assert_refused_lineage "F1-R4 probe2: declare-hide + printf-forge cannot shape the helper (wrapper boundary)" \ seatE caller-identity-spoof-refused \ MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG BASH_ENV="$INJ_P2" # 11o. RED CONTROL for the wrapper boundary (enforcement-removal): invoke the # IMPLEMENTATION directly, bypassing the wrapper, with the PROBE-1 shape # (unset/exit no-ops) and a forged clean marker — exactly the falsified # in-bash world the reviewer measured: the refusal prints, exit is # no-oped, execution continues, and the forged ancestry SERVES seatG. # The wrapper boundary is the enforcement; this arm proves it bites. set +e bypass_out=$(cd "$REPO_DIR" && printf 'host=git.mosaicstack.dev\nusername=probe\n\n' | \ env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$SPOOL_DIR" \ MOSAIC_BRAIN_HOME="$BRAIN_DIR" _MOSAIC_HELPER_CLEAN=1 BASH_ENV="$INJ_P1" \ MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG \ bash "$IMPL" get 2>/dev/null) set -e if [[ "$(echo "$bypass_out" | grep -c '^password=')" -lt 1 ]]; then echo "FAIL: wrapper red control — direct shaped .impl should have served (wrapper is the enforcement), got: $bypass_out" >&2 fail=1 else echo "ok: red control — wrapper bypassed + probe1 shape serves (the wrapper boundary is the enforcement)" fi # 11p. Direct .impl invocation WITHOUT the clean marker: refused by the # implementation's own entrypoint assert. set +e direct_out=$(cd "$REPO_DIR" && printf 'host=git.mosaicstack.dev\nusername=probe\n\n' | \ env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$SPOOL_DIR" \ MOSAIC_BRAIN_HOME="$BRAIN_DIR" MOSAIC_GIT_IDENTITY=seatE \ bash "$IMPL" get 2>"$WORK_DIR/stderr-direct.tmp") direct_rc=$? set -e if [[ "$direct_rc" -eq 0 || -n "$direct_out" ]]; then echo "FAIL: direct .impl without marker must refuse (got rc=$direct_rc out='$direct_out')" >&2 fail=1 elif ! grep -q 'direct-entrypoint-refused' "$WORK_DIR/stderr-direct.tmp"; then echo "FAIL: direct .impl refusal lacks direct-entrypoint-refused" >&2 fail=1 fi if [[ "$fail" -eq 0 ]]; then echo "git-credential-mosaic identity resolution regression passed" fi exit "$fail"