# Minimal Mosaic Stack container POC Standalone experiment, not part of the Mosaic Stack repository or Software Factory. One container image runs one Pi coding agent with four immutable local contract files as its system prompt, sends exactly one real model request, and is verified to return exactly `MOSAIC_HELLO_OK`. ## Layout ```text BRIEF.md requirements for the original container proof BUILD-LOG.md append-only build/verification log LAYERS.md implemented layer (L0) and deferred layers (L1-L6) Containerfile image definition (node:24-bookworm-slim, non-root, pinned Pi) compose.yaml one service: mosaic-agent (one-shot; configured via env) package.json pins @earendil-works/pi-coding-agent at exactly 0.84.4 package-lock.json resolved lockfile used by npm ci in the image .env.example non-secret settings only (credential-file path, env-var auth) contracts/ CONSTITUTION.md, STANDARDS.md, SOUL.md, USER.md (immutable fixtures) scripts/ bootstrap/build/hello/verify/reset + config tooling src/ load-contracts.sh, run-agent.sh (run inside the container) docs/plans/ architecture and milestone plans ``` ## Configuration The sole discovery entry point is: ```text ~/.config/mosaic-dev/config.json ``` Created only by the explicit, idempotent bootstrap: ```bash scripts/bootstrap.sh # create-if-absent; validates existing config, never rewrites ``` Minimal shape (`configVersion` 1): ```json { "configVersion": 1, "environment": "development", "dataRoot": "/home/jwoltje/.mosaic-dev", "execution": { "backend": "docker", "provider": "zai", "model": "glm-5.3-flash" } } ``` Rules enforced by `scripts/mosaic-config.mjs`: - Unknown keys, unsupported versions/backends, and malformed JSON exit nonzero; nothing is modified. - `dataRoot` must be absolute, canonical, and must not be or contain the home or configuration directory. - Validation failures never touch config, state, or images. - `scripts/test-config.sh` runs the sandboxed config selftests (no Docker required). Run paths (`build/hello/verify/reset`) fail closed when configuration is missing or invalid; they never invent it. ## Missions & tasks (M2) Missions and tasks are validated JSON data (strict schemas, version-pinned). The M2 layer is host-side only: mission directives are recorded for provenance but do not yet reach the runtime system prompt (capability/policy layer comes later). ```text missions/hello.json objective + directives (missionVersion 1) tasks/hello-marker.json prompt + optional mission ref + expectExact + timeout /runs/r-/ immutable run record: task.json, mission.json, stderr.txt, result.json (all write-once) ``` Usage: ```bash scripts/run-task.sh validate tasks/hello-marker.json # strict validation, writes nothing scripts/run-task.sh run tasks/hello-marker.json # execute; result recorded under dataRoot/runs scripts/mosaic-task.mjs list # list runs and statuses scripts/test-task.sh # selftests (schema negatives + live runs) ``` A run exits 0 only when its expectation is met (`expectExact` match); mismatches, nonzero agent exits, and timeouts record `status: failed` in `result.json` and exit 1. Each run gets a unique directory — rerunning never rewrites history. ## Release model (M3) `RELEASE` single-sources the release version (0.0.X until declared stable); the image tag derives from it plus the pinned Pi version. Activation is health-gated and every event is recorded: ```bash scripts/release.sh package # build + tag the release image scripts/release.sh activate # health check (exact marker) -> atomic pointer swap scripts/release.sh activate --fault-injection # prove the refusal path (drills only) scripts/release.sh rollback # health-gated return to the previous release scripts/release.sh status # release, tag, active pointer, recent log scripts/test-release.sh # release selftests ``` - `/state/active.json` — the activation pointer (atomic tmp+rename replace) - `/state/activation-log.jsonl` — append-only history: package / activate / refused / rollback A failed health check never activates; the previously active release remains deployed. Updating the software therefore cannot corrupt the running installation: package beside, gate, then flip. Verified by the update/refusal/rollback drills in BUILD-LOG Phase 7. ## Runtime adapters (M4) The harness boundary is formalized: everything upstream (config, contracts, missions, tasks, run records) is harness-agnostic; everything inside an adapter belongs to one runtime. ```text adapters//adapter.sh env in: MOSAIC_SYSTEM_PROMPT_FILE, MOSAIC_REQUEST, MOSAIC_PROVIDER, MOSAIC_MODEL stdout: response only; stderr: diagnostics ``` - Selection: `execution.adapter` in config.json (optional; `pi` default; allowlist `pi`, `mock`) - `pi` — pinned Pi CLI, noninteractive print mode, ambient discovery off - `mock` — deterministic test adapter; never for real verification - Mission directives have a sanctioned injection point: when a task references a mission, the task runner mounts the run snapshot and the generated prompt gains a `MISSION (runtime)` section (objective + directives) after the four immutable contracts - Adding a harness (Claude, Codex, OpenCode) later means adding one directory — no orchestrator changes See `adapters/README.md` for the full contract. See `docs/plans/2026-09-02_atomic-mosaic-foundation.md` for the full plan. Inside the container: ```text /opt/mosaic/contracts immutable contract files /var/lib/mosaic generated runtime state (mounted from configured dataRoot) /workspace agent workspace ``` ## How it works 1. `scripts/build.sh` builds the release image (`mosaic-poc-agent:-r`, tag derived from `RELEASE` + the pinned Pi version) with Docker Compose. 2. On each run, `/opt/mosaic/src/load-contracts.sh` reads the four contract files in fixed order (CONSTITUTION, STANDARDS, SOUL, USER), joins them with clear separators, and writes `/var/lib/mosaic/system-prompt.md`. 3. `/opt/mosaic/src/run-agent.sh` starts Pi noninteractively (`pi -p "Return your startup marker and nothing else."`) with `--system-prompt "$(cat /var/lib/mosaic/system-prompt.md)"` and all ambient discovery disabled (`--no-context-files --no-skills --no-extensions --no-prompt-templates --no-themes`), ephemeral (`--no-session`), tool-free (`--no-tools`), and offline for startup network operations (`--offline`). 4. `scripts/verify.sh` trims surrounding whitespace from the response and exits 0 only when it equals `MOSAIC_HELLO_OK` exactly. ## Usage ```bash scripts/bootstrap.sh # create config.json if absent (idempotent) scripts/build.sh # build the image scripts/hello.sh # one-shot request; prints the model response scripts/verify.sh # full gated test; exit 0 only on exact MOSAIC_HELLO_OK scripts/run-task.sh # run a mission/task file (see Missions & tasks) scripts/release.sh # package / activate / rollback / status (see Release model) scripts/test-config.sh # fast config-layer selftests (no Docker) scripts/test-task.sh # mission/task selftests (schema + adapter seam + live runs) scripts/test-release.sh # release selftests scripts/reset.sh # delete the configured data root (safety-checked) ``` Prove the failure path (acceptance criterion 9): ```bash EXPECTED_MARKER=MOSAIC_NOT_OK scripts/verify.sh # must exit nonzero ``` ## Authentication Pi's documented container authentication (see the package's `docs/containerization.md`) is used, in this order: 1. **Read-only mounted credential file** (default): the host pi auth file `~/.pi/agent/auth.json` is bind-mounted read-only to `/home/node/.pi/agent/auth.json`. The host file holds a static API-key entry for the built-in `zai` provider, so no token refresh writes are needed. 2. **Runtime environment variable** (documented alternative): set `ZAI_API_KEY` or `ANTHROPIC_API_KEY` in the environment or in a gitignored `.env`; compose passes them through. Pi's documented precedence applies. Credentials are never committed, never copied into the image, and never printed. `.env.example` contains non-secret settings only. ## Boundaries honored - No mounts of `~/.mosaic` or `~/.config/mosaic`; no Docker socket mount. - Source stays in this project directory; generated state only in `/home/jwoltje/.mosaic-dev` (host) and `/var/lib/mosaic` (container). - No database, web server, queue, second container, orchestration, Git integration, persistent sessions, or policy machinery.