feat(config): wire launcher scripts and compose to config.json (#2)
- common.sh: load_config() exports MOSAIC_DATA_ROOT/PROVIDER/MODEL; fails closed - compose.yaml: dataRoot mount and provider/model are required env (:? errors) - build/hello/verify load config before any mutation; no silent bootstrap - reset.sh: target resolved from configured dataRoot; all safety checks kept Verified: compose fails without launcher env; verify/reset fail on missing config; config-driven hello+verify pass; symlink refusal with sandboxed config (canary survived); config checksum unchanged across reset+rerun. Closes #2
This commit is contained in:
+6
-5
@@ -6,17 +6,18 @@ services:
|
||||
image: mosaic-poc-agent:0.84.4
|
||||
user: "1000:1000"
|
||||
environment:
|
||||
# Non-secret settings (see .env.example)
|
||||
PI_PROVIDER: ${PI_PROVIDER:-zai}
|
||||
PI_MODEL: ${PI_MODEL:-glm-5.3-flash}
|
||||
# Resolved from config.json by scripts/common.sh (load_config).
|
||||
# Required: compose fails fast when the launcher did not supply them.
|
||||
PI_PROVIDER: ${MOSAIC_PROVIDER:?MOSAIC_PROVIDER must be set by scripts/load_config (run via scripts/*.sh)}
|
||||
PI_MODEL: ${MOSAIC_MODEL:?MOSAIC_MODEL must be set by scripts/load_config (run via scripts/*.sh)}
|
||||
# Documented container auth alternative: provider API key via
|
||||
# runtime environment variable. Empty by default; when empty Pi
|
||||
# falls back to the read-only mounted auth.json credential file.
|
||||
ZAI_API_KEY: ${ZAI_API_KEY:-}
|
||||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||
volumes:
|
||||
# Generated runtime state (host dir per brief)
|
||||
- /home/jwoltje/.mosaic-dev:/var/lib/mosaic
|
||||
# Configured runtime state root (from config.json dataRoot).
|
||||
- ${MOSAIC_DATA_ROOT:?MOSAIC_DATA_ROOT must be set by scripts/load_config (run via scripts/*.sh)}:/var/lib/mosaic
|
||||
# Runtime credential only: pi auth file mounted READ-ONLY.
|
||||
# Never copied into the image.
|
||||
- ${PI_AUTH_FILE:-/home/jwoltje/.pi/agent/auth.json}:/home/node/.pi/agent/auth.json:ro
|
||||
|
||||
@@ -5,6 +5,8 @@ cd "$(dirname "$0")/.."
|
||||
# shellcheck source=common.sh
|
||||
source scripts/common.sh
|
||||
|
||||
load_config
|
||||
|
||||
bootstrap_runtime_dir
|
||||
|
||||
docker compose build
|
||||
|
||||
+21
-5
@@ -1,11 +1,27 @@
|
||||
# Shared helpers for the POC host scripts. Not a documented entry point.
|
||||
# Shared helpers for the Mosaic host scripts. Not a documented entry point.
|
||||
|
||||
MOSAIC_DEV_DIR="/home/jwoltje/.mosaic-dev"
|
||||
POC_ROOT_MARKER=".mosaic-poc-root"
|
||||
|
||||
# Ensure the runtime state directory exists and carries this project's
|
||||
# ownership marker. The marker is what scripts/reset.sh requires before
|
||||
# it will delete anything.
|
||||
# Load and validate the Mosaic configuration (config.json), exporting
|
||||
# MOSAIC_DATA_ROOT, MOSAIC_PROVIDER, and MOSAIC_MODEL.
|
||||
#
|
||||
# Fails closed: a missing or invalid configuration aborts the calling
|
||||
# script before any container or filesystem mutation. Run paths never
|
||||
# auto-bootstrap; use scripts/bootstrap.sh explicitly.
|
||||
load_config() {
|
||||
local config_env
|
||||
if ! config_env="$(node scripts/mosaic-config.mjs env)"; then
|
||||
echo "common: configuration load failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
eval "$config_env"
|
||||
export MOSAIC_DATA_ROOT MOSAIC_PROVIDER MOSAIC_MODEL
|
||||
MOSAIC_DEV_DIR="$MOSAIC_DATA_ROOT"
|
||||
}
|
||||
|
||||
# Ensure the configured runtime data directory exists and carries this
|
||||
# project's ownership marker. The marker is what scripts/reset.sh requires
|
||||
# before it will delete anything.
|
||||
bootstrap_runtime_dir() {
|
||||
if [ ! -d "$MOSAIC_DEV_DIR" ]; then
|
||||
mkdir -p "$MOSAIC_DEV_DIR"
|
||||
|
||||
@@ -10,6 +10,8 @@ cd "$(dirname "$0")/.."
|
||||
# shellcheck source=common.sh
|
||||
source scripts/common.sh
|
||||
|
||||
load_config
|
||||
|
||||
bootstrap_runtime_dir
|
||||
|
||||
# -T: no pseudo-TTY, so stdout is clean model output.
|
||||
|
||||
+8
-2
@@ -7,9 +7,15 @@
|
||||
# created by this project.
|
||||
# Any failed check aborts with nothing deleted.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
# shellcheck source=common.sh
|
||||
source scripts/common.sh
|
||||
|
||||
TARGET="/home/jwoltje/.mosaic-dev"
|
||||
MARKER=".mosaic-poc-root"
|
||||
# Reset operates on the CONFIGURED data root. Configuration itself is
|
||||
# never a reset target; a missing/invalid configuration aborts here.
|
||||
load_config
|
||||
TARGET="$MOSAIC_DATA_ROOT"
|
||||
MARKER="$POC_ROOT_MARKER"
|
||||
|
||||
fail() {
|
||||
echo "reset: refusing to delete: $*" >&2
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fast, sandboxed selftests for the configuration layer.
|
||||
#
|
||||
# No Docker, no network, no credentials: every case runs against a
|
||||
# temporary config via MOSAIC_CONFIG. Suitable for frequent local runs.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
SANDBOX="$(mktemp -d)"
|
||||
trap 'rm -rf "$SANDBOX"' EXIT
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# expect_exit NAME EXPECTED_RC -- command...
|
||||
expect_exit() {
|
||||
local name="$1" expected="$2"
|
||||
shift 3 # name, expected, "--"
|
||||
local rc
|
||||
"$@" >/dev/null 2>&1
|
||||
rc=$?
|
||||
if [ "$rc" -eq "$expected" ]; then
|
||||
PASS=$((PASS + 1))
|
||||
echo "ok $name (exit $rc)"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
echo "FAIL $name (exit $rc, expected $expected)"
|
||||
fi
|
||||
}
|
||||
|
||||
CONFIG_OP="node scripts/mosaic-config.mjs"
|
||||
|
||||
valid_body() {
|
||||
cat <<EOF
|
||||
{"configVersion":1,"environment":"development","dataRoot":"$1","execution":{"backend":"docker","provider":"zai","model":"glm-5.3-flash"}}
|
||||
EOF
|
||||
}
|
||||
|
||||
cfg() { printf '%s' "$2" > "$SANDBOX/$1"; }
|
||||
|
||||
DATA_ROOT="$SANDBOX/data"
|
||||
|
||||
# --- bootstrap ---
|
||||
rm -f "$SANDBOX/config.json"
|
||||
expect_exit "bootstrap creates default when absent" 0 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/config.json" $CONFIG_OP bootstrap
|
||||
[ -f "$SANDBOX/config.json" ] && { PASS=$((PASS+1)); echo "ok bootstrap wrote config file"; } \
|
||||
|| { FAIL=$((FAIL+1)); echo "FAIL bootstrap wrote config file"; }
|
||||
|
||||
SUM_BEFORE=$(sha256sum "$SANDBOX/config.json" | cut -d' ' -f1)
|
||||
MTIME_BEFORE=$(stat -c %Y "$SANDBOX/config.json")
|
||||
sleep 1.1
|
||||
expect_exit "bootstrap is idempotent on existing config" 0 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/config.json" $CONFIG_OP bootstrap
|
||||
SUM_AFTER=$(sha256sum "$SANDBOX/config.json" | cut -d' ' -f1)
|
||||
MTIME_AFTER=$(stat -c %Y "$SANDBOX/config.json")
|
||||
if [ "$SUM_BEFORE" = "$SUM_AFTER" ] && [ "$MTIME_BEFORE" = "$MTIME_AFTER" ]; then
|
||||
PASS=$((PASS+1)); echo "ok bootstrap did not rewrite existing config"
|
||||
else
|
||||
FAIL=$((FAIL+1)); echo "FAIL bootstrap rewrote existing config"
|
||||
fi
|
||||
|
||||
# --- validate ---
|
||||
expect_exit "validate missing config exits 3" 3 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/absent.json" $CONFIG_OP validate
|
||||
|
||||
cfg invalid.json '{'
|
||||
expect_exit "malformed JSON exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/invalid.json" $CONFIG_OP validate
|
||||
|
||||
cfg badversion.json '{"configVersion":2,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
||||
expect_exit "unsupported configVersion exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/badversion.json" $CONFIG_OP validate
|
||||
|
||||
cfg unknownkey.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","extra":true,"execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
||||
expect_exit "unknown top-level key exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/unknownkey.json" $CONFIG_OP validate
|
||||
|
||||
cfg unknownexec.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m","extra":1}}'
|
||||
expect_exit "unknown execution key exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/unknownexec.json" $CONFIG_OP validate
|
||||
|
||||
cfg badbackend.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"podman","provider":"zai","model":"m"}}'
|
||||
expect_exit "unsupported backend exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/badbackend.json" $CONFIG_OP validate
|
||||
|
||||
cfg badenv.json '{"configVersion":1,"environment":"staging","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
||||
expect_exit "unsupported environment exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/badenv.json" $CONFIG_OP validate
|
||||
|
||||
cfg relative.json '{"configVersion":1,"environment":"development","dataRoot":"relative/path","execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
||||
expect_exit "relative dataRoot exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/relative.json" $CONFIG_OP validate
|
||||
|
||||
cfg traversal.json '{"configVersion":1,"environment":"development","dataRoot":"/tmp/../home/x","execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
||||
expect_exit "non-canonical dataRoot exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/traversal.json" $CONFIG_OP validate
|
||||
|
||||
cfg root.json '{"configVersion":1,"environment":"development","dataRoot":"/","execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
||||
expect_exit "filesystem root dataRoot exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/root.json" $CONFIG_OP validate
|
||||
|
||||
cfg home.json '{"configVersion":1,"environment":"development","dataRoot":"'$HOME'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
||||
expect_exit "home directory dataRoot exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/home.json" $CONFIG_OP validate
|
||||
|
||||
cfg cfgdir.json '{"configVersion":1,"environment":"development","dataRoot":"'$SANDBOX'","execution":{"backend":"docker","provider":"zai","model":"m"}}'
|
||||
expect_exit "dataRoot containing config dir exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/cfgdir.json" $CONFIG_OP validate
|
||||
|
||||
cfg ctrlchar.json '{"configVersion":1,"environment":"development","dataRoot":"'$DATA_ROOT'","execution":{"backend":"docker","provider":"z\nai","model":"m"}}'
|
||||
expect_exit "control character in provider exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/ctrlchar.json" $CONFIG_OP validate
|
||||
|
||||
cfg symlink.json
|
||||
ln -sf "$SANDBOX/invalid.json" "$SANDBOX/symlink.json" 2>/dev/null
|
||||
expect_exit "symlinked config file exits 2" 2 -- \
|
||||
env MOSAIC_CONFIG="$SANDBOX/symlink.json" $CONFIG_OP validate
|
||||
|
||||
# --- env resolution ---
|
||||
cfg valid.json "$(valid_body "$DATA_ROOT")"
|
||||
EVAL_OUT="$(MOSAIC_CONFIG="$SANDBOX/valid.json" $CONFIG_OP env)" || true
|
||||
if eval "$EVAL_OUT" 2>/dev/null && [ "$MOSAIC_DATA_ROOT" = "$DATA_ROOT" ] \
|
||||
&& [ "$MOSAIC_PROVIDER" = "zai" ] && [ "$MOSAIC_MODEL" = "glm-5.3-flash" ]; then
|
||||
PASS=$((PASS+1)); echo "ok env exports resolve correctly"
|
||||
else
|
||||
FAIL=$((FAIL+1)); echo "FAIL env exports resolve correctly"
|
||||
fi
|
||||
|
||||
# --- validation must not modify the file ---
|
||||
SUM_INVALID_BEFORE=$(sha256sum "$SANDBOX/invalid.json" | cut -d' ' -f1)
|
||||
MOSAIC_CONFIG="$SANDBOX/invalid.json" $CONFIG_OP validate >/dev/null 2>&1
|
||||
SUM_INVALID_AFTER=$(sha256sum "$SANDBOX/invalid.json" | cut -d' ' -f1)
|
||||
if [ "$SUM_INVALID_BEFORE" = "$SUM_INVALID_AFTER" ]; then
|
||||
PASS=$((PASS+1)); echo "ok failed validation modified nothing"
|
||||
else
|
||||
FAIL=$((FAIL+1)); echo "FAIL failed validation modified the file"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "selftest: $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
@@ -17,6 +17,8 @@ source scripts/common.sh
|
||||
IMAGE="mosaic-poc-agent:0.84.4"
|
||||
EXPECTED="${EXPECTED_MARKER:-MOSAIC_HELLO_OK}"
|
||||
|
||||
load_config
|
||||
|
||||
# 1. Build the image only if it is not already present.
|
||||
if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
|
||||
echo "verify: image $IMAGE not found, building..." >&2
|
||||
|
||||
Reference in New Issue
Block a user