chore: consolidate new foundation and archive v1 (#1495)
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# Mosaic tmux Fleet PoC
|
||||
|
||||
This directory contains the first durable tmux-backed fleet primitives for the
|
||||
Mosaic software-factory model.
|
||||
|
||||
The lifecycle model follows the organization-neutral AI Guide playbook
|
||||
`mosaicstack/aiguide:playbooks/tmux-fleet.md` (commit `2a0b0b5`): a dedicated
|
||||
holder owns the tmux server/socket; agent units join it and stop only their own
|
||||
exact-match session.
|
||||
|
||||
## Layout
|
||||
|
||||
- `mosaic-tmux-holder.service` — user-mode holder that owns the named tmux server.
|
||||
- `[email protected]` — user-mode template for one reusable agent session.
|
||||
- `[email protected]` — generic Pi operator-interaction template
|
||||
that fails fast when its pinned runtime policy is incomplete or changed.
|
||||
- `test-fleet-units.sh` — validates unit syntax and required relationships.
|
||||
|
||||
The agent template calls:
|
||||
|
||||
```text
|
||||
~/.config/mosaic/tools/fleet/start-agent-session.sh <agent-name>
|
||||
```
|
||||
|
||||
which starts or reuses a tmux session on `MOSAIC_TMUX_SOCKET`.
|
||||
|
||||
## Generated environment and local data
|
||||
|
||||
The roster-derived projection is written outside the package at:
|
||||
|
||||
```text
|
||||
~/.config/mosaic/fleet/agents/<agent>.env.generated
|
||||
```
|
||||
|
||||
Systemd does not read either environment file. It starts the launcher with a fixed cleared bootstrap
|
||||
environment; before it creates, queries, or stops an exact agent tmux session, `start-agent-session.sh`
|
||||
strictly parses the generated projection and the optional local data file:
|
||||
|
||||
```text
|
||||
~/.config/mosaic/fleet/agents/<agent>.env.local
|
||||
```
|
||||
|
||||
The local file may contain only safe machine-specific data (`MOSAIC_RUNTIME_BIN`, heartbeat paths or
|
||||
interval, and Claude configuration paths). It cannot override roster-derived keys, carry a command,
|
||||
or contain secret-like/unknown keys. Both files must be private regular files. Do not hand-edit the
|
||||
generated projection; update the roster and regenerate it instead. A legacy `<agent>.env` is
|
||||
consumed only for regeneration, strict relocation, or private quarantine and is never launch input.
|
||||
|
||||
See `docs/fleet/reference/generated-env-boundary.md` for the full contract.
|
||||
|
||||
## Manual canary sequence
|
||||
|
||||
Use the roster and the supported installer; do not pre-create the agent environment directory or
|
||||
edit a generated projection. `mosaic fleet install` validates the roster, installs the units and
|
||||
helpers, and writes private roster-derived projections before any service is started.
|
||||
|
||||
```bash
|
||||
# Create a site-owned canary roster. Inspect an existing roster before using --force.
|
||||
mosaic fleet init --profile minimal --write
|
||||
mosaic fleet install
|
||||
systemctl --user daemon-reload
|
||||
mosaic fleet start canary-pi
|
||||
tmux -L mosaic-fleet ls
|
||||
```
|
||||
|
||||
For an operator-interaction service, first put `<agent-name>` in the roster with the pinned Pi
|
||||
runtime, model, reasoning, and `operator-interaction` tool policy. Re-run `mosaic fleet install` after
|
||||
that roster change so it writes `<agent-name>.env.generated`; ambient `MOSAIC_AGENT_*` values are not
|
||||
launch authority. The generic unit instance uses that generated identity, and no service source is
|
||||
renamed for an instance:
|
||||
|
||||
```bash
|
||||
mosaic fleet install
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start mosaic-interaction-agent@<agent-name>.service
|
||||
~/.config/mosaic/tools/fleet/print-interaction-effective-policy.sh <agent-name>
|
||||
```
|
||||
|
||||
Do not use `tmux kill-server` without `-L mosaic-fleet`; this pattern is meant
|
||||
to avoid disturbing the user's default tmux server.
|
||||
@@ -0,0 +1,37 @@
|
||||
[Unit]
|
||||
Description=Mosaic tmux fleet agent %i
|
||||
Documentation=https://git.mosaicstack.dev/mosaicstack/stack
|
||||
Requires=mosaic-tmux-holder.service
|
||||
After=mosaic-tmux-holder.service
|
||||
PartOf=mosaic-tmux-holder.service
|
||||
# Do not attempt a seat before its generated env exists. `install` enables this
|
||||
# unit (WantedBy=default.target) but on a roster-v2 fleet the reconciler owns the
|
||||
# generated env, so between `install` and the first `apply`/`regen --write` there
|
||||
# is a boot window where ExecStart would run against an absent env file and the
|
||||
# launcher would fail the unit. A skipped unit is the honest state for "enabled
|
||||
# but not yet configured"; systemd re-evaluates the condition on every start, so
|
||||
# the seat comes up on the next start once the reconciler has written env.
|
||||
#
|
||||
# #1408: the reconciler writes projections into the BRAIN home when one is
|
||||
# active (~/.mosaic/fleet/agents, mirroring start-agent-session.sh's brain-home
|
||||
# resolution), and into MOSAIC_HOME on a legacy single-tree host. A single
|
||||
# config-home condition therefore skipped every seat on brain-home estates —
|
||||
# measured on two estates: 27 projections vs 0, and 5 vs 0, gate never fired.
|
||||
# Two TRIGGERING conditions (the `|` prefix ORs same-type conditions, which
|
||||
# otherwise AND): either shape arms the unit; the launcher still resolves the
|
||||
# authoritative copy itself.
|
||||
ConditionPathExists=|%h/.config/mosaic/fleet/agents/%i.env.generated
|
||||
ConditionPathExists=|%h/.mosaic/fleet/agents/%i.env.generated
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Remove loader and noninteractive-shell controls before ExecStart loads env.
|
||||
UnsetEnvironment=LD_PRELOAD BASH_ENV ENV
|
||||
RemainAfterExit=yes
|
||||
# Never preload the projection. The launcher starts from a fixed minimal
|
||||
# environment and strictly validates generated/local data before tmux effects.
|
||||
ExecStart=/usr/bin/env -i HOME=%h MOSAIC_AGENT_NAME=%i PATH=/usr/bin:/bin /bin/bash --noprofile --norc %h/.config/mosaic/tools/fleet/start-agent-session.sh %i
|
||||
ExecStop=-/usr/bin/env -i HOME=%h MOSAIC_AGENT_NAME=%i PATH=/usr/bin:/bin /bin/bash --noprofile --norc %h/.config/mosaic/tools/fleet/start-agent-session.sh --stop %i
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=Mosaic operator interaction agent %i
|
||||
Documentation=https://git.mosaicstack.dev/mosaicstack/stack
|
||||
Requires=mosaic-tmux-holder.service
|
||||
After=mosaic-tmux-holder.service
|
||||
PartOf=mosaic-tmux-holder.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Remove loader and noninteractive-shell controls before ExecStart loads env.
|
||||
UnsetEnvironment=LD_PRELOAD BASH_ENV ENV
|
||||
RemainAfterExit=yes
|
||||
# The interaction wrapper delegates to the shared strict parser before pinned
|
||||
# profile checks; no projection data reaches Bash through systemd.
|
||||
ExecStart=/usr/bin/env -i HOME=%h MOSAIC_AGENT_NAME=%i PATH=/usr/bin:/bin /bin/bash --noprofile --norc %h/.config/mosaic/tools/fleet/start-interaction-service.sh %i
|
||||
ExecStop=-/usr/bin/env -i HOME=%h MOSAIC_AGENT_NAME=%i PATH=/usr/bin:/bin /bin/bash --noprofile --norc %h/.config/mosaic/tools/fleet/start-agent-session.sh --stop %i
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=Mosaic lease broker daemon (framework tools/lease-broker/daemon.py)
|
||||
Documentation=https://git.mosaicstack.dev/mosaicstack/stack
|
||||
After=default.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# The broker socket lives under the runtime directory so it disappears with
|
||||
# the user session instead of surviving as stale state across logins.
|
||||
# daemon.py's secure_parent() fails closed unless this directory is exactly
|
||||
# 0700, so RuntimeDirectoryMode is not cosmetic.
|
||||
RuntimeDirectory=mosaic-lease
|
||||
RuntimeDirectoryMode=0700
|
||||
# Remove loader and noninteractive-shell controls before ExecStart loads env,
|
||||
# matching the tmux fleet units in this same directory.
|
||||
UnsetEnvironment=LD_PRELOAD BASH_ENV ENV
|
||||
ExecStart=/usr/bin/env -i HOME=%h PATH=/usr/bin:/bin XDG_RUNTIME_DIR=%t /bin/bash --noprofile --norc %h/.config/mosaic/tools/lease-broker/start-lease-broker.sh
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Mosaic tmux fleet holder
|
||||
Documentation=https://git.mosaicstack.dev/mosaicstack/stack
|
||||
After=default.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
# The holder owns the tmux server, so clear loader, shell-control, and stale
|
||||
# manager/session variables before the server process starts.
|
||||
UnsetEnvironment=LD_PRELOAD BASH_ENV ENV
|
||||
ExecStart=/usr/bin/env -i HOME=%h PATH=/usr/bin:/bin MOSAIC_TMUX_SOCKET=mosaic-fleet MOSAIC_TMUX_HOLDER=_holder /bin/bash --noprofile --norc %h/.config/mosaic/tools/fleet/start-tmux-holder.sh
|
||||
ExecStop=-/usr/bin/env -i HOME=%h PATH=/usr/bin:/bin MOSAIC_TMUX_SOCKET=mosaic-fleet /bin/bash --noprofile --norc -c 'tmux -L "$MOSAIC_TMUX_SOCKET" kill-server'
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,41 @@
|
||||
[Unit]
|
||||
# Mosaic wake FALLBACK safety drain (F7 replacement-before-retirement, EPIC #892,
|
||||
# W7). This is the framework-shipped canon-side FALLBACK WAKE: a LOW-FREQUENCY
|
||||
# SAFETY drain that fires the canon drain (digest.sh render --from-store) on a
|
||||
# per-class cadence bound, INDEPENDENT of the event-driven detector daemon
|
||||
# (mosaic-wake.service). Its whole reason to exist is that a stalled/dead detector
|
||||
# or daemon can never SILENTLY STARVE delivery: even with nothing pushing, this
|
||||
# oneshot periodically drains the durable pending-inbox so the cumulative unacked
|
||||
# set still reaches the consumer. It is the §5 retirement precondition (F7) — it
|
||||
# must be live + proven-firing BEFORE the legacy mosaic-heartbeat@ timer is reaped,
|
||||
# so there is never a coverage gap. Fixed-interval heartbeats are forbidden as the
|
||||
# PRIMARY wake mechanism (WAKE-DOCTRINE); they survive ONLY as this per-class
|
||||
# fallback cadence, bounded by urgency SLO, never as the steady state.
|
||||
#
|
||||
# This is a oneshot SERVICE activated by mosaic-wake-fallback.timer; the cadence
|
||||
# lives on the TIMER (OnUnitActiveSec), set per-class by the A10 installer via the
|
||||
# blank-reset drop-in — never here. The service therefore carries NO [Install]
|
||||
# section (the TIMER is what is enabled/wanted); it is triggered, not wanted.
|
||||
Description=Mosaic wake fallback safety drain (canon drain: digest.sh render --from-store)
|
||||
After=default.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Strip loader / noninteractive-shell controls before ExecStart, matching the
|
||||
# detector, lease-broker and tmux fleet units in this same directory (defense-in-
|
||||
# depth against an injected BASH_ENV/ENV/LD_PRELOAD in the user manager environment).
|
||||
UnsetEnvironment=LD_PRELOAD BASH_ENV ENV
|
||||
# Operator-owned runtime configuration. This EnvironmentFile carries only WAKE_*
|
||||
# NAMES (the per-agent namespace WAKE_AGENT, the lane WAKE_LANE, and — reused from
|
||||
# the detector — the pluggable adapter COMMANDS resolved BY NAME at runtime). It
|
||||
# carries NEVER any secret and NEVER any endpoint value. The '-' prefix keeps a
|
||||
# missing file from masking the installer's dedicated fail-closed install-validate.
|
||||
EnvironmentFile=-%h/.config/mosaic/wake/fallback.env
|
||||
# THE CANON DRAIN. digest.sh render --from-store drains the durable pending-inbox
|
||||
# (store.sh drain) and renders the cumulative-state digest. Running it here, on the
|
||||
# timer cadence, is the safety net: it is the SAME drain the delivery path uses, so
|
||||
# a stalled detector cannot starve it. Delivery/paste of the rendered digest is the
|
||||
# same operator-wired send seam the detector path uses (out of framework scope);
|
||||
# this unit guarantees the DRAIN fires on a bounded cadence regardless of detector
|
||||
# health. digest render exits 0 on an empty inbox, so a quiet cycle is a clean no-op.
|
||||
ExecStart=/bin/bash --noprofile --norc %h/.config/mosaic/tools/wake/digest.sh render --from-store
|
||||
@@ -0,0 +1,30 @@
|
||||
[Unit]
|
||||
# Cadence timer for the Mosaic wake FALLBACK safety drain (F7, EPIC #892, W7).
|
||||
# Drives mosaic-wake-fallback.service on a LOW-FREQUENCY per-class cadence bound,
|
||||
# INDEPENDENT of the event-driven detector daemon, so a stalled detector can never
|
||||
# silently starve delivery. This is the framework-shipped canon-side FALLBACK WAKE:
|
||||
# a heartbeat-shaped timer that survives ONLY as the per-class fallback cadence
|
||||
# (WAKE-DOCTRINE) bounded by urgency SLO — never the steady-state wake mechanism.
|
||||
Description=Mosaic wake fallback cadence timer (per-class safety wake)
|
||||
After=default.target
|
||||
|
||||
[Timer]
|
||||
# BASE cadence placeholder. The A10 installer OVERRIDES this per-class from the
|
||||
# watch-list schema's per-class `fallback_cadence` bound, via a BLANK-RESET drop-in
|
||||
# (an empty OnUnitActiveSec= reset line, then the new value) written under
|
||||
# mosaic-wake-fallback.timer.d/. systemd merges base + drop-ins so exactly ONE
|
||||
# effective OnUnitActiveUSec results (wake-install.sh verify-single). The base value
|
||||
# here is a conservative safety floor for a host installed before any per-class
|
||||
# drop-in is written — it is deliberately low-frequency (never the primary wake).
|
||||
OnUnitActiveSec=1h
|
||||
# Also fire shortly after boot so a freshly-booted host does not wait a full cadence
|
||||
# for its first safety drain. OnBootSec is a distinct key from OnUnitActiveSec and
|
||||
# does NOT count toward the exactly-one-OnUnitActiveUSec blank-reset invariant.
|
||||
OnBootSec=15min
|
||||
# Catch up a missed elapse (host asleep/off) rather than silently skipping it — a
|
||||
# fallback that silently skips is exactly the starvation this unit exists to prevent.
|
||||
Persistent=true
|
||||
Unit=mosaic-wake-fallback.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
# Mosaic wake DETECTOR daemon (A1/W7 of the wake canon, EPIC #892). A LONG-LIVED
|
||||
# single-instance detector: tools/wake/detector.sh run. This is a SERVICE, not a
|
||||
# timer — the per-class SLO lives INSIDE the daemon's run-loop (WAKE_DETECTOR_INTERVAL
|
||||
# poll cadence + the per-cycle off-host beacon emit), never as a systemd
|
||||
# OnUnitActiveSec interval. The blank-reset cadence idiom therefore does NOT apply
|
||||
# to this unit; it applies only to the legacy mosaic-heartbeat@ timer during retire.
|
||||
Description=Mosaic wake detector daemon (framework tools/wake/detector.sh run)
|
||||
After=default.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Strip loader / noninteractive-shell controls before ExecStart, matching the
|
||||
# lease-broker and tmux fleet units in this same directory (defense-in-depth
|
||||
# against an injected BASH_ENV/ENV/LD_PRELOAD in the user manager environment).
|
||||
UnsetEnvironment=LD_PRELOAD BASH_ENV ENV
|
||||
# Operator-owned runtime configuration. This EnvironmentFile carries only the
|
||||
# WAKE_* NAMES and the pluggable adapter COMMANDS (the off-host beacon/alarm sink
|
||||
# and the HMAC key NAME) — NEVER the HMAC key material and NEVER the alarm
|
||||
# endpoint value. Both are resolved BY NAME at runtime via load_credentials, so
|
||||
# no secret and no endpoint is ever written into this unit. The installer's
|
||||
# fail-closed install-validate (wake-install.sh validate-targets) is what proves
|
||||
# the required names are configured + reachable BEFORE this unit is enabled; the
|
||||
# '-' prefix keeps a missing file from masking that dedicated validation.
|
||||
EnvironmentFile=-%h/.config/mosaic/wake/detector.env
|
||||
ExecStart=/bin/bash --noprofile --norc %h/.config/mosaic/tools/wake/detector.sh run
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||
HOLDER="$SCRIPT_DIR/mosaic-tmux-holder.service"
|
||||
AGENT="$SCRIPT_DIR/[email protected]"
|
||||
INTERACTION="$SCRIPT_DIR/[email protected]"
|
||||
HOLDER_START="$SCRIPT_DIR/../../tools/fleet/start-tmux-holder.sh"
|
||||
START_AGENT="$SCRIPT_DIR/../../tools/fleet/start-agent-session.sh"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -f "$HOLDER" ] || fail "missing mosaic-tmux-holder.service"
|
||||
[ -f "$AGENT" ] || fail "missing [email protected]"
|
||||
[ -f "$INTERACTION" ] || fail "missing [email protected]"
|
||||
[ -x "$HOLDER_START" ] || fail "missing executable start-tmux-holder.sh"
|
||||
[ -x "$START_AGENT" ] || fail "missing executable start-agent-session.sh"
|
||||
|
||||
grep -qF 'ExecStart=' "$HOLDER" || fail "holder has no ExecStart"
|
||||
grep -qF 'tmux -L' "$HOLDER" || fail "holder does not use named tmux socket"
|
||||
grep -qF '_holder' "$HOLDER" || fail "holder session is not explicit"
|
||||
grep -qF 'UnsetEnvironment=LD_PRELOAD BASH_ENV ENV' "$HOLDER" || \
|
||||
fail "holder does not remove loader and shell-control variables"
|
||||
grep -qF 'ExecStart=/usr/bin/env -i HOME=%h PATH=/usr/bin:/bin MOSAIC_TMUX_SOCKET=mosaic-fleet MOSAIC_TMUX_HOLDER=_holder /bin/bash --noprofile --norc %h/.config/mosaic/tools/fleet/start-tmux-holder.sh' "$HOLDER" || \
|
||||
fail "holder does not clear manager environment before starting tmux"
|
||||
grep -qF 'ExecStop=-/usr/bin/env -i HOME=%h PATH=/usr/bin:/bin MOSAIC_TMUX_SOCKET=mosaic-fleet /bin/bash --noprofile --norc -c' "$HOLDER" || \
|
||||
fail "holder stop does not clear manager environment"
|
||||
if grep -qF -- '/bin/bash -lc' "$HOLDER"; then
|
||||
fail "holder must not start tmux through a login shell"
|
||||
fi
|
||||
grep -qF 'Requires=mosaic-tmux-holder.service' "$AGENT" || fail "agent does not require holder"
|
||||
# #1408: the projection condition must arm on EITHER home shape. Both lines must
|
||||
# carry the `|` triggering prefix — same-type conditions without it AND together,
|
||||
# which can never be true (one file cannot exist at two paths), so a bare-spelling
|
||||
# regression would disable autostart everywhere while reading as "has a condition".
|
||||
grep -qF 'ConditionPathExists=|%h/.config/mosaic/fleet/agents/%i.env.generated' "$AGENT" || \
|
||||
fail "agent lacks triggering condition for the config home projection"
|
||||
grep -qF 'ConditionPathExists=|%h/.mosaic/fleet/agents/%i.env.generated' "$AGENT" || \
|
||||
fail "agent lacks triggering condition for the brain home projection (#1408)"
|
||||
if grep -qE '^ConditionPathExists=[^|]' "$AGENT"; then
|
||||
fail "agent has a non-triggering ConditionPathExists — same-type conditions AND, re-arming #1408"
|
||||
fi
|
||||
grep -qF 'start-agent-session.sh' "$AGENT" || fail "agent unit does not call start-agent-session.sh"
|
||||
if grep -qE '^Environment(File)?=' "$AGENT" "$INTERACTION"; then
|
||||
fail "agent units must not accept ambient or projection environment before strict parsing"
|
||||
fi
|
||||
grep -qF 'UnsetEnvironment=LD_PRELOAD BASH_ENV ENV' "$AGENT" || \
|
||||
fail "agent unit does not remove loader and shell-control variables"
|
||||
grep -qF 'UnsetEnvironment=LD_PRELOAD BASH_ENV ENV' "$INTERACTION" || \
|
||||
fail "interaction unit does not remove loader and shell-control variables"
|
||||
grep -qF 'ExecStart=/usr/bin/env -i HOME=%h MOSAIC_AGENT_NAME=%i PATH=/usr/bin:/bin /bin/bash --noprofile --norc' "$AGENT" || \
|
||||
fail "agent unit does not clear bootstrap environment before strict parsing"
|
||||
grep -qF 'start-agent-session.sh --stop %i' "$AGENT" || \
|
||||
fail "agent stop does not use the validated exact-stop path"
|
||||
grep -qF 'Requires=mosaic-tmux-holder.service' "$INTERACTION" || fail "interaction service does not require holder"
|
||||
grep -qF 'ExecStart=/usr/bin/env -i HOME=%h MOSAIC_AGENT_NAME=%i PATH=/usr/bin:/bin /bin/bash --noprofile --norc' "$INTERACTION" || \
|
||||
fail "interaction unit does not clear bootstrap environment before strict parsing"
|
||||
grep -qF 'start-interaction-service.sh %i' "$INTERACTION" || fail "interaction service does not use shared strict parsing"
|
||||
grep -qF 'start-agent-session.sh --stop %i' "$INTERACTION" || \
|
||||
fail "interaction stop does not use the validated exact-stop path"
|
||||
|
||||
if command -v systemd-analyze >/dev/null 2>&1; then
|
||||
systemd-analyze verify --user "$HOLDER" "$AGENT" "$INTERACTION" >/tmp/mosaic-fleet-systemd-verify.log 2>&1 || {
|
||||
cat /tmp/mosaic-fleet-systemd-verify.log >&2
|
||||
fail "systemd-analyze verify failed"
|
||||
}
|
||||
fi
|
||||
|
||||
# Real isolated socket regression: a preexisting server with an LD_PRELOAD
|
||||
# constructor marker must fail closed, while a fresh named server is created.
|
||||
if command -v tmux >/dev/null 2>&1 && command -v cc >/dev/null 2>&1; then
|
||||
TEST_ROOT=$(mktemp -d)
|
||||
TEST_SOCKET="mosaic-holder-test-$$"
|
||||
trap 'tmux -L "$TEST_SOCKET" kill-server >/dev/null 2>&1 || true; rm -rf "$TEST_ROOT"' EXIT
|
||||
MARKER="$TEST_ROOT/loader-marker"
|
||||
LIBRARY="$TEST_ROOT/marker.so"
|
||||
FIXTURE_READY="$TEST_ROOT/loader-ready"
|
||||
FIXTURE_FIFO="$TEST_ROOT/loader-block"
|
||||
HOLDER_HOME="$TEST_ROOT/holder-home"
|
||||
mkfifo "$FIXTURE_FIFO"
|
||||
mkdir -p "$HOLDER_HOME/.config/mosaic/fleet/run"
|
||||
chmod 700 "$HOLDER_HOME/.config" "$HOLDER_HOME/.config/mosaic" \
|
||||
"$HOLDER_HOME/.config/mosaic/fleet" "$HOLDER_HOME/.config/mosaic/fleet/run"
|
||||
printf '123e4567-e89b-12d3-a456-426614174000\n' > \
|
||||
"$HOLDER_HOME/.config/mosaic/fleet/run/holder-owner"
|
||||
chmod 600 "$HOLDER_HOME/.config/mosaic/fleet/run/holder-owner"
|
||||
cat > "$TEST_ROOT/marker.c" <<'EOF'
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
__attribute__((constructor)) static void mark_loader(void) {
|
||||
const char *path = getenv("MOSAIC_LOADER_MARKER");
|
||||
if (path != NULL) {
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0600);
|
||||
if (fd >= 0) { write(fd, "loaded\\n", 7); close(fd); }
|
||||
}
|
||||
}
|
||||
EOF
|
||||
cc -shared -fPIC -o "$LIBRARY" "$TEST_ROOT/marker.c"
|
||||
MOSAIC_LOADER_MARKER="$MARKER" LD_PRELOAD="$LIBRARY" \
|
||||
tmux -L "$TEST_SOCKET" new-session -d -s _holder \
|
||||
"touch '$FIXTURE_READY'; read _ < '$FIXTURE_FIFO'"
|
||||
# tmux starts the pane asynchronously. Wait until its contaminated shell has
|
||||
# loaded the constructor and reached a builtin-only FIFO barrier before
|
||||
# clearing the marker; otherwise that expected constructor can race with the
|
||||
# clean holder assertion below and create a false failure.
|
||||
for _attempt in {1..100}; do
|
||||
[ -e "$FIXTURE_READY" ] && break
|
||||
sleep 0.01
|
||||
done
|
||||
[ -e "$FIXTURE_READY" ] || fail "contaminated fixture pane did not become ready"
|
||||
[ -s "$MARKER" ] || fail "contaminated fixture did not execute loader constructor"
|
||||
server_pid=$(tmux -L "$TEST_SOCKET" display-message -p '#{pid}')
|
||||
: > "$MARKER"
|
||||
if /usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin \
|
||||
MOSAIC_TMUX_SOCKET="$TEST_SOCKET" MOSAIC_TMUX_HOLDER=_holder "$HOLDER_START" \
|
||||
>"$TEST_ROOT/holder.out" 2>&1; then
|
||||
fail "holder adopted contaminated named server"
|
||||
fi
|
||||
grep -qF 'global environment does not match the owned-server contract' "$TEST_ROOT/holder.out" || \
|
||||
fail "holder did not report contaminated server environment"
|
||||
[ "$(tmux -L "$TEST_SOCKET" display-message -p '#{pid}')" = "$server_pid" ] || \
|
||||
fail "holder replaced a contaminated server instead of failing closed"
|
||||
[ ! -s "$MARKER" ] || fail "holder execution triggered a contaminated loader"
|
||||
|
||||
# Agent validation must reject the same unmanaged server without cleaning its
|
||||
# global environment or adding a managed session.
|
||||
AGENT_HOME="$HOLDER_HOME/.config/mosaic"
|
||||
AGENT_NAME=loader-safe
|
||||
AGENT_WORKDIR="$AGENT_HOME/work"
|
||||
AGENT_BIN="$TEST_ROOT/agent-bin"
|
||||
mkdir -p "$AGENT_HOME/fleet/agents" "$AGENT_WORKDIR" "$AGENT_BIN"
|
||||
chmod 700 "$AGENT_HOME/fleet/agents"
|
||||
cat > "$AGENT_HOME/fleet/agents/$AGENT_NAME.env.generated" <<EOF
|
||||
MOSAIC_AGENT_NAME=$AGENT_NAME
|
||||
MOSAIC_GIT_IDENTITY=$AGENT_NAME
|
||||
MOSAIC_AGENT_CLASS=code
|
||||
MOSAIC_AGENT_RUNTIME=pi
|
||||
MOSAIC_AGENT_MODEL=
|
||||
MOSAIC_AGENT_REASONING=
|
||||
MOSAIC_AGENT_TOOL_POLICY=code
|
||||
MOSAIC_AGENT_WORKDIR=$AGENT_WORKDIR
|
||||
MOSAIC_TMUX_SOCKET=$TEST_SOCKET
|
||||
EOF
|
||||
printf 'MOSAIC_RUNTIME_BIN=%s\n' "$AGENT_BIN" > "$AGENT_HOME/fleet/agents/$AGENT_NAME.env.local"
|
||||
chmod 600 "$AGENT_HOME/fleet/agents/$AGENT_NAME.env.generated" \
|
||||
"$AGENT_HOME/fleet/agents/$AGENT_NAME.env.local"
|
||||
cat > "$AGENT_BIN/mosaic" <<'EOF'
|
||||
#!/bin/sh
|
||||
sleep 30
|
||||
EOF
|
||||
chmod 700 "$AGENT_BIN/mosaic"
|
||||
# The launcher resolves the roster's runtime against PANE_PATH before it
|
||||
# spawns anything (#1241), so the runtime this projection names has to be
|
||||
# present here even though the fake `mosaic` above never execs it.
|
||||
cat > "$AGENT_BIN/pi" <<'EOF'
|
||||
#!/bin/sh
|
||||
sleep 30
|
||||
EOF
|
||||
chmod 700 "$AGENT_BIN/pi"
|
||||
server_environment_before=$(tmux -L "$TEST_SOCKET" show-environment -g | sort)
|
||||
server_sessions_before=$(tmux -L "$TEST_SOCKET" list-sessions | sort)
|
||||
if /usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin MOSAIC_HOME="$AGENT_HOME" \
|
||||
"$START_AGENT" "$AGENT_NAME" >"$TEST_ROOT/agent.out" 2>&1; then
|
||||
fail "agent launcher adopted contaminated named server"
|
||||
fi
|
||||
[ "$(tmux -L "$TEST_SOCKET" display-message -p '#{pid}')" = "$server_pid" ] || \
|
||||
fail "agent launcher changed unmanaged server PID"
|
||||
[ "$(tmux -L "$TEST_SOCKET" show-environment -g | sort)" = "$server_environment_before" ] || \
|
||||
fail "agent launcher changed unmanaged global environment"
|
||||
[ "$(tmux -L "$TEST_SOCKET" list-sessions | sort)" = "$server_sessions_before" ] || \
|
||||
fail "agent launcher changed unmanaged sessions"
|
||||
tmux -L "$TEST_SOCKET" kill-server
|
||||
/usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin \
|
||||
MOSAIC_TMUX_SOCKET="$TEST_SOCKET" MOSAIC_TMUX_HOLDER=_holder "$HOLDER_START"
|
||||
tmux -L "$TEST_SOCKET" has-session -t '=_holder:0.0' || fail "fresh holder was not created"
|
||||
ld_preload_env="$(tmux -L "$TEST_SOCKET" show-environment -g LD_PRELOAD 2>/dev/null)" || true
|
||||
if grep -q '^LD_PRELOAD=' <<<"$ld_preload_env"; then
|
||||
fail "fresh holder retained LD_PRELOAD"
|
||||
fi
|
||||
/usr/bin/env -i HOME="$HOLDER_HOME" PATH=/usr/bin:/bin MOSAIC_HOME="$AGENT_HOME" \
|
||||
"$START_AGENT" "$AGENT_NAME"
|
||||
tmux -L "$TEST_SOCKET" has-session -t "=$AGENT_NAME:0.0" || \
|
||||
fail "agent did not launch on a valid owned server"
|
||||
tmux -L "$TEST_SOCKET" kill-server
|
||||
trap - EXIT
|
||||
rm -rf "$TEST_ROOT"
|
||||
fi
|
||||
|
||||
echo "ok - fleet systemd unit templates"
|
||||
Reference in New Issue
Block a user