From 7e9e013f92e009cc52004af97d4ee543d89339e8 Mon Sep 17 00:00:00 2001 From: topher Date: Sat, 29 Aug 2026 19:14:28 -0500 Subject: [PATCH 1/3] fleet: split-home-safe mosaic launcher (T110, P5-RM-009 stack side) Ships fleet/bin/mosaic in the framework so the T106 shipped-first launcher contract survives split-home layouts. The shipped npm mosaic resolves from the passwd real home instead of $HOME: under a seat home (HOME=seat dir) the operator's npm prefix is still found, so 'HOME= fleet/bin/mosaic --version' returns the shipped version. The $HOME/.npm-global candidate is consulted only when the passwd lookup fails, and then every path component must be a non-symlink (secure descriptor traversal; a planted descriptor is refused without execution). T106 behavior preserved verbatim: worktree builds are MOSAIC_CLI_WORKTREE opt-in only, health-checked, shipped fallback on doubt; MOSAIC_FLEET_CLI_OFF forces pure pass-through; no runnable candidate exits 127 with the documented stderr line; stale worktree candidates never regain default precedence (without the opt-in env the worktree path is never read). Manifest: fleet/bin/** added to the [framework] section so the updater and installer both own the subtree (manifest-parity 17/17 green). Hermetic suite test-mosaic-launcher.sh covers the split-home positive, typed failure, stale-worktree non-precedence, opt-in healthy/unhealthy, OFF pass-through, and descriptor-refusal arms. Marker T110-DISPATCH-TOPHER-1N2O. --- packages/mosaic/framework/fleet/bin/mosaic | 123 ++++++++++++++++++ .../fleet/bin/test-mosaic-launcher.sh | 115 ++++++++++++++++ .../mosaic/framework/framework-manifest.txt | 1 + 3 files changed, 239 insertions(+) create mode 100755 packages/mosaic/framework/fleet/bin/mosaic create mode 100755 packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh diff --git a/packages/mosaic/framework/fleet/bin/mosaic b/packages/mosaic/framework/fleet/bin/mosaic new file mode 100755 index 00000000..ee681691 --- /dev/null +++ b/packages/mosaic/framework/fleet/bin/mosaic @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# mosaic — fleet launcher (shipped-first, split-home safe). +# +# T110 / P5-RM-009 stack side. Carries the T106 brain launcher contract +# (shipped-first, worktree dev opt-in, OFF pass-through, typed failure) with +# one split-home correction: the SHIPPED npm mosaic is resolved from the real +# user's home (passwd database), never from $HOME. Under split-home seat +# layouts HOME is a seat home: it carries no npm prefix, and a +# $HOME/.npm-global there would be a plantable descriptor, so the $HOME +# candidate is consulted only when the passwd lookup itself fails, and then +# only with a full symlink-component refusal (secure descriptor traversal). +# +# Contract: +# 1. SHIPPED npm mosaic is the default. Candidate order: +# a. /.npm-global/bin/mosaic — real home from the passwd +# database. The final component may be npm's own bin symlink into +# lib/node_modules; that indirection is npm's layout, not a plant. +# b. $HOME/.npm-global/bin/mosaic — ONLY when the passwd lookup +# fails. Untrusted: every path component must be a non-symlink or +# the candidate is refused without execution. +# 2. Worktree build is DEV OPT-IN: used only when MOSAIC_CLI_WORKTREE is +# explicitly set. Health-checked via --version; ANY doubt (absent, +# unreadable, or failing) falls back to the shipped npm mosaic with a +# warning on stderr. With no environment set, worktree candidates are +# never consulted — stale worktree builds cannot regain precedence. +# 3. MOSAIC_FLEET_CLI_OFF keeps its pass-through semantics: set (any +# value) forces pure pass-through. The dev path is not consulted even +# when MOSAIC_CLI_WORKTREE is also set. +# 4. Typed failure: with no runnable candidate the launcher prints one +# stderr line naming what was checked and exits 127. +# 5. NEVER writes to the mosaic home or the npm prefix. Deployment to the +# fleet goes through the real channel (PR to next -> mosaic update). +# +# Env: +# MOSAIC_CLI_WORKTREE dev opt-in: path to a stack worktree whose +# packages/mosaic/dist/cli.js is used (health-checked, +# shipped fallback on doubt) +# MOSAIC_FLEET_CLI_OFF set (any value) to force pure pass-through +# +# Component walk note: the descriptor guard splits on "/" without quoting so +# multi-byte HOME paths with spaces are not supported for the FALLBACK +# candidate; the passwd candidate needs no walk (trusted derivation). + +set -u + +fail() { + echo "mosaic: $*" >&2 + exit 127 +} + +# Real user home from the passwd database (HOME-independent). +real_home() { + getent passwd "$(id -u)" 2>/dev/null | cut -d: -f6 +} + +# Refuse when any component of the candidate path is a symlink. Used only for +# the untrusted $HOME fallback candidate (contract 1b). +path_has_symlink_component() { + local path="$1" dir base acc="" part + dir="$(dirname -- "$path")" + base="$(basename -- "$path")" + local IFS='/' + for part in $dir; do + acc="$acc/$part" + [ -L "$acc" ] && return 0 + done + [ -L "$dir/$base" ] && return 0 + return 1 +} + +# Print shipped candidates in contract order. Refusals are reported on stderr +# so the typed failure names the cause. +shipped_candidates() { + local rh home_candidate + rh="$(real_home)" + if [ -n "$rh" ]; then + printf '%s\n' "$rh/.npm-global/bin/mosaic" + return 0 + fi + # passwd lookup failed: the only fallback is $HOME, descriptor-guarded. + if [ -n "${HOME:-}" ]; then + home_candidate="$HOME/.npm-global/bin/mosaic" + if path_has_symlink_component "$home_candidate"; then + echo "mosaic: refusing \$HOME candidate $home_candidate: symlink component (untrusted without a passwd home)" >&2 + else + printf '%s\n' "$home_candidate" + fi + fi + return 0 +} + +resolve_shipped() { + local candidate + while IFS= read -r candidate; do + [ -n "$candidate" ] || continue + if [ -x "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done < <(shipped_candidates) + return 1 +} + +# Dev opt-in only: an explicit MOSAIC_CLI_WORKTREE reaches the worktree build, +# and pure pass-through (MOSAIC_FLEET_CLI_OFF) outranks it. +if [ -n "${MOSAIC_CLI_WORKTREE:-}" ] && [ -z "${MOSAIC_FLEET_CLI_OFF:-}" ]; then + CLI="$MOSAIC_CLI_WORKTREE/packages/mosaic/dist/cli.js" + if [ -r "$CLI" ]; then + if v="$(node "$CLI" --version 2>/dev/null)" && [ -n "$v" ]; then + exec node "$CLI" "$@" + fi + echo "mosaic: worktree build at $CLI failed its health check; using shipped npm mosaic" >&2 + else + echo "mosaic: worktree build at $CLI absent or unreadable; using shipped npm mosaic" >&2 + fi +fi + +SHIPPED="$(resolve_shipped)" || true +if [ -n "${SHIPPED:-}" ]; then + exec "$SHIPPED" "$@" +fi + +fail "no runnable CLI (shipped npm mosaic absent from the passwd-home npm prefix and \$HOME; worktree build requires MOSAIC_CLI_WORKTREE)" diff --git a/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh b/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh new file mode 100755 index 00000000..4b6e559e --- /dev/null +++ b/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Hermetic suite for the fleet/bin/mosaic launcher (T110 / P5-RM-009). +# +# Arms cover the plan acceptance: split-home shipped-first positive, typed +# failure on missing candidates, stale-worktree non-precedence, OFF +# pass-through, and secure-descriptor refusal on the untrusted $HOME +# fallback. No network, no real npm install, no node package build: the +# "shipped mosaic" is a stub script and getent is PATH-stubbed (set +# GETENT_STUB=fail to make the passwd lookup fail, exercising the guarded +# $HOME fallback). +set -euo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd) +LAUNCHER="$SCRIPT_DIR/mosaic" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +[ -f "$LAUNCHER" ] || fail "missing launcher" +[ -x "$LAUNCHER" ] || fail "launcher is not executable" +bash -n "$LAUNCHER" || fail "launcher fails bash -n" + +WORK=$(mktemp -d) +cleanup() { rm -rf "$WORK"; } +trap cleanup EXIT + +REAL_HOME="$WORK/real-home" +SEAT_HOME="$WORK/seat-home" +STUB_BIN="$WORK/stub-bin" +mkdir -p "$REAL_HOME/.npm-global/bin" "$SEAT_HOME" "$STUB_BIN" + +cat >"$REAL_HOME/.npm-global/bin/mosaic" <<'SH' +#!/bin/sh +echo "0.0.0-shipped-stub" +SH +chmod +x "$REAL_HOME/.npm-global/bin/mosaic" + +# PATH-stubbed getent: reports the real home for the current uid, unless +# GETENT_STUB=fail is in the launcher environment (exercises the guarded +# $HOME fallback path). +cat >"$STUB_BIN/getent" < [VAR=value ...] -- [args...] + local home="$1"; shift + [ "${1:-}" = "--" ] && shift + env -i PATH="$STUB_BIN:/usr/bin:/bin" HOME="$home" TERM="${TERM:-dumb}" "$LAUNCHER" "$@" +} + +# A1 — acceptance 1: split-home positive. HOME is an empty seat home; the +# shipped mosaic resolves through the passwd real home. +out="$(printf '' | run_launcher "$SEAT_HOME" -- --version)" +[ "$out" = "0.0.0-shipped-stub" ] || fail "A1 split-home positive: got '$out', want shipped stub version" + +# A3 — acceptance 3: a stale worktree build is NEVER consulted without the +# explicit opt-in, even when a worktree exists on disk. +WT="$WORK/stale-wt" +mkdir -p "$WT/packages/mosaic/dist" +printf 'console.log("0.0.0-stale-worktree")\n' >"$WT/packages/mosaic/dist/cli.js" +out="$(printf '' | run_launcher "$SEAT_HOME" -- --version)" +[ "$out" = "0.0.0-shipped-stub" ] || fail "A3 stale worktree regained precedence without opt-in: got '$out'" + +# A2 (opt-in healthy) — explicit MOSAIC_CLI_WORKTREE reaches the worktree. +out="$(printf '' | env MOSAIC_CLI_WORKTREE="$WT" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version)" +[ "$out" = "0.0.0-stale-worktree" ] || fail "A2 opt-in worktree not used: got '$out'" + +# A2b (opt-in unhealthy) — absent dist falls back to shipped with a warning. +out2="$(printf '' | env MOSAIC_CLI_WORKTREE="$WORK/empty-wt" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>/dev/null)" +[ "$out2" = "0.0.0-shipped-stub" ] || fail "A2b unhealthy worktree fallback output: '$out2'" +err2="$(printf '' | env MOSAIC_CLI_WORKTREE="$WORK/empty-wt" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&1 >/dev/null)" +case "$err2" in *"absent or unreadable"*|*"health check"*) ;; *) fail "A2b unhealthy worktree fallback warning missing: '$err2'" ;; esac + +# A4 — OFF pass-through: worktree opt-in is ignored when OFF is set. +out="$(printf '' | env MOSAIC_FLEET_CLI_OFF=1 MOSAIC_CLI_WORKTREE="$WT" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version)" +[ "$out" = "0.0.0-shipped-stub" ] || fail "A4 OFF did not force pass-through: got '$out'" + +# A5 — acceptance 4: typed failure when no candidate exists (passwd lookup +# fails, seat home carries no npm prefix). Expect 127 + documented message. +set +e +err="$(printf '' | env GETENT_STUB=fail HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&1 >/dev/null)" +rc=$? +set -e +[ "$rc" = "127" ] || fail "A5 typed failure rc: got $rc, want 127" +case "$err" in *"no runnable CLI"*) ;; *) fail "A5 typed failure message missing: '$err'" ;; esac + +# A6 — secure descriptor traversal: passwd lookup fails and a symlink-planted +# $HOME/.npm-global is refused without execution. +PLANT="$WORK/planted-target" +mkdir -p "$PLANT/.npm-global/bin" +cat >"$PLANT/.npm-global/bin/mosaic" <&1)" +rc=$? +set -e +[ "$rc" = "127" ] || fail "A6 planted descriptor was followed (rc $rc, out '$err')" +case "$err" in *"symlink component"*) ;; *) fail "A6 refusal diagnostic missing: '$err'" ;; esac +[ ! -e "$WORK/planted-sentinel" ] || fail "A6 planted mosaic EXECUTED" + +echo "mosaic launcher suite: all arms passed" diff --git a/packages/mosaic/framework/framework-manifest.txt b/packages/mosaic/framework/framework-manifest.txt index 99320075..c7e36de8 100644 --- a/packages/mosaic/framework/framework-manifest.txt +++ b/packages/mosaic/framework/framework-manifest.txt @@ -47,6 +47,7 @@ templates/** tools/** # Fleet: only the framework-seeded fleet subtrees are framework-owned. fleet/README.md +fleet/bin/** fleet/examples/** fleet/profiles/** fleet/roles/** -- 2.54.0 From 2435e74b8ed4515c709b4a3a753bcbcd8f42cb08 Mon Sep 17 00:00:00 2001 From: topher Date: Sat, 29 Aug 2026 19:30:23 -0500 Subject: [PATCH 2/3] fleet: T110 rework - exact fleet/bin ownership, absolute fallback guard, mutation-controlled suite B1 (rev-code-02): fleet/bin/** made keep-mode update prune existing estate executables absent from the package source. Replaced with exact entries (fleet/bin/mosaic, fleet/bin/test-mosaic-launcher.sh); parity spec gains a dedicated ownership probe asserting the two shipped files are framework-owned while seat-up.sh, launch-seat.sh and the directory itself stay operator-owned, on both resolvers. B2: a relative HOME bypassed the fallback guard (component probes were absolute-prefixed, so a relative candidate walked nonexistent absolute paths and a planted cwd-relative CLI executed). The untrusted fallback candidate is now refused unless absolute, with parent-escape (..) components refused as well; the symlink walk then applies. B3: A6 planted the executable off the resolved candidate path, so a guard-bypass mutant stayed green (nothing executable at the candidate). The symlink now points at $PLANT/.npm-global so the candidate resolves exactly to the planted binary, and the suite gains two mutation controls: a guard-bypassed copy MUST execute both the absolute-symlink plant and the new relative-HOME plant (sentinels asserted present), while the real launcher refuses both (rc 127, diagnostics, sentinels absent). Marker T110-DISPATCH-TOPHER-1N2O / ORCH-T110-REWORK-M5N6. --- packages/mosaic/framework/fleet/bin/mosaic | 41 +++++++++++--- .../fleet/bin/test-mosaic-launcher.sh | 53 +++++++++++++++++-- .../mosaic/framework/framework-manifest.txt | 6 ++- .../src/framework/manifest-parity.spec.ts | 20 +++++++ 4 files changed, 109 insertions(+), 11 deletions(-) diff --git a/packages/mosaic/framework/fleet/bin/mosaic b/packages/mosaic/framework/fleet/bin/mosaic index ee681691..f1beace0 100755 --- a/packages/mosaic/framework/fleet/bin/mosaic +++ b/packages/mosaic/framework/fleet/bin/mosaic @@ -16,8 +16,11 @@ # database. The final component may be npm's own bin symlink into # lib/node_modules; that indirection is npm's layout, not a plant. # b. $HOME/.npm-global/bin/mosaic — ONLY when the passwd lookup -# fails. Untrusted: every path component must be a non-symlink or -# the candidate is refused without execution. +# fails, and then only when the candidate is a trusted-shape +# absolute path: relative HOME and parent-escape (..) components +# are refused outright, and every remaining component must be a +# non-symlink (secure descriptor traversal). Refused candidates +# are never executed. # 2. Worktree build is DEV OPT-IN: used only when MOSAIC_CLI_WORKTREE is # explicitly set. Health-checked via --version; ANY doubt (absent, # unreadable, or failing) falls back to the shipped npm mosaic with a @@ -53,8 +56,8 @@ real_home() { getent passwd "$(id -u)" 2>/dev/null | cut -d: -f6 } -# Refuse when any component of the candidate path is a symlink. Used only for -# the untrusted $HOME fallback candidate (contract 1b). +# True when any component of an ABSOLUTE candidate path is a symlink. Only +# ever called after fallback_candidate_usable's absolute-shape check. path_has_symlink_component() { local path="$1" dir base acc="" part dir="$(dirname -- "$path")" @@ -68,6 +71,32 @@ path_has_symlink_component() { return 1 } +# Reject the untrusted $HOME fallback candidate unless it is a trusted-shape +# absolute path: absolute, no parent-escape (..) components, and no symlink +# components anywhere on the path. Every rejection is named on stderr so the +# typed failure explains itself. This is the launcher's descriptor guard; the +# suite's mutation control (guard bypassed) must plant-exec, proving the guard +# is what stands between a hostile HOME and code execution. +fallback_candidate_usable() { + local candidate="$1" + case "$candidate" in + /*) ;; + *) + echo "mosaic: refusing \$HOME candidate $candidate: relative path is untrusted without a passwd home" >&2 + return 1 + ;; + esac + if printf '%s' "$candidate" | grep -qE '(^|/)\\.\\.(/|$)'; then + echo "mosaic: refusing \$HOME candidate $candidate: parent-escape component" >&2 + return 1 + fi + if path_has_symlink_component "$candidate"; then + echo "mosaic: refusing \$HOME candidate $candidate: symlink component (untrusted without a passwd home)" >&2 + return 1 + fi + return 0 +} + # Print shipped candidates in contract order. Refusals are reported on stderr # so the typed failure names the cause. shipped_candidates() { @@ -80,9 +109,7 @@ shipped_candidates() { # passwd lookup failed: the only fallback is $HOME, descriptor-guarded. if [ -n "${HOME:-}" ]; then home_candidate="$HOME/.npm-global/bin/mosaic" - if path_has_symlink_component "$home_candidate"; then - echo "mosaic: refusing \$HOME candidate $home_candidate: symlink component (untrusted without a passwd home)" >&2 - else + if fallback_candidate_usable "$home_candidate"; then printf '%s\n' "$home_candidate" fi fi diff --git a/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh b/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh index 4b6e559e..8cf221f7 100755 --- a/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh +++ b/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh @@ -93,8 +93,10 @@ set -e [ "$rc" = "127" ] || fail "A5 typed failure rc: got $rc, want 127" case "$err" in *"no runnable CLI"*) ;; *) fail "A5 typed failure message missing: '$err'" ;; esac -# A6 — secure descriptor traversal: passwd lookup fails and a symlink-planted -# $HOME/.npm-global is refused without execution. +# A6 — secure descriptor traversal, ABSOLUTE symlink plant (corrected per +# rev-code-02 B3: the symlink points at $PLANT/.npm-global so the candidate +# resolves EXACTLY to the planted executable). passwd lookup fails and a +# symlink-planted $HOME/.npm-global is refused without execution. PLANT="$WORK/planted-target" mkdir -p "$PLANT/.npm-global/bin" cat >"$PLANT/.npm-global/bin/mosaic" <&1)" rc=$? @@ -112,4 +114,49 @@ set -e case "$err" in *"symlink component"*) ;; *) fail "A6 refusal diagnostic missing: '$err'" ;; esac [ ! -e "$WORK/planted-sentinel" ] || fail "A6 planted mosaic EXECUTED" +# A6b — mutation control (rev-code-02 B3): a copy of the launcher with the +# descriptor guard bypassed MUST execute the plant under the identical hostile +# arm. If the mutant stays clean, the plant path is wrong and A6 proves +# nothing. +MUTANT="$WORK/mutant-mosaic" +sed 's/if fallback_candidate_usable "\$home_candidate"; then/if true; then/' "$LAUNCHER" >"$MUTANT" +chmod +x "$MUTANT" +[ "$(grep -c 'if true; then' "$MUTANT")" -eq 1 ] || fail "A6b mutant not created (guard call not replaced)" +set +e +mout="$(printf '' | env GETENT_STUB=fail HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$MUTANT" --version 2>&1)" +mrc=$? +set -e +[ "$mrc" = "0" ] || fail "A6b mutant did not execute the plant (rc $mrc, out '$mout') - A6 proves nothing" +[ -e "$WORK/planted-sentinel" ] || fail "A6b mutant ran but sentinel absent - plant path wrong, A6 proves nothing" + +# A7 — relative-HOME hostile arm (rev-code-02 B2): a relative HOME whose name +# is a symlink in the launcher CWD must be refused outright, never resolved +# against the working directory. +CWD_SANDBOX="$WORK/cwd-sandbox" +REL_PLANT="$WORK/relative-plant" +mkdir -p "$CWD_SANDBOX" "$REL_PLANT/.npm-global/bin" +cat >"$REL_PLANT/.npm-global/bin/mosaic" <&1)" +rrc=$? +set -e +[ "$rrc" = "127" ] || fail "A7 relative HOME was followed (rc $rrc, out '$rout')" +case "$rout" in *"relative path"*) ;; *) fail "A7 relative-refusal diagnostic missing: '$rout'" ;; esac +[ ! -e "$WORK/relative-sentinel" ] || fail "A7 relative plant EXECUTED" + +# A7b — mutation control for the absolute-shape check: the same mutant (guard +# bypassed) MUST execute the relative plant under the identical arm. +set +e +rmout="$(cd "$CWD_SANDBOX" && printf '' | env GETENT_STUB=fail HOME="relative-home" PATH="$STUB_BIN:/usr/bin:/bin" "$MUTANT" --version 2>&1)" +rmrc=$? +set -e +[ "$rmrc" = "0" ] || fail "A7b mutant did not execute the relative plant (rc $rmrc, out '$rmout') - A7 proves nothing" +[ -e "$WORK/relative-sentinel" ] || fail "A7b mutant ran but relative sentinel absent - arm wrong, A7 proves nothing" + echo "mosaic launcher suite: all arms passed" diff --git a/packages/mosaic/framework/framework-manifest.txt b/packages/mosaic/framework/framework-manifest.txt index c7e36de8..4e6906cc 100644 --- a/packages/mosaic/framework/framework-manifest.txt +++ b/packages/mosaic/framework/framework-manifest.txt @@ -46,8 +46,12 @@ systemd/** templates/** tools/** # Fleet: only the framework-seeded fleet subtrees are framework-owned. +# fleet/bin is exact-entry on purpose (T110 B1): the estate's fleet/bin carries +# operator-owned executables this package does not ship; a subtree glob here +# would make keep-mode update prune them. fleet/README.md -fleet/bin/** +fleet/bin/mosaic +fleet/bin/test-mosaic-launcher.sh fleet/examples/** fleet/profiles/** fleet/roles/** diff --git a/packages/mosaic/src/framework/manifest-parity.spec.ts b/packages/mosaic/src/framework/manifest-parity.spec.ts index 5cf910e8..3cb19725 100644 --- a/packages/mosaic/src/framework/manifest-parity.spec.ts +++ b/packages/mosaic/src/framework/manifest-parity.spec.ts @@ -148,6 +148,26 @@ describe.skipIf(!hasBash)('bash ↔ TS manifest parity (§6.1)', () => { const manifest = loadManifest(FRAMEWORK_ROOT); expect(bashSubtreeRoots().sort()).toEqual(frameworkSubtreeRoots(manifest).sort()); }); + + it('fleet/bin ownership is exact and does not prune existing executables (T110 B1)', () => { + // fleet/bin carries estate executables this package does not ship. A + // subtree glob here would classify them framework-owned and keep-mode + // update would prune them. The manifest must own EXACTLY the two shipped + // launcher files and nothing else in fleet/bin, on BOTH resolvers. + const manifest = loadManifest(FRAMEWORK_ROOT); + const expected: Array<[string, string]> = [ + ['fleet/bin/mosaic', 'framework'], + ['fleet/bin/test-mosaic-launcher.sh', 'framework'], + ['fleet/bin/seat-up.sh', 'operator'], // shipped-by-estate, unshipped here + ['fleet/bin/launch-seat.sh', 'operator'], + ['fleet/bin', 'operator'], // the directory itself is unlisted + ]; + for (const [path, want] of expected) { + const ts = resolveOwnership(manifest, path); + expect(ts).toBe(want); + expect(bashResolve(path)).toBe(want); + } + }); }); /** -- 2.54.0 From 9154a006dea4fe61e4371e113de6e39c0bb03fca Mon Sep 17 00:00:00 2001 From: topher Date: Sat, 29 Aug 2026 22:42:21 -0500 Subject: [PATCH 3/3] fleet: fix parent-escape ERE double-escape + arm (T110 delta B2) The parent-escape rejection grep used a double-escaped ERE ('\\.\\.'), which matches a literal backslash followed by any character - never a normal '..' component - so an absolute HOME containing '/../' traversed to a planted tree and executed it (rev-code-02 delta probe: rc 0, no diagnostic, sentinel present). Corrected to the single-escaped ERE ('\.\.'), which matches a literal dot-dot component. Suite: A8 parent-escape hostile arm added (absolute HOME /../escape-target, executable planted at the resolved tree outside the seat home) - the real launcher must refuse with the parent-escape diagnostic and leave the sentinel absent, and a guard-bypassed mutant copy MUST execute the same plant under the identical arm (sentinel present), proving the check is what stands. The arm's HOME traversal was also corrected during authoring: the first draft double-prefixed the worktree path, which made the candidate nonexistent and would have let the mutant pass vacuously (the same wrong-resolved-path class B3 caught). Marker T110-DISPATCH-TOPHER-1N2O / ORCH-T110-B2FIX-X8Y9. --- packages/mosaic/framework/fleet/bin/mosaic | 2 +- .../fleet/bin/test-mosaic-launcher.sh | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/mosaic/framework/fleet/bin/mosaic b/packages/mosaic/framework/fleet/bin/mosaic index f1beace0..ce8362ee 100755 --- a/packages/mosaic/framework/fleet/bin/mosaic +++ b/packages/mosaic/framework/fleet/bin/mosaic @@ -86,7 +86,7 @@ fallback_candidate_usable() { return 1 ;; esac - if printf '%s' "$candidate" | grep -qE '(^|/)\\.\\.(/|$)'; then + if printf '%s' "$candidate" | grep -qE '(^|/)\.\.(/|$)'; then echo "mosaic: refusing \$HOME candidate $candidate: parent-escape component" >&2 return 1 fi diff --git a/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh b/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh index 8cf221f7..77832143 100755 --- a/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh +++ b/packages/mosaic/framework/fleet/bin/test-mosaic-launcher.sh @@ -159,4 +159,35 @@ set -e [ "$rmrc" = "0" ] || fail "A7b mutant did not execute the relative plant (rc $rmrc, out '$rmout') - A7 proves nothing" [ -e "$WORK/relative-sentinel" ] || fail "A7b mutant ran but relative sentinel absent - arm wrong, A7 proves nothing" +# A8 — parent-escape hostile arm (rev-code-02 delta, B2 remains): an absolute +# HOME containing a literal '..' component must be refused by the +# parent-escape check — the traversal would otherwise land on a planted tree +# OUTSIDE the seat home with no symlink involved. +ESC_BASE="$WORK/escape-base" +ESC_TARGET="$WORK/escape-target" +mkdir -p "$ESC_BASE" "$ESC_TARGET/.npm-global/bin" +cat >"$ESC_TARGET/.npm-global/bin/mosaic" <&1)" +erc=$? +set -e +[ "$erc" = "127" ] || fail "A8 parent-escape HOME was followed (rc $erc, out '$eout')" +case "$eout" in *"parent-escape component"*) ;; *) fail "A8 parent-escape diagnostic missing: '$eout'" ;; esac +[ ! -e "$WORK/escape-sentinel" ] || fail "A8 escape plant EXECUTED" + +# A8b — mutation control: the guard-bypassed copy MUST execute the parent- +# escape plant under the identical arm (sentinel present, rc 0), proving the +# parent-escape check is what stands. +set +e +emout="$(printf '' | env GETENT_STUB=fail HOME="$ESC_BASE/../escape-target" PATH="$STUB_BIN:/usr/bin:/bin" "$MUTANT" --version 2>&1)" +emrc=$? +set -e +[ "$emrc" = "0" ] || fail "A8b mutant did not execute the escape plant (rc $emrc, out '$emout') - A8 proves nothing" +[ -e "$WORK/escape-sentinel" ] || fail "A8b mutant ran but escape sentinel absent - arm wrong, A8 proves nothing" + echo "mosaic launcher suite: all arms passed" -- 2.54.0