compose: add wrapper-first dogfood workspace (#1487)
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
This commit is contained in:
@@ -6,3 +6,9 @@ VALKEY_HOST_PORT=6380
|
||||
GATEWAY_HOST_PORT=14242
|
||||
# Registry image override (defaults to a local build of docker/gateway.Dockerfile):
|
||||
# GATEWAY_IMAGE=git.mosaicstack.dev/mosaicstack/stack/gateway:sha-acf640d
|
||||
|
||||
# Optional explicit dogfood overlay (docker-compose.dogfood.yml).
|
||||
# Both paths are required when that overlay is used. Use a dedicated next-based
|
||||
# worktree and the external home of the unprivileged stack-dogfood seat.
|
||||
# MOSAIC_DOGFOOD_WORKTREE=/home/example/src/mosaic-stack-worktrees/dogfood-1487
|
||||
# MOSAIC_DOGFOOD_SEAT_HOME=/home/example/.mosaic/fleet/agents/stack-dogfood
|
||||
|
||||
@@ -208,6 +208,46 @@ mosaic telemetry upload # Dry-run unless opted in
|
||||
|
||||
Consent state is persisted in config. Remote upload is a no-op until you run `mosaic telemetry opt-in`.
|
||||
|
||||
## Standalone container deployment
|
||||
|
||||
The `stack` profile runs PostgreSQL, Valkey, the gateway, and the bundled webUI. Copy
|
||||
`.env.example` to `.env`, generate `BETTER_AUTH_SECRET`, then start the profile:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
printf 'BETTER_AUTH_SECRET=%s\n' "$(openssl rand -hex 32)" >> .env
|
||||
docker compose --profile stack up -d
|
||||
```
|
||||
|
||||
The optional dogfood overlay gives one dedicated in-stack agent a writable stack
|
||||
worktree and its own read-only credential slot. It does not mount the fleet brain or
|
||||
any other seat. Prepare a `next`-based worktree and an unprivileged `stack-dogfood`
|
||||
seat outside the container, then set these paths in `.env`:
|
||||
|
||||
```dotenv
|
||||
MOSAIC_DOGFOOD_WORKTREE=/path/to/mosaic-stack-worktrees/dogfood-1487
|
||||
MOSAIC_DOGFOOD_SEAT_HOME=/path/to/.mosaic/fleet/agents/stack-dogfood
|
||||
```
|
||||
|
||||
The seat home must contain only that seat's credential at
|
||||
`secrets/gitea-mosaicstack-stack-dogfood.token`. Never place the token value in
|
||||
`.env`. Start the overlay with:
|
||||
|
||||
```bash
|
||||
docker compose \
|
||||
-f docker-compose.yml \
|
||||
-f docker-compose.dogfood.yml \
|
||||
--profile stack up -d
|
||||
```
|
||||
|
||||
The overlay scopes regular-agent tools to the mounted checkout. For issue and PR
|
||||
operations, instruct the agent to use `/opt/mosaic/tools/git/`. The gateway image
|
||||
configures `git-credential-mosaic` as Git's system credential helper, so pushes and
|
||||
`pr-create.sh` resolve only the `stack-dogfood` slot and fail if it is absent.
|
||||
|
||||
This deployment route is separate from the local source-development restrictions
|
||||
below.
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Explicit, single-seat dogfood mode for stack-containerization B2.
|
||||
# Use with docker-compose.yml. The base stack remains credential-free.
|
||||
services:
|
||||
gateway:
|
||||
environment:
|
||||
# Identity and credential layout match a fleet seat. This fixed name prevents
|
||||
# an operator from mounting one seat while attributing actions to another.
|
||||
MOSAIC_AGENT_NAME: stack-dogfood
|
||||
MOSAIC_GIT_IDENTITY: stack-dogfood
|
||||
MOSAIC_BRAIN_HOME: /opt/mosaic/brain
|
||||
AGENT_FILE_SANDBOX_DIR: /workspace/stack
|
||||
AGENT_USER_TOOLS: fs_read_file,fs_write_file,fs_list_directory,fs_edit_file,git_status,git_log,git_diff,shell_exec
|
||||
volumes:
|
||||
# Mount a dedicated worktree, never the canonical clone or divergent local main.
|
||||
- type: bind
|
||||
source: ${MOSAIC_DOGFOOD_WORKTREE:?set to a dedicated next-based stack worktree}
|
||||
target: /workspace/stack
|
||||
# Only this seat home enters the container. Other fleet credentials stay outside.
|
||||
- type: bind
|
||||
source: ${MOSAIC_DOGFOOD_SEAT_HOME:?set to the external stack-dogfood seat directory}
|
||||
target: /opt/mosaic/brain/fleet/agents/stack-dogfood
|
||||
read_only: true
|
||||
@@ -29,11 +29,19 @@ ENV NODE_ENV=production
|
||||
# $MOSAIC_ROOT/.workspaces (apps/gateway/src/workspace/workspace.service.ts);
|
||||
# mount a volume over /opt/mosaic to persist workspaces across container restarts.
|
||||
# Intentionally unpinned: Alpine's signed repository is the trust anchor; pinning
|
||||
# git was declined so routine base-image security updates remain maintainable.
|
||||
RUN apk add --no-cache git \
|
||||
# packages was declined so routine base-image security updates remain maintainable.
|
||||
# bash/curl/python3 are runtime dependencies of the provider-neutral Mosaic git
|
||||
# wrappers. jq supports wrapper discovery for non-canonical Gitea hosts.
|
||||
RUN apk add --no-cache bash curl git jq python3 \
|
||||
&& mkdir -p /opt/mosaic/.workspaces \
|
||||
&& chown -R node:node /opt/mosaic /app
|
||||
ENV MOSAIC_ROOT=/opt/mosaic
|
||||
# Dogfood agents use the same fail-closed credential helper and PR wrappers as
|
||||
# fleet seats. The complete tools directory is copied because pr-create.sh shares
|
||||
# provider detection, declaration validation, and credential libraries with the
|
||||
# rest of the wrapper suite.
|
||||
COPY --from=builder /app/packages/mosaic/framework/tools /opt/mosaic/tools
|
||||
RUN git config --system credential.helper /opt/mosaic/tools/git/git-credential-mosaic
|
||||
# Use the pnpm deploy output — resolves all deps into a flat, self-contained node_modules
|
||||
COPY --chown=node:node --from=builder /deploy/node_modules ./node_modules
|
||||
COPY --chown=node:node --from=builder /deploy/package.json ./package.json
|
||||
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hermetic structural check for the explicit dogfood Compose overlay.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
mkdir -p "$tmp/worktree" "$tmp/seat/secrets"
|
||||
|
||||
base_config_json=$(
|
||||
cd "$repo_root"
|
||||
BETTER_AUTH_SECRET=test-only-not-a-credential \
|
||||
docker compose --profile stack config --format json
|
||||
)
|
||||
|
||||
BASE_CONFIG_JSON="$base_config_json" python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
config = json.loads(os.environ["BASE_CONFIG_JSON"])
|
||||
gateway = config["services"]["gateway"]
|
||||
env = gateway["environment"]
|
||||
for key in (
|
||||
"MOSAIC_AGENT_NAME",
|
||||
"MOSAIC_GIT_IDENTITY",
|
||||
"MOSAIC_BRAIN_HOME",
|
||||
"AGENT_FILE_SANDBOX_DIR",
|
||||
"AGENT_USER_TOOLS",
|
||||
):
|
||||
assert key not in env, f"base compose unexpectedly sets dogfood variable {key}"
|
||||
|
||||
targets = {mount["target"] for mount in gateway["volumes"]}
|
||||
assert "/workspace/stack" not in targets
|
||||
assert not any(target.startswith("/opt/mosaic/brain/") for target in targets)
|
||||
PY
|
||||
|
||||
config_json=$(
|
||||
cd "$repo_root"
|
||||
BETTER_AUTH_SECRET=test-only-not-a-credential \
|
||||
MOSAIC_DOGFOOD_WORKTREE="$tmp/worktree" \
|
||||
MOSAIC_DOGFOOD_SEAT_HOME="$tmp/seat" \
|
||||
docker compose \
|
||||
-f docker-compose.yml \
|
||||
-f docker-compose.dogfood.yml \
|
||||
--profile stack \
|
||||
config --format json
|
||||
)
|
||||
|
||||
CONFIG_JSON="$config_json" EXPECT_WORKTREE="$tmp/worktree" EXPECT_SEAT="$tmp/seat" python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
config = json.loads(os.environ["CONFIG_JSON"])
|
||||
gateway = config["services"]["gateway"]
|
||||
env = gateway["environment"]
|
||||
|
||||
expected_env = {
|
||||
"MOSAIC_AGENT_NAME": "stack-dogfood",
|
||||
"MOSAIC_GIT_IDENTITY": "stack-dogfood",
|
||||
"MOSAIC_BRAIN_HOME": "/opt/mosaic/brain",
|
||||
"AGENT_FILE_SANDBOX_DIR": "/workspace/stack",
|
||||
}
|
||||
for key, value in expected_env.items():
|
||||
assert env.get(key) == value, f"{key}: expected {value!r}, got {env.get(key)!r}"
|
||||
|
||||
allowed = set(env["AGENT_USER_TOOLS"].split(","))
|
||||
assert allowed == {
|
||||
"fs_read_file",
|
||||
"fs_write_file",
|
||||
"fs_list_directory",
|
||||
"fs_edit_file",
|
||||
"git_status",
|
||||
"git_log",
|
||||
"git_diff",
|
||||
"shell_exec",
|
||||
}, f"unexpected dogfood tool set: {sorted(allowed)}"
|
||||
|
||||
mounts = {mount["target"]: mount for mount in gateway["volumes"]}
|
||||
worktree = mounts["/workspace/stack"]
|
||||
assert worktree["type"] == "bind"
|
||||
assert worktree["source"] == os.environ["EXPECT_WORKTREE"]
|
||||
assert not worktree.get("read_only", False), "dogfood worktree must be writable"
|
||||
|
||||
seat = mounts["/opt/mosaic/brain/fleet/agents/stack-dogfood"]
|
||||
assert seat["type"] == "bind"
|
||||
assert seat["source"] == os.environ["EXPECT_SEAT"]
|
||||
assert seat.get("read_only") is True, "seat credential slot must be read-only"
|
||||
|
||||
other_seat_mounts = [
|
||||
target
|
||||
for target in mounts
|
||||
if target.startswith("/opt/mosaic/brain/fleet/agents/")
|
||||
and target != "/opt/mosaic/brain/fleet/agents/stack-dogfood"
|
||||
]
|
||||
assert other_seat_mounts == [], f"other seat mounts leaked: {other_seat_mounts}"
|
||||
PY
|
||||
|
||||
# Each required path must fail closed rather than falling back to the current checkout.
|
||||
expect_missing_path() {
|
||||
local missing=$1 output rc
|
||||
set +e
|
||||
case "$missing" in
|
||||
MOSAIC_DOGFOOD_WORKTREE)
|
||||
output=$(
|
||||
cd "$repo_root"
|
||||
env -u MOSAIC_DOGFOOD_WORKTREE \
|
||||
BETTER_AUTH_SECRET=test-only-not-a-credential \
|
||||
MOSAIC_DOGFOOD_SEAT_HOME="$tmp/seat" \
|
||||
docker compose -f docker-compose.yml -f docker-compose.dogfood.yml \
|
||||
--profile stack config 2>&1
|
||||
)
|
||||
rc=$?
|
||||
;;
|
||||
MOSAIC_DOGFOOD_SEAT_HOME)
|
||||
output=$(
|
||||
cd "$repo_root"
|
||||
env -u MOSAIC_DOGFOOD_SEAT_HOME \
|
||||
BETTER_AUTH_SECRET=test-only-not-a-credential \
|
||||
MOSAIC_DOGFOOD_WORKTREE="$tmp/worktree" \
|
||||
docker compose -f docker-compose.yml -f docker-compose.dogfood.yml \
|
||||
--profile stack config 2>&1
|
||||
)
|
||||
rc=$?
|
||||
;;
|
||||
*)
|
||||
echo "FAIL: test requested unknown path variable $missing" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
set -e
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
echo "FAIL: dogfood compose accepted missing $missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$output" != *"$missing"* ]]; then
|
||||
echo "FAIL: missing-path failure did not name $missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
expect_missing_path MOSAIC_DOGFOOD_WORKTREE
|
||||
expect_missing_path MOSAIC_DOGFOOD_SEAT_HOME
|
||||
|
||||
printf 'dogfood compose verification passed\n'
|
||||
Reference in New Issue
Block a user