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
|
GATEWAY_HOST_PORT=14242
|
||||||
# Registry image override (defaults to a local build of docker/gateway.Dockerfile):
|
# Registry image override (defaults to a local build of docker/gateway.Dockerfile):
|
||||||
# GATEWAY_IMAGE=git.mosaicstack.dev/mosaicstack/stack/gateway:sha-acf640d
|
# 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`.
|
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
|
## Development
|
||||||
|
|
||||||
### Prerequisites
|
### 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,23 @@ ENV NODE_ENV=production
|
|||||||
# $MOSAIC_ROOT/.workspaces (apps/gateway/src/workspace/workspace.service.ts);
|
# $MOSAIC_ROOT/.workspaces (apps/gateway/src/workspace/workspace.service.ts);
|
||||||
# mount a volume over /opt/mosaic to persist workspaces across container restarts.
|
# mount a volume over /opt/mosaic to persist workspaces across container restarts.
|
||||||
# Intentionally unpinned: Alpine's signed repository is the trust anchor; pinning
|
# Intentionally unpinned: Alpine's signed repository is the trust anchor; pinning
|
||||||
# git was declined so routine base-image security updates remain maintainable.
|
# packages was declined so routine base-image security updates remain maintainable.
|
||||||
RUN apk add --no-cache git \
|
# 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 \
|
&& mkdir -p /opt/mosaic/.workspaces \
|
||||||
&& chown -R node:node /opt/mosaic /app
|
&& chown -R node:node /opt/mosaic /app
|
||||||
ENV MOSAIC_ROOT=/opt/mosaic
|
ENV MOSAIC_ROOT=/opt/mosaic
|
||||||
|
# Dogfood agents use the same fail-closed credential helper and PR-create wrapper
|
||||||
|
# as fleet seats. Copy only that operation and its shared dependencies. Unrelated
|
||||||
|
# fleet operations, including merge and infrastructure tools, stay out of the image.
|
||||||
|
COPY --from=builder /app/packages/mosaic/framework/tools/git/pr-create.sh /opt/mosaic/tools/git/pr-create.sh
|
||||||
|
COPY --from=builder /app/packages/mosaic/framework/tools/git/detect-platform.sh /opt/mosaic/tools/git/detect-platform.sh
|
||||||
|
COPY --from=builder /app/packages/mosaic/framework/tools/git/repo-decl.sh /opt/mosaic/tools/git/repo-decl.sh
|
||||||
|
COPY --from=builder /app/packages/mosaic/framework/tools/git/git-credential-mosaic /opt/mosaic/tools/git/git-credential-mosaic
|
||||||
|
COPY --from=builder /app/packages/mosaic/framework/tools/_lib/credentials.sh /opt/mosaic/tools/_lib/credentials.sh
|
||||||
|
COPY --from=builder /app/packages/mosaic/framework/tools/structure/validate-repo-json.sh /opt/mosaic/tools/structure/validate-repo-json.sh
|
||||||
|
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
|
# 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/node_modules ./node_modules
|
||||||
COPY --chown=node:node --from=builder /deploy/package.json ./package.json
|
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