fleet: split-home-safe mosaic launcher (T110, P5-RM-009 stack side) (#1480)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
This commit was merged in pull request #1480.
This commit is contained in:
Executable
+150
@@ -0,0 +1,150 @@
|
|||||||
|
#!/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. <real-home>/.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, 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
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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")"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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() {
|
||||||
|
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 fallback_candidate_usable "$home_candidate"; then
|
||||||
|
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)"
|
||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
#!/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" <<SH
|
||||||
|
#!/bin/sh
|
||||||
|
if [ "\${GETENT_STUB:-}" = "fail" ]; then exit 2; fi
|
||||||
|
if [ "\$1" = "passwd" ]; then
|
||||||
|
echo "stub:x:$(id -u):$(id -g):stub:$REAL_HOME:/bin/sh"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 2
|
||||||
|
SH
|
||||||
|
chmod +x "$STUB_BIN/getent"
|
||||||
|
|
||||||
|
run_launcher() { # run_launcher <home> [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, 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" <<SH
|
||||||
|
#!/bin/sh
|
||||||
|
touch "$WORK/planted-sentinel"
|
||||||
|
echo "0.0.0-planted"
|
||||||
|
SH
|
||||||
|
chmod +x "$PLANT/.npm-global/bin/mosaic"
|
||||||
|
ln -s "$PLANT/.npm-global" "$SEAT_HOME/.npm-global"
|
||||||
|
set +e
|
||||||
|
err="$(printf '' | env GETENT_STUB=fail HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&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"
|
||||||
|
|
||||||
|
# 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" <<SH
|
||||||
|
#!/bin/sh
|
||||||
|
touch "$WORK/relative-sentinel"
|
||||||
|
echo "0.0.0-relative-planted"
|
||||||
|
SH
|
||||||
|
chmod +x "$REL_PLANT/.npm-global/bin/mosaic"
|
||||||
|
ln -s "$REL_PLANT" "$CWD_SANDBOX/relative-home"
|
||||||
|
set +e
|
||||||
|
rout="$(cd "$CWD_SANDBOX" && printf '' | env GETENT_STUB=fail HOME="relative-home" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&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"
|
||||||
|
|
||||||
|
# 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" <<SH
|
||||||
|
#!/bin/sh
|
||||||
|
touch "$WORK/escape-sentinel"
|
||||||
|
echo "0.0.0-escape-planted"
|
||||||
|
SH
|
||||||
|
chmod +x "$ESC_TARGET/.npm-global/bin/mosaic"
|
||||||
|
set +e
|
||||||
|
eout="$(printf '' | env GETENT_STUB=fail HOME="$ESC_BASE/../escape-target" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&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"
|
||||||
@@ -46,7 +46,12 @@ systemd/**
|
|||||||
templates/**
|
templates/**
|
||||||
tools/**
|
tools/**
|
||||||
# Fleet: only the framework-seeded fleet subtrees are framework-owned.
|
# 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/README.md
|
||||||
|
fleet/bin/mosaic
|
||||||
|
fleet/bin/test-mosaic-launcher.sh
|
||||||
fleet/examples/**
|
fleet/examples/**
|
||||||
fleet/profiles/**
|
fleet/profiles/**
|
||||||
fleet/roles/**
|
fleet/roles/**
|
||||||
|
|||||||
@@ -148,6 +148,26 @@ describe.skipIf(!hasBash)('bash ↔ TS manifest parity (§6.1)', () => {
|
|||||||
const manifest = loadManifest(FRAMEWORK_ROOT);
|
const manifest = loadManifest(FRAMEWORK_ROOT);
|
||||||
expect(bashSubtreeRoots().sort()).toEqual(frameworkSubtreeRoots(manifest).sort());
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user