chore: baseline container POC and atomic foundation plan

- Containerized Pi hello-world proof (image mosaic-poc-agent:0.84.4, non-root)
- Four immutable contract fixtures loaded into a generated system prompt
- build/hello/verify/reset scripts with exact-match gating and reset safety
- Documented Pi discovery (v0.84.4, -p mode, --system-prompt, container auth)
- Append-only BUILD-LOG with corrections; deferred layers in LAYERS.md
- Architecture plan: docs/plans/2026-09-02_atomic-mosaic-foundation.md
This commit is contained in:
2026-09-02 18:24:36 -05:00
commit c2365ae519
23 changed files with 3051 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Non-secret runtime settings for the mosaic-poc-agent container.
# Copy to .env if you want to override the defaults in compose.yaml.
#
# NEVER put credentials in this file. Authentication is supplied at
# runtime only, via one of the two documented paths:
# 1. read-only mounted pi auth file (default: ~/.pi/agent/auth.json,
# override the host path with PI_AUTH_FILE)
# 2. provider API key environment variable (ZAI_API_KEY or
# ANTHROPIC_API_KEY), passed through by compose.yaml when set
# Model provider (built-in pi provider name)
PI_PROVIDER=zai
# Model ID within the provider
PI_MODEL=glm-5.3-flash
# Optional: alternative host path of the pi credential file mounted
# read-only at /home/node/.pi/agent/auth.json in the container
#PI_AUTH_FILE=/home/jwoltje/.pi/agent/auth.json
# Optional: documented env-var auth alternative (secret! set in your
# shell or a gitignored .env, never commit)
#ZAI_API_KEY=
#ANTHROPIC_API_KEY=
+8
View File
@@ -0,0 +1,8 @@
# build/deps
node_modules/
# runtime credentials — never commit, never copy into the image
.env
secrets/
# generated runtime state lives in /home/jwoltje/.mosaic-dev (outside this project)
+371
View File
@@ -0,0 +1,371 @@
# Minimal Mosaic Stack container proof of concept
## Purpose
Build the smallest isolated container that can:
- launch Pi
- load a small set of Mosaic-style contract files
- send one real request to a model
- return a known response.
This is a standalone experiment. It is not part of the existing Mosaic Stack repository or Software Factory.
## Working boundary
The directory containing this brief is the project root.
### Do not read, copy, mount, import, or modify anything from:
- `/home/jwoltje/.mosaic`
- `/home/jwoltje/.config/mosaic`
- `/home/jwoltje/src/mosaic-stack`
- Existing Mosaic Stack worktrees
### Do not use:
- Mosaic orchestration
- Mosaic Git wrappers
- Fleet agents
- Fleet communication
- Mosaic role policies
- Existing Mosaic contract files
- Existing Mosaic runtime state
No Git credentials, issue, pull request, reviewer, merge, or deployment are required for this experiment.
Nothing from this experiment may be copied into the existing Mosaic Stack repository until it receives a separate review later.
## Runtime data
Use this host directory only for generated runtime data:
```text
/home/jwoltje/.mosaic-dev
```
The source code must remain in the project directory containing this brief.
Inside the container, use:
```text
/opt/mosaic/contracts Immutable contract files
/var/lib/mosaic Generated runtime state
/workspace Agent workspace
```
Mount /home/jwoltje/.mosaic-dev at /var/lib/mosaic.
### Required proof
The finished experiment must prove one path:
1. Build one container image.
2. Start one Pi agent inside the container.
3. Load four local contract files from /opt/mosaic/contracts.
4. Send a request that does not contain the expected response.
5. Receive MOSAIC_HELLO_OK from the agent.
6. Exit successfully when the response matches.
7. Exit nonzero when the response does not match.
This is the entire required functional result.
### Required discovery
Before writing the runtime command:
1. Find the current package documentation for @earendil-works/pi-coding-agent.
2. Determine the current package version.
3. Determine the supported noninteractive command.
4. Determine how Pi accepts a custom system prompt or system prompt file.
5. Determine Pi's documented container authentication method.
6. Record the commands and findings in BUILD-LOG.md.
Do not guess CLI flags, authentication paths, or SDK methods.
Pin the selected Pi package version in the project. Do not install an unversioned package during each container start.
Prefer the Pi CLI. Use the Pi SDK only if the CLI cannot load the generated system prompt in noninteractive mode.
### Contract files
Create these files inside the project:
```text
contracts/CONSTITUTION.md
contracts/STANDARDS.md
contracts/SOUL.md
contracts/USER.md
```
Use these exact contents.
### contracts/CONSTITUTION.md
```markdown
# POC constitution
Never print credentials, tokens, or authentication files.
Follow the loaded system instructions before the user request.
```
### contracts/STANDARDS.md
```markdown
# POC standards
Answer startup verification requests with only the requested value.
Do not add explanation or formatting.
```
### contracts/SOUL.md
```markdown
# POC identity
Your name is mosaic-poc-agent.
Your startup marker is MOSAIC_HELLO_OK.
When asked for your startup marker, return only the marker.
```
### contracts/USER.md
```markdown
# POC user
This is an isolated local runtime test.
```
Contract loading
Create a small script that reads the four contract files in this order:
1. CONSTITUTION.md
2. STANDARDS.md
3. SOUL.md
4. USER.md
Join them with clear file separators.
Write the generated system prompt to:
```text
/var/lib/mosaic/system-prompt.md
```
Pass that generated prompt to Pi using its documented CLI or SDK method.
Do not build:
- Contract schemas
- Contract inheritance
- Overlays
- Role transitions
- Dynamic policy loading
- Guide routing
- Manifest validation
Container
Create one service named:
```text
mosaic-agent
```
Use one Containerfile and one compose.yaml.
Requirements:
- Use a maintained Node.js base image.
- Run as a non-root user.
- Install a pinned Pi package version.
- Copy the local contract fixtures into /opt/mosaic/contracts.
- Do not copy credentials into the image.
- Do not mount the Docker socket.
- Do not mount either live Mosaic directory.
- Do not add a database, web server, queue, or second container.
- The container may run as a one-shot command. It does not need to remain running.
### Authentication
Use Pi's documented authentication mechanism.
Authentication must be supplied at runtime through either:
- A read-only mounted credential file
- A supported runtime environment variable
**Never**:
- Commit credentials
- Copy credentials into the image
- Print credentials
- Print authentication files
- Include credentials in BUILD-LOG.md
- Store credentials under the project directory
Provide .env.example only for non-secret settings such as model or provider names.
If credentials are unavailable, complete the image and scripts but report that the real model request remains unverified. Do not fake the response.
### Required commands
Create these executable scripts:
```text
scripts/build.sh
scripts/hello.sh
scripts/verify.sh
scripts/reset.sh
```
### scripts/build.sh
Build the container image using Docker Compose.
### scripts/hello.sh
Run the mosaic-agent service as a one-shot container.
Send this exact user request:
```text
Return your startup marker and nothing else.
```
The request must not contain MOSAIC_HELLO_OK.
Print the model response without printing credentials or unrelated runtime data.
### scripts/verify.sh
Run the complete test.
**It must**:
1. Build or confirm the image is built.
2. Run the agent request.
3. Remove surrounding whitespace from the response.
4. Compare the response with MOSAIC_HELLO_OK.
5. Exit 0 only when they match exactly.
6. Exit nonzero with a clear error when they do not match.
### scripts/reset.sh
Delete generated POC state only when all checks pass:
1. The resolved path is exactly /home/jwoltje/.mosaic-dev.
2. The path is not a symbolic link.
3. The directory contains a .mosaic-poc-root ownership marker created by this project.
Refuse to delete anything if a check fails.
## Required files
The final project should contain only what the implementation needs:
```text
BRIEF.md
BUILD-LOG.md
README.md
LAYERS.md
Containerfile
compose.yaml
package.json
package-lock.json
.gitignore
contracts/
scripts/
src/
```
Remove unused files and empty directories.
Build log
Create BUILD-LOG.md.
Treat it as append-only.
Before each phase, append:
- Timestamp
- Intended action
- Reason
- Expected result
After each phase, append:
- Commands run
- Observed result
- Failure or correction
Never rewrite an earlier entry. Add a correction as a new entry.
Do not record credentials.
Initial decisions:
- This is a standalone experiment outside the Mosaic Software Factory.
- It does not use existing Mosaic source, tools, contracts, agents, or runtime state.
- The first proof uses one Pi agent and four small local contract files.
- The only required model result is MOSAIC_HELLO_OK.
- Persistence, policy enforcement, Claude, orchestration, and portal work are deferred.
## Acceptance criteria
The experiment passes when:
1. scripts/build.sh exits 0.
2. The image contains the four local contract files.
3. The image contains no credentials.
4. The container has no mounts from ~/.mosaic or ~/.config/mosaic.
5. scripts/hello.sh performs a real model request.
6. The request does not contain the expected marker.
7. The agent returns exactly MOSAIC_HELLO_OK.
8. scripts/verify.sh exits 0.
9. Changing the expected value makes scripts/verify.sh exit nonzero.
10. scripts/reset.sh refuses unsafe paths.
11. Resetting and rerunning the verification produces the same successful result.
## Deferred layers
Document these in LAYERS.md. Do not implement them.
- L0: Container builds and returns MOSAIC_HELLO_OK.
- L1: Persist and resume a named Pi session.
- L2: Add a fixed tool permission policy.
- L3: Load full versioned contract bundles.
- L4: Add Claude as a second runtime.
- L5: Add multiple agents and communication.
- L6: Add orchestration, knowledge storage, and portal features.
## Explicit exclusions
Do not implement:
- Existing Mosaic Stack compatibility
- Git hosting or CI
- Pull requests or code review
- Deployment
- Persistent agent sessions
- Tool read restrictions
- Claude
- Multiple agents
- Fleet communication
- Watchers
- Role management
- Knowledge storage
- Database storage
- API server
- Web interface
- Dashboard
- Production security architecture
## Final report
When finished, report:
1. Files created.
2. Pi package version.
3. Exact build command.
4. Exact verification command.
5. Verification output with credentials removed.
6. Whether the real model request passed.
7. Any remaining failure.
8. Anything implemented beyond this brief.
Do not describe the experiment as production-ready.
+109
View File
@@ -0,0 +1,109 @@
# BUILD-LOG
Append-only build log for the Minimal Mosaic Stack container proof of concept.
Each phase records the plan before it runs and the observed result after it runs.
No credentials are recorded in this file.
---
## Phase 1: Pi package discovery
### Entry 1.1 — before
- Timestamp: 2026-02-02 (session start, local)
- Intended action: Locate the current package documentation for `@earendil-works/pi-coding-agent`, determine the current version, the supported noninteractive command, the custom system prompt mechanism, and the documented container authentication method.
- Reason: The brief forbids guessing CLI flags, authentication paths, or SDK methods; all runtime commands must be derived from the package documentation.
- Expected result: Documented answers for all five discovery questions, recorded below, with the Pi package version pinned in the project.
### Entry 1.2 — after
- Timestamp: 2026-02-02
- Commands run:
- Read `README.md` of the locally installed `@earendil-works/pi-coding-agent` package
- Read `docs/containerization.md`, excerpts of `docs/models.md` and `docs/providers.md`
- `grep '"version"' .../pi-coding-agent/package.json`
- `npm view @earendil-works/pi-coding-agent version`
- Inspected host auth store structure (keys only, values never printed)
- Observed result:
1. **Package documentation**: full docs ship inside the installed package (`README.md` plus a `docs/` directory including `docs/containerization.md`).
2. **Current package version**: `0.84.4` — the locally installed version and the npm registry latest are identical. Selected version to pin: `0.84.4`.
3. **Supported noninteractive command**: `pi -p` / `pi --print` — "Print response and exit". Documented in the CLI Reference. Print mode also merges piped stdin into the initial prompt (not used here).
4. **Custom system prompt**: documented CLI flag `--system-prompt <text>` — "Replace default prompt (context files and skills still appended)". Because the generated contract prompt must fully control behavior, the run also passes the documented discovery-off switches (`--no-context-files`, `--no-skills`, `--no-extensions`, `--no-prompt-templates`, `--no-themes`) so nothing is appended from ambient files. (`APPEND_SYSTEM.md` / `.pi/SYSTEM.md` file conventions exist but the CLI flag is the documented noninteractive path.)
5. **Documented container authentication**: `docs/containerization.md` documents the "Plain Docker" pattern: run the whole `pi` process in the container and pass provider API keys at runtime via environment variables (example: `docker run -e ANTHROPIC_API_KEY ...`). The documented alternative credential store is `auth.json` written by `/login` under the pi agent directory (`~/.pi/agent/auth.json`).
- Decision: supply the credential at runtime as a **read-only mounted credential file** (host `auth.json`, which contains a static API-key entry for the built-in `zai` provider) mounted at the container pi agent directory, and additionally allow the documented environment-variable path (`ZAI_API_KEY` / `ANTHROPIC_API_KEY`) as an alternative. Provider and model are non-secret settings supplied via `.env` (`PI_PROVIDER=zai`, `PI_MODEL=glm-5.3-flash`).
- Failure or correction: none. Host check confirmed no API-key environment variables are exported on the host, so the read-only auth.json mount is the working runtime credential path for this experiment.
---
## Phase 2: Project scaffold
### Entry 2.1 — before
- Timestamp: 2026-02-02
- Intended action: Create the contract fixtures (exact brief contents), the contract loader (`src/load-contracts.sh`), the one-shot agent runner (`src/run-agent.sh`), the four required scripts (`scripts/build.sh`, `hello.sh`, `verify.sh`, `reset.sh`), `Containerfile`, `compose.yaml`, pinned `package.json` + `package-lock.json`, `.gitignore`, `README.md`, `LAYERS.md`.
- Reason: Implement exactly the file set the brief requires, with no extra machinery (no schemas, overlays, manifests, or policy loading).
- Expected result: A complete project whose only remaining unknown is whether the pinned image builds and the real model request returns `MOSAIC_HELLO_OK`.
### Entry 2.2 — after
- Timestamp: 2026-02-02
- Commands run: file creation; `npm install --package-lock-only --ignore-scripts` to generate the lockfile from the pinned dependency.
- Observed result: All files created; `package-lock.json` pins `@earendil-works/[email protected]` (exact, no range).
- Failure or correction: none.
---
## Phase 3: Container image build
### Entry 3.1 — before
- Timestamp: 2026-02-02
- Intended action: Run `scripts/build.sh` (Docker Compose build) to produce image `mosaic-poc-agent:0.84.4` from `node:24-bookworm-slim` with the pinned Pi, the four contract fixtures at `/opt/mosaic/contracts`, and a non-root user (uid/gid 1000).
- Reason: Phase 1 of the required proof path; `node:24-bookworm-slim` is the maintained base image used in Pi's own documented containerization example.
- Expected result: `docker compose build` exits 0 and the image contains the contracts, the runner scripts, and the pinned `pi` binary, with no credentials baked in.
### Entry 3.2 — after
- Timestamp: 2026-02-02
- Commands run: `scripts/build.sh`; `docker run --rm mosaic-poc-agent:0.84.4 --version`; `id` via `--entrypoint`; contract listing; credential file scan.
- Observed result:
- Build exit 0; image tagged `mosaic-poc-agent:0.84.4`.
- `pi --version` inside the image reports `0.84.4` (and this run also executed the contract loader successfully, writing `/var/lib/mosaic/system-prompt.md`).
- Container user is `uid=1000(node) gid=1000(node)` — non-root.
- All four contract files present at `/opt/mosaic/contracts` with read-only permissions (0555).
- Credential scan: no `auth.json` or other auth files exist in the image; `/home/node/.pi/agent/` is empty in the image.
- Failure or correction:
1. First build failed: Docker Compose expects `Dockerfile` by default; fixed by setting `build.dockerfile: Containerfile` in `compose.yaml`.
2. Second build failed: `useradd` exit 4 (uid 1000 already exists) because the maintained node image ships a `node` user at uid/gid 1000. Fixed by reusing the built-in `node` user (same 1000:1000 host mapping) instead of creating a duplicate `mosaic` user; container paths updated from `/home/mosaic/...` to `/home/node/...` in `Containerfile`, `compose.yaml`, `README.md`, `.env.example`.
---
## Phase 4: Runtime verification
### Entry 4.1 — before
- Timestamp: 2026-02-02
- Intended action: Run `scripts/hello.sh` (one-shot request: "Return your startup marker and nothing else."), then `scripts/verify.sh` (exact-match gate against `MOSAIC_HELLO_OK`), then the negative test (`EXPECTED_MARKER=MOSAIC_NOT_OK scripts/verify.sh` must exit nonzero), then the `scripts/reset.sh` safety tests and a final rerun after reset.
- Reason: Phases 27 of the required proof path plus acceptance criteria 511.
- Expected result: hello prints only the marker; verify exits 0; negative test exits nonzero; reset refuses unsafe paths and succeeds on the real path; rerun after reset reproduces the success.
### Entry 4.2 — after
- Timestamp: 2026-02-02
- Commands run: `scripts/hello.sh`; `scripts/verify.sh`; `EXPECTED_MARKER=MOSAIC_NOT_OK scripts/verify.sh`; `scripts/reset.sh` (refusal tests: missing marker, symlink with canary file, then real reset, then missing dir); `scripts/build.sh && scripts/verify.sh` after reset; `docker compose config` mount inspection.
- Observed result:
- `hello.sh`: stdout exactly `MOSAIC_HELLO_OK` — a real model request (provider `zai`, model `glm-5.3-flash`, auth via the read-only mounted auth.json credential file). The request string contains no marker.
- `verify.sh`: `PASS: response matches expected marker`, exit 0.
- Negative test: `FAIL: response does not match expected marker` (expected `MOSAIC_NOT_OK`, actual `MOSAIC_HELLO_OK`), exit 1.
- `reset.sh` refusal tests: missing marker → exit 1, nothing deleted; symlink (with canary file at the target) → exit 1, canary survived; real path with marker → removed, exit 0; missing dir → "nothing to remove", exit 0.
- Rerun after reset: build + verify → PASS, exit 0 (criterion 11).
- Resolved compose mounts: only `/home/jwoltje/.mosaic-dev → /var/lib/mosaic` (rw) and `~/.pi/agent/auth.json → /home/node/.pi/agent/auth.json` (read-only). No `~/.mosaic` or `~/.config/mosaic` mounts, no Docker socket.
- Failure or correction:
1. First hello run: the contract loader's status line was printed on stdout, mixing runtime data into the model response stream and contaminating the exact-match capture. Fixed by sending the loader's status message to stderr (`src/load-contracts.sh`), rebuilt the image, reran: stdout is exactly the model response.
- Credential check: no credential material appears in this log, in hello/verify output, or in the image (image scan found no auth files).
## Result
All 11 acceptance criteria demonstrated. The real model request passed.
+40
View File
@@ -0,0 +1,40 @@
# Minimal Mosaic Stack POC agent image.
# Base: maintained Node.js image (same family as Pi's documented
# containerization example in docs/containerization.md).
FROM node:24-bookworm-slim
# Tools Pi's documented container image expects (bash, CA certs, git, ripgrep).
RUN apt-get update \
&& apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \
&& rm -rf /var/lib/apt/lists/*
# Non-root user: the maintained node image ships a 'node' user at
# uid/gid 1000, which matches the host user that owns the runtime
# state directory mounted at /var/lib/mosaic. It is reused as-is.
# Pinned Pi install: package.json pins the exact version and
# package-lock.json is installed with npm ci. No unversioned installs.
WORKDIR /opt/app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
# Immutable contract fixtures (required location) and runtime scripts.
COPY contracts /opt/mosaic/contracts
COPY src /opt/mosaic/src
RUN chmod 0555 /opt/mosaic/contracts /opt/mosaic/contracts/* \
&& chmod 0555 /opt/mosaic/src /opt/mosaic/src/*.sh
# Writable state, workspace, and pi agent directory (auth.json is
# bind-mounted read-only at runtime; nothing is copied into the image).
RUN mkdir -p /var/lib/mosaic /workspace /home/node/.pi/agent \
&& chown -R node:node /var/lib/mosaic /workspace /home/node /opt/app
USER node
WORKDIR /workspace
ENV HOME=/home/node \
PATH="/opt/app/node_modules/.bin:${PATH}" \
PI_OFFLINE=1
# One-shot agent: args form the user request (default is the startup
# verification request defined in compose.yaml).
ENTRYPOINT ["/opt/mosaic/src/run-agent.sh"]
+50
View File
@@ -0,0 +1,50 @@
# LAYERS
Deferred capability layers for the Mosaic experiment. Only L0 is implemented by
this proof of concept; everything below it is documented here and deliberately
not implemented (see BRIEF.md, "Explicit exclusions").
## L0 — Implemented: container returns MOSAIC_HELLO_OK
One image (`mosaic-poc-agent:0.84.4`, built on `node:24-bookworm-slim`, non-root,
pinned Pi) runs one Pi agent one-shot. Four immutable local contract files are
loaded in fixed order into the generated system prompt
(`/var/lib/mosaic/system-prompt.md`). One real model request is sent
noninteractively; the response must equal `MOSAIC_HELLO_OK` exactly or the
verification exits nonzero. Authentication is supplied at runtime only
(read-only mounted pi auth file, or a provider API key environment variable).
## L1 — Deferred: persist and resume a named Pi session
Keep a named Pi session across container runs (`--name`, session storage under
`/var/lib/mosaic`), resume it with the documented session flags, and verify
state survives a container restart.
## L2 — Deferred: fixed tool permission policy
Add a fixed allow/deny policy for Pi tools (e.g. restricting built-in tools via
documented `--tools` / `--exclude-tools` or an extension-based permission gate),
so contract files can constrain what the agent may do, not just what it says.
## L3 — Deferred: load full versioned contract bundles
Replace the four static fixtures with versioned contract bundles: bundle
manifests, contract versions, and deterministic ordering/hashing, loaded from
an immutable bundle artifact instead of files copied at image build time.
## L4 — Deferred: Claude as a second runtime
Add a second runtime (Claude) alongside the Pi agent in the same container
stack, behind the same contract-loading path, to compare behavior across
runtimes.
## L5 — Deferred: multiple agents and communication
Run several named agents with defined roles and a communication channel between
them (message passing or shared state under `/var/lib/mosaic`).
## L6 — Deferred: orchestration, knowledge storage, and portal features
Fleet-level orchestration, knowledge storage, monitoring, and portal UI on top
of L1-L5. This is where the existing Mosaic Stack concepts would be re-evaluated
from first principles.
+85
View File
@@ -0,0 +1,85 @@
# 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 this experiment
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)
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 (provider, model)
contracts/ CONSTITUTION.md, STANDARDS.md, SOUL.md, USER.md (immutable fixtures)
scripts/ build.sh, hello.sh, verify.sh, reset.sh (+ shared common.sh)
src/ load-contracts.sh, run-agent.sh (run inside the container)
```
Inside the container:
```text
/opt/mosaic/contracts immutable contract files
/var/lib/mosaic generated runtime state (mounted from /home/jwoltje/.mosaic-dev)
/workspace agent workspace
```
## How it works
1. `scripts/build.sh` builds `mosaic-poc-agent:0.84.4` 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/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/reset.sh # delete /home/jwoltje/.mosaic-dev (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.
+25
View File
@@ -0,0 +1,25 @@
services:
mosaic-agent:
build:
context: .
dockerfile: Containerfile
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}
# 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
# 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
# One-shot: the exact startup verification request. It deliberately
# does NOT contain the expected marker MOSAIC_HELLO_OK.
command: ["Return your startup marker and nothing else."]
+5
View File
@@ -0,0 +1,5 @@
# POC constitution
Never print credentials, tokens, or authentication files.
Follow the loaded system instructions before the user request.
+7
View File
@@ -0,0 +1,7 @@
# POC identity
Your name is mosaic-poc-agent.
Your startup marker is MOSAIC_HELLO_OK.
When asked for your startup marker, return only the marker.
+4
View File
@@ -0,0 +1,4 @@
# POC standards
Answer startup verification requests with only the requested value.
Do not add explanation or formatting.
+3
View File
@@ -0,0 +1,3 @@
# POC user
This is an isolated local runtime test.
@@ -0,0 +1,256 @@
# Atomic Mosaic Foundation Plan
**Date:** 2026-09-02
**Status:** Planning; configuration-driven L0 not yet implemented
**Project:** Standalone Mosaic Stack rebuild experiment
## Purpose
Reimplement Mosaic Stack from atomic, independently verifiable layers. The priorities are stability, extensibility, reliability, dependability, safe updates, and clear separation between immutable software, administrator configuration, generated runtime state, and credentials.
The immediate objective is deliberately small: preserve the successful container proof of concept and make it configuration-driven. Mission/task abstraction comes only after the foundation is stable.
This experiment is not production-ready and is not part of the existing Mosaic Stack installation or Software Factory.
## Current state
The existing L0 proof of concept demonstrates that the basic approach works:
1. One container image builds successfully.
2. It runs as a non-root user.
3. It contains a pinned Pi installation (`@earendil-works/[email protected]`).
4. It loads four local contract files in a deterministic order.
5. It generates `/var/lib/mosaic/system-prompt.md`.
6. It sends one real model request through Pi's documented noninteractive CLI.
7. The request does not contain the expected marker.
8. The model returns exactly `MOSAIC_HELLO_OK`.
9. Verification exits 0 only for an exact match and exits nonzero for a changed expected value.
10. Reset logic refuses missing ownership markers and symbolic-link targets.
11. Resetting and rerunning produces the same successful result.
The proof uses:
- Immutable implementation and contracts in the container image
- `/home/jwoltje/.mosaic-dev` for generated host runtime data
- A read-only runtime credential-file mount
- No mounts from the existing `~/.mosaic` or `~/.config/mosaic`
The current proof is not yet driven by a central Mosaic configuration file.
## Problem being addressed
The existing `~/.mosaic` and `~/.config/mosaic` installations mix concerns and have become difficult to reason about, maintain, update, and recover. The rebuild must avoid repeating that design.
Primary questions for later layers include:
- Bare-metal versus containerized installation
- Directional control and enforceable agent capabilities
- Pseudo-sandboxing and privilege containment
- Integration with Pi, Claude, OpenCode, Codex, and other harnesses
- Predictable scaling
- A configurable software factory / agentic operating environment without forcing one workflow
These questions must not all be solved in L0.
## Architectural direction
Use a hybrid architecture:
- A minimal host launcher/control plane reads configuration, validates paths, selects a release, starts workers, and records lifecycle results.
- Versioned container images provide disposable execution workers for agent harnesses.
- Agent execution does not occur directly in the host control plane.
- Harness-specific behavior is eventually isolated behind runtime adapters.
Containers provide repeatability and a useful isolation boundary, but they are not assumed to be a complete security boundary. Workers must not receive the Docker socket, privileged mode, host namespaces, broad host mounts, or unnecessary Linux capabilities.
## Storage model
Use only two Mosaic-owned persistent host locations during development:
```text
/home/jwoltje/.config/mosaic-dev/config.json
/home/jwoltje/.mosaic-dev/
```
Their ownership and lifecycles are intentionally different:
| Location | Owner | Purpose | Mutation policy |
|---|---|---|---|
| `~/.config/mosaic-dev/config.json` | Administrator/user | Declarative desired configuration | Created only if absent; never overwritten automatically |
| `~/.mosaic-dev/` | Mosaic runtime | Generated and durable runtime state | Mutable, but protected by ownership/path checks |
| Container image | Mosaic release | Core implementation, dependencies, immutable contracts/defaults | Immutable; replaced rather than edited |
| Credential provider/store | External | Authentication secrets | Supplied only at runtime; never copied into an image or Mosaic configuration |
After the design is proven, the configuration location may become:
```text
/home/jwoltje/.config/mosaic/config.json
```
The existing `~/.mosaic` and `~/.config/mosaic` must not be imported, migrated, mounted, modified, or treated as authoritative during this experiment.
### Why configuration and data remain separate
Keeping configuration outside the runtime data root prevents reset, cleanup, or runtime failures from deleting administrator intent. Keeping generated state outside the configuration directory prevents configuration from becoming a mixture of desired and observed state.
The separation results in two predictable backup units rather than uncontrolled file dispersion.
## Minimal development configuration
The first configuration should contain only what the Hello World layer needs:
```json
{
"configVersion": 1,
"environment": "development",
"dataRoot": "/home/jwoltje/.mosaic-dev",
"execution": {
"backend": "docker",
"provider": "zai",
"model": "glm-5.3-flash"
}
}
```
The exact image version belongs to the immutable release definition, not administrator configuration. Credentials must not appear in this file.
Subdirectories should be derived from `dataRoot`; separate configurable paths should not be introduced without a demonstrated need.
## Configuration invariants
1. `~/.config/mosaic-dev/config.json` is the sole Mosaic discovery entry point during development.
2. Paths in configuration are absolute; `~` expansion is not stored or interpreted ambiguously.
3. Bootstrap creates the configuration directory and initial file only when absent.
4. Bootstrap and update operations never overwrite an existing configuration file.
5. Configuration has an explicit `configVersion`.
6. Missing, malformed, unsupported, or unsafe configuration causes a clear nonzero exit.
7. Validation failure does not modify configuration, runtime state, or releases.
8. Generated and observed values are never written back into `config.json`.
9. Secrets and credential contents are never stored in `config.json`.
10. Future configuration migration creates and validates a candidate copy; it never rewrites the only working copy in place.
The configuration file is declarative. The bootstrap and activation operations around it must be idempotent.
## Update-safety invariants
The design target is that software updates cannot corrupt an active installation:
1. Releases are immutable and versioned.
2. A new release is installed beside existing releases.
3. Active implementation files are never patched in place.
4. Configuration and state are not owned by a release directory.
5. Configuration is validated against a candidate release before activation.
6. Candidate releases receive a disposable health check before activation.
7. Activation is an atomic pointer/reference change.
8. The prior release remains available for rollback.
9. State migrations are deferred until required.
10. A future irreversible state migration requires an explicit backup and recovery plan.
Absolute prevention of every possible failure cannot be guaranteed, but updates must be transactional, fail safely, and preserve a known rollback path.
## Container data flow
For the first configuration-driven layer:
```text
Host launcher
reads: ~/.config/mosaic-dev/config.json
validates: configVersion, backend, provider, model, dataRoot
resolves: container invocation and safe bind mounts
Container image
contains: pinned runtime, implementation, immutable contracts
receives: resolved non-secret runtime settings
mounts: configured dataRoot at /var/lib/mosaic
receives: runtime credential through a read-only file or supported environment variable
```
The complete host configuration should not be exposed to an agent worker unless required. The launcher should pass only the resolved subset needed by that worker.
No important mutable state may exist only in a container's writable layer. Containers must remain disposable.
## Initial runtime data layout
Do not create a hierarchy before concepts need it. L0 requires only:
```text
~/.mosaic-dev/
├── .mosaic-root
└── system-prompt.md
```
Potential future directories are reserved but not part of L0:
```text
~/.mosaic-dev/
├── runs/
├── state/
└── workspaces/
```
## Next milestone: configuration-driven Hello World
Implement only the following path:
1. Create a small bootstrap/launcher.
2. If absent, bootstrap creates `~/.config/mosaic-dev/config.json` with the minimal development configuration.
3. If configuration already exists, bootstrap does not change it.
4. The launcher reads and validates the configuration.
5. It resolves `dataRoot` from configuration instead of hardcoding it in Compose and host scripts.
6. It safely creates or validates the data-root ownership marker.
7. It builds or selects the current immutable container image.
8. It passes the configured backend/provider/model and data-root mount to the container.
9. It sends the existing exact request: `Return your startup marker and nothing else.`
10. It receives and verifies exactly `MOSAIC_HELLO_OK`.
### L0 acceptance criteria
1. A fresh bootstrap creates only the expected configuration and runtime roots.
2. Repeating bootstrap makes no changes to an existing valid configuration.
3. Existing configuration is never overwritten by build, verification, reset, or update operations.
4. Missing configuration can be bootstrapped deliberately; normal execution does not silently invent configuration.
5. Malformed JSON exits nonzero without modifying files.
6. Unsupported `configVersion` exits nonzero without modifying files.
7. A relative or unsafe `dataRoot` exits nonzero without modifying files.
8. The container receives the configured data root at `/var/lib/mosaic`.
9. The image and container contain no credentials.
10. The real model request returns exactly `MOSAIC_HELLO_OK`.
11. Changing the expected marker produces a nonzero verification exit.
12. Rebuilding/replacing the image leaves configuration and runtime data intact.
13. Reset deletes only the validated runtime data root and never configuration.
14. Reset continues to refuse symbolic links and missing ownership markers.
## Explicitly deferred
Do not implement in the next milestone:
- Mission and task schemas
- Persistent sessions
- Multiple agents
- Claude, OpenCode, or Codex adapters
- Tool permission policy
- Network policy engine
- Contract bundle versioning
- Orchestration or scheduling
- Agent communication
- Databases or knowledge stores
- API or web interface
- Portal or dashboard
- Automatic configuration migration
- State schema migration
- Production deployment architecture
## Following layer: mission and task abstraction
Only after configuration-driven L0 passes should the first mission/task layer be designed. Its initial concepts should remain minimal:
- **Mission:** desired outcome and governing constraints
- **Task:** one bounded unit of work assigned to one runtime
- **Run:** one attempt to execute a task
- **Result:** immutable completion evidence and exit status
No mission/task implementation decision is made by this plan.
## Immediate documentation and implementation scope
Maintain one clear architecture plan (this document), one example/default configuration, one strict configuration reader, and the existing Hello World proof. Avoid new services, generalized frameworks, and abstractions until a passing acceptance test requires them.
+1729
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
{
"name": "mosaic-stack-dev-test",
"version": "0.1.0",
"private": true,
"description": "Minimal Mosaic Stack container proof of concept: one Pi agent, four local contract files, one real model request returning MOSAIC_HELLO_OK.",
"license": "UNLICENSED",
"dependencies": {
"@earendil-works/pi-coding-agent": "0.84.4"
}
}
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Build the mosaic-agent container image using Docker Compose.
set -euo pipefail
cd "$(dirname "$0")/.."
# shellcheck source=common.sh
source scripts/common.sh
bootstrap_runtime_dir
docker compose build
+15
View File
@@ -0,0 +1,15 @@
# Shared helpers for the POC 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.
bootstrap_runtime_dir() {
if [ ! -d "$MOSAIC_DEV_DIR" ]; then
mkdir -p "$MOSAIC_DEV_DIR"
echo "bootstrap: created $MOSAIC_DEV_DIR"
fi
touch "$MOSAIC_DEV_DIR/$POC_ROOT_MARKER"
}
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env node
/**
* Repository-local Git credential helper for git.mosaicstack.dev.
*
* Git invokes this helper with "get", "store", or "erase" and consumes its
* stdout directly. Never invoke it manually, enable shell tracing around it,
* or add credential values to logs.
*
* The credential file is intentionally not part of Git and must remain mode
* 0600. Override its location with MOSAIC_GITEA_CREDENTIAL_FILE if needed.
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
const operation = process.argv[2] ?? "";
// Git may offer credentials back through stdin for store/erase. This helper is
// read-only: ignore those operations and never persist or print their input.
if (operation !== "get") {
process.exit(0);
}
const defaultCredentialFile = path.join(
os.homedir(),
"secrets",
"mosaic.gitea.json",
);
const credentialFile =
process.env.MOSAIC_GITEA_CREDENTIAL_FILE ?? defaultCredentialFile;
function fail(message) {
process.stderr.write(`git-credential-mosaic: ${message}\n`);
process.exit(1);
}
let stat;
try {
stat = fs.lstatSync(credentialFile);
} catch {
fail("credential file is unavailable");
}
if (!stat.isFile() || stat.isSymbolicLink()) {
fail("credential path must be a regular, non-symbolic-link file");
}
if ((stat.mode & 0o077) !== 0) {
fail("credential file permissions must be 0600 or stricter");
}
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
fail("credential file must be owned by the current user");
}
let document;
try {
document = JSON.parse(fs.readFileSync(credentialFile, "utf8"));
} catch {
fail("credential file is not valid JSON");
}
const entry = document?.mosaicstack;
const configuredUrl = entry?.url;
const username = entry?.user;
const token = entry?.api_token;
if (
typeof configuredUrl !== "string" ||
typeof username !== "string" ||
typeof token !== "string" ||
username.length === 0 ||
token.length === 0 ||
/[\r\n]/.test(username) ||
/[\r\n]/.test(token)
) {
fail("credential file is missing valid mosaicstack url/user/api_token fields");
}
let credentialUrl;
try {
credentialUrl = new URL(configuredUrl);
} catch {
fail("configured credential URL is invalid");
}
if (
credentialUrl.protocol !== "https:" ||
credentialUrl.hostname !== "git.mosaicstack.dev"
) {
fail("credential URL is not the approved HTTPS Gitea host");
}
const request = {};
for (const line of fs.readFileSync(0, "utf8").split("\n")) {
const separator = line.indexOf("=");
if (separator > 0) {
request[line.slice(0, separator)] = line.slice(separator + 1);
}
}
// Fail closed: emit credentials only for the approved HTTPS host. A host may
// include an explicit port; it must match the configured URL exactly.
if (
request.protocol !== "https" ||
request.host !== credentialUrl.host
) {
process.exit(0);
}
// stdout is the Git credential-helper protocol channel, consumed directly by
// Git. Do not add status messages here.
process.stdout.write(`username=${username}\npassword=${token}\n`);
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Run the mosaic-agent service as a one-shot container and print the
# model response for the exact startup verification request.
#
# The request deliberately does NOT contain the expected marker
# MOSAIC_HELLO_OK. Only the model response is printed to stdout; no
# credentials or unrelated runtime data are printed.
set -euo pipefail
cd "$(dirname "$0")/.."
# shellcheck source=common.sh
source scripts/common.sh
bootstrap_runtime_dir
# -T: no pseudo-TTY, so stdout is clean model output.
# Errors, if any, go to stderr for diagnostics.
docker compose run --rm -T mosaic-agent
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Delete the generated POC runtime state at /home/jwoltje/.mosaic-dev,
# but ONLY when every safety check passes:
# 1. The resolved path is exactly /home/jwoltje/.mosaic-dev.
# 2. The path is not a symbolic link.
# 3. The directory contains the .mosaic-poc-root ownership marker
# created by this project.
# Any failed check aborts with nothing deleted.
set -euo pipefail
TARGET="/home/jwoltje/.mosaic-dev"
MARKER=".mosaic-poc-root"
fail() {
echo "reset: refusing to delete: $*" >&2
exit 1
}
# Nothing to do when the directory does not exist.
if [ ! -e "$TARGET" ]; then
echo "reset: $TARGET does not exist; nothing to remove"
exit 0
fi
# Check 2 (before resolution): the path itself must not be a symlink.
if [ -L "$TARGET" ]; then
fail "$TARGET is a symbolic link"
fi
# Check 1: resolved path must be exactly the POC runtime directory.
RESOLVED="$(realpath "$TARGET")"
if [ "$RESOLVED" != "$TARGET" ]; then
fail "resolved path $RESOLVED is not $TARGET"
fi
# Check 3: ownership marker created by this project must be present.
if [ ! -f "$TARGET/$MARKER" ]; then
fail "missing $MARKER ownership marker in $TARGET"
fi
rm -rf -- "$TARGET"
echo "reset: removed $TARGET"
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Complete verification test:
# 1. Build or confirm the image is built.
# 2. Run the agent request.
# 3. Remove surrounding whitespace from the response.
# 4. Compare with the expected marker (default MOSAIC_HELLO_OK).
# 5. Exit 0 only on exact match; nonzero otherwise.
#
# EXPECTED_MARKER may be overridden to prove the failure path
# (acceptance criterion 9), e.g.:
# EXPECTED_MARKER=MOSAIC_NOT_OK scripts/verify.sh
set -euo pipefail
cd "$(dirname "$0")/.."
# shellcheck source=common.sh
source scripts/common.sh
IMAGE="mosaic-poc-agent:0.84.4"
EXPECTED="${EXPECTED_MARKER:-MOSAIC_HELLO_OK}"
# 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
bootstrap_runtime_dir
docker compose build
fi
# 2. Run the agent request (stdout only = model response).
set +e
RESPONSE="$(docker compose run --rm -T mosaic-agent 2>/tmp/mosaic-poc-stderr.$$)"
RC=$?
set -e
STDERR_FILE="/tmp/mosaic-poc-stderr.$$"
if [ $RC -ne 0 ]; then
echo "verify: agent run failed (exit $RC):" >&2
cat "$STDERR_FILE" >&2
rm -f "$STDERR_FILE"
exit 1
fi
rm -f "$STDERR_FILE"
# 3. Remove surrounding whitespace.
TRIMMED="$(printf '%s' "$RESPONSE" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
# 4-6. Exact comparison gate.
if [ "$TRIMMED" = "$EXPECTED" ]; then
echo "PASS: response matches expected marker"
exit 0
fi
echo "FAIL: response does not match expected marker" >&2
printf 'expected: %s\n' "$EXPECTED" >&2
printf 'actual : %s\n' "$TRIMMED" >&2
exit 1
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
# Load the four immutable contract files in fixed order and write the
# generated system prompt to /var/lib/mosaic/system-prompt.md.
#
# Order is normative: CONSTITUTION.md, STANDARDS.md, SOUL.md, USER.md.
set -eu
CONTRACT_DIR="${1:-/opt/mosaic/contracts}"
OUT="${2:-/var/lib/mosaic/system-prompt.md}"
FILES="CONSTITUTION.md STANDARDS.md SOUL.md USER.md"
if [ ! -d "$CONTRACT_DIR" ]; then
echo "load-contracts: contract directory not found: $CONTRACT_DIR" >&2
exit 1
fi
PARENT="$(dirname "$OUT")"
mkdir -p "$PARENT"
TEMP="$OUT.partial"
: > "$TEMP"
for f in $FILES; do
path="$CONTRACT_DIR/$f"
if [ ! -r "$path" ]; then
echo "load-contracts: missing contract file: $path" >&2
rm -f "$TEMP"
exit 1
fi
printf '===== CONTRACT: %s =====\n' "$f" >> "$TEMP"
cat "$path" >> "$TEMP"
printf '\n' >> "$TEMP"
done
mv "$TEMP" "$OUT"
echo "load-contracts: wrote $OUT from $CONTRACT_DIR" >&2
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
# One-shot Pi agent runner inside the container.
# Loads the contract-generated system prompt, then sends exactly one
# user request through Pi's documented noninteractive mode and prints
# the model response on stdout.
set -eu
: "${PI_PROVIDER:=zai}"
: "${PI_MODEL:=glm-5.3-flash}"
export PI_PROVIDER PI_MODEL
REQUEST="${*:-Return your startup marker and nothing else.}"
/opt/mosaic/src/load-contracts.sh /opt/mosaic/contracts /var/lib/mosaic/system-prompt.md
# All flags are documented in the package README (CLI Reference):
# -p / --print noninteractive: print the response and exit
# --system-prompt replace the default system prompt with the
# contract-generated prompt
# --no-* switches prevent ambient context files, skills, extensions,
# prompt templates, and themes from being appended
# --no-session ephemeral: no persistent agent session
# --no-tools the startup request needs no tool execution
# --offline disable startup network operations (update checks,
# package update checks, install/update telemetry)
exec pi \
--offline \
--no-session \
--no-extensions \
--no-skills \
--no-prompt-templates \
--no-themes \
--no-context-files \
--no-tools \
--provider "$PI_PROVIDER" \
--model "$PI_MODEL" \
--system-prompt "$(cat /var/lib/mosaic/system-prompt.md)" \
-p "$REQUEST"