chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
AGENT_NAME=${MOSAIC_AGENT_NAME:-}
RUNTIME=${MOSAIC_AGENT_RUNTIME:-}
MODEL=${MOSAIC_AGENT_MODEL:-}
REASONING=${MOSAIC_AGENT_REASONING:-}
TOOL_POLICY=${MOSAIC_AGENT_TOOL_POLICY:-}
[ -n "$AGENT_NAME" ] || { echo 'ERROR: MOSAIC_AGENT_NAME is required' >&2; exit 64; }
[[ "$AGENT_NAME" =~ ^[A-Za-z0-9_.-]+$ ]] || { echo 'ERROR: invalid agent name' >&2; exit 64; }
[ "$RUNTIME" = 'pi' ] || { echo 'ERROR: invalid runtime policy' >&2; exit 64; }
[ "$MODEL" = 'openai/gpt-5.6-sol' ] || { echo 'ERROR: invalid model policy' >&2; exit 64; }
[ "$REASONING" = 'high' ] || { echo 'ERROR: invalid reasoning policy' >&2; exit 64; }
[ "$TOOL_POLICY" = 'operator-interaction' ] || { echo 'ERROR: invalid tool policy' >&2; exit 64; }
printf '{"agentName":"%s","runtime":"%s","model":"%s","reasoning":"%s","toolPolicy":"%s"}\n' \
"$AGENT_NAME" "$RUNTIME" "$MODEL" "$REASONING" "$TOOL_POLICY"
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env bash
# seat-logins.sh — project seat credentials into tea's login config.
#
# Issue: mosaicstack/stack#1356 (tea login resolution fails open).
#
# WHY THIS EXISTS. tea 0.14.0 has no --token on its operations; it can only use a
# login already stored in ~/.config/tea/config.yml. So the wrappers cannot read the
# seat secrets dir on the tea path. The secrets dir stays authoritative and this
# script projects it into tea's config, which is a DERIVED CACHE: regenerate it,
# never hand-edit it. Same shape as the config-registry projector, same reason —
# a third-party tool that cannot read our store has to be fed.
#
# Canonical login name is "<instance>-<seat>", which is what the identity ladder in
# detect-platform.sh computes from the seat name. A login the ladder cannot compute
# is a fail-open surface, so an ad-hoc name is a defect, not a style.
#
# COLLISIONS. tea refuses to store one token under two names ("token already been
# used, delete login 'X' first"). A hand-made alias holding a seat's token there-
# fore BLOCKS its canonical name. Detected up front by hashing, so a dry run shows
# it; --adopt resolves it by deleting the alias and re-minting canonically. Same
# token, same access, only the label changes.
#
# Tokens are never printed, never logged, and never passed on a visible command
# line beyond tea's own --token, which is unavoidable with this client. tea's
# stderr is echoed on failure with any token-shaped string redacted.
#
# Usage:
# seat-logins.sh # dry run, all seats (default: changes nothing)
# seat-logins.sh --apply # mint/refresh all seats
# seat-logins.sh --seat <seat> # limit to one seat
# seat-logins.sh --apply --adopt # also rename ad-hoc aliases to canonical names
set -euo pipefail
BRAIN_HOME="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
TEA_CONFIG="${TEA_CONFIG:-$HOME/.config/tea/config.yml}"
APPLY=0
ADOPT=0
ONLY_SEAT=""
# Instance -> server URL.
#
# Instances are named here because there is no registry to read them from yet.
# Override per-instance without editing this file, which is how a deployment adds
# its own hosts: MOSAIC_GITEA_URL_<INSTANCE>=https://...
declare -A INSTANCE_URL=(
[mosaicstack]="https://git.mosaicstack.dev"
[usc]="https://git.uscllc.com"
)
while [ $# -gt 0 ]; do
case "$1" in
--apply) APPLY=1; shift ;;
--adopt) ADOPT=1; shift ;;
--seat) ONLY_SEAT="${2:?--seat needs a name}"; shift 2 ;;
-h|--help) sed -n '2,33p' "$0"; exit 0 ;;
*) echo "seat-logins.sh: unknown argument '$1'" >&2; exit 2 ;;
esac
done
command -v tea >/dev/null || { echo "seat-logins.sh: tea not on PATH" >&2; exit 1; }
url_for() {
local inst="$1" ovr
ovr="MOSAIC_GITEA_URL_$(printf '%s' "$inst" | tr '[:lower:]-' '[:upper:]_')"
if [ -n "${!ovr:-}" ]; then printf '%s' "${!ovr}"; return 0; fi
printf '%s' "${INSTANCE_URL[$inst]:-}"
}
# Redact anything token-shaped before any tea output reaches a log.
redact() { sed -E 's/[A-Za-z0-9]{30,}/<REDACTED>/g'; }
# token sha256 -> login name, for every login tea already holds. This is what
# makes collisions visible in a DRY RUN instead of only as an apply-time error.
declare -A TOKEN_OWNER=()
if [ -r "$TEA_CONFIG" ]; then
while read -r sha lname; do
[ -n "${sha:-}" ] && TOKEN_OWNER["$sha"]="$lname"
done < <(python3 - "$TEA_CONFIG" <<'PY'
import sys, yaml, hashlib
try:
cfg = yaml.safe_load(open(sys.argv[1])) or {}
except Exception:
sys.exit(0)
for l in (cfg.get('logins') or []):
t = l.get('token')
if t:
print(hashlib.sha256(t.encode()).hexdigest(), l.get('name'))
PY
)
fi
minted=0; refreshed=0; skipped=0; failed=0; planned=0; adopted=0; blocked=0
existing="$(tea login list --output simple 2>/dev/null | awk '{print $1}' || true)"
shopt -s nullglob
for tokfile in "$BRAIN_HOME"/fleet/agents/*/secrets/gitea-*.token; do
seat="${tokfile#"$BRAIN_HOME"/fleet/agents/}"; seat="${seat%%/*}"
[ -n "$ONLY_SEAT" ] && [ "$seat" != "$ONLY_SEAT" ] && continue
base="$(basename "$tokfile" .token)" # gitea-<instance>-<seat>
inst="${base#gitea-}"; inst="${inst%-"$seat"}"
name="${inst}-${seat}"
url="$(url_for "$inst")"
if [ -z "$url" ]; then
echo " SKIP $name — no URL known for instance '$inst' (set MOSAIC_GITEA_URL_${inst^^})"
skipped=$((skipped+1)); continue
fi
if [ ! -r "$tokfile" ]; then
echo " SKIP $name — token not readable"
skipped=$((skipped+1)); continue
fi
action="mint"
grep -qx "$name" <<<"$existing" && action="refresh"
# Is this exact token already stored under some OTHER name?
tsha="$(sha256sum < "$tokfile" | awk '{print $1}')"
owner="${TOKEN_OWNER[$tsha]:-}"
collision=""
[ -n "$owner" ] && [ "$owner" != "$name" ] && collision="$owner"
if [ "$APPLY" -eq 0 ]; then
if [ -n "$collision" ]; then
if [ "$ADOPT" -eq 1 ]; then
echo " PLAN adopt $collision -> $name ($url)"
else
echo " BLOCK $name — token already stored as '$collision'; re-run with --adopt"
blocked=$((blocked+1)); continue
fi
else
echo " PLAN $action $name -> $url"
fi
planned=$((planned+1)); continue
fi
if [ -n "$collision" ]; then
if [ "$ADOPT" -eq 0 ]; then
echo " BLOCK $name — token already stored as '$collision'; re-run with --adopt"
blocked=$((blocked+1)); continue
fi
tea login delete "$collision" >/dev/null 2>&1 || true
action="adopt"
fi
# tea has no idempotent add; refresh is delete-then-add so a rotated token lands.
[ "$action" = refresh ] && tea login delete "$name" >/dev/null 2>&1 || true
if err="$(tea login add --name "$name" --url "$url" \
--token "$(cat "$tokfile")" --no-version-check 2>&1 >/dev/null)"; then
case "$action" in
mint) minted=$((minted+1)) ;;
refresh) refreshed=$((refreshed+1)) ;;
adopt) adopted=$((adopted+1)) ;;
esac
if [ "$action" = adopt ]; then
echo " OK adopt $collision -> $name ($url)"
else
echo " OK $action $name -> $url"
fi
else
# A failure here is real information: the seat's token is dead, or the server
# refused it. Do not paper over it; the seat cannot act until it is reminted.
# tea's own words, redacted — a summarised FAIL hides whether the cause is the
# credential or the client, which cost a diagnosis on 2026-08-21.
echo " FAIL $action $name -> $url"
echo " tea: $(printf '%s' "$err" | redact | head -1)"
failed=$((failed+1))
fi
done
echo
if [ "$APPLY" -eq 0 ]; then
echo "dry run: $planned login(s) would be written, $skipped skipped, $blocked blocked."
[ "$blocked" -gt 0 ] && echo "re-run with --adopt to rename ad-hoc aliases to canonical names."
echo "no changes made. re-run with --apply."
else
echo "minted=$minted adopted=$adopted refreshed=$refreshed skipped=$skipped blocked=$blocked failed=$failed"
fi
[ "$failed" -eq 0 ] && [ "$blocked" -eq 0 ]
@@ -0,0 +1,519 @@
#!/usr/bin/env bash
set -euo pipefail
# FCM-M2-001 boundary: only a roster-derived .env.generated projection and a
# separately parsed data-only .env.local can influence launch. Never source an
# environment file and never accept a command string from either file.
MODE=launch
case "${1:-}" in
--stop)
MODE=stop
AGENT_NAME=${2:-}
;;
--interaction)
MODE=interaction
AGENT_NAME=${2:-}
;;
*) AGENT_NAME=${1:-${MOSAIC_AGENT_NAME:-}} ;;
esac
MOSAIC_HOME=${MOSAIC_HOME:-$HOME/.config/mosaic}
fail() {
echo "ERROR: $*" >&2
exit 64
}
hash_value() {
printf '%s' "$1" | sha256sum | awk '{print $1}'
}
fail_env() {
local code="$1"
local key="$2"
local value="$3"
echo "ERROR: agent environment rejected: code=${code} key=${key} sha256=$(hash_value "$value")" >&2
exit 64
}
safe_agent_name() {
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]
}
safe_policy_name() {
[[ "$1" =~ ^[a-z][a-z0-9-]*$ ]]
}
safe_path() {
[[ "$1" == /* ]] || return 1
[[ "$1" != *".."* ]] || return 1
[[ ! "$1" =~ [[:space:]\"\'\`\$\\\;\|\&\<\>\(\)\{\}] ]]
}
assert_private_regular_file() {
local file="$1"
[ -f "$file" ] && [ ! -L "$file" ] || fail_env unsafe-file '(file)' "$file"
local mode
mode=$(stat -c '%a' -- "$file") || fail_env unsafe-file '(file)' "$file"
(( (8#$mode & 8#077) == 0 )) || fail_env unsafe-permissions '(file)' "$file"
}
assert_managed_directory() {
local directory="$1"
[ -d "$directory" ] && [ ! -L "$directory" ] || fail_env unsafe-directory '(directory)' "$directory"
local mode
mode=$(stat -c '%a' -- "$directory") || fail_env unsafe-directory '(directory)' "$directory"
(( (8#$mode & 8#022) == 0 )) || fail_env unsafe-permissions '(directory)' "$directory"
}
assert_private_directory() {
local directory="$1"
assert_managed_directory "$directory"
local mode
mode=$(stat -c '%a' -- "$directory") || fail_env unsafe-directory '(directory)' "$directory"
(( (8#$mode & 8#077) == 0 )) || fail_env unsafe-permissions '(directory)' "$directory"
}
[ -n "$AGENT_NAME" ] || fail "agent name argument or MOSAIC_AGENT_NAME is required"
safe_agent_name "$AGENT_NAME" || fail_env unsafe-agent-name MOSAIC_AGENT_NAME "$AGENT_NAME"
safe_path "$MOSAIC_HOME" || fail_env unsafe-path MOSAIC_HOME "$MOSAIC_HOME"
FLEET_DIR="$MOSAIC_HOME/fleet"
AGENT_ENV_DIR="$FLEET_DIR/agents"
# Brain-home split (canon docs/STRUCTURE-CANON.md §2): seat launch envs live
# under the brain home's fleet/agents when a brain is active; roster, roles
# baseline, and runtime state (fleet/run) stay under MOSAIC_HOME.
# Resolution mirrors packages/mosaic/src/fleet/brain-home.ts:
# 1. MOSAIC_BRAIN_HOME env (explicit, always wins)
# 2. ~/.mosaic — adopted only when MOSAIC_HOME is the default config home AND
# ~/.mosaic/fleet/agents exists
# 3. MOSAIC_HOME (legacy single-tree)
BRAIN_HOME="${MOSAIC_BRAIN_HOME:-}"
if [ -z "$BRAIN_HOME" ]; then
BRAIN_HOME="$MOSAIC_HOME"
if [ "$(cd "$MOSAIC_HOME" 2>/dev/null && pwd -P)" = "$HOME/.config/mosaic" ] \
&& [ -d "$HOME/.mosaic/fleet/agents" ]; then
BRAIN_HOME="$HOME/.mosaic"
fi
fi
if [ "$BRAIN_HOME" != "$MOSAIC_HOME" ]; then
AGENT_ENV_DIR="$BRAIN_HOME/fleet/agents"
fi
assert_managed_directory "$MOSAIC_HOME"
assert_managed_directory "$FLEET_DIR"
assert_private_directory "$AGENT_ENV_DIR"
GENERATED_ENV="$AGENT_ENV_DIR/$AGENT_NAME.env.generated"
LOCAL_ENV="$AGENT_ENV_DIR/$AGENT_NAME.env.local"
declare -A GENERATED_VALUES=()
declare -A LOCAL_VALUES=()
declare -A SEEN_KEYS=()
is_sensitive_key() {
[[ "$1" =~ (API[_-]?KEY|AUTH|CREDENTIAL|PASSWORD|PRIVATE|SECRET|TOKEN) ]]
}
is_generated_key() {
case "$1" in
MOSAIC_AGENT_NAME|MOSAIC_GIT_IDENTITY|MOSAIC_AGENT_CLASS|MOSAIC_AGENT_RUNTIME|MOSAIC_AGENT_MODEL|MOSAIC_AGENT_REASONING|MOSAIC_AGENT_TOOL_POLICY|MOSAIC_AGENT_WORKDIR|MOSAIC_TMUX_SOCKET) return 0 ;;
*) return 1 ;;
esac
}
is_local_key() {
case "$1" in
MOSAIC_RUNTIME_BIN|MOSAIC_HEARTBEAT_RUN_DIR|MOSAIC_HEARTBEAT_INTERVAL|MOSAIC_CLAUDE_JSON|CLAUDE_CONFIG_DIR) return 0 ;;
*) return 1 ;;
esac
}
validate_generated_value() {
local key="$1"
local value="$2"
case "$key" in
MOSAIC_AGENT_NAME) safe_agent_name "$value" || fail_env unsafe-agent-name "$key" "$value" ;;
MOSAIC_GIT_IDENTITY) safe_agent_name "$value" || fail_env unsafe-git-identity "$key" "$value" ;;
MOSAIC_AGENT_CLASS) safe_policy_name "$value" || fail_env unsafe-class "$key" "$value" ;;
MOSAIC_AGENT_RUNTIME)
case "$value" in claude|codex|opencode|pi) ;; *) fail_env unsupported-runtime "$key" "$value" ;; esac
;;
MOSAIC_AGENT_MODEL) [[ "$value" =~ ^[A-Za-z0-9._/:+-]*$ ]] || fail_env unsafe-model "$key" "$value" ;;
MOSAIC_AGENT_REASONING)
case "$value" in ''|low|medium|high) ;; *) fail_env unsupported-reasoning "$key" "$value" ;; esac
;;
MOSAIC_AGENT_TOOL_POLICY) [ -z "$value" ] || safe_policy_name "$value" || fail_env unsafe-tool-policy "$key" "$value" ;;
MOSAIC_AGENT_WORKDIR) safe_path "$value" || fail_env unsafe-path "$key" "$value" ;;
MOSAIC_TMUX_SOCKET) [[ "$value" =~ ^[A-Za-z0-9_.-]*$ ]] || fail_env unsafe-socket "$key" "$value" ;;
esac
}
validate_local_value() {
local key="$1"
local value="$2"
if [ "$key" = MOSAIC_HEARTBEAT_INTERVAL ]; then
[[ "$value" =~ ^[1-9][0-9]*$ ]] || fail_env invalid-interval "$key" "$value"
else
safe_path "$value" || fail_env unsafe-path "$key" "$value"
fi
}
load_environment_file() {
local file="$1"
local kind="$2"
[ -e "$file" ] || {
[ "$kind" = generated ] && fail_env missing-file '(generated)' "$file"
return 0
}
assert_private_regular_file "$file"
SEEN_KEYS=()
local line key value
while IFS= read -r line || [ -n "$line" ]; do
[ -z "$line" ] && continue
if [[ ! "$line" =~ ^([A-Z][A-Z0-9_]*)=(.*)$ ]]; then
fail_env malformed-line '(malformed)' "$line"
fi
key=${BASH_REMATCH[1]}
value=${BASH_REMATCH[2]}
[ -z "${SEEN_KEYS[$key]+set}" ] || fail_env duplicate-key "$key" "$value"
SEEN_KEYS[$key]=1
is_sensitive_key "$key" && fail_env sensitive-key "$key" "$value"
if [ "$kind" = generated ]; then
is_generated_key "$key" || fail_env unknown-key "$key" "$value"
validate_generated_value "$key" "$value"
GENERATED_VALUES[$key]=$value
else
is_generated_key "$key" && fail_env generated-key-shadow "$key" "$value"
is_local_key "$key" || fail_env unknown-key "$key" "$value"
validate_local_value "$key" "$value"
LOCAL_VALUES[$key]=$value
fi
done < "$file"
}
load_environment_file "$GENERATED_ENV" generated
for required_key in \
MOSAIC_AGENT_NAME MOSAIC_GIT_IDENTITY MOSAIC_AGENT_CLASS MOSAIC_AGENT_RUNTIME MOSAIC_AGENT_MODEL \
MOSAIC_AGENT_REASONING MOSAIC_AGENT_TOOL_POLICY MOSAIC_AGENT_WORKDIR MOSAIC_TMUX_SOCKET; do
[ -n "${GENERATED_VALUES[$required_key]+set}" ] || fail_env missing-key "$required_key" ''
done
load_environment_file "$LOCAL_ENV" local
[ "${GENERATED_VALUES[MOSAIC_AGENT_NAME]}" = "$AGENT_NAME" ] || \
fail_env agent-name-mismatch MOSAIC_AGENT_NAME "${GENERATED_VALUES[MOSAIC_AGENT_NAME]}"
[ "${GENERATED_VALUES[MOSAIC_GIT_IDENTITY]}" = "$AGENT_NAME" ] || \
fail_env git-identity-mismatch MOSAIC_GIT_IDENTITY "${GENERATED_VALUES[MOSAIC_GIT_IDENTITY]}"
MOSAIC_TMUX_SOCKET=${GENERATED_VALUES[MOSAIC_TMUX_SOCKET]}
MOSAIC_AGENT_RUNTIME=${GENERATED_VALUES[MOSAIC_AGENT_RUNTIME]}
MOSAIC_AGENT_MODEL=${GENERATED_VALUES[MOSAIC_AGENT_MODEL]}
MOSAIC_AGENT_REASONING=${GENERATED_VALUES[MOSAIC_AGENT_REASONING]}
MOSAIC_AGENT_WORKDIR=${GENERATED_VALUES[MOSAIC_AGENT_WORKDIR]}
MOSAIC_GIT_IDENTITY=${GENERATED_VALUES[MOSAIC_GIT_IDENTITY]}
MOSAIC_AGENT_CLASS=${GENERATED_VALUES[MOSAIC_AGENT_CLASS]}
MOSAIC_AGENT_TOOL_POLICY=${GENERATED_VALUES[MOSAIC_AGENT_TOOL_POLICY]}
MOSAIC_RUNTIME_BIN=${LOCAL_VALUES[MOSAIC_RUNTIME_BIN]:-}
MOSAIC_HEARTBEAT_RUN_DIR=${LOCAL_VALUES[MOSAIC_HEARTBEAT_RUN_DIR]:-$MOSAIC_HOME/fleet/run}
MOSAIC_HEARTBEAT_INTERVAL=${LOCAL_VALUES[MOSAIC_HEARTBEAT_INTERVAL]:-15}
MOSAIC_CLAUDE_JSON=${LOCAL_VALUES[MOSAIC_CLAUDE_JSON]:-}
CLAUDE_CONFIG_DIR=${LOCAL_VALUES[CLAUDE_CONFIG_DIR]:-}
if ! command -v tmux >/dev/null 2>&1; then
echo "ERROR: tmux is required" >&2
exit 69
fi
_tmux() {
if [ -n "$MOSAIC_TMUX_SOCKET" ]; then
tmux -L "$MOSAIC_TMUX_SOCKET" "$@"
else
tmux "$@"
fi
}
assert_owned_tmux_server() {
local owner_file="$MOSAIC_HOME/fleet/run/holder-owner"
[ -f "$owner_file" ] && [ ! -L "$owner_file" ] || fail "private tmux ownership identity is missing"
local owner_mode
owner_mode=$(stat -c '%a' -- "$owner_file") || fail "private tmux ownership identity is unreadable"
(( (8#$owner_mode & 8#077) == 0 )) || fail "private tmux ownership identity has unsafe permissions"
local owner
owner=$(tr -d '\n' < "$owner_file")
[[ "$owner" =~ ^[a-f0-9-]{36}$ ]] || fail "private tmux ownership identity is malformed"
_tmux has-session -t '=_holder:0.0' 2>/dev/null || fail "owned tmux holder session is absent"
local environment expected
environment=$(_tmux show-environment -g 2>/dev/null) || fail "owned tmux global environment is unreadable"
expected=$(printf '%s\n' \
"HOME=$HOME" \
'PATH=/usr/bin:/bin' \
"PWD=$HOME" \
"MOSAIC_FLEET_OWNER=$owner" \
'MOSAIC_TMUX_HOLDER=_holder' \
"MOSAIC_TMUX_SOCKET=$MOSAIC_TMUX_SOCKET" | sort)
[ "$(printf '%s\n' "$environment" | sort)" = "$expected" ] || \
fail "tmux server ownership or environment validation failed"
}
# Lease-broker socket preflight (#1292). The gated runtime (`mosaic yolo …` →
# launch-runtime.py) registers with the broker or dies ~4 seconds in, with the
# diagnostic invisible because tmux destroys the dead pane. This check runs
# BEFORE any tmux effect — including the ownership probe below — so a host
# without a broker produces a named, surviving refusal instead of a doomed
# pane. Exit 75 (EX_TEMPFAIL), distinct from 64 (bad projection) and 69 (host
# not ready for other reasons); the agent@ unit is Type=oneshot with no
# Restart=, so the failed unit keeps its message instead of looping. Socket
# resolution matches launch.ts's defaultLeaseBrokerSocket precedence exactly.
# This preflight DETECTS and REFUSES — it never starts the broker (activation
# belongs to the fleet control plane; a component that both detects and fixes
# cannot be used to measure whether the fix worked).
broker_socket_path() {
if [ -n "${MOSAIC_LEASE_BROKER_SOCKET:-}" ]; then
printf '%s\n' "$MOSAIC_LEASE_BROKER_SOCKET"
return 0
fi
local runtime_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
printf '%s\n' "${runtime_dir}/mosaic-lease/broker.sock"
}
if [ "$MODE" = "launch" ]; then
_broker_socket=$(broker_socket_path)
if [ ! -S "$_broker_socket" ]; then
echo "[fleet] FAIL_LAUNCH broker-absent: lease broker socket ${_broker_socket} missing; runtime launch denied (#1292)." >&2
echo "[fleet] remedy: systemctl --user enable --now mosaic-lease-broker.service (or reinstall via: mosaic fleet install)" >&2
exit 75
fi
fi
assert_owned_tmux_server
if [ "$MODE" = interaction ]; then
[ "$MOSAIC_AGENT_RUNTIME" = pi ] || fail "operator interaction service requires runtime pi"
[ "$MOSAIC_AGENT_MODEL" = openai/gpt-5.6-sol ] || \
fail "operator interaction service requires the pinned model"
[ "$MOSAIC_AGENT_REASONING" = high ] || \
fail "operator interaction service requires high reasoning"
[ "$MOSAIC_AGENT_TOOL_POLICY" = operator-interaction ] || \
fail "operator interaction service requires the operator-interaction tool policy"
fi
if [ "$MODE" = stop ]; then
_tmux kill-session -t "=${AGENT_NAME}" >/dev/null 2>&1 || true
exit 0
fi
# #1408 hazard: a seat still living on the DEFAULT tmux socket is invisible to the
# declared-socket guard below, and launching over it creates a same-name duplicate that
# name-addressed comms delivery cannot tell apart. Refuse with a distinct code (76,
# after 75 broker-absent) so a cutover wave script can branch on "seat still on legacy
# socket" vs "already running" (0) vs "broker absent" (75). Stopping the legacy session
# belongs to the cutover procedure, never to this launcher.
if [ -n "$MOSAIC_TMUX_SOCKET" ] && tmux has-session -t "=${AGENT_NAME}" 2>/dev/null; then
echo "[fleet] FAIL_LAUNCH seat-on-legacy-socket: session '${AGENT_NAME}' exists on the DEFAULT tmux socket; stop it before launching on '${MOSAIC_TMUX_SOCKET}'." >&2
exit 76
fi
if _tmux has-session -t "=${AGENT_NAME}:0.0" 2>/dev/null; then
echo "Mosaic agent session already running: $AGENT_NAME on socket ${MOSAIC_TMUX_SOCKET:-(default)}"
exit 0
fi
# Systemd passes HOME as %h, and the installed service fixes MOSAIC_HOME under
# that home. Derive the pane home from the canonical path when available so an
# inherited pane/session HOME cannot become runtime authority.
PANE_HOME=$HOME
case "$MOSAIC_HOME" in
*/.config/mosaic) PANE_HOME=${MOSAIC_HOME%/.config/mosaic} ;;
esac
_build_runtime_bin_prefix() {
local candidates=()
if [ -n "$MOSAIC_RUNTIME_BIN" ]; then candidates+=("$MOSAIC_RUNTIME_BIN"); fi
# A host with no system Node gets one bootstrapped here by tools/install.sh, which
# records it in ~/.profile. The fleet unit runs `env -i ... bash --noprofile --norc`
# by design, so ~/.profile is never read and the directory has to be named here.
# The npm probe below cannot cover this: it reports a package prefix
# (~/.npm-global), never a Node runtime directory. It sits ahead of the npm probe so
# the bootstrapped runtime wins on a host that has both — that is the one the installer
# verified — while an explicit MOSAIC_RUNTIME_BIN still outranks it.
# Runtime binaries are `#!/usr/bin/env node`, so without this the pane resolves the
# binary and then dies on `env: 'node': No such file or directory`.
candidates+=("$PANE_HOME/.mosaic/node/current/bin")
if command -v npm >/dev/null 2>&1; then
local npm_prefix
npm_prefix=$(npm config get prefix 2>/dev/null) || true
if [ -n "$npm_prefix" ]; then candidates+=("${npm_prefix}/bin"); fi
fi
candidates+=("$PANE_HOME/.npm-global/bin" "$PANE_HOME/.local/bin")
local prefix="" dir
for dir in "${candidates[@]}"; do
[ -d "$dir" ] || continue
case ":${prefix}:" in *":${dir}:"*) ;; *) prefix="${prefix:+$prefix:}$dir" ;; esac
done
printf '%s' "$prefix"
}
MOSAIC_RUNTIME_BIN_PREFIX=$(_build_runtime_bin_prefix)
PANE_PATH=${MOSAIC_RUNTIME_BIN_PREFIX:+${MOSAIC_RUNTIME_BIN_PREFIX}:}/usr/local/bin:/usr/bin:/bin
# #1241. The pane runs `mosaic yolo <runtime>` under PANE_PATH with a cleared
# environment. A binary missing from *that* path is a pane that dies in under a
# second, inside a session nobody is attached to, with its diagnostic scrolled
# into a pane tmux then destroys. Resolve both here, before any effect, where
# the failure is still attributable to the thing that caused it.
#
# `mosaic yolo <runtime>` runs checkRuntime(runtime) and the binary it looks for
# is named exactly like the runtime, so resolving the runtime name is the same
# question the pane will ask a moment later — asked while an operator can still
# see the answer.
_resolve_in_pane_path() {
PATH="$PANE_PATH" command -v -- "$1" 2>/dev/null
}
# Exit 69 (EX_UNAVAILABLE): the seat cannot be provided. Distinguished from the
# 64 (EX_USAGE) rejections above, which mean the projection itself was bad —
# here the data is fine and the host is not ready. Callers tell the individual
# cases apart by `code=`, the same way fail_env's many codes share exit 64.
fail_launch() {
local code="$1"
shift
echo "ERROR: agent launch aborted: code=${code} agent=${AGENT_NAME} $*" >&2
exit 69
}
for required_binary in mosaic "$MOSAIC_AGENT_RUNTIME"; do
_resolve_in_pane_path "$required_binary" >/dev/null ||
fail_launch missing-binary "'${required_binary}' is not on the pane PATH (${PANE_PATH})"
done
_ensure_claude_workdir_trusted() {
local workdir="$1"
local resolved
resolved=$(cd "$workdir" 2>/dev/null && pwd -P) || resolved="$workdir"
local claude_json="${MOSAIC_CLAUDE_JSON:-${CLAUDE_CONFIG_DIR:+$CLAUDE_CONFIG_DIR/.claude.json}}"
claude_json="${claude_json:-$HOME/.claude.json}"
command -v python3 >/dev/null 2>&1 || return 1
MOSAIC_CJ="$claude_json" MOSAIC_TRUST_DIR="$resolved" python3 - <<'PY'
import json, os, sys, tempfile
cj = os.environ["MOSAIC_CJ"]
d = os.environ["MOSAIC_TRUST_DIR"]
try:
data = json.load(open(cj)) if os.path.exists(cj) else {}
if not isinstance(data, dict):
data = {}
except Exception:
sys.exit(2)
projects = data.setdefault("projects", {})
entry = projects.get(d)
if not isinstance(entry, dict):
entry = {}
projects[d] = entry
entry["hasTrustDialogAccepted"] = True
tmp_dir = os.path.dirname(cj) or "."
fd, tmp = tempfile.mkstemp(dir=tmp_dir, prefix=".claude.json.mosaic.")
try:
with os.fdopen(fd, "w") as f:
json.dump(data, f, indent=2)
os.replace(tmp, cj)
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
sys.exit(3)
PY
}
if [ "$MOSAIC_AGENT_RUNTIME" = claude ]; then
_ensure_claude_workdir_trusted "$MOSAIC_AGENT_WORKDIR" || \
echo "WARNING: could not pre-trust workdir for claude agent $AGENT_NAME" >&2
fi
# #1408 hazard: prefer the seat's own launch.sh when the brain provides one. It is the
# path that binds the auth profile (CLAUDE_SECURESTORAGE_CONFIG_DIR) and seeds the seat
# config; `mosaic yolo` relocates CLAUDE_CONFIG_DIR to the seat dir (launch.ts
# activeSeatDir/harnessEnv) but performs neither, so a yolo-launched seat points its
# config at a directory holding no credentials. The env -i allowlist below still
# applies: launch.sh reads its own launch.env.
SEAT_LAUNCH="${BRAIN_HOME}/fleet/agents/${AGENT_NAME}/launch.sh"
if [ -x "$SEAT_LAUNCH" ]; then
LAUNCH_COMMAND=("$SEAT_LAUNCH")
echo "[fleet] launch path: seat launch.sh ($SEAT_LAUNCH)"
else
LAUNCH_COMMAND=(mosaic yolo "$MOSAIC_AGENT_RUNTIME")
if [ -n "$MOSAIC_AGENT_MODEL" ]; then LAUNCH_COMMAND+=(--model "$MOSAIC_AGENT_MODEL"); fi
if [ -n "$MOSAIC_AGENT_REASONING" ]; then LAUNCH_COMMAND+=(--thinking "$MOSAIC_AGENT_REASONING"); fi
echo "[fleet] launch path: mosaic yolo (no executable seat launch.sh)"
fi
# The tmux holder owns a named server. Explicitly clear the pane environment
# so server/session variables cannot cross the launch boundary; retain only
# trusted bootstrap, generated, and approved local data as argv assignments.
LAUNCH_ENV=(
/usr/bin/env
-i
"HOME=$PANE_HOME"
"PATH=$PANE_PATH"
"MOSAIC_HOME=$MOSAIC_HOME"
"MOSAIC_AGENT_NAME=$AGENT_NAME"
"MOSAIC_GIT_IDENTITY=$MOSAIC_GIT_IDENTITY"
"MOSAIC_AGENT_CLASS=$MOSAIC_AGENT_CLASS"
"MOSAIC_AGENT_RUNTIME=$MOSAIC_AGENT_RUNTIME"
"MOSAIC_AGENT_MODEL=$MOSAIC_AGENT_MODEL"
"MOSAIC_AGENT_REASONING=$MOSAIC_AGENT_REASONING"
"MOSAIC_AGENT_TOOL_POLICY=$MOSAIC_AGENT_TOOL_POLICY"
"MOSAIC_AGENT_WORKDIR=$MOSAIC_AGENT_WORKDIR"
"MOSAIC_TMUX_SOCKET=$MOSAIC_TMUX_SOCKET"
"MOSAIC_HEARTBEAT_RUN_DIR=$MOSAIC_HEARTBEAT_RUN_DIR"
)
mkdir -p "$MOSAIC_AGENT_WORKDIR"
_tmux new-session -d -s "$AGENT_NAME" -c "$MOSAIC_AGENT_WORKDIR" \
"${LAUNCH_ENV[@]}" "${LAUNCH_COMMAND[@]}"
PANE_PID=""
for _retry in 1 2 3 4 5; do
PANE_PID=$(_tmux list-panes -t "=${AGENT_NAME}:0.0" -F '#{pane_pid}' 2>/dev/null || true)
[ -n "$PANE_PID" ] && break
sleep 0.2
done
_start_heartbeat_sidecar() {
local agent="$1" pane_pid="$2" run_dir="$3" interval="$4"
local hb_file="${run_dir}/${agent}.hb"
mkdir -p "$run_dir"
local sidecar_script
sidecar_script=$(printf \
'hb=%q; pid=%q; iv=%q; native="$hb.native"; mkdir -p "$(dirname "$hb")"; while kill -0 "$pid" 2>/dev/null; do now=$(date +%%s); marker=$(stat -c %%Y -- "$native" 2>/dev/null || true); if [ -z "$marker" ] || [ -L "$native" ] || (( now - marker > iv * 2 + 1 )); then tmp="$hb.tmp.$$"; printf "ts=%%s\npid=%%s\nstatus=ok\n" "$(date +%%Y-%%m-%%dT%%H:%%M:%%S%%z)" "$pid" > "$tmp" && mv "$tmp" "$hb"; fi; sleep "$iv"; done' \
"$hb_file" "$pane_pid" "$interval")
if command -v setsid >/dev/null 2>&1; then
setsid bash -c "$sidecar_script" </dev/null >/dev/null 2>&1 &
else
bash -c "$sidecar_script" </dev/null >/dev/null 2>&1 &
fi
disown $! 2>/dev/null || true
}
if [ -n "$PANE_PID" ]; then
_start_heartbeat_sidecar "$AGENT_NAME" "$PANE_PID" \
"$MOSAIC_HEARTBEAT_RUN_DIR" "$MOSAIC_HEARTBEAT_INTERVAL" || \
echo "WARNING: heartbeat sidecar could not be started for $AGENT_NAME" >&2
elif _tmux has-session -t "=${AGENT_NAME}:0.0" 2>/dev/null; then
# #1241. Session present, no pane PID after a second of retries. Whatever this
# is, it is not a seat an operator can use, so it is not a success either.
fail_launch pane-pid-unresolved \
"tmux reports the session but no pane PID after 5 attempts"
else
# #1241. This branch used to print a WARNING about the heartbeat sidecar and
# exit 0. It is not a heartbeat problem: tmux destroys a session when its pane
# command exits, so an absent session one second after new-session means the
# runtime died on startup. Reporting it as success is what let `fleet start`
# return 0 over three dead panes — the launcher knew, and said the wrong thing
# at the wrong severity to the wrong layer.
fail_launch pane-did-not-survive \
"the pane exited immediately and tmux destroyed the session;" \
"run 'mosaic yolo ${MOSAIC_AGENT_RUNTIME}' in ${MOSAIC_AGENT_WORKDIR} to see why"
fi
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
AGENT_NAME=${1:-}
fail() {
echo "ERROR: $*" >&2
exit 64
}
[ -n "$AGENT_NAME" ] || fail "agent name argument is required"
# The shared launcher strictly validates the generated/local data boundary
# before it applies this interaction service's pinned profile checks.
exec "$(cd -- "$(dirname -- "$0")" && pwd)/start-agent-session.sh" --interaction "$AGENT_NAME"
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
# A holder may create only the configured named socket. Existing servers are
# accepted only when their private install-derived ownership identity, exact
# holder session, and complete approved global environment all match.
MOSAIC_HOME=${MOSAIC_HOME:-$HOME/.config/mosaic}
MOSAIC_TMUX_SOCKET=${MOSAIC_TMUX_SOCKET:-mosaic-fleet}
MOSAIC_TMUX_HOLDER=${MOSAIC_TMUX_HOLDER:-_holder}
OWNER_FILE="$MOSAIC_HOME/fleet/run/holder-owner"
TMUX_BIN=/usr/bin/tmux
fail() {
echo "ERROR: refusing unmanaged Mosaic tmux server on socket ${MOSAIC_TMUX_SOCKET}: $1" >&2
exit 64
}
[ -x "$TMUX_BIN" ] || fail "tmux binary is unavailable"
[ -f "$OWNER_FILE" ] && [ ! -L "$OWNER_FILE" ] || fail "private ownership identity is missing"
owner_mode=$(stat -c '%a' -- "$OWNER_FILE") || fail "private ownership identity is unreadable"
(( (8#$owner_mode & 8#077) == 0 )) || fail "private ownership identity has unsafe permissions"
MOSAIC_FLEET_OWNER=$(tr -d '\n' < "$OWNER_FILE")
[[ "$MOSAIC_FLEET_OWNER" =~ ^[a-f0-9-]{36}$ ]] || fail "private ownership identity is malformed"
_tmux() {
"$TMUX_BIN" -L "$MOSAIC_TMUX_SOCKET" "$@"
}
server_running() {
_tmux list-sessions >/dev/null 2>&1
}
assert_owned_server() {
_tmux has-session -t "=${MOSAIC_TMUX_HOLDER}:0.0" 2>/dev/null || fail "exact holder session is absent"
local environment
environment=$(_tmux show-environment -g 2>/dev/null) || fail "global environment is unreadable"
local expected
expected=$(printf '%s\n' \
"HOME=$HOME" \
'PATH=/usr/bin:/bin' \
"PWD=$HOME" \
"MOSAIC_FLEET_OWNER=$MOSAIC_FLEET_OWNER" \
"MOSAIC_TMUX_HOLDER=$MOSAIC_TMUX_HOLDER" \
"MOSAIC_TMUX_SOCKET=$MOSAIC_TMUX_SOCKET" | sort)
[ "$(printf '%s\n' "$environment" | sort)" = "$expected" ] || \
fail "global environment does not match the owned-server contract"
}
if server_running; then
assert_owned_server
else
cd "$HOME" || fail "trusted home is unavailable"
# Start the tmux server itself under the approved environment. The holder pane
# receives the same closed environment rather than arbitrary server globals.
/usr/bin/env -i \
"HOME=$HOME" \
PATH=/usr/bin:/bin \
"MOSAIC_FLEET_OWNER=$MOSAIC_FLEET_OWNER" \
"MOSAIC_TMUX_HOLDER=$MOSAIC_TMUX_HOLDER" \
"MOSAIC_TMUX_SOCKET=$MOSAIC_TMUX_SOCKET" \
"$TMUX_BIN" -L "$MOSAIC_TMUX_SOCKET" new-session -d -s "$MOSAIC_TMUX_HOLDER" \
/usr/bin/env -i "HOME=$HOME" PATH=/usr/bin:/bin /bin/sh -c 'while true; do sleep 3600; done'
fi
@@ -0,0 +1,222 @@
#!/usr/bin/env bash
# CI-fit regression suite for the #1292 lease-broker socket preflight in
# start-agent-session.sh.
#
# WHY THIS SUITE IS CI-FIT WHERE test-start-agent-session.sh IS NOT (#1017/#1270
# context): that older suite's precondition is "the host does not have the pi
# binary", which a CI image that ships pi violates — its guard correctly
# refuses to report a pass there, so it is excluded from the chain. THIS suite
# controls its own preconditions instead of inheriting them from the host: a
# fake tmux on PATH, a fake mosaic on PATH, a real unix socket created in a
# tmpdir, a hermetic env (env -i, fake HOME, GIT_CONFIG_GLOBAL severed). It
# never depends on what the host has installed, so a green here means the same
# thing on every host. Anyone adding cases: keep that property — no case may
# depend on host state.
#
# The failure this suite is written down to catch (#1292): a seat launched on a
# host with no lease broker dies ~4 seconds in at registration, with the
# diagnostic invisible because tmux destroys the dead pane. The preflight runs
# BEFORE any tmux effect and refuses with a NAMED code (exit 75, EX_TEMPFAIL)
# so the message survives. The agent@ unit is Type=oneshot with no Restart=,
# so a failed unit keeps its output instead of looping.
#
# Cases:
# 1. absent socket -> exit 75, message names broker-absent + socket path +
# remedy, and NO tmux session was ever created (the doomed-pane half).
# 2. present socket (real unix socket in tmpdir) -> proceeds PAST the
# preflight (the suite then stops at the next precondition, proving the
# preflight was not the refusal).
# 3. explicit MOSAIC_LEASE_BROKER_SOCKET wins over XDG_RUNTIME_DIR default.
# 4. --stop mode does NOT require the broker (teardown must not be fenced on
# a component whose absence is exactly what teardown may follow).
#
# Sabotage control, run by the developer (not in-suite): remove the preflight
# block from start-agent-session.sh, re-run — case 1 fails (a tmux session is
# created / exit is not 75), cases 2-4 still pass; restore byte-identically.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/agent-session-broker-preflight}"
FAKE_HOME="$WORK_DIR/home"
BIN_DIR="$WORK_DIR/bin"
ENV_DIR="$WORK_DIR/env"
SOCK_DIR="$WORK_DIR/sockets"
LOG_FILE="$WORK_DIR/tmux-calls.log"
rm -rf "$WORK_DIR"
# The script asserts a managed directory tree under MOSAIC_HOME: mosaic/,
# mosaic/fleet/, mosaic/fleet/agents/ — private (0700/0750-style) modes, no
# symlinks — plus a per-agent env projection. Build the full tree the launcher
# expects so the suite reaches the BROKER preflight rather than dying at
# environment validation.
mkdir -p "$FAKE_HOME/.config/mosaic/fleet/agents" "$BIN_DIR" "$SOCK_DIR"
chmod 700 "$FAKE_HOME/.config/mosaic" "$FAKE_HOME/.config/mosaic/fleet/agents"
chmod 750 "$FAKE_HOME/.config/mosaic/fleet"
cat > "$FAKE_HOME/.config/mosaic/fleet/agents/preflight-test.env.generated" <<'ENVEOF'
MOSAIC_AGENT_NAME=preflight-test
MOSAIC_GIT_IDENTITY=preflight-test
MOSAIC_AGENT_CLASS=worker
MOSAIC_AGENT_RUNTIME=pi
MOSAIC_AGENT_MODEL=
MOSAIC_AGENT_REASONING=
MOSAIC_AGENT_TOOL_POLICY=code
MOSAIC_AGENT_WORKDIR=/tmp
MOSAIC_TMUX_SOCKET=mosaic-fleet
ENVEOF
chmod 600 "$FAKE_HOME/.config/mosaic/fleet/agents/preflight-test.env.generated"
# ─── Fake tmux: records every invocation; new-session marks the marker. ────
: > "$LOG_FILE"
cat > "$BIN_DIR/tmux" <<SH
#!/usr/bin/env bash
printf 'tmux %s\n' "\$*" >> "$LOG_FILE"
if [[ "\$*" == *new-session* ]]; then
echo "TMUX-NEW-SESSION-INVOKED" >> "$LOG_FILE"
fi
exit 0
SH
chmod +x "$BIN_DIR/tmux"
# ─── Fake mosaic/pi binaries so the script proceeds past its own lookups. ───
for bin in mosaic pi claude; do
printf '#!/usr/bin/env bash\nexit 0\n' > "$BIN_DIR/$bin"
chmod +x "$BIN_DIR/$bin"
done
# ─── Minimal launch environment the script expects. ────────────────────────
# (Enough for the preflight to be reached; later stages will still fail in
# case 2 — that is expected and asserted.)
run_session_script() {
local mode="$1"; shift
(
cd "$WORK_DIR"
env -i HOME="$FAKE_HOME" PATH="$BIN_DIR:/usr/bin:/bin" \
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
MOSAIC_HOME="$FAKE_HOME/.config/mosaic" \
AGENT_NAME=preflight-test \
"$@" \
bash "$SCRIPT_DIR/start-agent-session.sh" $mode preflight-test
)
}
fail=0
assert() {
local desc="$1" expected="$2" actual="$3"
if [[ "$expected" != "$actual" ]]; then
echo "FAIL: $desc — expected '$expected', got '$actual'" >&2
fail=1
fi
}
assert_contains() {
local desc="$1" haystack="$2" needle="$3"
[[ "$haystack" == *"$needle"* ]] || { echo "FAIL: $desc — missing '$needle' in: $haystack" >&2; fail=1; }
}
assert_not_contains() {
local desc="$1" haystack="$2" needle="$3"
if [[ "$haystack" == *"$needle"* ]]; then
echo "FAIL: $desc — must not contain '$needle'" >&2
fail=1
fi
return 0
}
# ─── 1. Absent socket → named refusal, NO tmux session. ────────────────────
: > "$LOG_FILE"
stderr_file="$WORK_DIR/stderr-1.tmp"
set +e
out=$(run_session_script "" MOSAIC_LEASE_BROKER_SOCKET="$SOCK_DIR/absent.sock" 2>"$stderr_file")
rc=$?
set -e
assert "absent socket exit code" "75" "$rc"
err=$(cat "$stderr_file")
assert_contains "absent socket names the failure" "$err" "FAIL_LAUNCH broker-absent"
assert_contains "absent socket names the socket path" "$err" "$SOCK_DIR/absent.sock"
assert_contains "absent socket names a remedy" "$err" "mosaic fleet install"
log1=$(cat "$LOG_FILE")
assert_not_contains "absent socket must not create a tmux session" "$log1" "TMUX-NEW-SESSION-INVOKED"
# ─── 2. Present socket → passes the preflight. ─────────────────────────────
# Expected: ownership/env checks AFTER the preflight may refuse (fixture is
# minimal by design); the assertion is only that the refusal is NOT
# broker-absent and the exit is NOT 75.
# Create a REAL unix socket: a detached python holder binds it and stays alive
# for the duration (bash cannot create sockets; a foreground python would
# close the socket on exit and -S on a closed-but-unlinked path fails). Written
# as a script file + setsid nohup so no job-control/heredoc interaction with
# set -e can silently kill the suite.
# AF_UNIX binds cap at 108 path bytes; the suite's workdir exceeds that, so
# the live socket lives at a SHORT path under /tmp (unique per run, cleaned
# with the suite). The preflight takes its socket path explicitly, so this
# stays fully controlled.
# A real unix socket at a SHORT absolute path (AF_UNIX limit is 108 bytes,
# so the repo-deep SOCK_DIR cannot host it). The name is composed, not
# `mktemp -u`: the CI image's mktemp dialect rejects that invocation
# (pipeline 2562: "mktemp: : Invalid argument"), and no pre-existing file is
# wanted anyway — the holder binds it fresh.
LIVE_SOCK="/tmp/mosaic-preflight-$RANDOM-$$.sock"
trap 'rm -f "$LIVE_SOCK"' EXIT
rm -f "$SOCK_DIR/live.sock" "$LIVE_SOCK"
cat > "$SOCK_DIR/holder.py" <<'PY'
import socket, sys, time
path = sys.argv[1]
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind(path)
s.listen(1)
time.sleep(120)
PY
python3 "$SOCK_DIR/holder.py" "$LIVE_SOCK" >/dev/null 2>"$SOCK_DIR/holder.err" &
HOLDER_PID=$!
# Wait for the socket object to exist (bind is near-instant, but do not race it).
for _ in $(seq 1 50); do
[ -S "$LIVE_SOCK" ] && break
sleep 0.1
done
if [ ! -S "$LIVE_SOCK" ]; then
echo "FAIL: could not create live socket fixture (holder pid $HOLDER_PID)" >&2
ps -p "$HOLDER_PID" -o pid,stat,cmd --no-headers >&2 || echo "(holder exited)" >&2
cat "$SOCK_DIR/holder.err" >&2 || true
exit 1
fi
: > "$LOG_FILE"
set +e
out=$(run_session_script "" MOSAIC_LEASE_BROKER_SOCKET="$LIVE_SOCK" 2>"$WORK_DIR/stderr-2.tmp")
rc=$?
set -e
# The preflight PASSED if the failure (whatever later stage refused) is NOT
# the broker refusal, and tmux was reached or a later precondition named
# something else.
err2=$(cat "$WORK_DIR/stderr-2.tmp")
assert_not_contains "live socket must not refuse broker-absent" "$err2" "broker-absent"
if [[ "$rc" == "75" ]]; then
echo "FAIL: live socket — preflight still refused (exit 75) with a live socket" >&2
fail=1
fi
# ─── 3. Explicit socket env wins over XDG default. ─────────────────────────
set +e
out=$(run_session_script "" XDG_RUNTIME_DIR="$SOCK_DIR/no-runtime-here" MOSAIC_LEASE_BROKER_SOCKET="$SOCK_DIR/absent2.sock" 2>"$WORK_DIR/stderr-3.tmp")
rc=$?
set -e
assert "explicit env wins (exit 75)" "75" "$rc"
assert_contains "explicit env path named" "$(cat "$WORK_DIR/stderr-3.tmp")" "$SOCK_DIR/absent2.sock"
# ─── 4. --stop is not fenced on the broker. ────────────────────────────────
: > "$LOG_FILE"
set +e
out=$(run_session_script "--stop" MOSAIC_LEASE_BROKER_SOCKET="$SOCK_DIR/absent3.sock" 2>"$WORK_DIR/stderr-4.tmp")
rc=$?
set -e
err4=$(cat "$WORK_DIR/stderr-4.tmp")
assert_not_contains "--stop must not refuse broker-absent" "$err4" "broker-absent"
if [[ "$rc" == "75" ]]; then
echo "FAIL: --stop — exit 75 means teardown was fenced on the broker" >&2
fail=1
fi
kill "$HOLDER_PID" 2>/dev/null || true
if [[ "$fail" -eq 0 ]]; then
echo "start-agent-session lease-broker preflight regression passed"
fi
exit "$fail"
@@ -0,0 +1,216 @@
#!/usr/bin/env bash
# CI-fit regression suite for the #1408 legacy-socket guard in
# start-agent-session.sh.
#
# Same hermeticity contract as test-agent-session-broker-preflight.sh: a fake
# tmux on PATH that scripts its own answers, a real unix socket in a tmpdir so
# the broker preflight passes, env -i with a fake HOME. No case depends on host
# state.
#
# The failure this suite is written down to catch: during a socket cutover a
# seat's session still lives on the DEFAULT tmux socket while the launcher
# targets the named one. The declared-socket has-session check cannot see the
# legacy session (measured 2026-08-24: rc=1, script proceeds), so launch
# creates a same-name duplicate — and comms delivery, which addresses sessions
# by NAME, cannot tell the two apart. The guard refuses with its own code
# (exit 76, after 75 broker-absent) BEFORE any tmux mutation.
#
# Cases:
# 1. legacy session present -> exit 76, message names seat-on-legacy-socket
# + both sockets' roles, and NO tmux session was created.
# 2. legacy session absent -> proceeds PAST the guard (the run then stops at
# a later precondition; asserted: exit != 76, stderr lacks the guard's
# code, proving the guard was not the refusal).
# 3. MOSAIC_TMUX_SOCKET empty (single-socket host) -> guard is inert: the
# default-socket probe must not fire at all.
#
# Sabotage control, run by the developer (not in-suite): remove the guard
# block, re-run — case 1 fails (exit is not 76), cases 2-3 still pass;
# restore byte-identically.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/agent-session-legacy-socket-guard}"
FAKE_HOME="$WORK_DIR/home"
BIN_DIR="$WORK_DIR/bin"
SOCK_DIR="$WORK_DIR/sockets"
LOG_FILE="$WORK_DIR/tmux-calls.log"
LEGACY_FLAG="$WORK_DIR/legacy-session-present"
rm -rf "$WORK_DIR"
mkdir -p "$FAKE_HOME/.config/mosaic/fleet/agents" "$BIN_DIR" "$SOCK_DIR"
chmod 700 "$FAKE_HOME/.config/mosaic" "$FAKE_HOME/.config/mosaic/fleet/agents"
chmod 750 "$FAKE_HOME/.config/mosaic/fleet"
cat > "$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-test.env.generated" <<'ENVEOF'
MOSAIC_AGENT_NAME=lsguard-test
MOSAIC_GIT_IDENTITY=lsguard-test
MOSAIC_AGENT_CLASS=worker
MOSAIC_AGENT_RUNTIME=pi
MOSAIC_AGENT_MODEL=
MOSAIC_AGENT_REASONING=
MOSAIC_AGENT_TOOL_POLICY=code
MOSAIC_AGENT_WORKDIR=/tmp
MOSAIC_TMUX_SOCKET=mosaic-fleet
ENVEOF
chmod 600 "$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-test.env.generated"
# A projection with NO named socket, for case 3. Same file minus the socket line.
sed '/^MOSAIC_TMUX_SOCKET=/d; s/lsguard-test/lsguard-nosock/' \
"$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-test.env.generated" \
> "$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-nosock.env.generated"
echo 'MOSAIC_TMUX_SOCKET=' >> "$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-nosock.env.generated"
chmod 600 "$FAKE_HOME/.config/mosaic/fleet/agents/lsguard-nosock.env.generated"
# Ownership identity the launcher validates before anything touches tmux:
# a 0600 uuid file plus a tmux global environment that matches it exactly.
mkdir -p "$FAKE_HOME/.config/mosaic/fleet/run"
chmod 750 "$FAKE_HOME/.config/mosaic/fleet/run"
OWNER_UUID="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
printf '%s' "$OWNER_UUID" > "$FAKE_HOME/.config/mosaic/fleet/run/holder-owner"
chmod 600 "$FAKE_HOME/.config/mosaic/fleet/run/holder-owner"
# The exact env block assert_owned_tmux_server expects; the socket value differs
# per case, so cases rewrite it via write_tmux_env before each run.
write_tmux_env() {
printf '%s\n' \
"HOME=$FAKE_HOME" \
'PATH=/usr/bin:/bin' \
"PWD=$FAKE_HOME" \
"MOSAIC_FLEET_OWNER=$OWNER_UUID" \
'MOSAIC_TMUX_HOLDER=_holder' \
"MOSAIC_TMUX_SOCKET=$1" > "$WORK_DIR/tmux-env"
}
# ─── Fake tmux ──────────────────────────────────────────────────────────────
# Scripted answers: a DEFAULT-socket has-session (argv carries no -L) answers
# by the flag file; every named-socket call succeeds (holder present, no
# existing session is fine for these cases since refusal happens first).
cat > "$BIN_DIR/tmux" <<SH
#!/usr/bin/env bash
printf 'tmux %s\n' "\$*" >> "$LOG_FILE"
if [[ "\$*" == *new-session* ]]; then
echo "TMUX-NEW-SESSION-INVOKED" >> "$LOG_FILE"
fi
if [[ "\$*" == *show-environment* ]]; then
cat "$WORK_DIR/tmux-env"
exit 0
fi
if [[ "\$*" == *has-session* ]]; then
# holder session always present; the seat's DEFAULT-socket presence is the
# flag file; the seat is never already-running on the NAMED socket.
[[ "\$*" == *_holder* ]] && exit 0
if [[ "\$1" == "-L" ]]; then exit 1; fi
[[ -e "$LEGACY_FLAG" ]] && exit 0 || exit 1
fi
exit 0
SH
chmod +x "$BIN_DIR/tmux"
for bin in mosaic pi claude; do
printf '#!/usr/bin/env bash\nexit 0\n' > "$BIN_DIR/$bin"
chmod +x "$BIN_DIR/$bin"
done
# Real socket so the #1292 broker preflight passes and the run reaches the guard.
# Same idiom as the broker-preflight suite: AF_UNIX binds cap at 108 path bytes,
# so the socket lives at a SHORT /tmp path held by a detached python holder (a
# foreground bind would close on exit; -S on a closed-but-unlinked path fails).
LIVE_SOCK="/tmp/mosaic-lsguard-$RANDOM-$$.sock"
trap 'rm -f "$LIVE_SOCK"' EXIT
rm -f "$LIVE_SOCK"
cat > "$SOCK_DIR/holder.py" <<'PY'
import socket, sys, time
path = sys.argv[1]
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind(path)
s.listen(1)
time.sleep(120)
PY
python3 "$SOCK_DIR/holder.py" "$LIVE_SOCK" >/dev/null 2>"$SOCK_DIR/holder.err" &
for _ in $(seq 1 50); do
[ -S "$LIVE_SOCK" ] && break
sleep 0.1
done
[ -S "$LIVE_SOCK" ] || { echo "FAIL: could not create live socket" >&2; exit 1; }
run_session_script() {
local agent="$1"; shift
(
cd "$WORK_DIR"
env -i HOME="$FAKE_HOME" PATH="$BIN_DIR:/usr/bin:/bin" \
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
MOSAIC_HOME="$FAKE_HOME/.config/mosaic" \
MOSAIC_LEASE_BROKER_SOCKET="$LIVE_SOCK" \
"$@" \
bash "$SCRIPT_DIR/start-agent-session.sh" "$agent"
)
}
fail=0
assert() {
local desc="$1" expected="$2" actual="$3"
[[ "$expected" == "$actual" ]] || { echo "FAIL: $desc — expected '$expected', got '$actual'" >&2; fail=1; }
}
assert_contains() {
local desc="$1" haystack="$2" needle="$3"
[[ "$haystack" == *"$needle"* ]] || { echo "FAIL: $desc — missing '$needle'" >&2; fail=1; }
}
assert_not_contains() {
local desc="$1" haystack="$2" needle="$3"
if [[ "$haystack" == *"$needle"* ]]; then
echo "FAIL: $desc — must not contain '$needle'" >&2
fail=1
fi
return 0
}
# ─── 1. Legacy session present → exit 76, no tmux mutation. ─────────────────
write_tmux_env "mosaic-fleet"
: > "$LOG_FILE"; touch "$LEGACY_FLAG"
stderr_file="$WORK_DIR/stderr-1.tmp"
set +e
run_session_script lsguard-test >/dev/null 2>"$stderr_file"
rc=$?
set -e
err=$(cat "$stderr_file")
assert "legacy present exit code" "76" "$rc"
assert_contains "names the failure" "$err" "FAIL_LAUNCH seat-on-legacy-socket"
assert_contains "names the agent" "$err" "lsguard-test"
assert_contains "names the target socket" "$err" "mosaic-fleet"
assert_not_contains "no session created" "$(cat "$LOG_FILE")" "TMUX-NEW-SESSION-INVOKED"
# ─── 2. Legacy session absent → guard is not the refusal. ───────────────────
write_tmux_env "mosaic-fleet"
: > "$LOG_FILE"; rm -f "$LEGACY_FLAG"
stderr_file="$WORK_DIR/stderr-2.tmp"
set +e
run_session_script lsguard-test >/dev/null 2>"$stderr_file"
rc=$?
set -e
err=$(cat "$stderr_file")
if [[ "$rc" == "76" ]]; then
echo "FAIL: legacy absent must not exit 76" >&2; fail=1
fi
assert_not_contains "guard code absent from stderr" "$err" "seat-on-legacy-socket"
# ─── 3. Empty MOSAIC_TMUX_SOCKET → guard inert, no default-socket probe. ────
write_tmux_env ""
: > "$LOG_FILE"; touch "$LEGACY_FLAG" # even with a legacy session present
stderr_file="$WORK_DIR/stderr-3.tmp"
set +e
run_session_script lsguard-nosock >/dev/null 2>"$stderr_file"
rc=$?
set -e
err=$(cat "$stderr_file")
if [[ "$rc" == "76" ]]; then
echo "FAIL: empty socket must never exit 76 (single-socket host)" >&2; fail=1
fi
assert_not_contains "guard code absent on single-socket host" "$err" "seat-on-legacy-socket"
rm -f "$LEGACY_FLAG"
if [[ "$fail" -ne 0 ]]; then
echo "start-agent-session legacy-socket guard regression FAILED" >&2
exit 1
fi
echo "start-agent-session legacy-socket guard regression passed"
@@ -0,0 +1,812 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
START="$SCRIPT_DIR/start-agent-session.sh"
INTERACTION_START="$SCRIPT_DIR/start-interaction-service.sh"
ROOT=$(mktemp -d)
FAKE_BIN=$(mktemp -d)
TMUX_CALLS=$(mktemp)
trap 'rm -rf "$ROOT" "$FAKE_BIN" "$TMUX_CALLS"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
pane_command_clears_environment() {
local calls_file="$1"
local -a argv=()
local index
mapfile -d '' -t argv < "$calls_file"
for ((index = 0; index + 1 < ${#argv[@]}; index++)); do
if [ "${argv[$index]}" = /usr/bin/env ] && [ "${argv[$((index + 1))]}" = -i ]; then
return 0
fi
done
return 1
}
print_pane_argv() {
local calls_file="$1"
local -a argv=()
local bytes index
mapfile -d '' -t argv < "$calls_file"
bytes=$(wc -c < "$calls_file")
printf 'observed pane argv: records=%s bytes=%s\n' "${#argv[@]}" "$bytes" >&2
for ((index = 0; index < ${#argv[@]}; index++)); do
printf ' [%03d] %q\n' "$index" "${argv[$index]}" >&2
done
}
check_pane_environment_boundary() {
local calls_file="$1"
if pane_command_clears_environment "$calls_file"; then
return 0
fi
print_pane_argv "$calls_file"
return 1
}
contains_literal() {
grep -F -- "$2" <<< "$1" >/dev/null
}
contains_line() {
grep -xF -- "$2" <<< "$1" >/dev/null
}
# Portability regression: inspect the authoritative NUL-delimited argv instead
# of piping a newline reconstruction through `grep -q` under pipefail. The old
# pipeline could report failure after a successful match when an upstream
# producer received SIGPIPE. A large trailing argument keeps that failure class
# covered without making stream size part of the semantic contract.
PORTABILITY_CALLS="$ROOT/portability-calls"
printf -v PORTABILITY_PADDING '%*s' 32768 ''
PORTABILITY_PADDING=${PORTABILITY_PADDING// /x}
printf '%s\0' /usr/bin/env -i "$PORTABILITY_PADDING" > "$PORTABILITY_CALLS"
pane_command_clears_environment "$PORTABILITY_CALLS" || \
fail "valid large pane argv was rejected by the environment-boundary assertion"
assert_pane_boundary_rejected() {
local case_name="$1"
local expected_records="$2"
local diagnostic
if diagnostic=$(check_pane_environment_boundary "$PORTABILITY_CALLS" 2>&1); then
fail "pane boundary accepted invalid $case_name fixture"
fi
contains_literal "$diagnostic" "records=$expected_records bytes=" || \
fail "pane argv diagnostic omitted counts for $case_name fixture"
contains_literal "$diagnostic" '[000]' || \
fail "pane argv diagnostic omitted indexed arguments for $case_name fixture"
}
printf '%s\0' tmux -i > "$PORTABILITY_CALLS"
assert_pane_boundary_rejected missing-env 2
printf '%s\0' /usr/bin/env HOME=/untrusted > "$PORTABILITY_CALLS"
assert_pane_boundary_rejected missing-i 2
printf '%s\0' /usr/bin/env HOME=/untrusted -i > "$PORTABILITY_CALLS"
assert_pane_boundary_rejected non-adjacent-i 3
printf '%s\0' -i /usr/bin/env > "$PORTABILITY_CALLS"
assert_pane_boundary_rejected reversed-boundary 2
cat > "$FAKE_BIN/tmux" <<'SHIM'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\0' "$@" >> "${MOSAIC_TEST_TMUX_CALLS:?}"
args=("$@")
index=0
if [ "${args[0]:-}" = -L ]; then index=2; fi
case "${args[$index]:-}" in
has-session)
# The holder always answers. MOSAIC_TEST_HELD_SESSIONS lets a case add
# other targets that should answer too — without it there is no way to
# model "tmux still reports the session" for a non-holder agent, and the
# launcher's pane-pid-unresolved branch is unreachable from this harness.
#
# A listed target answers only AFTER new-session, because the launcher asks
# this question twice about the same name: once before launching, where a
# yes means "already running, nothing to do, exit 0", and once after, where
# a yes means "the session survived". A shim that answered yes to both
# would short-circuit at the first and never reach the branch under test —
# it would look like coverage and measure the idempotency path instead.
for argument in "${args[@]}"; do
[ "$argument" = '=_holder:0.0' ] && exit 0
case " ${MOSAIC_TEST_HELD_SESSIONS:-} " in
*" $argument "*)
if tr '\0' '\n' < "${MOSAIC_TEST_TMUX_CALLS:?}" | grep -qxF new-session; then
exit 0
fi
;;
esac
done
exit 1
;;
show-environment)
printf '%s\n' \
"HOME=${MOSAIC_TEST_HOME:?}" \
'PATH=/usr/bin:/bin' \
"PWD=${MOSAIC_TEST_HOME:?}" \
"MOSAIC_FLEET_OWNER=${MOSAIC_TEST_FLEET_OWNER:?}" \
'MOSAIC_TMUX_HOLDER=_holder' \
'MOSAIC_TMUX_SOCKET=mosaic-test'
exit 0
;;
list-panes) printf '%s\n' "${MOSAIC_TEST_PANE_PID:-}"; exit 0 ;;
new-session)
if [ "${MOSAIC_TEST_EXECUTE_PANE:-}" = 1 ]; then
for ((index = 0; index < ${#args[@]}; index++)); do
if [ "${args[$index]}" = /usr/bin/env ]; then
"${args[@]:$index}"
break
fi
done
fi
exit 0
;;
*) exit 0 ;;
esac
SHIM
chmod +x "$FAKE_BIN/tmux"
cat > "$FAKE_BIN/mosaic" <<'SHIM'
#!/usr/bin/env bash
set -euo pipefail
env -0 > "${MOSAIC_HOME:?}/fleet/pane-environment"
SHIM
chmod +x "$FAKE_BIN/mosaic"
# The runtime the rosters below name. The launcher resolves it against PANE_PATH
# before spawning (#1241), so it has to exist somewhere the pane would find it —
# not merely on the launcher's own PATH.
printf '#!/usr/bin/env bash\nexit 0\n' > "$FAKE_BIN/pi"
chmod +x "$FAKE_BIN/pi"
# PANE_PATH is derived partly from `npm config get prefix`. Left to the real npm
# it would splice whatever the host has installed into the path under test, and
# the missing-binary cases below would pass or fail by accident of the machine.
cat > "$FAKE_BIN/npm" <<'SHIM'
#!/usr/bin/env bash
printf '%s\n' "${MOSAIC_TEST_NPM_PREFIX:-/nonexistent}"
SHIM
chmod +x "$FAKE_BIN/npm"
# PANE_PATH always ends in the system path. A host that installs these there can
# not measure the missing-binary cases at all, and a green run would mean
# nothing — so say so instead of passing.
for host_binary in mosaic pi; do
if PATH=/usr/local/bin:/usr/bin:/bin command -v "$host_binary" >/dev/null 2>&1; then
fail "host provides '$host_binary' in the system path; missing-binary cases are not measurable here"
fi
done
write_generated() {
local home="$1"
local agent="$2"
mkdir -p "$home/fleet/agents" "$home/fleet/run"
chmod 700 "$home" "$home/fleet" "$home/fleet/agents" "$home/fleet/run"
printf '123e4567-e89b-12d3-a456-426614174000\n' > "$home/fleet/run/holder-owner"
chmod 600 "$home/fleet/run/holder-owner"
cat > "$home/fleet/agents/$agent.env.generated" <<EOF
MOSAIC_AGENT_NAME=$agent
MOSAIC_GIT_IDENTITY=$agent
MOSAIC_AGENT_CLASS=code
MOSAIC_AGENT_RUNTIME=pi
MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol
MOSAIC_AGENT_REASONING=high
MOSAIC_AGENT_TOOL_POLICY=code
MOSAIC_AGENT_WORKDIR=$home/work
MOSAIC_TMUX_SOCKET=mosaic-test
EOF
chmod 600 "$home/fleet/agents/$agent.env.generated"
mkdir -p "$home/work"
install_pane_binaries "$home"
}
# `$PANE_HOME/.npm-global/bin` is one of the prefixes the launcher folds into
# PANE_PATH, so this is the pane's own view of "installed", distinct from the
# launcher's PATH. Tests that need a binary *absent* remove it from here.
install_pane_binaries() {
local pane_home="$1"
mkdir -p "$pane_home/.npm-global/bin"
local binary
for binary in mosaic pi; do
ln -sf "$FAKE_BIN/$binary" "$pane_home/.npm-global/bin/$binary"
done
}
run_start() {
local home="$1"
local agent="$2"
HOME="$home" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
MOSAIC_TEST_PANE_PID="${MOSAIC_TEST_PANE_PID:-}" \
MOSAIC_TEST_HELD_SESSIONS="${MOSAIC_TEST_HELD_SESSIONS:-}" \
MOSAIC_TEST_FIXED_EPOCH="${MOSAIC_TEST_FIXED_EPOCH:-}" \
MOSAIC_TEST_HOME="$home" \
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
MOSAIC_HOME="$home" "$START" "$agent"
}
# Valid generated data launches only the fixed runtime argument array. It never
# reads an agent-command string or constructs a bash -c pane payload.
HOME_VALID="$ROOT/valid"
AGENT_VALID="coder0"
write_generated "$HOME_VALID" "$AGENT_VALID"
# A live pane PID is part of what "valid launch" means. Until #1241 this case
# ran with none, so the suite's one success path was itself a dead pane the
# launcher reported as fine.
MOSAIC_TEST_PANE_PID=$$ run_start "$HOME_VALID" "$AGENT_VALID"
valid_args=$(tr '\0' '\n' < "$TMUX_CALLS")
contains_literal "$valid_args" new-session || fail "valid generated projection did not reach tmux"
contains_literal "$valid_args" mosaic || fail "fixed mosaic launcher command missing"
contains_literal "$valid_args" yolo || fail "fixed yolo launcher command missing"
contains_literal "$valid_args" pi || fail "roster runtime missing"
if contains_literal "$valid_args" 'bash -c'; then
fail "launcher constructed a shell command payload"
fi
# ── Brain-home split (canon §2) ─────────────────────────────────────────
# When MOSAIC_HOME is the default config home under $HOME and the host carries
# $HOME/.mosaic/fleet/agents, seat envs resolve from the brain tree; the config
# home still owns fleet/run (holder-owner) and remains a managed boundary.
: > "$TMUX_CALLS"
HOME_BRAIN="$ROOT/brain-home"
CONFIG_HOME="$HOME_BRAIN/.config/mosaic"
BRAIN="$HOME_BRAIN/.mosaic"
mkdir -p "$CONFIG_HOME/fleet/run" "$BRAIN/fleet/agents" "$HOME_BRAIN/work"
chmod 700 "$CONFIG_HOME" "$CONFIG_HOME/fleet" "$CONFIG_HOME/fleet/run" \
"$BRAIN/fleet/agents" "$HOME_BRAIN/work"
printf '123e4567-e89b-12d3-a456-426614174000\n' > "$CONFIG_HOME/fleet/run/holder-owner"
chmod 600 "$CONFIG_HOME/fleet/run/holder-owner"
cat > "$BRAIN/fleet/agents/coder-brain.env.generated" <<EOF
MOSAIC_AGENT_NAME=coder-brain
MOSAIC_AGENT_CLASS=code
MOSAIC_AGENT_RUNTIME=pi
MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol
MOSAIC_AGENT_REASONING=high
MOSAIC_AGENT_TOOL_POLICY=code
MOSAIC_AGENT_WORKDIR=$HOME_BRAIN/work
MOSAIC_TMUX_SOCKET=mosaic-test
EOF
chmod 600 "$BRAIN/fleet/agents/coder-brain.env.generated"
install_pane_binaries "$HOME_BRAIN"
HOME="$HOME_BRAIN" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
MOSAIC_TEST_PANE_PID=$$ MOSAIC_TEST_HOME="$HOME_BRAIN" \
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
MOSAIC_HOME="$CONFIG_HOME" "$START" coder-brain
brain_args=$(tr '\0' '\n' < "$TMUX_CALLS")
echo "$brain_args" | grep -qF new-session || fail "brain-home generated projection did not reach tmux"
echo "$brain_args" | grep -qF 'coder-brain' || fail "brain-home agent env was not the launch source"
[ -f "$BRAIN/fleet/agents/coder-brain.env.generated" ] || fail "brain generated env vanished"
# Negative control: the SAME default-config-home shape but without
# ~/.mosaic/fleet/agents — the config-home env tree is used directly (legacy).
: > "$TMUX_CALLS"
HOME_NOBRAIN="$ROOT/brainless-home"
CONFIG_HOME_NOBRAIN="$HOME_NOBRAIN/.config/mosaic"
write_generated "$CONFIG_HOME_NOBRAIN" "coder-legacy"
install_pane_binaries "$HOME_NOBRAIN"
HOME="$HOME_NOBRAIN" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
MOSAIC_TEST_PANE_PID=$$ MOSAIC_TEST_HOME="$HOME_NOBRAIN" \
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
MOSAIC_HOME="$CONFIG_HOME_NOBRAIN" "$START" coder-legacy
legacy_args=$(tr '\0' '\n' < "$TMUX_CALLS")
echo "$legacy_args" | grep -qF new-session || fail "legacy single-tree launch regressed"
# The pane must start through an absolute clean-environment boundary. Its
# runtime command remains an argv vector, but no holder/session environment
# control variable can pass through the pane command.
check_pane_environment_boundary "$TMUX_CALLS" || \
fail "pane command did not use an adjacent /usr/bin/env -i boundary"
# Git identity is generated authority, not an optional or independently mutable
# local value. Each invalid form must fail before fake tmux receives a call.
assert_git_identity_rejected() {
local case_name="$1"
local expected_code="$2"
local home="$ROOT/git-identity-$case_name"
local agent="coder-git-identity-$case_name"
local generated="$home/fleet/agents/$agent.env.generated"
write_generated "$home" "$agent"
case "$case_name" in
missing) grep -v '^MOSAIC_GIT_IDENTITY=' "$generated" > "$generated.next" && mv "$generated.next" "$generated" ;;
unsafe) sed -i 's|^MOSAIC_GIT_IDENTITY=.*$|MOSAIC_GIT_IDENTITY=bad/identity|' "$generated" ;;
mismatch) sed -i 's|^MOSAIC_GIT_IDENTITY=.*$|MOSAIC_GIT_IDENTITY=other-agent|' "$generated" ;;
local-shadow)
printf 'MOSAIC_GIT_IDENTITY=%s\n' "$agent" > "$home/fleet/agents/$agent.env.local"
chmod 600 "$home/fleet/agents/$agent.env.local"
;;
*) fail "unknown Git identity rejection case: $case_name" ;;
esac
chmod 600 "$generated"
: > "$TMUX_CALLS"
if output=$(run_start "$home" "$agent" 2>&1); then
fail "Git identity case $case_name was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before Git identity $case_name rejection"
contains_literal "$output" "code=$expected_code" || \
fail "Git identity $case_name diagnostic omitted code $expected_code"
}
assert_git_identity_rejected missing missing-key
assert_git_identity_rejected unsafe unsafe-git-identity
assert_git_identity_rejected mismatch git-identity-mismatch
assert_git_identity_rejected local-shadow generated-key-shadow
# The generated-file parent is a security boundary too: even a private regular
# file is untrusted if its parent can be replaced or written by another user.
# Validation must happen before fake tmux receives even a has-session call.
: > "$TMUX_CALLS"
HOME_UNSAFE_PARENT="$ROOT/unsafe-parent"
write_generated "$HOME_UNSAFE_PARENT" "coder-parent"
chmod 777 "$HOME_UNSAFE_PARENT/fleet/agents"
if output=$(run_start "$HOME_UNSAFE_PARENT" coder-parent 2>&1); then
fail "generated file under a world-writable parent was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before unsafe parent rejection"
contains_literal "$output" 'code=unsafe-permissions' || fail "unsafe parent diagnostic missing"
: > "$TMUX_CALLS"
HOME_SYMLINK_PARENT="$ROOT/symlink-parent"
write_generated "$HOME_SYMLINK_PARENT" "coder-symlink-parent"
mv "$HOME_SYMLINK_PARENT/fleet/agents" "$HOME_SYMLINK_PARENT/private-agents"
ln -s "$HOME_SYMLINK_PARENT/private-agents" "$HOME_SYMLINK_PARENT/fleet/agents"
if output=$(run_start "$HOME_SYMLINK_PARENT" coder-symlink-parent 2>&1); then
fail "generated file under a symlinked parent was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before symlinked parent rejection"
contains_literal "$output" 'code=unsafe-directory' || fail "symlinked parent diagnostic missing"
# Every managed ancestor is a boundary: MOSAIC_HOME, fleet, and agents. A
# symlink or group/world-writable ancestor must fail before environment parsing,
# workdir creation, or tmux effects. The malformed local input proves parsing
# was not reached when the ancestor rejection is reported.
assert_managed_ancestor_rejected() {
local ancestor="$1"
local hazard="$2"
local home="$ROOT/managed-${ancestor//\//-}-${hazard}"
local agent="coder-managed-${ancestor//\//-}-${hazard}"
local node
write_generated "$home" "$agent"
printf 'MOSAIC_AGENT_COMMAND=must-not-be-parsed\n' > "$home/fleet/agents/$agent.env.local"
chmod 600 "$home/fleet/agents/$agent.env.local"
rm -rf "$home/work"
case "$ancestor" in
MOSAIC_HOME) node="$home" ;;
MOSAIC_HOME/fleet) node="$home/fleet" ;;
MOSAIC_HOME/fleet/agents) node="$home/fleet/agents" ;;
*) fail "unknown managed ancestor: $ancestor" ;;
esac
if [ "$hazard" = symlink ]; then
local target="${node}-target"
mv "$node" "$target"
ln -s "$target" "$node"
else
chmod 777 "$node"
fi
: > "$TMUX_CALLS"
if output=$(run_start "$home" "$agent" 2>&1); then
fail "${hazard} $ancestor was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before $hazard $ancestor rejection"
[ ! -e "$home/work" ] || fail "workdir was created before $hazard $ancestor rejection"
contains_literal "$output" 'code=unsafe-' || fail "managed ancestor diagnostic missing"
if contains_literal "$output" 'key=MOSAIC_AGENT_COMMAND'; then
fail "environment parsing ran before $hazard $ancestor rejection"
fi
}
for managed_ancestor in MOSAIC_HOME MOSAIC_HOME/fleet MOSAIC_HOME/fleet/agents; do
assert_managed_ancestor_rejected "$managed_ancestor" symlink
assert_managed_ancestor_rejected "$managed_ancestor" group-world-writable
done
# A local file cannot shadow any roster-derived generated key. Validation must
# happen before fake tmux receives even a has-session call.
: > "$TMUX_CALLS"
HOME_SHADOW="$ROOT/shadow"
write_generated "$HOME_SHADOW" "coder1"
printf 'MOSAIC_AGENT_RUNTIME=codex\n' > "$HOME_SHADOW/fleet/agents/coder1.env.local"
chmod 600 "$HOME_SHADOW/fleet/agents/coder1.env.local"
if output=$(run_start "$HOME_SHADOW" coder1 2>&1); then
fail "generated-key shadow was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before generated-key shadow rejection"
contains_literal "$output" 'key=MOSAIC_AGENT_RUNTIME' || fail "shadow diagnostic omitted key"
contains_literal "$output" 'sha256=' || fail "shadow diagnostic omitted hash"
if contains_literal "$output" codex; then
fail "shadow diagnostic leaked value"
fi
# Arbitrary command compatibility is quarantined/rejected as data. Diagnostics
# may name the key and hash but must never echo the privileged command text.
: > "$TMUX_CALLS"
HOME_COMMAND="$ROOT/command"
write_generated "$HOME_COMMAND" "coder2"
COMMAND_VALUE='mosaic yolo codex --dangerous'
printf 'MOSAIC_AGENT_COMMAND=%s\n' "$COMMAND_VALUE" > "$HOME_COMMAND/fleet/agents/coder2.env.local"
chmod 600 "$HOME_COMMAND/fleet/agents/coder2.env.local"
if output=$(run_start "$HOME_COMMAND" coder2 2>&1); then
fail "arbitrary command override was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before command rejection"
contains_literal "$output" 'key=MOSAIC_AGENT_COMMAND' || fail "command diagnostic omitted key"
contains_literal "$output" 'sha256=' || fail "command diagnostic omitted hash"
if contains_literal "$output" "$COMMAND_VALUE"; then
fail "command diagnostic leaked command value"
fi
# Group/world-readable local input is not trusted even when its syntax is safe.
: > "$TMUX_CALLS"
HOME_PERMS="$ROOT/perms"
write_generated "$HOME_PERMS" "coder3"
printf 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n' > "$HOME_PERMS/fleet/agents/coder3.env.local"
chmod 644 "$HOME_PERMS/fleet/agents/coder3.env.local"
if output=$(run_start "$HOME_PERMS" coder3 2>&1); then
fail "world-readable local input was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before permissions rejection"
contains_literal "$output" 'code=unsafe-permissions' || fail "permission diagnostic missing"
# A unit/holder-like clean bootstrap must yield a pane with trusted HOME and
# computed PATH only. The pane command itself must not carry loader, shell
# control, arbitrary sentinel, or stale bootstrap variables.
: > "$TMUX_CALLS"
HOME_PANE_BOUNDARY="$ROOT/pane-boundary/.config/mosaic"
write_generated "$HOME_PANE_BOUNDARY" "coder-pane-boundary"
PANE_TRUSTED_HOME="${HOME_PANE_BOUNDARY%/.config/mosaic}"
PANE_STALE_HOME="$ROOT/stale-home"
PANE_STALE_PATH="$ROOT/stale-bin"
PANE_BASH_ENV="$ROOT/pane-boundary.bash-env"
printf 'MOSAIC_RUNTIME_BIN=%s\n' "$FAKE_BIN" > \
"$HOME_PANE_BOUNDARY/fleet/agents/coder-pane-boundary.env.local"
chmod 600 "$HOME_PANE_BOUNDARY/fleet/agents/coder-pane-boundary.env.local"
# This case does not go through run_start, so its pane binaries come from
# MOSAIC_RUNTIME_BIN=$FAKE_BIN in the env.local written above — not from the
# symlinks install_pane_binaries planted under the generated home, which this
# launcher never consults because HOME here is the trusted parent. That is a
# legitimate resolution path, but it means dropping MOSAIC_RUNTIME_BIN from
# this case on the belief that the symlinks cover it would break the #1241
# binary check rather than exercise it.
LD_PRELOAD='/not/loaded/by-clean-bootstrap.so' \
BASH_ENV="$PANE_BASH_ENV" \
MOSAIC_UNTRUSTED_SENTINEL='must-not-reach-pane' \
HOME="$PANE_STALE_HOME" \
PATH="$PANE_STALE_PATH" \
/usr/bin/env -i \
"HOME=$PANE_TRUSTED_HOME" \
"PATH=$FAKE_BIN:/usr/bin:/bin" \
"MOSAIC_HOME=$HOME_PANE_BOUNDARY" \
"MOSAIC_TEST_TMUX_CALLS=$TMUX_CALLS" \
"MOSAIC_TEST_HOME=$PANE_TRUSTED_HOME" \
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
MOSAIC_TEST_EXECUTE_PANE=1 \
"MOSAIC_TEST_PANE_PID=$$" \
"$START" coder-pane-boundary
pane_args=$(tr '\0' '\n' < "$TMUX_CALLS")
contains_line "$pane_args" "HOME=$PANE_TRUSTED_HOME" || \
fail "pane did not restore trusted HOME"
contains_literal "$pane_args" "HOME=$PANE_STALE_HOME" && \
fail "pane inherited stale HOME"
contains_literal "$pane_args" "$PANE_STALE_PATH" && fail "pane inherited stale PATH"
for blocked in LD_PRELOAD= BASH_ENV= MOSAIC_UNTRUSTED_SENTINEL=; do
contains_literal "$pane_args" "$blocked" && fail "pane inherited $blocked"
done
check_pane_environment_boundary "$TMUX_CALLS" || \
fail "pane command did not use an adjacent /usr/bin/env -i boundary"
pane_environment=$(tr '\0' '\n' < "$HOME_PANE_BOUNDARY/fleet/pane-environment")
# Exercise the repository launcher at $START, not the independently installed
# host copy. Set-compare every declared generated projection entry with the
# launched process environment so a newly declared identity cannot be omitted
# by a hand-maintained per-variable assertion.
declared_generated_environment=$(sort "$HOME_PANE_BOUNDARY/fleet/agents/coder-pane-boundary.env.generated")
missing_or_changed_generated_environment=$(comm -23 \
<(printf '%s\n' "$declared_generated_environment") \
<(printf '%s\n' "$pane_environment" | sort))
if [ -n "$missing_or_changed_generated_environment" ]; then
missing_or_changed_keys=$(printf '%s\n' "$missing_or_changed_generated_environment" | cut -d= -f1 | paste -sd, -)
fail "runtime pane omitted or changed generated environment keys: $missing_or_changed_keys"
fi
contains_line "$pane_environment" "HOME=$PANE_TRUSTED_HOME" || \
fail "runtime pane did not receive trusted HOME"
contains_literal "$pane_environment" "$PANE_STALE_PATH" && fail "runtime pane received stale PATH"
for blocked in LD_PRELOAD= BASH_ENV= MOSAIC_UNTRUSTED_SENTINEL=; do
contains_literal "$pane_environment" "$blocked" && fail "runtime pane received $blocked"
done
# #1256. On a host with no system Node, tools/install.sh bootstraps one into
# ~/.mosaic/node/ and writes that directory to ~/.profile. The fleet unit runs
# `env -i ... bash --noprofile --norc`, so ~/.profile is never read — correctly, by
# design — and _build_runtime_bin_prefix does not list the bootstrap directory. Its
# `npm config get prefix` branch cannot cover the gap either: the installer points
# npm's prefix at ~/.npm-global, so that branch contributes the npm-global directory
# and never the Node one, however it resolves.
#
# The property under test is not "the string is in PATH". It is that the pane can
# EXECUTE a Node-shebang runtime binary — which is what `mosaic` is
# (`#!/usr/bin/env node`) and what actually failed: measured on a greenfield VM as
# `env: 'node': No such file or directory` after a clean install that reported success.
#
# So this case runs the pane for real and requires it to have run. A PATH-substring
# assertion would pass on a fix that put the directory in the wrong position, and it
# would keep passing if the pane later stopped running for some unrelated reason.
: > "$TMUX_CALLS"
HOME_NODE="$ROOT/bootstrap-node/.config/mosaic"
write_generated "$HOME_NODE" "coder-node"
NODE_PANE_HOME="${HOME_NODE%/.config/mosaic}"
NODE_BOOTSTRAP_BIN="$NODE_PANE_HOME/.mosaic/node/current/bin"
mkdir -p "$NODE_BOOTSTRAP_BIN"
# The bootstrapped runtime. It records that it ran, which is the evidence this case
# turns on: no node reachable from the pane means no marker.
cat > "$NODE_BOOTSTRAP_BIN/node" <<'SHIM'
#!/usr/bin/env bash
set -euo pipefail
env -0 > "${MOSAIC_HOME:?}/fleet/pane-environment"
SHIM
chmod +x "$NODE_BOOTSTRAP_BIN/node"
# write_generated plants its symlinks under the MOSAIC_HOME it is given; here the
# pane's HOME is the trusted parent, so the pane's view of "installed" is this
# directory instead. `pi` is what #1241 resolves against PANE_PATH; `mosaic` is what
# the pane then executes, and it is a Node script — not a bash script that would run
# anywhere and quietly hide the defect.
mkdir -p "$NODE_PANE_HOME/.npm-global/bin"
ln -sf "$FAKE_BIN/pi" "$NODE_PANE_HOME/.npm-global/bin/pi"
printf '#!/usr/bin/env node\n' > "$NODE_PANE_HOME/.npm-global/bin/mosaic"
chmod +x "$NODE_PANE_HOME/.npm-global/bin/mosaic"
# The npm branch is modelled ALIVE and still cannot close the gap, which is the
# stronger statement. An earlier draft of this case tried to model npm as absent —
# true on a real bootstrap host, where npm lives only in the Node directory — and it
# refused to run anywhere npm is in the system path, i.e. most machines. It was also
# the weaker claim: it would have proven only that a dead branch supplies nothing.
#
# On a bootstrap host the installer sets npm's prefix to ~/.npm-global. So even with
# `command -v npm` true and the branch executing, `npm config get prefix` yields the
# npm-global directory and never the Node one. The gap does not depend on whether
# that branch runs.
NODE_LAUNCHER_BIN="$ROOT/bootstrap-node-launcher-bin"
mkdir -p "$NODE_LAUNCHER_BIN"
ln -sf "$FAKE_BIN/tmux" "$NODE_LAUNCHER_BIN/tmux"
ln -sf "$FAKE_BIN/npm" "$NODE_LAUNCHER_BIN/npm"
/usr/bin/env -i \
"HOME=$NODE_PANE_HOME" \
"PATH=$NODE_LAUNCHER_BIN:/usr/bin:/bin" \
"MOSAIC_HOME=$HOME_NODE" \
"MOSAIC_TEST_TMUX_CALLS=$TMUX_CALLS" \
"MOSAIC_TEST_HOME=$NODE_PANE_HOME" \
"MOSAIC_TEST_NPM_PREFIX=$NODE_PANE_HOME/.npm-global" \
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
MOSAIC_TEST_EXECUTE_PANE=1 \
"MOSAIC_TEST_PANE_PID=$$" \
"$START" coder-node
[ -f "$HOME_NODE/fleet/pane-environment" ] || \
fail "pane could not execute a Node-shebang runtime: $NODE_BOOTSTRAP_BIN is absent from PANE_PATH (#1256)"
node_pane_environment=$(tr '\0' '\n' < "$HOME_NODE/fleet/pane-environment")
# Colon-pad and match a whole element. A regex with `(^|:)` after `.*` looks like it
# does this and does not: an anchor cannot match mid-pattern, so it silently requires
# a leading colon and rejects the directory in FIRST position — which is where THIS
# FIXTURE puts it: it runs under `env -i` with no MOSAIC_RUNTIME_BIN, so the bootstrap
# directory leads. That is a property of the fixture, not of the fix — in general the
# directory sits second, after MOSAIC_RUNTIME_BIN. The colon padding makes the
# assertion position-independent either way, which is why it is written this way and
# not with an anchor. That produced a failure reading "pane ran but PANE_PATH does not
# carry <dir>" against a PATH whose first element was that dir.
node_pane_path=":$(printf '%s\n' "$node_pane_environment" | sed -n 's/^PATH=//p' | head -1):"
case "$node_pane_path" in
*":$NODE_BOOTSTRAP_BIN:"*) ;;
*) fail "pane ran but PANE_PATH does not carry $NODE_BOOTSTRAP_BIN (PATH=$node_pane_path)" ;;
esac
write_interaction_generated() {
local home="$1"
local agent="$2"
mkdir -p "$home/fleet/agents" "$home/fleet/run" "$home/work"
chmod 700 "$home" "$home/fleet" "$home/fleet/agents" "$home/fleet/run"
printf '123e4567-e89b-12d3-a456-426614174000\n' > "$home/fleet/run/holder-owner"
chmod 600 "$home/fleet/run/holder-owner"
cat > "$home/fleet/agents/$agent.env.generated" <<EOF
MOSAIC_AGENT_NAME=$agent
MOSAIC_GIT_IDENTITY=$agent
MOSAIC_AGENT_CLASS=operator-interaction
MOSAIC_AGENT_RUNTIME=pi
MOSAIC_AGENT_MODEL=openai/gpt-5.6-sol
MOSAIC_AGENT_REASONING=high
MOSAIC_AGENT_TOOL_POLICY=operator-interaction
MOSAIC_AGENT_WORKDIR=$home/work
MOSAIC_TMUX_SOCKET=mosaic-test
EOF
chmod 600 "$home/fleet/agents/$agent.env.generated"
}
run_interaction() {
local home="$1"
local agent="$2"
HOME="$home" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
MOSAIC_TEST_HOME="$home" \
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
MOSAIC_HOME="$home" "$INTERACTION_START" "$agent"
}
write_heartbeat_local() {
local home="$1"
local agent="$2"
mkdir -p "$home/run"
cat > "$home/fleet/agents/$agent.env.local" <<EOF
MOSAIC_HEARTBEAT_RUN_DIR=$home/run
MOSAIC_HEARTBEAT_INTERVAL=1
EOF
chmod 600 "$home/fleet/agents/$agent.env.local"
}
wait_for_sidecar_status() {
local file="$1"
for _retry in $(seq 1 30); do
grep -qF 'status=ok' "$file" 2>/dev/null && return 0
sleep 0.1
done
fail "heartbeat sidecar did not resume after native marker became stale or absent"
}
# A fresh Pi-native marker is authoritative: the shell sidecar may start but
# must not overwrite Pi's busy/ok/model heartbeat. It must resume only when
# the marker is stale or absent.
HOME_NATIVE_FRESH="$ROOT/native-fresh"
write_generated "$HOME_NATIVE_FRESH" "coder-native-fresh"
write_heartbeat_local "$HOME_NATIVE_FRESH" "coder-native-fresh"
FRESH_HB="$HOME_NATIVE_FRESH/run/coder-native-fresh.hb"
printf 'ts=native\npid=1\nstatus=busy\nmodel=authoritative-model\n' > "$FRESH_HB"
touch "$FRESH_HB.native"
MOSAIC_TEST_PANE_PID=$$ run_start "$HOME_NATIVE_FRESH" coder-native-fresh
sleep 0.3
fresh_content=$(cat "$FRESH_HB")
[ "$fresh_content" = 'ts=native
pid=1
status=busy
model=authoritative-model' ] || fail "fresh native heartbeat was overwritten"
HOME_NATIVE_STALE="$ROOT/native-stale"
write_generated "$HOME_NATIVE_STALE" "coder-native-stale"
write_heartbeat_local "$HOME_NATIVE_STALE" "coder-native-stale"
STALE_HB="$HOME_NATIVE_STALE/run/coder-native-stale.hb"
printf 'ts=native\npid=1\nstatus=busy\nmodel=stale-model\n' > "$STALE_HB"
touch -t 200001010000.00 "$STALE_HB.native"
# Hold the sidecar's observation epoch constant: assertion runtime must not age
# a fresh-marker mutant into the stale state that this fixture must distinguish.
STALE_OBSERVATION_EPOCH=$(date +%s)
MOSAIC_TEST_FIXED_EPOCH="$STALE_OBSERVATION_EPOCH" \
MOSAIC_TEST_PANE_PID=$$ run_start "$HOME_NATIVE_STALE" coder-native-stale
wait_for_sidecar_status "$STALE_HB"
HOME_NATIVE_ABSENT="$ROOT/native-absent"
write_generated "$HOME_NATIVE_ABSENT" "coder-native-absent"
write_heartbeat_local "$HOME_NATIVE_ABSENT" "coder-native-absent"
ABSENT_HB="$HOME_NATIVE_ABSENT/run/coder-native-absent.hb"
printf 'ts=native\npid=1\nstatus=busy\nmodel=absent-model\n' > "$ABSENT_HB"
MOSAIC_TEST_PANE_PID=$$ run_start "$HOME_NATIVE_ABSENT" coder-native-absent
wait_for_sidecar_status "$ABSENT_HB"
# The interaction wrapper delegates to the shared strict parser before applying
# its pinned policy, so malformed projection data wins over profile diagnostics.
: > "$TMUX_CALLS"
HOME_INTERACTION_MALFORMED="$ROOT/interaction-malformed"
write_interaction_generated "$HOME_INTERACTION_MALFORMED" "interaction-malformed"
printf 'UNTRUSTED_BOOTSTRAP=value\n' >> "$HOME_INTERACTION_MALFORMED/fleet/agents/interaction-malformed.env.generated"
if output=$(run_interaction "$HOME_INTERACTION_MALFORMED" interaction-malformed 2>&1); then
fail "interaction wrapper accepted malformed generated data"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before interaction strict-parser rejection"
contains_literal "$output" 'code=unknown-key' || fail "interaction did not use shared strict parser first"
# A syntactically valid but policy-incompatible projection reaches the pinned
# interaction policy check only after strict parsing and never starts tmux.
: > "$TMUX_CALLS"
HOME_INTERACTION_POLICY="$ROOT/interaction-policy"
write_interaction_generated "$HOME_INTERACTION_POLICY" "interaction-policy"
sed -i 's|^MOSAIC_AGENT_RUNTIME=pi$|MOSAIC_AGENT_RUNTIME=codex|' \
"$HOME_INTERACTION_POLICY/fleet/agents/interaction-policy.env.generated"
if output=$(run_interaction "$HOME_INTERACTION_POLICY" interaction-policy 2>&1); then
fail "interaction wrapper accepted a policy-incompatible projection"
fi
interaction_policy_args=$(tr '\0' '\n' < "$TMUX_CALLS")
contains_literal "$interaction_policy_args" new-session && \
fail "interaction pinned-policy rejection created a tmux session"
contains_literal "$output" 'operator interaction service requires runtime pi' || \
fail "interaction pinned-policy check did not follow strict parsing"
# #1241. The pane runs `mosaic yolo <runtime>` against PANE_PATH. A binary
# missing from that path is a launch failure, and it has to be named before the
# session is created — after it, the diagnostic dies with the pane.
assert_missing_pane_binary_rejected() {
local binary="$1"
local home="$ROOT/missing-$binary"
local agent="coder-missing-$binary"
write_generated "$home" "$agent"
rm -f "$home/.npm-global/bin/$binary"
: > "$TMUX_CALLS"
local output
if output=$(MOSAIC_TEST_PANE_PID=$$ run_start "$home" "$agent" 2>&1); then
fail "launch succeeded with '$binary' absent from the pane PATH"
fi
echo "$output" | grep -qF 'code=missing-binary' || fail "missing '$binary' diagnostic missing"
echo "$output" | grep -qF "'$binary'" || fail "missing-binary diagnostic did not name $binary"
if tr '\0' '\n' < "$TMUX_CALLS" | grep -qF new-session; then
fail "launcher created a session it knew would die ($binary absent)"
fi
}
assert_missing_pane_binary_rejected mosaic
assert_missing_pane_binary_rejected pi
# #1241. tmux destroys a session when its pane command exits, so no pane PID a
# second after new-session means the runtime died on startup. This used to be a
# WARNING about the heartbeat sidecar followed by exit 0 — three layers above it
# then reported a fleet that was not running.
: > "$TMUX_CALLS"
HOME_DEAD_PANE="$ROOT/dead-pane"
write_generated "$HOME_DEAD_PANE" "coder-dead-pane"
if output=$(MOSAIC_TEST_PANE_PID='' run_start "$HOME_DEAD_PANE" coder-dead-pane 2>&1); then
fail "launcher reported success over a pane that did not survive"
fi
echo "$output" | grep -qF 'code=pane-did-not-survive' || fail "dead-pane diagnostic missing"
if echo "$output" | grep -qiF 'heartbeat'; then
fail "dead pane is still being reported as a heartbeat-sidecar problem"
fi
tr '\0' '\n' < "$TMUX_CALLS" | grep -qF new-session || \
fail "dead-pane case did not reach the launch it is measuring"
# #1241, the other way a pane fails. Above, tmux destroyed the session and
# has-session said so. Here the session is still there and no PID comes back
# after the retries — a different fault (the pane is alive but unusable, or
# tmux is answering inconsistently) that an operator has to be told apart from
# a runtime that died on startup.
#
# This case exists because the branch that handles it shipped with nothing able
# to reach it: the shim answered has-session only for the holder, so every
# non-holder agent landed in the session-is-gone branch no matter what. A
# defensive branch nothing exercises is the same shape as the bug this whole
# change is about, one layer down.
: > "$TMUX_CALLS"
HOME_NO_PID="$ROOT/pane-no-pid"
write_generated "$HOME_NO_PID" "coder-no-pid"
if output=$(MOSAIC_TEST_PANE_PID='' MOSAIC_TEST_HELD_SESSIONS='=coder-no-pid:0.0' \
run_start "$HOME_NO_PID" coder-no-pid 2>&1); then
fail "launcher reported success over a session with no resolvable pane PID"
fi
echo "$output" | grep -qF 'code=pane-pid-unresolved' || \
fail "session-present/no-PID was not reported as pane-pid-unresolved: $output"
if echo "$output" | grep -qF 'code=pane-did-not-survive'; then
fail "a session tmux still reports was diagnosed as a destroyed session"
fi
if echo "$output" | grep -qiF 'heartbeat'; then
fail "an unresolvable pane PID is still being reported as a heartbeat-sidecar problem"
fi
# Exact stop derives the socket exclusively from the validated generated
# projection and ignores an ambient socket supplied by the caller.
: > "$TMUX_CALLS"
HOME_STOP="$ROOT/stop"
write_generated "$HOME_STOP" "coder-stop"
HOME="$HOME_STOP" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
MOSAIC_TEST_HOME="$HOME_STOP" \
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
MOSAIC_HOME="$HOME_STOP" MOSAIC_TMUX_SOCKET=ambient-socket "$START" --stop coder-stop
stop_args=$(tr '\0' '\n' < "$TMUX_CALLS")
contains_line "$stop_args" mosaic-test || fail "exact stop did not use the validated generated socket"
contains_line "$stop_args" kill-session || fail "exact stop did not request session termination"
contains_line "$stop_args" '=coder-stop' || fail "exact stop did not exact-match the generated agent name"
if contains_literal "$stop_args" ambient-socket; then
fail "exact stop trusted an ambient socket"
fi
echo 'ok - start-agent-session generated environment boundary'