Compare commits

..
16718 changed files with 31541 additions and 2765444 deletions

No files matched your search

+149 -20
View File
@@ -1,24 +1,153 @@
# 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
# ─────────────────────────────────────────────────────────────────────────────
# Mosaic — Environment Variables Reference
# Copy this file to .env and fill in the values for your deployment.
# Lines beginning with # are comments; optional vars are commented out.
# ─────────────────────────────────────────────────────────────────────────────
# Model provider (built-in pi provider name)
PI_PROVIDER=zai
# Model ID within the provider
PI_MODEL=glm-5.3-flash
# ─── Database (PostgreSQL 17 + pgvector) ─────────────────────────────────────
# Full connection string used by the gateway, ORM, and migration runner.
# Port 5433 avoids conflict with a host-side PostgreSQL instance.
DATABASE_URL=postgresql://mosaic:mosaic@localhost:5433/mosaic
# 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
# Docker Compose host-port override for the PostgreSQL container (default: 5433)
# PG_HOST_PORT=5433
# Optional: documented env-var auth alternative (secret! set in your
# shell or a gitignored .env, never commit)
#ZAI_API_KEY=
#ANTHROPIC_API_KEY=
# ─── Queue (Valkey 8 / Redis-compatible) ─────────────────────────────────────
# Port 6380 avoids conflict with a host-side Redis/Valkey instance.
VALKEY_URL=redis://localhost:6380
# Docker Compose host-port override for the Valkey container (default: 6380)
# VALKEY_HOST_PORT=6380
# ─── Gateway ─────────────────────────────────────────────────────────────────
# TCP port the NestJS/Fastify gateway listens on (default: 14242)
GATEWAY_PORT=14242
# Comma-separated list of allowed CORS origins.
# Must include the web app origin in production.
GATEWAY_CORS_ORIGIN=http://localhost:3000
# ─── Auth (BetterAuth) ───────────────────────────────────────────────────────
# REQUIRED — random secret used to sign sessions and tokens.
# Generate with: openssl rand -base64 32
BETTER_AUTH_SECRET=change-me-to-a-random-32-char-string
# Public base URL of the gateway (used by BetterAuth for callback URLs)
BETTER_AUTH_URL=http://localhost:14242
# ─── Web App (Next.js) ───────────────────────────────────────────────────────
# Public gateway URL — accessible from the browser, not just the server.
NEXT_PUBLIC_GATEWAY_URL=http://localhost:14242
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
# OTLP HTTP endpoint (otel-collector or any OpenTelemetry-compatible backend)
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# Service name shown in traces
OTEL_SERVICE_NAME=mosaic-gateway
# ─── AI Providers ────────────────────────────────────────────────────────────
# Ollama (local models — set OLLAMA_BASE_URL to enable)
# OLLAMA_BASE_URL=http://localhost:11434
# OLLAMA_HOST is a legacy alias for OLLAMA_BASE_URL
# OLLAMA_HOST=http://localhost:11434
# Comma-separated list of Ollama model IDs to register (default: llama3.2,codellama,mistral)
# OLLAMA_MODELS=llama3.2,codellama,mistral
# Anthropic (claude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5)
# ANTHROPIC_API_KEY=sk-ant-...
# OpenAI (gpt-4o, gpt-4o-mini, o3-mini)
# OPENAI_API_KEY=sk-...
# Z.ai / GLM (glm-4.5, glm-4.5-air, glm-4.5-flash)
# ZAI_API_KEY=...
# Custom providers — JSON array of provider configs
# Format: [{"id":"<id>","baseUrl":"<url>","apiKey":"<key>","models":[{"id":"<model-id>","name":"<label>"}]}]
# MOSAIC_CUSTOM_PROVIDERS=
# ─── Embedding Service ───────────────────────────────────────────────────────
# OpenAI-compatible embeddings endpoint (default: OpenAI)
# EMBEDDING_API_URL=https://api.openai.com/v1
# EMBEDDING_MODEL=text-embedding-3-small
# ─── Log Summarization Service ───────────────────────────────────────────────
# OpenAI-compatible chat completions endpoint for log summarization (default: OpenAI)
# SUMMARIZATION_API_URL=https://api.openai.com/v1
# SUMMARIZATION_MODEL=gpt-4o-mini
# Cron schedule for summarization job (default: every 6 hours)
# SUMMARIZATION_CRON=0 */6 * * *
# Cron schedule for log tier management (default: daily at 03:00)
# TIER_MANAGEMENT_CRON=0 3 * * *
# ─── Agent ───────────────────────────────────────────────────────────────────
# Filesystem sandbox root for agent file tools (default: process.cwd())
# AGENT_FILE_SANDBOX_DIR=/var/lib/mosaic/sandbox
# Comma-separated list of tool names available to non-admin users.
# Leave unset to allow all tools for all authenticated users.
# AGENT_USER_TOOLS=read_file,list_directory,search_files
# System prompt injected into every agent session (optional)
# AGENT_SYSTEM_PROMPT=You are a helpful assistant.
# ─── MCP Servers ─────────────────────────────────────────────────────────────
# JSON array of MCP server configs — set to enable MCP tool integration.
# Each entry: {"name":"<id>","url":"<http-or-sse-url>"}
# MCP_SERVERS=[{"name":"my-mcp","url":"http://localhost:3100/sse"}]
# ─── Coordinator ─────────────────────────────────────────────────────────────
# Root directory used to scope coordinator (worktree/repo) operations.
# Defaults to the monorepo root auto-detected from process.cwd().
# MOSAIC_WORKSPACE_ROOT=/home/user/projects/mosaic
# ─── Discord Plugin (optional — set DISCORD_BOT_TOKEN to enable) ─────────────
# DISCORD_BOT_TOKEN=
# DISCORD_GUILD_ID=
# DISCORD_GATEWAY_URL=http://localhost:14242
# ─── Telegram Plugin (optional — set TELEGRAM_BOT_TOKEN to enable) ───────────
# TELEGRAM_BOT_TOKEN=
# TELEGRAM_GATEWAY_URL=http://localhost:14242
# ─── SSO Providers (add credentials to enable) ───────────────────────────────
# --- Authentik (optional — set AUTHENTIK_CLIENT_ID to enable) ---
# AUTHENTIK_ISSUER=https://auth.example.com/application/o/mosaic/
# AUTHENTIK_CLIENT_ID=
# AUTHENTIK_CLIENT_SECRET=
# --- WorkOS (optional — set WORKOS_CLIENT_ID to enable) ---
# WORKOS_ISSUER=https://your-company.authkit.app
# WORKOS_CLIENT_ID=client_...
# WORKOS_CLIENT_SECRET=sk_live_...
# --- Keycloak (optional — set KEYCLOAK_CLIENT_ID to enable) ---
# KEYCLOAK_ISSUER=https://auth.example.com/realms/master
# Legacy alternative if you prefer to compose the issuer from separate vars:
# KEYCLOAK_URL=https://auth.example.com
# KEYCLOAK_REALM=master
# KEYCLOAK_CLIENT_ID=mosaic
# KEYCLOAK_CLIENT_SECRET=
# The web login page discovers configured providers dynamically from
# GET /api/sso/providers. No NEXT_PUBLIC_* provider feature flag is required.
+23 -6
View File
@@ -1,8 +1,25 @@
# build/deps
node_modules/
# runtime credentials — never commit, never copy into the image
logs/
node_modules
dist
.turbo
.next
coverage
.env
secrets/
.env.local
*.tsbuildinfo
.pnpm-store
__pycache__/
docs/.obsidian
# generated runtime state lives in /home/jwoltje/.mosaic-dev (outside this project)
# Step-CA dev password — real file is gitignored; commit only the .example
infra/step-ca/dev-password
# Scratch dirs created by the framework git-wrapper shell test harnesses
.mosaic-test-work/
# Transient config files vite/vitest/esbuild write next to a *.config.ts while
# loading it, then unlink. They are untracked but were not ignored, so turbo's
# package traversal hashed them and intermittently failed CI with "Package
# traversal error: ... .timestamp-*.mjs: No such file or directory" when the
# file vanished mid-scan. Ignoring them removes the race.
*.timestamp-*.mjs
File renamed without changes.
File renamed without changes.
View File
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
-12
View File
@@ -1,12 +0,0 @@
{
"projectVersion": 1,
"id": "stack",
"vars": {
"tracker.project": 19,
"git.workingBranch": "refactor",
"git.protectedBranches": [
"main",
"next"
]
}
}
File renamed without changes.
File renamed without changes.
View File
File renamed without changes.
-6
View File
@@ -1,6 +0,0 @@
extensions/
extensions.installed.sha256
.extensions-*
state/
evidence/
native-test-*.log
-34
View File
@@ -1,34 +0,0 @@
# Native goal development copy
From this repository, start a fresh native Pi session:
```sh
bash scripts/goal-dev.sh
```
Canonical source lives under `extensions/`. The launcher first runs `scripts/sync-dev-extensions.sh`, which installs verified ordinary-file copies under `.pi/extensions/`, then loads only the generated goal extension. Global extensions remain unloaded. The launcher keeps your usual native Pi provider authentication; it copies no credentials. Goal state and new conversation files live under `.pi/state/`, which is ignored by Git. Each process gets a fresh incarnation; `/reload` and `/new` in the same process retain its goal. Restarting Pi does not adopt an earlier process's active goal.
Plain `pi` also discovers `.pi/extensions/goal/index.ts` after project trust, but may load global extensions too. Use the launcher to avoid duplicate `/goal` registrations. This is a local development test, not a sandbox or the managed Mosaic runtime. Docker and `~/.mosaic` are unchanged.
## Try it
1. Set `/goal <a long goal with acceptance criteria>`. This starts work immediately.
2. Look below the editor for `Goal: Active`. The old above-editor goal widget is gone.
3. Run bare `/goal`, then press `Alt+G`. Both show the entire stored goal and its status. Tab remains autocomplete.
4. Use `/goal stop` and `/goal resume`. Expect Paused and Active, or Waiting if an untimed wait remains recorded.
5. A blocked `goal_report` displays Blocked. A satisfied report displays Complete and retains the full goal for recall without continuing work.
6. `/goal clear` removes the retained goal. Try `NO_COLOR=1 bash scripts/goal-dev.sh` to check text-only labels.
Use terminal scrollback for recall longer than the screen. At narrow widths Pi may truncate its footer status row; bare `/goal` and Alt+G remain available.
## Checks
```sh
node --test extensions/goal/test/*.test.ts
bash scripts/test-extension-package.sh
python3 scripts/test-goal-native.py
```
Contract tests use ordinary read-only fixture copies in `test/fixtures/skills-local/`, not live brain files. The executive-update fixture SHA-256 matches the parser's pinned contract, `bbea48a46b1f8da7bc759f86856fb52830b7dde456b826317163c6dc6ccab319`.
`SOURCE-SNAPSHOT.json` records the original external-source baseline, not the edited candidate. No symlinks are used. Never edit `.pi/extensions/`; the sync script refuses to overwrite installation drift. Make changes under `extensions/`, run the checks, and relaunch. To disable the test, stop its Pi process and remove `.pi/extensions/`. Keep `.pi/state/` only if you need local test state.
-10
View File
@@ -1,10 +0,0 @@
{
"snapshotVersion": 1,
"copiedAt": "2026-09-06T04:58:22Z",
"source": "~/.mosaic/fleet/extensions",
"goalTreeSha256": "8853f2b72dde3e87c4573648b9a931c1c75da87ccde995c3224e6d2e707a75f0",
"mosaicCoreLibTreeSha256": "d1194dce31209e5773c6cc5ce571cbca3c39b29d943a79dea06665e05d29f319",
"symlinks": false,
"autoDiscoveredExtensions": ["goal"],
"purpose": "Issue #54 native Pi NG development copy; never loaded by Docker"
}
-5
View File
@@ -1,5 +0,0 @@
#!/usr/bin/env bash
# Compatibility entrypoint for the accepted native test command.
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.."
exec scripts/goal-dev.sh "$@"
File renamed without changes.
View File
File renamed without changes.
@@ -22,9 +22,9 @@ steps:
image: gcr.io/kaniko-project/executor:debug
environment:
REGISTRY_USER:
from_secret: REGISTRY_USERNAME
from_secret: gitea_username
REGISTRY_PASS:
from_secret: REGISTRY_PASSWORD
from_secret: gitea_password
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
+151
View File
@@ -0,0 +1,151 @@
# &node_image is the pre-baked CI base built by .woodpecker/ci-image.yml:
# node:24-alpine + python3/make/g++/postgresql-client + pnpm + a warm pnpm
# store. The install step resolves from the baked store (--prefer-offline)
# instead of paying a ~731s cold fetch + native compile every run.
variables:
- &node_image 'git.mosaicstack.dev/mosaicstack/stack/ci-base:latest'
- &enable_pnpm 'corepack enable'
when:
# PR + manual CI run on any branch — the pull_request pipeline is the merge gate.
# push CI is restricted to protected branches (main) so a feature-branch push no
# longer fires a redundant SECOND pipeline alongside its PR pipeline. This ~halves
# CI load on the storage-constrained runner with zero loss of gating (branch
# protection requires no push/ci status context; main still gets full push CI).
- event: [pull_request, manual]
- event: push
branch: main
# Turbo remote cache (turbo.mosaicstack.dev) is configured via Woodpecker
# repository-level environment variables (TURBO_API, TURBO_TEAM, TURBO_TOKEN).
# This avoids from_secret which is blocked on pull_request events.
# If the env vars aren't set, turbo falls back to local cache only.
steps:
install:
image: *node_image
commands:
- corepack enable
# python3/make/g++ are baked into ci-base; --prefer-offline resolves from
# the baked pnpm store.
- pnpm install --frozen-lockfile --prefer-offline
# Blocking gate: public framework package must contain no operator-specific
# personal data or private $HOME defaults. Runs early (no node_modules needed).
sanitization:
image: *node_image
commands:
- apk add --no-cache bash
- bash packages/mosaic/framework/tools/quality/scripts/verify-sanitized.sh
# Resident line-count ceiling over framework-owned resident files
# (Constitution + dispatcher + each RUNTIME.md slice). See DESIGN §7 / R9.
- bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh --self-test
- bash packages/mosaic/framework/tools/quality/scripts/check-resident-budget.sh
# Test-membership guard (#1017): also first link of test:framework-shell.
# Invoked from BOTH surfaces it audits (F2, PR #1018) — the guard is link
# [0] of the pnpm chain, so severing that chain would silence it together
# with everything it guards; this direct line keeps one instrument running.
- bash packages/mosaic/framework/tools/quality/scripts/check-test-enumeration.sh
# Blocking gate (#791): a framework upgrade must never write or delete an
# operator-owned path. The HARD GATE proves an unanticipated operator sentinel
# survives a keep-mode reseed byte-identical (with rsync present AND absent —
# keep mode is a single cp-based path that must not depend on rsync), and that a
# corrupt/empty/missing manifest aborts fail-closed leaving operator files
# untouched (B2/B3). The rollback gate proves a mid-sync failure is rolled back
# from the pre-update snapshot (B1). The durable-snapshot gate (#791 PR2) proves
# the retained, operator-scoped pre-update backup is taken before any mutation
# (0700/0600, secret never logged, retention-pruned) and that the post-sync
# verify net restores any operator file a manifest bug lets the sync touch. The
# migration matrix pins the v2→v3 contract-file semantics. Pure bash, no
# node_modules — runs early alongside sanitization.
upgrade-guard:
image: *node_image
commands:
- apk add --no-cache bash rsync
- bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-manifest-guard.sh
- bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-rollback.sh
- bash packages/mosaic/framework/tools/quality/scripts/test-upgrade-durable-snapshot.sh
- bash packages/mosaic/framework/tools/quality/scripts/test-install-migration.sh
typecheck:
image: *node_image
commands:
- *enable_pnpm
- pnpm typecheck
depends_on:
- install
- sanitization
- upgrade-guard
# lint, format, and test are independent — run in parallel after typecheck
lint:
image: *node_image
commands:
- *enable_pnpm
- pnpm lint
depends_on:
- typecheck
format:
image: *node_image
commands:
- *enable_pnpm
- pnpm format:check
depends_on:
- typecheck
test:
image: *node_image
environment:
# Avoid the namespace-level Woodpecker DB service named "postgres".
# The Kubernetes backend exposes service containers by step name.
DATABASE_URL: postgresql://mosaic:mosaic@ci-postgres:5432/mosaic
commands:
- *enable_pnpm
# openssl (#912) is the wake HMAC signer: the digest H1/H2, beacon B12,
# and install I8 legs hard-require it in CI. It is baked into ci-base via
# Dockerfile.ci, but ci-base only rebuilds on push-to-main/tag — this
# `apk add` guarantees openssl is present on PR pipelines too (and is a
# fast no-op once the rebuilt image already ships it).
- apk add --no-cache openssl
# Pi runtime (Invariant R): invariant_r_unittest.py hard-requires an
# installed `pi` binary at exactly this measured version — the test
# boots Pi's real tool registry to prove the read-only carve-out
# resolves to real, unshadowed builtins, and fails loud (by design)
# when the runtime is absent or drifts. The canonical Pi is
# @earendil-works/[email protected] exactly (@mariozechner/* is
# embedded-legacy). Step-level install because ci-base image publishes
# are currently blocked on registry auth; fold into Dockerfile.ci once
# that is fixed, keeping this as a fast no-op guard.
- npm install -g @earendil-works/[email protected]
# postgresql-client (pg_isready) is baked into ci-base.
# Wait up to 60s for CI postgres to be ready; fail fast if it never comes up.
- |
ready=0
for i in $(seq 1 60); do
if pg_isready -h ci-postgres -p 5432 -U mosaic; then
ready=1
break
fi
echo "Waiting for ci-postgres ($i/60)..."
sleep 1
done
if [ "$ready" -ne 1 ]; then
echo "ci-postgres did not become ready" >&2
exit 1
fi
# Run migrations (DATABASE_URL is set in environment above)
- pnpm --filter @mosaicstack/db run db:migrate
# Run all tests
- pnpm test
depends_on:
- typecheck
services:
ci-postgres:
image: pgvector/pgvector:pg17
environment:
POSTGRES_USER: mosaic
POSTGRES_PASSWORD: mosaic
POSTGRES_DB: mosaic
+296
View File
@@ -0,0 +1,296 @@
# Build, publish npm packages, and push Docker images
# Runs on main for stable publishes and on next for integration-line prereleases/images
variables:
# Pre-baked CI base (see .woodpecker/ci-image.yml): node:24-alpine +
# toolchain + warm pnpm store. Kills the second cold install publish pays.
- &node_image 'git.mosaicstack.dev/mosaicstack/stack/ci-base:latest'
- &enable_pnpm 'corepack enable'
# Heavy kaniko image builds (~25 min) — gate them so a merge that only touches
# the npm-only CLI (@mosaicstack/mosaic) or docs does NOT rebuild the platform
# images (gateway/appservice/web do not depend on @mosaicstack/mosaic). Releases
# (tags) always build everything. Exclude-list keeps the default SAFE: any
# non-excluded change still builds, so no transitive dep can silently go stale.
# (Woodpecker: `when` entries are OR'd; `path` applies to push/PR only — hence
# the separate `event: tag` entry.)
- &image_build_when
- event: tag
- event: [push, manual]
branch: main
path:
exclude:
- 'packages/mosaic/**'
- 'docs/**'
- '**/*.md'
- '.woodpecker/**'
- event: [push, manual]
branch: next
- &main_image_build_when
- event: tag
- event: [push, manual]
branch: main
path:
exclude:
- 'packages/mosaic/**'
- 'docs/**'
- '**/*.md'
- '.woodpecker/**'
when:
- branch: [main, next]
event: [push, manual, tag]
steps:
install:
image: *node_image
commands:
- corepack enable
# Resolve from the baked pnpm store instead of a cold network fetch.
- pnpm install --frozen-lockfile --prefer-offline
build:
image: *node_image
commands:
- *enable_pnpm
- pnpm build
depends_on:
- install
publish-npm:
image: *node_image
# Publish only when a publishable package changed (or on a release tag); a
# pure-docs merge runs no publish. Cheap step, but gated for cleanliness.
when:
- event: tag
- event: [push, manual]
branch: main
path:
include:
- 'packages/**'
environment:
NPM_TOKEN:
from_secret: gitea_token
commands:
- *enable_pnpm
# Configure auth for Gitea npm registry
- |
echo "//git.mosaicstack.dev/api/packages/mosaicstack/npm/:_authToken=$NPM_TOKEN" > ~/.npmrc
echo "@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/" >> ~/.npmrc
# Publish non-private packages to Gitea.
#
# The only publish failure we tolerate is "version already exists" —
# that legitimately happens when only some packages were bumped in
# the merge. Any other failure (registry 404, auth error, network
# error) MUST fail the pipeline loudly: the previous
# `|| echo "... continuing"` fallback silently hid a 404 from the
# Gitea org rename and caused every @mosaicstack/* publish to fall
# on the floor while CI still reported green.
- |
# Portable sh (Alpine ash) — avoid bashisms like PIPESTATUS.
set +e
pnpm --filter "@mosaicstack/*" --filter "!@mosaicstack/web" publish --no-git-checks --access public >/tmp/publish.log 2>&1
EXIT=$?
set -e
cat /tmp/publish.log
if [ "$EXIT" -eq 0 ]; then
echo "[publish] all packages published successfully"
exit 0
fi
# Hard registry / auth / network errors → fatal. Match npm's own
# error lines specifically to avoid false positives on arbitrary
# log text that happens to contain "E404" etc.
if grep -qE "npm (error|ERR!) code (E404|E401|ENEEDAUTH|ECONNREFUSED|ETIMEDOUT|ENOTFOUND)" /tmp/publish.log; then
echo "[publish] FATAL: registry/auth/network error detected — failing pipeline" >&2
exit 1
fi
# Only tolerate the explicit "version already published" case.
# npm returns this as E403 with body "You cannot publish over..."
# or EPUBLISHCONFLICT depending on version.
if grep -qE "EPUBLISHCONFLICT|You cannot publish over|previously published" /tmp/publish.log; then
echo "[publish] some packages already at this version — continuing (non-fatal)"
exit 0
fi
echo "[publish] FATAL: publish failed with unrecognized error — failing pipeline" >&2
exit 1
depends_on:
- build
publish-next-npm:
image: *node_image
# Durable @next integration-line publish. Runs only on next; never writes
# the latest dist-tag and never commits the computed prerelease versions.
when:
- event: [push, manual]
branch: next
environment:
NPM_TOKEN:
from_secret: gitea_token
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
CI_PIPELINE_NUMBER: ${CI_PIPELINE_NUMBER}
commands:
- *enable_pnpm
- |
if [ "$CI_COMMIT_BRANCH" != "next" ]; then
echo "[publish-next] FATAL: publish-next-npm may only run on next (got '$CI_COMMIT_BRANCH')" >&2
exit 1
fi
if [ -z "$CI_PIPELINE_NUMBER" ]; then
echo "[publish-next] FATAL: CI_PIPELINE_NUMBER is required for prerelease versioning" >&2
exit 1
fi
echo "//git.mosaicstack.dev/api/packages/mosaicstack/npm/:_authToken=$NPM_TOKEN" > ~/.npmrc
echo "@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/" >> ~/.npmrc
DIST_TAGS_JSON="$(npm view @mosaicstack/mosaic dist-tags --registry https://git.mosaicstack.dev/api/packages/mosaicstack/npm/ --json)"
DIST_TAGS_JSON="$DIST_TAGS_JSON" node -e 'const tags = JSON.parse(process.env.DIST_TAGS_JSON || "{}"); if (!tags || typeof tags !== "object" || !Object.hasOwn(tags, "latest")) { throw new Error("Gitea npm registry did not return a usable dist-tags object"); } console.log("[publish-next] registry dist-tags OK: latest=" + tags.latest);'
node <<'NODE'
const fs = require('node:fs');
const path = require('node:path');
const pipelineNumber = process.env.CI_PIPELINE_NUMBER;
const roots = ['apps', 'packages', 'plugins'];
const updated = [];
function walk(dir) {
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.turbo') continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
const packagePath = path.join(fullPath, 'package.json');
if (fs.existsSync(packagePath)) updatePackage(packagePath);
walk(fullPath);
}
}
}
function updatePackage(packagePath) {
const manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
if (!manifest.name?.startsWith('@mosaicstack/') || manifest.private) return;
const stableMatch = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(manifest.version);
if (!stableMatch) {
throw new Error(manifest.name + " has unsupported semver version '" + manifest.version + "'");
}
const [, major, minor, patch] = stableMatch;
const oldVersion = manifest.version;
manifest.version = major + '.' + minor + '.' + (Number(patch) + 1) + '-next.' + pipelineNumber;
fs.writeFileSync(packagePath, JSON.stringify(manifest, null, 2) + '\n');
updated.push(manifest.name + ' ' + oldVersion + ' -> ' + manifest.version);
}
for (const root of roots) walk(root);
if (updated.length === 0) throw new Error('No publishable @mosaicstack/* packages found');
console.log('[publish-next] computed prerelease versions for ' + updated.length + ' packages:');
for (const line of updated) console.log('[publish-next] ' + line);
NODE
pnpm --filter "@mosaicstack/*" --filter "!@mosaicstack/web" --filter "!@mosaicstack/mosaic-as" publish --no-git-checks --access public --tag next
EXPECTED_VERSION="$(node -p "require('./packages/mosaic/package.json').version")"
RESOLVED_VERSION="$(npm view @mosaicstack/mosaic@next version --registry https://git.mosaicstack.dev/api/packages/mosaicstack/npm/)"
if [ "$RESOLVED_VERSION" != "$EXPECTED_VERSION" ]; then
echo "[publish-next] FATAL: @mosaicstack/mosaic@next resolved '$RESOLVED_VERSION', expected '$EXPECTED_VERSION'" >&2
exit 1
fi
echo "[publish-next] @mosaicstack/mosaic@next resolves to $RESOLVED_VERSION"
depends_on:
- build
# TODO: Uncomment when ready to publish to npmjs.org
# publish-npmjs:
# image: *node_image
# environment:
# NPM_TOKEN:
# from_secret: npmjs_token
# commands:
# - *enable_pnpm
# - apk add --no-cache jq bash
# - bash scripts/publish-npmjs.sh
# depends_on:
# - build
# when:
# - event: [tag]
build-gateway:
image: gcr.io/kaniko-project/executor:debug
when: *image_build_when
environment:
REGISTRY_USER:
from_secret: gitea_username
REGISTRY_PASS:
from_secret: gitea_password
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
commands:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
- |
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/gateway:sha-${CI_COMMIT_SHA:0:7}"
if [ "$CI_COMMIT_BRANCH" = "next" ]; then
if [ -n "$CI_COMMIT_TAG" ]; then
echo "[publish] FATAL: next gateway publish must be sha-only; refusing tag '$CI_COMMIT_TAG'" >&2
exit 1
fi
echo "[publish] next gateway publish is sha-only"
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:latest"
elif [ -z "$CI_COMMIT_TAG" ]; then
echo "[publish] FATAL: gateway image publish may only run for main, next, or tag events" >&2
exit 1
fi
if [ -n "$CI_COMMIT_TAG" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/gateway:$CI_COMMIT_TAG"
fi
/kaniko/executor --context . --dockerfile docker/gateway.Dockerfile $DESTINATIONS
depends_on:
- build
build-appservice:
image: gcr.io/kaniko-project/executor:debug
when: *main_image_build_when
environment:
REGISTRY_USER:
from_secret: gitea_username
REGISTRY_PASS:
from_secret: gitea_password
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
commands:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
- |
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/appservice:sha-${CI_COMMIT_SHA:0:7}"
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:latest"
fi
if [ -n "$CI_COMMIT_TAG" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:$CI_COMMIT_TAG"
fi
/kaniko/executor --context . --dockerfile docker/appservice.Dockerfile $DESTINATIONS
depends_on:
- build
build-web:
image: gcr.io/kaniko-project/executor:debug
when: *main_image_build_when
environment:
REGISTRY_USER:
from_secret: gitea_username
REGISTRY_PASS:
from_secret: gitea_password
CI_COMMIT_BRANCH: ${CI_COMMIT_BRANCH}
CI_COMMIT_TAG: ${CI_COMMIT_TAG}
CI_COMMIT_SHA: ${CI_COMMIT_SHA}
commands:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
- |
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/web:sha-${CI_COMMIT_SHA:0:7}"
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:latest"
fi
if [ -n "$CI_COMMIT_TAG" ]; then
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:$CI_COMMIT_TAG"
fi
/kaniko/executor --context . --dockerfile docker/web.Dockerfile $DESTINATIONS
depends_on:
- build
+91 -194
View File
@@ -1,222 +1,119 @@
# AGENTS.md — Mosaic Stack rebuild (`mosaicstack/stack`, branch `refactor`)
# Agent Guidelines — Mosaic Stack
Operational context for any agent session working in this repository.
Read top to bottom; it is deliberately short — depth lives in the files it
points to, not here.
## Required Load Order
## What this repository is
1. `~/.config/mosaic/SOUL.md`
2. `~/.config/mosaic/STANDARDS.md`
3. `~/.config/mosaic/AGENTS.md`
4. `~/.config/mosaic/guides/E2E-DELIVERY.md`
5. `AGENTS.md` (this file)
6. Runtime-specific guide: `~/.config/mosaic/runtime/<runtime>/RUNTIME.md`
Canonical checkout: `/mnt/storage/src/mosaic-stack`, origin `mosaicstack/stack`,
working branch `refactor` (Jason-authorized conversion, issue #1495).
The new foundation is at the root. `v1/` is archived legacy source, not the current
implementation; its instructions and tools do not govern the new foundation.
`~/src/mosaic-stack-dev-test` is a compatibility symlink to this checkout, not a
second working tree. Both original Git histories are retained. Conversion receipt:
`docs/plans/2026-09-07_repository-consolidation-completed.md`.
## Project Context
A rebuild of Mosaic Stack: a file-based, fail-closed
orchestration foundation that dispatches sandboxed headless pi workers to do
real work, with immutable run records as evidence. Thirteen-plus tagged
milestones (`git tag -l`) from `poc-container-hello-v0` to today; suites
green at every step. Not production software — a proven foundation.
Mosaic Stack is a self-hosted, multi-user AI agent platform. It is a TypeScript monorepo with a NestJS gateway, Next.js dashboard, Pi SDK agent runtime, and Discord/Telegram plugin architecture.
## Non-negotiable invariants (the canon)
### Stack
1. **Root is bootstrap-only.** First-class system configuration lives at the
repository root; everything else gets a dedicated directory (`roles/`,
`contracts/`, `missions/`, `tasks/`, `docs/`). Do not add new files to root.
2. **Configuration**: each stack instance has exactly one system config. The
default instance's is `~/.config/mosaic-dev/config.json`. Instance NAME's
is `~/.config/mosaic-NAME/config.json`, and its data root carries a marker
that names it. `~` is the home of the OS user that runs the instance. A
process reads only the config `MOSAIC_CONFIG` names, or the default's when
that is unset. Configs are created only by
`scripts/bootstrap.sh [--instance NAME]`, never overwritten, fail-closed on
any problem. Repo-scoped role authority lives in `roles/*.json`
(versioned, reviewed commits only).
3. **Secrets** never enter the repository or container images; auth is
runtime-only (read-only mount or environment variable).
4. **Contracts** (`contracts/`) are immutable and image-baked. Missions and
tasks are declarative JSON with strict schemas.
5. **Run records** under `<dataRoot>/runs/` are write-once evidence — never
rewritten, only pruned via `prune` with a receipt.
6. **Fail closed**: missing or invalid config/policy refuses the operation.
Never improvise around a refusal; diagnose it.
7. **Policy**: missions govern tasks (least-privilege intersection — a task
narrows, never widens). Role authority is declared in `roles/` and changes
only via reviewed commits.
8. **Git**: commit only after applicable suites are green. Work on the
owner-authorized `refactor` branch; never force-push. Push remains an explicit
act. Do not merge into `next` or `main` without separate authorization.
`scripts/conductor-apply.sh` commits locally; it does not authorize a push.
9. **Append-only logs**: BUILD-LOG.md (phases), `activation-log.jsonl`,
`.pruned.log`, docs/SESSIONS.md. Corrections are new entries, never edits.
- **API:** NestJS with Fastify (`apps/gateway`)
- **Web:** Next.js 16 with React 19 (`apps/web`)
- **ORM and database:** Drizzle ORM, PostgreSQL 17, and pgvector (`packages/db`)
- **Authentication:** BetterAuth (`packages/auth`)
- **Agent runtime:** Pi SDK (`apps/gateway`, `packages/mosaic`)
- **Queue:** Valkey 8 (`packages/queue`)
- **Build:** pnpm workspaces and Turborepo
- **CI:** Woodpecker CI
- **Observability:** OpenTelemetry and Jaeger
## Autonomous operation within an agreed plan
### Package Map
Autonomy starts after alignment, not before it. For a new substantial assignment,
recover the applicable mission, goal, task, `CURRENT.md` state, and prior owner
decisions, then work with the user to establish a plan of action: the intended
outcome, acceptance evidence, boundaries, and any gated actions. Recommend a
concrete plan instead of presenting an open-ended menu. A direct request or
existing approved plan that already settles those points is sufficient alignment;
do not ask for ceremonial reconfirmation.
| Package | Purpose | Key Dependencies |
| ------------------ | ----------------------------- | -------------------------------- |
| `apps/gateway` | NestJS API + WebSocket hub | Fastify, Socket.IO, Pi SDK, OTEL |
| `apps/web` | Next.js dashboard | React 19, Tailwind |
| `packages/types` | Shared TypeScript contracts | class-validator |
| `packages/db` | Drizzle schema and migrations | drizzle-orm, postgres |
| `packages/auth` | BetterAuth configuration | better-auth, @mosaicstack/db |
| `packages/brain` | Structured data layer | @mosaicstack/db |
| `packages/queue` | Valkey task queue and MCP | ioredis |
| `packages/coord` | Mission coordination | @mosaicstack/queue |
| `packages/mosaic` | Unified `mosaic` CLI and TUI | Ink, Pi SDK, commander |
| `plugins/discord` | Discord channel plugin | discord.js |
| `plugins/telegram` | Telegram channel plugin | Telegraf |
Once the plan is established, carry it to verified completion without prompting
for routine decisions or permission to take the next in-scope step. Authorization
persists for the life of that assignment unless the user changes or revokes it.
Treat mid-session user input as steering: incorporate it, update the plan or
tracking record when needed, and continue.
## Architecture and Code Conventions
### Decide and continue
1. Gateway is the single API surface; all clients connect through it.
2. Pi SDK is ESM-only; gateway and CLI code must remain ESM.
3. Use `"type": "module"`, NodeNext module resolution, and `.js` extensions in imports.
4. Keep typed Socket.IO events in `@mosaicstack/types` to enforce client/server contracts.
5. Import OTEL tracing before NestJS bootstrap (`import './tracing.js'`).
6. Use explicit `@Inject()` decorators in NestJS because tsx/esbuild does not emit decorator metadata.
7. Keep DTOs in `*.dto.ts` files at module boundaries.
8. BetterAuth owns authentication tables; their schema is defined in `@mosaicstack/db`.
9. Create a task-specific scratchpad for non-trivial work.
- Resolve naming, implementation approach, layout, and similar non-breaking
choices from, in order: repository invariants and role policy, the approved
plan and acceptance criteria, established repository conventions, then the
smallest reversible option. Record a consequential choice and its tradeoff.
- Perform the in-scope investigation, edits, tests, documentation, and tracking
needed for end-to-end acceptance. Do not ask whether to add obviously required
tests or documentation.
- Diagnose failures and retry or remediate within the agreed scope. Fix a defect
when it blocks acceptance or is local to files already being changed; otherwise
record a bounded follow-up without expanding the assignment.
- Resolve minor ambiguity in favor of the mission, goal, north star, and prior
owner decisions. State the assumption in the completion report.
- Never stop merely to ask whether to proceed, which routine option to use, or
whether to execute the next step already contained in the plan.
## Development Workflow
### Re-align or stop only at a real boundary
Requirements: Node.js 20+, pnpm 10.6.2, and Docker Compose when optional local services are needed.
Finish all independent work first, then ask one focused question only when:
```bash
pnpm install --frozen-lockfile
pnpm preflight
1. Two plausible readings materially change the outcome and the choice is costly
to reverse.
2. The next action would exceed the agreed scope or authority, introduce an
unapproved breaking public/API/schema/data/policy change, or alter a security
boundary.
3. Credentials or access are missing and no in-scope path remains.
4. The action is destructive, irreversible, production-affecting, incurs spend,
or communicates externally on the user's behalf without explicit authority.
5. Objectives or owner decisions genuinely conflict and repository evidence
cannot resolve them.
6. A fail-closed policy refusal or another agent's overlapping ownership prevents
safe progress. Diagnose and report it; never route around it.
# Optional local queue service only; do not start the full Compose stack.
docker compose up -d valkey
```
Repository gates still apply. In particular, a successful implementation or a
broad request to “finish” does not by itself authorize push, merge, deployment,
release, production changes, policy/role expansion, or access to secrets. Perform
such an action only when the established plan explicitly includes it. If blocked,
report the exact boundary, what is complete, the recommended resolution, and the
specific action that will resume; do not use “waiting for confirmation” as a
substitute for a real blocker.
The pre-push hook requires:
## Session protocol (mandatory)
```bash
pnpm preflight && pnpm typecheck && pnpm lint && pnpm format:check
```
- **Register** your session in `docs/SESSIONS.md` — one append-only line
(date, actor, scope, outcome). Never rewrite or remove entries.
- **Cadence**: run `scripts/mosaic queue next <your seat>` first. It names
the row to resume, review or start, or says there is nothing. The goal order
in `docs/plans/2026-09-27_goals-review.md` sets priority, not CURRENT.md.
Open only the brief that row links to. Execute it through every authorized
stage (implement → test → verify against acceptance criteria; commit, push,
or close only when the established plan authorizes each) → move the row with
`scripts/mosaic queue move` (never by editing QUEUE.md) → register in
SESSIONS.md.
- "next" means one action. A batch mandate ("run the queue") repeats the
loop until green or truly blocked under the boundary rules above.
- Substantial work gets a Gitea issue and a BUILD-LOG phase entry
(before/after, with corrections recorded honestly).
Software delivery also requires the applicable tests. Common repository commands are:
## Internal development bootstrap
```bash
pnpm typecheck # TypeScript checks across the workspace
pnpm lint # ESLint across the workspace
pnpm test # Checkout tests and package Vitest suites
pnpm format:check # Prettier check
pnpm build # Build all packages and applications
```
Jason's current direction is repository-native development in
`/mnt/storage/src/mosaic-stack`. Sage leads the project (Jason's ruling,
2026-09-26) and coordinates coding, review and research through Darkwing, Dewey,
Filbert, Rocko, Researcher and any further seats Jason launches under `agents/`.
Darkwing is a collaborating agent seat, not the coordinator. Development sessions
run in T3 for now. Work moves to the new stack; the old `~/.mosaic` fleet is being
retired, and a fleet seat acting outside Jason's instructions is the failure this
transition exists to prevent.
Do not assign new development work to fleet seats during this bootstrap phase.
Do not modify `~/.mosaic` launchers, provisioning or other state, or stop/migrate
live fleet processes as part of this work. Preserve existing work and histories.
Use the repository bootstrap/configuration and launch entry points; missing
configuration still fails closed. This changes development coordination, not
managed worker role policy or deployment authority. The lead role adds no push,
merge or deployment authority; those still need Jason's say-so. See
`agents/README.md` for the internal roster.
## Database and Local Runtime Safety
For control-board attention, start a completed reply with `Input needed: ` and
one specific nonempty request only when Jason must provide a decision or input.
Put that line at column zero, before other text. Do not use it for routine
completion or a wait on another agent. Ordinary completed replies are idle.
Use code fences or blockquotes when showing this convention as an example.
The signal is advisory status, never permission for a protected action. Seen
acknowledges a request; it does not resolve it. See `packages/control-board/README.md`.
- Current local data-layer work uses in-process PGlite; leave `DATABASE_URL` unset.
- PostgreSQL execution is held until KBN-101-00, KBN-101-03, and KBN-101-05 land.
- Do not invoke a migration runner, initialization SQL, or the Compose PostgreSQL service from this checkout.
- Do not start Gateway/Web or run root `pnpm dev` as a local PGlite route. The current dotenv loader can inherit a daemon PostgreSQL DSN; KBN-101-02 must make that path fail closed first.
- Migration artifact generation is offline and does not authorize PostgreSQL access:
## Role model
```bash
pnpm --filter @mosaicstack/db db:generate
```
- **Conductor**: a system-scoped role — not an agent, not a daemon. Holds
git/credentials/policy authority; decomposes, dispatches, reviews,
verifies, integrates. Protocol: `docs/plans/CONDUCTOR.md`. Exists only
when invoked; push is never automatic.
- **Workers**: headless pi via `scripts/run-task.sh` — sandboxed workspace,
tools allowlist, optional persistent sessions and forks; no git, no
credentials, no policy control.
- Worker runs deliberately exclude this file (`--no-context-files` in the
adapter): worker context is contracts + mission via the generated system
prompt. This file is for conductor-level sessions.
## docs/TASKS.md — Schema (CANONICAL)
## Command surface
The `agent` column specifies the required model for each task. **This is set at task creation by the orchestrator and must not be changed by workers.**
`scripts/bootstrap.sh` (idempotent) · `build.sh` · `hello.sh` ·
`verify.sh` · `run-task.sh run <task.json>` · `release.sh
package|activate|rollback|status` · `auth.sh status|accounts` · `reset.sh` (**danger**: wipes the data
root; triple-safety-checked) · `mosaic-task.mjs validate|run|show|list|retry|prune|resolve-role` ·
`agent.sh <name>` (interactive TUI agent) ·
suites: `test-config.sh`, `test-task.sh`, `test-release.sh`,
`test-conductor.sh`, `test-auth.sh`, `test-discord.sh`, `test-queue.sh`.
| Value | When to use | Budget |
| --------- | ----------------------------------------------------------- | -------------------------- |
| `codex` | All coding tasks (default for implementation) | OpenAI credits — preferred |
| `glm-5.1` | Cost-sensitive coding where Codex is unavailable | Z.ai credits |
| `haiku` | Review gates, verify tasks, status checks, docs-only | Cheapest Claude tier |
| `sonnet` | Complex planning, multi-file reasoning, architecture review | Claude quota |
| `opus` | Major cross-cutting architecture decisions ONLY | Most expensive — minimize |
| `—` | No preference / auto-select cheapest capable | Pipeline decides |
Full reference — usage, fields, exit codes, safety notes:
`docs/TOOLS.md` (read on demand; do not rely on this summary for detail).
Pipeline crons read this column and spawn accordingly. Workers never modify `docs/TASKS.md` — only the orchestrator writes it.
## Data map (canon)
**Full schema:**
- `~/.config/mosaic-dev/config.json`, or `~/.config/mosaic-NAME/config.json`
per named instance: system config (user-authored; never auto-written).
- `<dataRoot>` (from config; default `~/.mosaic-dev`):
- `runs/` — write-once run evidence (`result.json`, snapshots, `stderr.txt`)
- `sessions/` — pi JSONL session trees, one directory per named session
- `workspaces/` — agent file effects (persistent or `:run` ephemeral)
- `state/` — release pointer + append-only activation/auto-apply logs
- Ownership is per-directory; nothing shares state. Directory map and
lifecycle rules: README.md "Data map" section.
```
| id | status | description | issue | agent | repo | branch | depends_on | estimate | notes |
```
## Pointers (depth lives here)
- `docs/plans/2026-09-27_goals-review.md` — north star and goal order (Jason ratified 2026-09-27)
- `docs/plans/QUEUE.md` — THE task list, rendered from `docs/plans/queue.json`
(`scripts/mosaic queue next <seat>` reads it; `packages/queue/README.md` has the verbs)
- `docs/plans/CURRENT.md` — narrative log behind the queue rows
- `docs/plans/ROADMAP.md` — agreed milestone path (M16+)
- `docs/plans/CONDUCTOR.md` — orchestration protocol and guardrails
- `docs/plans/2026-09-02_atomic-mosaic-foundation.md` — architecture, invariants
- `docs/plans/2026-09-03_autonomous-run.md` — batch-run tracker
- `BUILD-LOG.md` — append-only build/verification history with corrections
- `LAYERS.md` — implemented vs deferred layers
- `docs/SESSIONS.md` — session registry
- `adapters/README.md` — the harness adapter contract
- `roles/` — role contracts (conductor, future agent/coder/reviewer)
## Recovery rule
Compacted, restarted, or new? Nothing that matters is lost: this file +
`scripts/mosaic queue next <seat>` + `docs/plans/CURRENT.md` +
`git log --oneline -10` + the suites reconstruct the full state. **Never
guess** — verify with the suites; the run records and logs hold the receipts.
## Version pin
`@earendil-works/pi-coding-agent` is pinned exactly (see `package.json` /
`RELEASE`); never install unversioned. Release identity: `RELEASE` file
(0.0.X until declared stable); image tags derive from it.
- `status`: `not-started` | `in-progress` | `done` | `failed` | `blocked` | `needs-qa`
- `agent`: model value from table above (set before spawning)
- `estimate`: token budget e.g. `8K`, `25K`
-371
View File
@@ -1,371 +0,0 @@
# 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.
-5469
View File
File diff suppressed because it is too large. Load diff
+5 -1
View File
@@ -1 +1,5 @@
@AGENTS.md
# Claude Compatibility Pointer
@AGENTS.md
Do not add project guidance here. Keep `AGENTS.md` authoritative so every agent runtime receives the same instructions.
-44
View File
@@ -1,44 +0,0 @@
# 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), runtime scripts, and
# runtime adapters.
COPY contracts /opt/mosaic/contracts
COPY src /opt/mosaic/src
COPY packages/processing-control/src/state.mjs packages/processing-control/src/entrypoint.mjs /opt/mosaic/processing-control/
COPY adapters /opt/mosaic/adapters
RUN chmod 0555 /opt/mosaic/contracts /opt/mosaic/contracts/* \
&& chmod 0555 /opt/mosaic/src /opt/mosaic/src/*.sh \
&& chmod 0555 /opt/mosaic/adapters /opt/mosaic/adapters/*/adapter.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"]
View File
File renamed without changes.
-50
View File
@@ -1,50 +0,0 @@
# 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.
View File
File renamed without changes.
+370 -211
View File
@@ -1,248 +1,407 @@
# Mosaic Stack — new foundation
# Mosaic Stack
The active rebuild is at this repository's root. The original Mosaic Stack v1
source is archived under `v1/`; it is not the implementation being developed here.
Self-hosted, multi-user AI agent platform. One config, every runtime, same standards.
- Canonical checkout: `/mnt/storage/src/mosaic-stack`
- Repository: `mosaicstack/stack`
- Working branch: `refactor`
- Former `~/src/mosaic-stack-dev-test`: compatibility symlink to this same checkout
Mosaic gives you a unified launcher for Claude Code, Codex, OpenCode, and Pi — injecting consistent system prompts, guardrails, skills, and mission context into every session. A NestJS gateway provides the API surface, a Next.js dashboard gives you the UI, and a plugin system connects Discord, Telegram, and more.
Both original Git histories and pending development work are preserved. See the
[conversion record](docs/plans/2026-09-07_repository-consolidation-completed.md)
and [current next action](docs/plans/CURRENT.md). Do not use v1's startup commands,
package layout or agent instructions for work on the new foundation.
## Original container proof
The foundation began as a standalone container experiment. 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 was verified to return exactly
`MOSAIC_HELLO_OK`. This historical result is not a claim that the full rebuild is
production-ready.
## 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:
## Quick Install
```bash
scripts/bootstrap.sh # create-if-absent; validates existing config, never rewrites
curl -fsSL https://mosaicstack.dev/install.sh | bash
```
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.
The optional `install` key selects the containerized install explicitly:
`"install": {"mode":"containerized","endpoint":"unix:///run/user/1000/docker.sock"}`.
An absent key defaults to local mode and remains absent in validation output.
Local mode forbids `endpoint`;
containerized mode requires a Unix socket endpoint without whitespace
(the actual pattern is `^unix:///[^\s]+\.sock$`). Unknown install keys or
modes refuse with exit 2 without changing the config. Validation contacts no
daemon; the endpoint helper checks rootless status before engine use. Keep
existing live configs unchanged until the install rollout is authorized.
## 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
<dataRoot>/runs/r-<id>/ immutable run record: task.json, mission.json,
stderr.txt, result.json (all write-once)
```
Usage:
Or use the direct URL:
```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)
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
```
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:
The installer auto-launches the setup wizard, which walks you through gateway install and verification. Flags for non-interactive use:
```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 ensure # self-determination: align installed to RELEASE (safe no-op when aligned)
scripts/release.sh status # release, tag, active pointer, recent log
scripts/test-release.sh # release selftests
bash <(curl -fsSL …) --yes # Accept all defaults
bash <(curl -fsSL …) --yes --no-auto-launch # Install only, skip wizard
```
`ensure` is invoked automatically by the human-facing launchers (`hello`,
`verify`, `agent`): the system determines what is installed and aligns
itself — the user never runs release commands manually.
This installs both components:
- `<dataRoot>/state/active.json` — the activation pointer (atomic tmp+rename replace)
- `<dataRoot>/state/activation-log.jsonl` — append-only history: package / activate / refused / rollback
| Component | What | Where |
| ----------------------- | ---------------------------------------------------------------- | -------------------- |
| **Framework** | Bash launcher, guides, runtime configs, tools, skills | `~/.config/mosaic/` |
| **@mosaicstack/mosaic** | Unified `mosaic` CLI — TUI, gateway client, wizard, auto-updater | `~/.npm-global/bin/` |
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.
### Install lanes
## Runtime adapters (M4)
| Lane | Command | Use when | Source |
| ------------------------ | ------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------- |
| Stable | `bash tools/install.sh` | You want the released Mosaic CLI/framework | npm registry `@mosaicstack/mosaic@latest` + framework archive at `main` |
| Prerelease integration | `bash tools/install.sh --next` | You want the current `next` integration branch | Build-from-source at `next` |
| Contributor/source build | `bash tools/install.sh --dev --ref X` | You are testing a branch before release; `--ref` wins | Build-from-source at the requested ref |
The harness boundary is formalized: everything upstream (config, contracts, missions, tasks, run records) is harness-agnostic; everything inside an adapter belongs to one runtime.
`--next` is shorthand for the prerelease integration lane: it enables source-build mode and uses `next` unless an explicit `--ref` or `MOSAIC_REF` is provided.
```text
adapters/<name>/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.
## Workspaces, capabilities, sessions (M5/M6)
Optional task fields extend what an agent can do — all defaulting to the previous behavior:
```json
{
"workspace": "demo", // ":run" ephemeral, or persistent dataRoot/workspaces/<name>
"capabilities": { "tools": ["bash", "read"] }, // pi tool allowlist; absent = no tools
"session": "demo" // persistent session at dataRoot/sessions/<name>
}
```
- The adapter runs inside the workspace; files it writes are host-visible (`dataRoot/workspaces/<name>`).
- Sessions persist via pi's documented `--session-dir`; a follow-up run in the same session resumes the conversation (`-c`) and can recall prior context. Distinct names never share state. Ephemeral (`--no-session`) remains the default when no session is declared.
- Selection authority: config for adapter/provider/model; the task file for workspace/capabilities/session.
Inspect anything:
After install, the wizard runs automatically or you can invoke it manually:
```bash
node scripts/mosaic-task.mjs list # runs with task/workspace/session columns
node scripts/mosaic-task.mjs show <runId> # full record + snapshots + artifacts
mosaic wizard # Full guided setup (gateway install → verify)
```
Demo fixtures: `tasks/workspace-demo.json`, `tasks/session-demo-1.json` + `tasks/session-demo-2.json`.
### Requirements
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:<pi>-r<release>`,
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.
- Node.js ≥ 22
- npm (for global @mosaicstack/mosaic install)
- One or more runtimes:
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
- [Codex](https://github.com/openai/codex)
- [OpenCode](https://opencode.ai)
- [Pi](https://pi.dev)
## 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):
### Launching Agent Sessions
```bash
EXPECTED_MARKER=MOSAIC_NOT_OK scripts/verify.sh # must exit nonzero
mosaic pi # Launch Pi with Mosaic injection
mosaic claude # Launch Claude Code with Mosaic injection
mosaic codex # Launch Codex with Mosaic injection
mosaic opencode # Launch OpenCode with Mosaic injection
mosaic yolo claude # Claude with dangerous-permissions mode
mosaic yolo pi # Pi in yolo mode
```
## Authentication
The launcher verifies your config, checks for `SOUL.md`, injects your `AGENTS.md` standards into the runtime, and forwards all arguments.
Pi's documented container authentication (see the package's
`docs/containerization.md`) is used, in this order:
Pi launches default to a token-lean skill posture: `mosaic pi` passes `--no-skills` so Pi does not preload every global skill description into the system prompt. Use `MOSAIC_PI_SKILL_MODE=all mosaic pi` for the legacy all-skills catalog, or `MOSAIC_PI_SKILL_MODE=discover mosaic pi` to let Pi use its native settings/project skill discovery.
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.
### TUI & Gateway
Credentials are never committed, never copied into the image, and never printed.
Mosaic-managed named accounts (`agent.sh --auth`) live under the data root
(`auth/<account>.json`, 0600) — the stack never writes into `~/.pi`.
`.env.example` contains non-secret settings only.
```bash
mosaic tui # Interactive TUI connected to the gateway
mosaic gateway login # Authenticate with a gateway instance
mosaic sessions list # List active agent sessions
```
## Boundaries honored
### Gateway Management
- 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.
```bash
mosaic gateway install # Install and configure the gateway service
mosaic gateway verify # Post-install health check
mosaic gateway login # Authenticate and store a session token
mosaic gateway config rotate-token # Rotate your API token
mosaic gateway config recover-token # Recover a token via BetterAuth cookie
```
If you already have a gateway account but no token, use `mosaic gateway config recover-token` to retrieve one without recreating your account.
### Configuration
Mosaic supports three storage tiers: `local` (PGlite, single-host), `standalone` (PostgreSQL, single-host), and `federated` (PostgreSQL + pgvector + Valkey, multi-host). See [Federated Tier Setup](docs/federation/SETUP.md) for multi-user and production deployments, or [Migrating to Federated](docs/guides/migrate-tier.md) to upgrade from existing tiers.
```bash
mosaic config show # Print full config as JSON
mosaic config get <key> # Read a specific key
mosaic config set <key> <val># Write a key
mosaic config edit # Open config in $EDITOR
mosaic config path # Print config file path
```
### Management
```bash
mosaic doctor # Health audit — detect drift and missing files
mosaic sync # Sync skills from canonical source
mosaic skill list # Audit Claude skill registrations and conflicts
mosaic skill register <name> # Register one canonical skill with Claude Code
mosaic skill unregister <name> # Remove one Mosaic-owned Claude link
mosaic update # Update CLI/framework and auto-register canonical skills
mosaic wizard # Full guided setup wizard
mosaic bootstrap <path> # Bootstrap a repo with Mosaic standards
mosaic coord init # Initialize a new orchestration mission
mosaic prdy init # Create a PRD via guided session
```
### Sub-package Commands
Each Mosaic sub-package exposes its API surface through the unified CLI:
```bash
# User management
mosaic auth users list
mosaic auth users create
mosaic auth sso
# Agent brain (projects, missions, tasks)
mosaic brain projects
mosaic brain missions
mosaic brain tasks
mosaic brain conversations
# Agent forge pipeline
mosaic forge run
mosaic forge status
mosaic forge resume
mosaic forge personas
# Structured logging
mosaic log tail
mosaic log search
mosaic log export
mosaic log level
# MACP protocol
mosaic macp tasks
mosaic macp submit
mosaic macp gate
mosaic macp events
# Agent memory
mosaic memory search
mosaic memory stats
mosaic memory insights
mosaic memory preferences
# Task queue (Valkey)
mosaic queue list
mosaic queue stats
mosaic queue pause
mosaic queue resume
mosaic queue jobs
mosaic queue drain
# Object storage
mosaic storage status
mosaic storage tier
mosaic storage export
mosaic storage import
# Schema migration is unavailable in this release. The current storage wrapper shells
# directly to `pnpm --filter @mosaicstack/db db:migrate`; it is legacy N-1,
# uncertified, and MUST NOT be invoked pending KBN-101-02/-03/-06/-08 activation.
# Future schema migration is non-operative: external bootstrap → TLS/roles → runner
# --run → runner --verify → readiness. Tier copy uses only the separately held secure
# migrate-tier route.
```
### Telemetry
```bash
# Local observability (OTEL / Jaeger)
mosaic telemetry local status
mosaic telemetry local tail
mosaic telemetry local jaeger
# Remote telemetry (dry-run by default)
mosaic telemetry status
mosaic telemetry opt-in
mosaic telemetry opt-out
mosaic telemetry test
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`.
## Development
### Prerequisites
- Node.js ≥ 22
- pnpm 10.6+
- Docker & Docker Compose
### Setup
```bash
git clone [email protected]:mosaicstack/stack.git
cd stack
# Install dependencies. The local tier uses in-process PGlite; leave DATABASE_URL unset.
# The pnpm store defaults to $HOME/.local/share/pnpm/store. Override it without
# editing the checkout with NPM_CONFIG_STORE_DIR=$HOME/another-store if needed.
pnpm install
# Verify dependencies and generated state before running source-quality gates.
# Missing dependencies exit 42; stale/foreign apps/web/.next state exits 43.
# The web build certifies its exact standalone symlink manifest; added, removed,
# retargeted, or manifest-only-tampered generated links also exit 43. This detects
# accidental, independent, stale, and foreign-residue mutation—the class exposed by
# a five-month-stale .next that produced 19 phantom TS2307 errors.
# It does NOT defend against a same-UID actor that can rewrite both manifest and
# marker consistently (CWE-345). RM-59 tracks the required executor/spine-side
# trust anchor outside worktree authority.
pnpm preflight
# Optional local queue service only. This does not start PostgreSQL.
docker compose up -d valkey
# The current Gateway/Web local process is held; see docs/guides/dev-guide.md.
# Do not start it until KBN-101-02 makes inherited dotenv/DSN state fail closed.
```
### Held future procedure
The checked-in Compose PostgreSQL service mounts legacy initialization SQL and is **not** a
current PostgreSQL, standalone, or federated developer route. Do not start it with Compose,
invoke initialization SQL, or treat the planned migrator as currently executable.
**Held future activation procedure — non-operative and no current command authority until KBN-101-00, KBN-101-03, and KBN-101-05
land:** external bootstrap → TLS/roles → `mosaic-db-migrator --run` →
`mosaic-db-migrator --verify` → Gateway/Compose readiness. The future deployment artifacts—not
this README—will provide the reviewed commands and secret-consumer interface.
For local data-layer work, PGlite needs no PostgreSQL service. The optional Compose command above
starts only Valkey; OTEL Collector and Jaeger may likewise be started individually if needed,
without starting PostgreSQL. A Gateway/Web local process is not currently a safe PGlite route:
its unguarded dotenv loader may inherit a daemon PostgreSQL DSN. Do not use root `pnpm dev` or a
Gateway start command until KBN-101-02 makes that state fail closed.
### Quality Gates
```bash
pnpm preflight # Checkout/dependency/generated-state validation
pnpm typecheck # TypeScript type checking (all packages)
pnpm lint # ESLint (all packages)
pnpm test # Vitest (all packages)
pnpm format:check # Prettier check
pnpm format # Prettier auto-fix
```
### CI
Woodpecker CI runs on every push:
- `pnpm install --frozen-lockfile`
- **Legacy N-1 CI status only — active, uncertified, and non-authorizing as an operator route:** the checked-in job currently invokes `pnpm --filter @mosaicstack/db run db:migrate` with `DATABASE_URL` against an isolated disposable PostgreSQL CI database. It performs direct DDL in that CI database, is not approved ordinary behavior or an operator route, and remains a known exception pending KBN-101-06 removal/replacement by the certified runner-backed CI path.
- `pnpm test` (Turbo-orchestrated across all packages)
npm packages are published to the Gitea package registry on main merges.
## Architecture
```
stack/
├── apps/
│ ├── gateway/ NestJS API + WebSocket hub (Fastify, Socket.IO, OTEL)
│ └── web/ Next.js dashboard (React 19, Tailwind)
├── packages/
│ ├── mosaic/ Unified CLI — TUI, gateway client, wizard, sub-package commands
│ ├── types/ Shared TypeScript contracts (Socket.IO typed events)
│ ├── db/ Drizzle ORM schema + migrations (pgvector)
│ ├── auth/ BetterAuth configuration
│ ├── brain/ Data layer (PG-backed)
│ ├── queue/ Valkey task queue + MCP
│ ├── coord/ Mission coordination
│ ├── forge/ Multi-stage AI pipeline (intake → board → plan → code → review)
│ ├── macp/ MACP protocol — credential resolution, gate runner, events
│ ├── agent/ Agent session management
│ ├── memory/ Agent memory layer
│ ├── log/ Structured logging
│ ├── prdy/ PRD creation and validation
│ ├── quality-rails/ Quality templates (TypeScript, Next.js, monorepo)
│ └── design-tokens/ Shared design tokens
├── plugins/
│ ├── discord/ Discord channel plugin (discord.js)
│ ├── telegram/ Telegram channel plugin (Telegraf)
│ ├── macp/ OpenClaw MACP runtime plugin
│ └── mosaic-framework/ OpenClaw framework injection plugin
├── tools/
│ └── install.sh Unified installer (framework + npm CLI, --yes / --no-auto-launch)
├── scripts/agent/ Agent session lifecycle scripts
├── docker-compose.yml Dev infrastructure
└── .woodpecker/ CI pipeline configs
```
### Key Design Decisions
- **Gateway is the single API surface** — all clients (TUI, web, Discord, Telegram) connect through it
- **ESM everywhere** — `"type": "module"`, `.js` extensions in imports, NodeNext resolution
- **Socket.IO typed events** — defined in `@mosaicstack/types`, enforced at compile time
- **OTEL auto-instrumentation** — loads before NestJS bootstrap
- **Explicit `@Inject()` decorators** — required since tsx/esbuild doesn't emit decorator metadata
### Framework (`~/.config/mosaic/`)
The framework is the bash-based standards layer installed to every developer machine:
```
~/.config/mosaic/
├── AGENTS.md ← Central standards (loaded into every runtime)
├── SOUL.md ← Agent identity (name, style, guardrails)
├── USER.md ← User profile (name, timezone, preferences)
├── TOOLS.md ← Machine-level tool reference
├── bin/mosaic ← Unified launcher (claude, codex, opencode, pi, yolo)
├── guides/ ← E2E delivery, orchestrator protocol, PRD, etc.
├── runtime/ ← Per-runtime configs (claude/, codex/, opencode/, pi/)
├── skills/ ← Universal skills (synced from agent-skills repo)
├── tools/ ← Tool suites (orchestrator, git, quality, prdy, etc.)
└── memory/ ← Persistent agent memory (preserved across upgrades)
```
### Forge Pipeline
Forge is a multi-stage AI pipeline for autonomous feature delivery:
```
Intake → Discovery → Board Review → Planning (3 stages) → Coding → Review → Remediation → Test → Deploy
```
Each stage has a dispatch mode (`exec` for research/review, `yolo` for coding), quality gates, and timeouts. The board review uses multiple AI personas (CEO, CTO, CFO, COO + specialists) to evaluate briefs before committing resources.
## Upgrading
Run the installer again — it handles upgrades automatically:
```bash
curl -fsSL https://mosaicstack.dev/install.sh | bash
```
Or use the direct URL:
```bash
bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
```
Or use the CLI:
```bash
mosaic update # Check + install CLI updates
mosaic update --check # Check only, don't install
```
The CLI also performs a background update check on every invocation (cached for 1 hour).
### Installer Flags
```bash
bash tools/install.sh --check # Version check only
bash tools/install.sh --framework # Framework only (skip npm CLI)
bash tools/install.sh --cli # npm CLI only (skip framework)
bash tools/install.sh --next # Prerelease lane: source build from next
bash tools/install.sh --dev # Contributor lane: source build at --ref/main
bash tools/install.sh --ref v1.0 # Install from a specific git ref (--ref wins over --next)
bash tools/install.sh --yes # Non-interactive, accept all defaults
bash tools/install.sh --no-auto-launch # Skip auto-launch of wizard
```
The installer rejects unrecognized flags or positional arguments before making changes and prints the supported-option usage.
## Contributing
```bash
# Create a feature branch
git checkout -b feat/my-feature
# Make changes, then verify
pnpm typecheck && pnpm lint && pnpm test && pnpm format:check
# Commit (husky runs lint-staged automatically)
git commit -m "feat: description of change"
# Push and create PR
git push -u origin feat/my-feature
```
DTOs go in `*.dto.ts` files at module boundaries. Scratchpads (`docs/scratchpads/`) are mandatory for non-trivial tasks. See `AGENTS.md` for the full standards reference.
## License
Proprietary — all rights reserved.
-1
View File
@@ -1 +0,0 @@
0.0.12
File renamed without changes.
-30
View File
@@ -1,30 +0,0 @@
# Mosaic Stack
You are the default collaborator for Mosaic Stack: a practical engineering
partner helping people build, inspect, and operate a trustworthy foundation
for delegated work.
Mosaic Stack is deliberately small, file-based, and evidence-oriented. Its
purpose is not to perform confidence; it is to make useful work attributable,
bounded, reproducible, and reviewable. Treat the system's contracts, policies,
and run records as part of the product, not paperwork around it.
Work with calm precision. Start from what the user is trying to accomplish,
make the next useful step clear, and explain results in plain language. Be
decisive when the evidence supports a decision; be explicit about uncertainty
when it does not. Never claim a test, command, integration, or outcome that
you have not actually verified.
Respect boundaries. Ask before expanding scope, changing authority, touching
credentials, or taking an irreversible external action. Prefer the least
privileged path, preserve user work, and stop on a policy or validation
refusal rather than working around it. A clean refusal with a useful diagnosis
is better than a superficially successful but untrustworthy result.
Leave a legible trail. Make changes intentional, keep records honest, and
report what changed, how it was checked, and what remains unresolved. When
coordinating other workers, give each one a bounded objective and review their
evidence instead of treating their confidence as proof.
The aim is dependable progress: small enough to understand, safe enough to
trust, and concrete enough for a person to verify.
-128
View File
@@ -1,128 +0,0 @@
# Mosaic runtime adapters
An adapter is the entire harness-specific surface of the system. Everything
upstream of an adapter — configuration, contracts, missions, tasks, run
records — is harness-agnostic; everything inside an adapter may assume one
specific agent runtime.
## Contract
An adapter lives at:
```text
/opt/mosaic/adapters/<name>/adapter.sh
```
and must be executable. The dispatcher (`/opt/mosaic/src/run-agent.sh`)
selects it via `MOSAIC_ADAPTER` (default: `pi`) and execs it after the
system prompt has been generated.
**Inputs (environment):**
| Variable | Meaning |
|---|---|
| `MOSAIC_SYSTEM_PROMPT_FILE` | Absolute path to the generated system prompt (contracts + optional mission section). Read it; do not modify it. |
| `MOSAIC_REQUEST` | The exact user request text (may contain newlines). |
| `MOSAIC_PROVIDER` | Configured provider name. |
| `MOSAIC_MODEL` | Configured model id. |
Optional, adapter-specific (documented per adapter):
| Variable | Meaning |
|---|---|
| `MOSAIC_MOCK_RESPONSE` | mock only: the verbatim response to emit |
**Outputs:**
- `stdout`: the model response text — the only channel the orchestrator captures
- `stderr`: diagnostics (never credentials)
- exit `0`: success; nonzero: failure
## Rules
1. Adapters print ONLY the response on stdout. Status lines go to stderr.
2. Adapters never read configuration files; the resolved settings arrive via environment.
3. Adapters never write outside `/var/lib/mosaic`.
4. Adding a worker adapter requires: a new directory, the contract
implementation, and adding the name to the allowlist in
`scripts/mosaic-config.mjs`. A managed-session adapter (below) is selected
by the launch bundle, not by `execution.adapter`, and is not on that
allowlist unless it also works as a worker adapter.
## Included adapters
- `pi` — the pinned `@earendil-works/pi-coding-agent` CLI in noninteractive
print mode (`-p`), ambient discovery disabled, stdin detached.
- `mock` — deterministic echo of `MOSAIC_MOCK_RESPONSE`. Test-only: never use
it where a real model response is required.
- `claude` — the host's `claude` CLI, for managed sessions only (below):
`MOSAIC_CLAUDE_BIN` when set, else `claude` on `PATH`.
Not a worker adapter: the image has no `claude`, and the adapter refuses
without the bundle's hook and MCP files.
## Managed sessions (slice 1 S6)
The session runner (`packages/harness/src/runner.mjs`) runs one adapter
call per turn on the host, as `/bin/sh <adapters/<name>/adapter.sh>` (the
repository keeps adapters 0644; only the image sets the mode), in the
session's workspace and its own process group. Same contract, plus:
| Variable | Meaning |
|---|---|
| `MOSAIC_WORKSPACE` | The session's workspace; the adapter runs there. |
| `MOSAIC_SESSION_DIR` | The session's persistent directory; later turns resume the session kept there. |
| `MOSAIC_TOOLS` | The built-in tool limit (pi names for `pi`, Claude Code names for `claude`), comma-separated. |
| `MOSAIC_POLICY_FILE`, `MOSAIC_TOOLS_FILE` | The bundle's gate policy and typed tools. |
| `MOSAIC_TOOL_SOCKET` | The runner's tool socket, which the typed tools call. |
| `MOSAIC_TURN_MARKER` | `pi`: the extension writes it at `agent_end`; a pi turn that exits 0 without it failed. |
| `MOSAIC_EXTENSIONS` | `pi` only: extension files loaded with `-e`; a missing one exits 2. The pi module names two: the gate and typed tools extension, then the unslop check's `tools/unslop-hook/extension.ts` (row 66). |
| `MOSAIC_UNSLOP_LOG` | The unslop check's JSONL log, `<run>/unslop.jsonl`, which the runner sets for both adapters and reads after each turn (row 66). |
| `MOSAIC_CLAUDE_SETTINGS`, `MOSAIC_CLAUDE_MCP_CONFIG` | `claude` only: the bundle's gate hook and MCP server; required. |
| `MOSAIC_THINKING` | The route's thinking level (row 65), which the harness module's `levels()` already checked. `pi` passes `--thinking` (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`) and omits it when unset, as for `run-task.sh` workers. `claude` passes `--effort` (`low`, `medium`, `high`, `xhigh`, `max`) and requires it; unset exits 2. |
| `MOSAIC_CLAUDE_BIN` | `claude` only: absolute path of the CLI to run, from the host's environment (the bus unit pins it). Unset or empty runs `claude` from `PATH`; a relative path or a non-executable file exits 2. |
The layers each adapter relies on, and what they don't cover, are in
`packages/harness/README.md`.
### A role's skills (row 64)
A managed session's skills reach both adapters as text in the bundle's
`prompt.md`, which they pass as a file: `pi` with `--system-prompt <path>`
and `claude` with `--system-prompt-file <path>` (row 213). Neither adapter loads a
skill of its own: `pi` keeps `--no-skills`, since role sessions never get
`MOSAIC_SKILLS`, and `claude` keeps `--disable-slash-commands` and
`--restricted`. Sage chose this on 2026-10-19 from a probe, recorded in
`agents/sage/work/queue-64/probe.md`:
- Pi's `--skill` puts only a skill's name and description in the prompt,
and the model has to `read` the SKILL.md itself. The gate refuses every
file-tool path outside the workspace, so the session couldn't open its
own skills. Opening the gate to the bundle would change a security
boundary.
- Claude Code loads a bundle-local `--plugin-dir` skill under `--restricted`
only with its `Skill` tool on, and then it also offers its own bundled
skills. Which ones it offers changes with the Claude Code version, and the
`Skill` tool would pass through the gate hook.
The cost is prompt size: a role's skill text is in every turn, where the
other routes load a skill when it's used. The bundle caps a role's skills at
64 KiB together. The cap dates from when `claude` passed `prompt.md` as one
argument, which Linux caps at 128 KiB; row 213 passes the file instead, so no
local user reads the prompt in `/proc/<pid>/cmdline`. Pi 0.85.1 uses the path
string itself as the prompt when the file is missing or can't be read, so the
pi adapter exits 2 unless the path is a readable regular file.
### The unslop check (row 66)
Both adapters run the mechanical writing check from `tools/unslop-hook`,
driven by one `lists.json`. `pi` loads it as its second `-e` extension.
`claude` runs `claude-hook.js` from the bundle's `settings.json`: the Stop
hook checks the turn's replies, and the UserPromptSubmit hook adds the
correction notice as context on the next turn when the last reply had
tells. Each hook command ends `; exit 0`, so neither blocks. Both log to
`MOSAIC_UNSLOP_LOG`, and the runner adds a note for the operator to the
next RESULT it sends (`packages/harness/README.md`, the runner). The
harness modules mount the three files read-only at their own paths, and
the manifest records their digests. `MOSAIC_UNSLOP_HOOK` and `UNSLOP_LISTS`
aren't in the session environment's allowlist, so a managed session can't
switch the check off or swap its lists.
-101
View File
@@ -1,101 +0,0 @@
#!/bin/sh
# Claude Code adapter: implements the Mosaic adapter contract for the host's
# `claude` CLI, for managed sessions (slice 1 S6). Headless only. The CLI is
# MOSAIC_CLAUDE_BIN when set (the bus unit pins it), else `claude` on PATH.
#
# Contract: see adapters/README.md.
# stdout = the turn's answer only; stderr = diagnostics; exit 0 on success.
#
# The layers this relies on are row S0 lines 1-5 (docs/plans/2026-10-04_slice-1.md,
# "What S6 can rely on"): the PreToolUse command hook in MOSAIC_CLAUDE_SETTINGS,
# wrapped as `timeout -k 2 10 <gate> || exit 2`, and the tool limit (--tools).
# --bare is never passed: it turns settings hooks off.
set -eu
need() {
eval "v=\${$1:-}"
[ -n "$v" ] || { echo "claude adapter: $1 is required" >&2; exit 2; }
}
need MOSAIC_SYSTEM_PROMPT_FILE
need MOSAIC_REQUEST
need MOSAIC_WORKSPACE
need MOSAIC_SESSION_DIR
need MOSAIC_MODEL
need MOSAIC_THINKING
need MOSAIC_CLAUDE_SETTINGS
need MOSAIC_CLAUDE_MCP_CONFIG
for f in "$MOSAIC_SYSTEM_PROMPT_FILE" "$MOSAIC_CLAUDE_SETTINGS" "$MOSAIC_CLAUDE_MCP_CONFIG"; do
[ -r "$f" ] || { echo "claude adapter: not readable: $f" >&2; exit 2; }
done
PROMPT_FILE=$MOSAIC_SYSTEM_PROMPT_FILE
case "$PROMPT_FILE" in
/*) ;;
*) PROMPT_FILE="$(pwd)/$PROMPT_FILE" ;;
esac
[ "${MOSAIC_INTERACTIVE:-}" != "1" ] || { echo "claude adapter: interactive mode isn't supported" >&2; exit 2; }
CLAUDE=claude
if [ -n "${MOSAIC_CLAUDE_BIN:-}" ]; then
case "$MOSAIC_CLAUDE_BIN" in
/*) ;;
*) echo "claude adapter: MOSAIC_CLAUDE_BIN must be an absolute path: $MOSAIC_CLAUDE_BIN" >&2; exit 2 ;;
esac
[ -f "$MOSAIC_CLAUDE_BIN" ] && [ -x "$MOSAIC_CLAUDE_BIN" ] || { echo "claude adapter: MOSAIC_CLAUDE_BIN is not an executable file: $MOSAIC_CLAUDE_BIN" >&2; exit 2; }
CLAUDE=$MOSAIC_CLAUDE_BIN
fi
mkdir -p "$MOSAIC_WORKSPACE" "$MOSAIC_SESSION_DIR"
cd "$MOSAIC_WORKSPACE"
# Session: the first turn names a new session id, later turns resume it.
# The id is kept only after a turn succeeds, so a failed first turn never
# leaves a --resume of a session that doesn't exist.
ID_FILE="$MOSAIC_SESSION_DIR/claude-session-id"
if [ -s "$ID_FILE" ]; then
SESSION_FLAG=--resume
SESSION_ID=$(cat "$ID_FILE")
else
SESSION_FLAG=--session-id
SESSION_ID=$(cat /proc/sys/kernel/random/uuid)
fi
# Flags:
# -p one-shot: print the answer and exit
# --output-format text stdout is the answer only
# --system-prompt-file the generated prompt replaces the default, given
# as a file so no local user reads it in
# /proc/<pid>/cmdline (row 213); absolute, since
# the cd above changed the working directory
# --effort the instance's thinking var (row 65; 2.1.302 takes
# low, medium, high, xhigh, max)
# --restricted no user/project/local settings files; --settings
# (the gate hook) still applies; no code-running
# tool unless --tools names it; file tools confined
# to the working directory; no CLAUDE.md file or
# auto-memory in the prompt. The S0 lines above
# don't depend on it, but it is what keeps founder
# and repository memory out; a test fails without it.
# --tools the built-in tool limit (S0 line 5); empty = none
# --allowedTools the same tools plus the mosaic MCP server, so
# --permission-mode dontAsk nothing waits on a prompt and anything else is denied
# --settings the gate hook (S0 lines 1-4)
# --strict-mcp-config only the bundle's MCP server, which serves the
# --mcp-config typed bus tools
# --disable-slash-commands no skill discovery; the role's skills are
# text in the system prompt (row 64)
ALLOWED=mcp__mosaic
[ -z "${MOSAIC_TOOLS:-}" ] || ALLOWED="$MOSAIC_TOOLS,mcp__mosaic"
"$CLAUDE" -p "$MOSAIC_REQUEST" \
--output-format text \
--system-prompt-file "$PROMPT_FILE" \
--model "$MOSAIC_MODEL" \
--effort "$MOSAIC_THINKING" \
--restricted \
--tools "${MOSAIC_TOOLS:-}" \
--allowedTools "$ALLOWED" \
--permission-mode dontAsk \
--settings "$MOSAIC_CLAUDE_SETTINGS" \
--strict-mcp-config \
--mcp-config "$MOSAIC_CLAUDE_MCP_CONFIG" \
--disable-slash-commands \
"$SESSION_FLAG" "$SESSION_ID" || exit $?
[ "$SESSION_FLAG" = --resume ] || printf '%s\n' "$SESSION_ID" > "$ID_FILE"
-18
View File
@@ -1,18 +0,0 @@
#!/bin/sh
# Mock adapter: deterministic response for seam tests. NEVER use where a
# real model response is required.
#
# Contract: see /opt/mosaic/adapters/README.md.
set -eu
[ -n "${MOSAIC_SYSTEM_PROMPT_FILE:-}" ] || { echo "mock adapter: MOSAIC_SYSTEM_PROMPT_FILE is required" >&2; exit 2; }
if [ "${MOSAIC_INTERACTIVE:-}" != "1" ]; then
[ -n "${MOSAIC_REQUEST:-}" ] || { echo "mock adapter: MOSAIC_REQUEST is required" >&2; exit 2; }
fi
[ -r "$MOSAIC_SYSTEM_PROMPT_FILE" ] || { echo "mock adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
echo "mock adapter: responding verbatim from MOSAIC_MOCK_RESPONSE" >&2
# Deterministic plumbing evidence: which MOSAIC_* variables did the
# orchestrator actually deliver? (Auth secrets are not MOSAIC_-prefixed.)
(env | grep '^MOSAIC_' | sort) >&2 2>/dev/null || true
printf '%s\n' "${MOSAIC_MOCK_RESPONSE:-}"
-135
View File
@@ -1,135 +0,0 @@
#!/bin/sh
# Pi adapter: implements the Mosaic adapter contract for the pinned
# @earendil-works/pi-coding-agent CLI.
#
# Contract: see /opt/mosaic/adapters/README.md.
# Headless (default): stdout = response only; stderr = diagnostics; exit 0.
# Interactive (MOSAIC_INTERACTIVE=1): full pi TUI on the attached terminal.
set -eu
[ -n "${MOSAIC_SYSTEM_PROMPT_FILE:-}" ] || { echo "pi adapter: MOSAIC_SYSTEM_PROMPT_FILE is required" >&2; exit 2; }
# Row 213 (C2-08): pi gets the prompt as a path, so no local user reads it in /proc/<pid>/cmdline.
# Pi 0.85.1 reads --system-prompt as a file only when the path exists and reads, and otherwise
# uses the path string itself as the prompt, without a warning for a missing file
# (dist/core/resource-loader.js resolvePromptInput). So the path must be a regular readable
# file here, and is made absolute before the cd below, since pi resolves it from its own cwd.
PROMPT_FILE=$MOSAIC_SYSTEM_PROMPT_FILE
case "$PROMPT_FILE" in
/*) ;;
*) PROMPT_FILE="$(pwd)/$PROMPT_FILE" ;;
esac
[ -f "$PROMPT_FILE" ] && [ -r "$PROMPT_FILE" ] || { echo "pi adapter: system prompt not readable: $MOSAIC_SYSTEM_PROMPT_FILE" >&2; exit 2; }
# MOSAIC_AGENT_NAME is optional in headless mode (identity section is then
# omitted); interactive launches always set it via scripts/agent.sh.
: "${PI_PROVIDER:?pi adapter: PI_PROVIDER is required}"
: "${PI_MODEL:?pi adapter: PI_MODEL is required}"
INTERACTIVE="${MOSAIC_INTERACTIVE:-}"
if [ "$INTERACTIVE" != "1" ]; then
[ -n "${MOSAIC_REQUEST:-}" ] || { echo "pi adapter: MOSAIC_REQUEST is required" >&2; exit 2; }
fi
# Workspace (M5): run inside the provided workspace when present.
if [ -n "${MOSAIC_WORKSPACE:-}" ]; then
mkdir -p "$MOSAIC_WORKSPACE"
cd "$MOSAIC_WORKSPACE"
fi
# Session (M6/M11): default ephemeral (--no-session). With a declared
# session dir: persist there and resume the most recent session. With a
# fork source: branch the source session file into the target dir
# (pi --fork) - the ancestor session is never modified.
SESSION_FLAGS="--no-session"
if [ -n "${MOSAIC_SESSION_FORK:-}" ]; then
[ -n "${MOSAIC_SESSION_DIR:-}" ] || { echo "pi adapter: session fork requires MOSAIC_SESSION_DIR" >&2; exit 2; }
mkdir -p "$MOSAIC_SESSION_DIR"
SESSION_FLAGS="--fork $MOSAIC_SESSION_FORK --session-dir $MOSAIC_SESSION_DIR"
elif [ -n "${MOSAIC_SESSION_DIR:-}" ]; then
mkdir -p "$MOSAIC_SESSION_DIR"
SESSION_FLAGS="--session-dir $MOSAIC_SESSION_DIR"
if [ -n "$(ls -A "$MOSAIC_SESSION_DIR" 2>/dev/null)" ]; then
SESSION_FLAGS="$SESSION_FLAGS -c"
fi
fi
# Capabilities (M5): explicit allowlist or no tools.
TOOLS_FLAG="--no-tools"
[ -n "${MOSAIC_TOOLS:-}" ] && TOOLS_FLAG="--tools $MOSAIC_TOOLS"
# Skills (M17): explicitly provided skill dirs replace discovery. When none
# are provided the agent runs with --no-skills (nothing ambient to find).
# Role sessions never get MOSAIC_SKILLS (ENV_ALLOW in packages/seat/src/
# session.mjs leaves it out), so they run with --no-skills; their skills
# are text in the system prompt (row 64).
SKILLS_FLAG="--no-skills"
if [ -n "${MOSAIC_SKILLS:-}" ]; then
SKILLS_FLAG=""
OLDIFS=$IFS; IFS=','
for s in $MOSAIC_SKILLS; do
[ -d "$s" ] || { echo "pi adapter: skill dir missing: $s" >&2; exit 2; }
SKILLS_FLAG="$SKILLS_FLAG --skill $s"
done
IFS=$OLDIFS
fi
# Extensions (S6): explicitly provided extension files, loaded with -e
# after --no-extensions turns discovery off. A missing file refuses here;
# pi itself also refuses to start on a missing or broken -e (row S0 line 3).
EXT_FLAGS=""
if [ -n "${MOSAIC_EXTENSIONS:-}" ]; then
OLDIFS=$IFS; IFS=','
for e in $MOSAIC_EXTENSIONS; do
[ -f "$e" ] || { echo "pi adapter: extension missing: $e" >&2; exit 2; }
EXT_FLAGS="$EXT_FLAGS -e $e"
done
IFS=$OLDIFS
fi
# Thinking (row 65): a managed session's level, from its instance's thinking
# var (pi 0.85.1: off, minimal, low, medium, high, xhigh, max). Workers from
# run-task.sh set none and keep pi's default.
THINKING_FLAG=""
[ -n "${MOSAIC_THINKING:-}" ] && THINKING_FLAG="--thinking $MOSAIC_THINKING"
# Mode (M13): interactive TUI or one-shot print.
PRINT_MODE="-p"
REQUEST_ARG=""
if [ "$INTERACTIVE" = "1" ]; then
PRINT_MODE=""
else
REQUEST_ARG="$MOSAIC_REQUEST"
fi
# All flags documented in the pi package README (CLI Reference):
# -p/--print one-shot mode: print the response and exit (omitted in
# interactive TUI mode)
# --system-prompt replace the default prompt with the generated one, given
# as the file's absolute path (checked above), never its text
# --no-* no ambient context/skills/extensions/templates/themes
# EXT_FLAGS the explicitly provided extensions only (-e)
# SESSION_FLAGS ephemeral | persistent | forked (per env)
# TOOLS_FLAG per capabilities
# THINKING_FLAG --thinking <level> for a managed session, else nothing
# --offline no startup network operations (update checks/telemetry)
# --no-approve ignore project-local .pi/ files (settings, SYSTEM.md)
# whatever trust is saved for the workspace
set -- \
--offline \
--no-approve \
--no-extensions \
$EXT_FLAGS \
$SKILLS_FLAG \
--no-prompt-templates \
--no-themes \
--no-context-files \
$TOOLS_FLAG \
$SESSION_FLAGS \
--provider "$PI_PROVIDER" \
--model "$PI_MODEL" \
$THINKING_FLAG \
--system-prompt "$PROMPT_FILE"
# One-shot mode appends -p and the request (both safely quoted);
# interactive mode appends nothing - clean TUI.
[ "$INTERACTIVE" = "1" ] || set -- "$@" -p "$MOSAIC_REQUEST"
exec pi "$@"
-66
View File
@@ -1,66 +0,0 @@
# Mosaic Stack development team
These are interactive host development agents working in the canonical
checkout. They do not create managed fleet registrations or change role policy.
Sage leads the project and coordinates assignments, review, and integration
(Jason's ruling, 2026-09-26). Development sessions run in T3 for now.
For the current bootstrap phase, direct coding, review and research use Darkwing,
Dewey, Filbert, Rocko, Researcher and further seats Jason launches from this repository's `agents/` directory,
not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
`~/.mosaic` launchers/provisioning or migrate/stop live fleet processes.
| Agent | Responsibility | Runtime | Launch from repository root |
| --- | --- | --- | --- |
| Darkwing | Hands-on engineering; collaborating seat under Sage | Pi, configured Mosaic model | `agents/darkwing/launch.sh` |
| Dewey | Frontend design, UX, accessibility, and UI implementation | Pi, configured Mosaic model | `agents/dewey/launch.sh` |
| Rocko | General development, investigation, testing, and review | T3 session (Codex, gpt-6.1-sol, thread d99326cc, lead decision 122; Claude thread b84bb264 retired 2026-10-12); launcher Claude Code, Sonnet model | `agents/rocko/launch.sh` |
| Filbert | General development, investigation, testing, and review | T3 session (Claude, Opus 5.5, thread 048f3c63, non-sandbox reviews only, lead decisions 130 and 134; Codex threads f712d8af and 50342f6a retired 2026-10-13, Claude thread 9cb9731e retired 2026-10-12); launcher Pi, `openai-codex/gpt-6-astra:low`, not to be used under R26 until it changes (lead decision 69) | `agents/filbert/launch.sh` |
| Researcher | Source-grounded technical research and evidence | T3 session (Claude, Sonnet 5.5, thread 9bac0794, rows 97 and 98, lead decision 124); Haiku rows (71, 80, 84, 88, later 96) run as subagents Sage dispatches with model haiku, effort high and a worktree (lead decision 129); Haiku thread c98aaf4a retired 2026-10-13 unused; launcher Pi, configured Mosaic model | `agents/researcher/launch.sh` |
| Launchpad | Builder, MVP T1 row 192 (Q175 SSO phase 1) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Morgana | Builder, MVP T1 row 193 (human users and RBAC) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Honker | Builder, MVP T1 row 194 (agent lifecycle) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Gander | Builder, the human boundary across uids (P19 333, #1694) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
| Herb | Builder, a second gate lane (#1695) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
| Bushroot | Builder, retire the control board writer on 7331 (#1696) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1008 | none, T3 only |
| Megavolt | Builder, MVP T1 item 6 (agent credentials and git identity, #1697) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
| Quackerjack | Builder, federation R5 and R2 (#414) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1023 | none, T3 only |
| Gizmoduck | Federation design (#414) and builder of FED R1 | T3 session (Claude, Opus 5.5, thread 9312a896) | none, T3 only |
| Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only |
| Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only |
| Della | Builder, email plan (Vikunja 460) and row 224 (C2-05b) | T3 session (Codex, gpt-6.1-sol, thread 66735a7f), started by Mos | none, T3 only |
| Scrooge | Builder of row 252 and reviewer of row 272 | T3 session (Claude, Opus 5.5, thread 629b6cd3), started by Mos | none, T3 only |
| Huey | Builder, row 272 (T1 sessions) | T3 session (Codex, gpt-6.1-sol, thread cabd45ca), started by Mos | none, T3 only |
| Louie | Builder and research, editor evaluation and row 174 | T3 session (Codex, gpt-6-luna, thread bfd13d49), started by Mos | none, T3 only |
| Ludwig | Builder and reviewer, post-v1 email PRD and row 258 (E01) | T3 session (Claude, Opus 5.5, thread 8615b2a6), started by Mos | none, T3 only |
| Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` |
Each script supports `--check` and `--fresh`. Normal launches resume the agent's
own conversation; a first launch starts one. See each agent's README for
context inputs, authentication, and recovery details. Launch scripts can also
be invoked by absolute path from any directory. No assignment or model request
is submitted by the launcher itself.
The shared Pi helper supports `--provider NAME`, `--model ID`, and
`--thinking LEVEL` as per-launch overrides of the validated system defaults.
Filbert's wrapper appends the required provider, model and thinking flags so
its launch configuration remains fixed, including on resume. Use Filbert's
wrapper to select that configuration; a direct shared-helper invocation uses
its own supplied flags or the system defaults.
Any run that includes the webui suite, whether a seat's slot run or a landing
gate, sets `MOSAIC_REQUIRE_EDITOR_BUILD=1`, so a missing editor toolchain fails
the run instead of skipping the rebuild test. A worktree that links the
canonical `node_modules` also runs `npm ci --ignore-scripts --no-audit --no-fund`
in its own `packages/webui` first. The root install carries esbuild 0.28.1
through pi-coding-agent, and the rebuild needs the 0.28.2 that
`packages/webui/package-lock.json` pins (Vikunja 597, mos-1453).
All agents follow repository governance and current user direction. Team
leadership does not add deployment or push authority. Coordinate overlapping
work with Sage and preserve other sessions' changes.
Before 2026-09-26 Sage worked only on DYOR strategy and sat outside the
development queue. Jason then made Sage the project lead and Darkwing a
collaborating seat. A separate fleet Sage seat under `~/.mosaic` is being
decommissioned; it does not speak for this seat. Whether the DYOR strategy work
continues is Jason's call. Joe retains DYOR engineering.
-8
View File
@@ -1,8 +0,0 @@
# Beakley
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 217 (C2-06, handed over from Launchpad).
No launcher in this directory. The seat runs as a T3 thread (thread 88b97f65) and works in its own worktree. It has no
credentials of its own; it pushes only its own `beakley/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
-10
View File
@@ -1,10 +0,0 @@
# Bentina
Builder seat, Codex gpt-6.1-sol, started by Sage under Q246 and mos-1566. Owns row 157, I2: the install images, the
install config key, the endpoint helper and the egress proxy (docs/plans/2026-10-18_containerized-install-rows.md),
reassigned from Researcher so the coding runs on Codex.
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no credentials of its
own; it pushes only its own `bentina/*` branches through the repository credential helper (mos-0981), never `refactor`,
`main` or another seat's branch. Claude seats orchestrate and review the install path (mos-1542 rule 4). Registered by
Sage on 2026-10-24 so the queue can name the seat as row owner (store.mjs isSeatDir).
@@ -1,43 +0,0 @@
2026-10-24T04:05:53Z
23:05:53 up 10 days, 14:25, 2 users, load average: 2.29, 2.31, 1.89
CLEAR sage-3910 TMPDIR=/mnt/storage/scratch/tmp/lEvgx1
✔ install: absent (28.959485ms)
✔ install: local (24.660305ms)
✔ install: local endpoint (26.210523ms)
✔ install: containerized missing endpoint (25.495263ms)
✔ install: bad endpoint "tcp://localhost:2375" (28.62092ms)
✔ install: bad endpoint "ssh://host/run/docker.sock" (32.126399ms)
✔ install: bad endpoint "run/docker.sock" (26.69995ms)
✔ install: bad endpoint "unix://relative.sock" (33.36326ms)
✔ install: bad endpoint "unix:///a b.sock" (30.579866ms)
✔ install: bad endpoint "unix:///a.sock\n" (40.844622ms)
✔ install: bad endpoint "unix:///a" (36.371215ms)
✔ install: bad endpoint "" (31.603044ms)
✔ install: bad endpoint null (29.645166ms)
✔ install: bad endpoint 4 (30.638453ms)
✔ install: valid endpoint (28.088952ms)
✔ install: unknown mode (28.182929ms)
✔ install: missing mode (26.297287ms)
✔ install: unknown key (27.205488ms)
✔ install: null (29.189475ms)
✔ install: array (30.347371ms)
✔ helper refuses rootful (76.082505ms)
✔ helper refuses missing (58.354463ms)
✔ helper refuses substring (86.830586ms)
✔ helper refuses object (108.0825ms)
✔ helper refuses invalid JSON (85.203357ms)
✔ helper refuses null success (85.225204ms)
✔ helper refuses rootless output but failed (67.840402ms)
✔ helper checks once and explicitly names host for both calls (119.763305ms)
✔ local mode refuses before Docker null (35.42747ms)
✔ local mode refuses before Docker {"mode":"local"} (30.531767ms)
✔ invalid config refuses before Docker (30.092363ms)
ℹ tests 31
ℹ suites 0
ℹ pass 31
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 833.790665
EXIT=0
@@ -1,23 +0,0 @@
2026-10-24T04:06:01Z
23:06:01 up 10 days, 14:25, 2 users, load average: 2.27, 2.30, 1.89
CLEAR sage-3910 TMPDIR=/mnt/storage/scratch/tmp/lfgV9z
✔ allows exact host and port, fragmented headers, and preserves tunnel head (21.273322ms)
✔ unlisted host (1.324735ms)
✔ unlisted port (1.081626ms)
✔ GET (0.948442ms)
✔ invalid port (0.95243ms)
✔ userinfo (0.874803ms)
✔ bad header (0.782898ms)
✔ slow client has a total header deadline (81.829907ms)
✔ oversized header refuses (1.267008ms)
✔ missing, invalid and unknown lists fail closed (0.491274ms)
✔ refused upstream returns 502 while retaining one allowed decision (1.716299ms)
ℹ tests 11
ℹ suites 0
ℹ pass 11
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 156.342618
EXIT=0
@@ -1,90 +0,0 @@
2026-10-24T04:06:08Z
23:06:08 up 10 days, 14:25, 2 users, load average: 2.53, 2.36, 1.91
CLEAR sage-3910 TMPDIR=/mnt/storage/scratch/tmp/6eJjJz
OK absent adapter defaults to pi
OK adapter mock validates (exit 0)
OK unsupported adapter exits 2 (exit 2)
OK env exports adapter
OK bootstrap creates default when absent (exit 0)
OK bootstrap wrote config file
OK bootstrap is idempotent on existing config (exit 0)
OK bootstrap did not rewrite existing config
OK validate missing config exits 3 (exit 3)
OK malformed JSON exits 2 (exit 2)
OK unsupported configVersion exits 2 (exit 2)
OK unknown top-level key exits 2 (exit 2)
OK unknown execution key exits 2 (exit 2)
OK unsupported backend exits 2 (exit 2)
OK unsupported environment exits 2 (exit 2)
OK relative dataRoot exits 2 (exit 2)
OK non-canonical dataRoot exits 2 (exit 2)
OK filesystem root dataRoot exits 2 (exit 2)
OK home directory dataRoot exits 2 (exit 2)
OK dataRoot containing config dir exits 2 (exit 2)
OK control character in provider exits 2 (exit 2)
OK symlinked config file exits 2 (exit 2)
OK env exports resolve correctly
OK valid console.sso loads and is in the output
OK console.sso HS256 alg exits 2 naming the path (exit 2)
OK console.sso alg none exits 2 naming the path (exit 2)
OK console.sso missing slot field exits 2 naming the path (exit 2)
OK console.sso slot name outside the pattern exits 2 naming the path (exit 2)
OK unknown key under console exits 2 naming the path (exit 2)
OK valid federation key loads and is in the output
OK federation unknown sub-key exits 2 naming the key (exit 2)
OK federation peer unknown sub-key exits 2 naming the key (exit 2)
OK federation peer origin over http exits 2 naming the key (exit 2)
OK federation peer origin with a path exits 2 naming the key (exit 2)
OK federation peer origin with userinfo exits 2 naming the key (exit 2)
OK federation peer id equal to self exits 2 naming the key (exit 2)
OK federation peer label empty exits 2 naming the key (exit 2)
OK federation not an object exits 2 naming the key (exit 2)
OK federation self pattern exits 2 naming the key (exit 2)
OK federation peers not an array exits 2 naming the key (exit 2)
OK federation peer not an object exits 2 naming the key (exit 2)
OK federation peer id pattern exits 2 naming the key (exit 2)
OK federation peer label over 80 characters exits 2 naming the key (exit 2)
OK federation duplicate peer id exits 2 naming the key (exit 2)
OK federation peer origin with a trailing slash exits 2 naming the key (exit 2)
OK federation self missing exits 2 naming the key (exit 2)
OK federation peer id missing exits 2 naming the key (exit 2)
OK federation peer label missing exits 2 naming the key (exit 2)
OK config without federation loads and has no federation output
FAIL config without console loads with unchanged output
OK compactionReadBytes 8388608 loads and is in the output as written
OK compactionReadBytes "x" loads and is in the output as written
OK compactionStreams 2048 loads and is in the output as written
OK compactionStreams "x" loads and is in the output as written
OK failed validation modified nothing
✔ install: absent (28.022711ms)
✔ install: local (26.314077ms)
✔ install: local endpoint (27.030768ms)
✔ install: containerized missing endpoint (26.036164ms)
✔ install: bad endpoint "tcp://localhost:2375" (29.311242ms)
✔ install: bad endpoint "ssh://host/run/docker.sock" (28.012086ms)
✔ install: bad endpoint "run/docker.sock" (26.941261ms)
✔ install: bad endpoint "unix://relative.sock" (26.90999ms)
✔ install: bad endpoint "unix:///a b.sock" (23.646272ms)
✔ install: bad endpoint "unix:///a.sock\n" (26.383344ms)
✔ install: bad endpoint "unix:///a" (24.914913ms)
✔ install: bad endpoint "" (24.320663ms)
✔ install: bad endpoint null (25.688262ms)
✔ install: bad endpoint 4 (26.934437ms)
✔ install: valid endpoint (27.504294ms)
✔ install: unknown mode (27.111833ms)
✔ install: missing mode (26.365767ms)
✔ install: unknown key (26.974983ms)
✔ install: null (25.910901ms)
✔ install: array (25.507423ms)
ℹ tests 20
ℹ suites 0
ℹ pass 20
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 584.03015
OK install configuration cases
selftest: 55 passed, 1 failed
EXIT=1
@@ -1,43 +0,0 @@
2026-10-24T04:08:52Z
23:08:52 up 10 days, 14:28, 2 users, load average: 2.16, 2.49, 2.04
CLEAR sage-3919 TMPDIR=/mnt/storage/scratch/tmp/o2n7Db
✔ install: absent (31.8592ms)
✔ install: local (29.807282ms)
✔ install: local endpoint (24.009896ms)
✔ install: containerized missing endpoint (26.296453ms)
✔ install: bad endpoint "tcp://localhost:2375" (26.404525ms)
✔ install: bad endpoint "ssh://host/run/docker.sock" (27.486365ms)
✔ install: bad endpoint "run/docker.sock" (24.280876ms)
✔ install: bad endpoint "unix://relative.sock" (24.363468ms)
✔ install: bad endpoint "unix:///a b.sock" (24.920898ms)
✔ install: bad endpoint "unix:///a.sock\n" (25.784031ms)
✔ install: bad endpoint "unix:///a" (27.89803ms)
✔ install: bad endpoint "" (28.939674ms)
✔ install: bad endpoint null (26.129602ms)
✔ install: bad endpoint 4 (27.601233ms)
✔ install: valid endpoint (29.310223ms)
✔ install: unknown mode (30.307014ms)
✔ install: missing mode (26.432375ms)
✔ install: unknown key (29.208115ms)
✔ install: null (29.793ms)
✔ install: array (28.956899ms)
✔ helper refuses rootful (88.440017ms)
✔ helper refuses missing (49.868469ms)
✔ helper refuses substring (76.03165ms)
✔ helper refuses object (77.494513ms)
✔ helper refuses invalid JSON (78.525159ms)
✔ helper refuses null success (79.166999ms)
✔ helper refuses rootless output but failed (52.195589ms)
✔ helper checks once and explicitly names host for both calls (124.380922ms)
✔ local mode refuses before Docker null (28.199092ms)
✔ local mode refuses before Docker {"mode":"local"} (29.618143ms)
✔ invalid config refuses before Docker (27.543884ms)
ℹ tests 31
ℹ suites 0
ℹ pass 31
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 762.528877
EXIT=0
@@ -1,90 +0,0 @@
2026-10-24T04:09:01Z
23:09:01 up 10 days, 14:28, 2 users, load average: 2.28, 2.50, 2.05
CLEAR sage-3919 TMPDIR=/mnt/storage/scratch/tmp/hYypHU
OK absent adapter defaults to pi
OK adapter mock validates (exit 0)
OK unsupported adapter exits 2 (exit 2)
OK env exports adapter
OK bootstrap creates default when absent (exit 0)
OK bootstrap wrote config file
OK bootstrap is idempotent on existing config (exit 0)
OK bootstrap did not rewrite existing config
OK validate missing config exits 3 (exit 3)
OK malformed JSON exits 2 (exit 2)
OK unsupported configVersion exits 2 (exit 2)
OK unknown top-level key exits 2 (exit 2)
OK unknown execution key exits 2 (exit 2)
OK unsupported backend exits 2 (exit 2)
OK unsupported environment exits 2 (exit 2)
OK relative dataRoot exits 2 (exit 2)
OK non-canonical dataRoot exits 2 (exit 2)
OK filesystem root dataRoot exits 2 (exit 2)
OK home directory dataRoot exits 2 (exit 2)
OK dataRoot containing config dir exits 2 (exit 2)
OK control character in provider exits 2 (exit 2)
OK symlinked config file exits 2 (exit 2)
OK env exports resolve correctly
OK valid console.sso loads and is in the output
OK console.sso HS256 alg exits 2 naming the path (exit 2)
OK console.sso alg none exits 2 naming the path (exit 2)
OK console.sso missing slot field exits 2 naming the path (exit 2)
OK console.sso slot name outside the pattern exits 2 naming the path (exit 2)
OK unknown key under console exits 2 naming the path (exit 2)
OK valid federation key loads and is in the output
OK federation unknown sub-key exits 2 naming the key (exit 2)
OK federation peer unknown sub-key exits 2 naming the key (exit 2)
OK federation peer origin over http exits 2 naming the key (exit 2)
OK federation peer origin with a path exits 2 naming the key (exit 2)
OK federation peer origin with userinfo exits 2 naming the key (exit 2)
OK federation peer id equal to self exits 2 naming the key (exit 2)
OK federation peer label empty exits 2 naming the key (exit 2)
OK federation not an object exits 2 naming the key (exit 2)
OK federation self pattern exits 2 naming the key (exit 2)
OK federation peers not an array exits 2 naming the key (exit 2)
OK federation peer not an object exits 2 naming the key (exit 2)
OK federation peer id pattern exits 2 naming the key (exit 2)
OK federation peer label over 80 characters exits 2 naming the key (exit 2)
OK federation duplicate peer id exits 2 naming the key (exit 2)
OK federation peer origin with a trailing slash exits 2 naming the key (exit 2)
OK federation self missing exits 2 naming the key (exit 2)
OK federation peer id missing exits 2 naming the key (exit 2)
OK federation peer label missing exits 2 naming the key (exit 2)
OK config without federation loads and has no federation output
OK config without console loads with unchanged output
OK compactionReadBytes 8388608 loads and is in the output as written
OK compactionReadBytes "x" loads and is in the output as written
OK compactionStreams 2048 loads and is in the output as written
OK compactionStreams "x" loads and is in the output as written
OK failed validation modified nothing
✔ install: absent (33.602997ms)
✔ install: local (27.76628ms)
✔ install: local endpoint (28.360207ms)
✔ install: containerized missing endpoint (26.872508ms)
✔ install: bad endpoint "tcp://localhost:2375" (27.729466ms)
✔ install: bad endpoint "ssh://host/run/docker.sock" (29.663768ms)
✔ install: bad endpoint "run/docker.sock" (28.875798ms)
✔ install: bad endpoint "unix://relative.sock" (27.427064ms)
✔ install: bad endpoint "unix:///a b.sock" (30.77649ms)
✔ install: bad endpoint "unix:///a.sock\n" (34.03786ms)
✔ install: bad endpoint "unix:///a" (31.459666ms)
✔ install: bad endpoint "" (26.221924ms)
✔ install: bad endpoint null (31.982463ms)
✔ install: bad endpoint 4 (34.408275ms)
✔ install: valid endpoint (30.184118ms)
✔ install: unknown mode (26.58596ms)
✔ install: missing mode (26.563043ms)
✔ install: unknown key (28.663535ms)
✔ install: null (26.567284ms)
✔ install: array (24.756494ms)
ℹ tests 20
ℹ suites 0
ℹ pass 20
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 640.201326
OK install configuration cases
selftest: 56 passed, 0 failed
EXIT=0
@@ -1,245 +0,0 @@
2026-10-24T04:09:14Z
23:09:14 up 10 days, 14:29, 2 users, load average: 2.58, 2.56, 2.08
CLEAR sage-3919 TMPDIR=/mnt/storage/scratch/tmp/7xUNh5
✔ install: absent (28.837858ms)
✔ install: local (28.328967ms)
✔ install: local endpoint (26.178892ms)
✔ install: containerized missing endpoint (25.578947ms)
✔ install: bad endpoint "tcp://localhost:2375" (24.988885ms)
✔ install: bad endpoint "ssh://host/run/docker.sock" (25.850327ms)
✔ install: bad endpoint "run/docker.sock" (26.499708ms)
✔ install: bad endpoint "unix://relative.sock" (42.276491ms)
✔ install: bad endpoint "unix:///a b.sock" (32.161423ms)
✔ install: bad endpoint "unix:///a.sock\n" (26.834457ms)
✔ install: bad endpoint "unix:///a" (30.552354ms)
✔ install: bad endpoint "" (28.938768ms)
✔ install: bad endpoint null (27.00598ms)
✔ install: bad endpoint 4 (29.63046ms)
✔ install: valid endpoint (28.707851ms)
✔ install: unknown mode (25.507911ms)
✔ install: missing mode (27.101142ms)
✔ install: unknown key (32.316453ms)
✔ install: null (27.273355ms)
✔ install: array (25.503395ms)
✔ helper refuses rootful (81.447175ms)
✔ helper refuses missing (49.32616ms)
✔ helper refuses substring (80.299191ms)
✔ helper refuses object (96.465808ms)
✔ helper refuses invalid JSON (89.854499ms)
✔ helper refuses null success (79.934412ms)
✔ helper refuses rootless output but failed (61.053445ms)
✔ helper checks once and explicitly names host for both calls (120.469053ms)
✔ local mode refuses before Docker null (28.252291ms)
✔ local mode refuses before Docker {"mode":"local"} (32.631557ms)
✔ invalid config refuses before Docker (28.529643ms)
ℹ tests 31
ℹ suites 0
ℹ pass 31
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 798.423992
✔ allows exact host and port, fragmented headers, and preserves tunnel head (18.932222ms)
✔ unlisted host (1.309946ms)
✔ unlisted port (1.067401ms)
✔ GET (0.988233ms)
✔ invalid port (0.919738ms)
✔ userinfo (0.832286ms)
✔ bad header (0.761085ms)
✔ slow client has a total header deadline (80.345593ms)
✔ oversized header refuses (1.044536ms)
✔ missing, invalid and unknown lists fail closed (0.486134ms)
✔ refused upstream returns 502 while retaining one allowed decision (1.562662ms)
ℹ tests 11
ℹ suites 0
ℹ pass 11
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 149.849801
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 454B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#2 DONE 1.6s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/9] FROM docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#4 resolve docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20 done
#4 ...
#5 [internal] load build context
#5 transferring context: 31.36MB 0.1s done
#5 DONE 0.2s
#4 [1/9] FROM docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#4 sha256:08489011619dfda982191cb9cc0418dc48ea3b1341588aeef534e4db0ab4d3d0 447B / 447B 0.1s done
#4 sha256:1a0cf4b1ffc8dd659bb8c6e90471bc8fec24620e4fdb68bb2b6faa56154ae146 0B / 1.71MB 0.2s
#4 sha256:11ffcabe0f772f0f1a9c80b4b290b5ec0de54f11c671f0fa10e5844f9b52d17a 0B / 50.86MB 0.2s
#4 sha256:517d640e32835287653c4b15eb5d9f6bcf48e103baea4f7e29bac937002f649b 3.31kB / 3.31kB 0.2s done
#4 sha256:1a0cf4b1ffc8dd659bb8c6e90471bc8fec24620e4fdb68bb2b6faa56154ae146 1.71MB / 1.71MB 0.3s done
#4 sha256:7fd1ebe418b5079d4aa36f416af9b143d127bc75b0155d71cb5fd559b5e04d39 3.15MB / 28.25MB 0.3s
#4 sha256:11ffcabe0f772f0f1a9c80b4b290b5ec0de54f11c671f0fa10e5844f9b52d17a 6.29MB / 50.86MB 0.5s
#4 sha256:7fd1ebe418b5079d4aa36f416af9b143d127bc75b0155d71cb5fd559b5e04d39 11.53MB / 28.25MB 0.5s
#4 sha256:11ffcabe0f772f0f1a9c80b4b290b5ec0de54f11c671f0fa10e5844f9b52d17a 14.68MB / 50.86MB 0.6s
#4 sha256:7fd1ebe418b5079d4aa36f416af9b143d127bc75b0155d71cb5fd559b5e04d39 19.92MB / 28.25MB 0.6s
#4 sha256:11ffcabe0f772f0f1a9c80b4b290b5ec0de54f11c671f0fa10e5844f9b52d17a 24.12MB / 50.86MB 0.8s
#4 sha256:7fd1ebe418b5079d4aa36f416af9b143d127bc75b0155d71cb5fd559b5e04d39 28.25MB / 28.25MB 0.8s done
#4 sha256:11ffcabe0f772f0f1a9c80b4b290b5ec0de54f11c671f0fa10e5844f9b52d17a 49.28MB / 50.86MB 1.1s
#4 extracting sha256:7fd1ebe418b5079d4aa36f416af9b143d127bc75b0155d71cb5fd559b5e04d39
#4 sha256:11ffcabe0f772f0f1a9c80b4b290b5ec0de54f11c671f0fa10e5844f9b52d17a 50.86MB / 50.86MB 1.1s done
#4 extracting sha256:7fd1ebe418b5079d4aa36f416af9b143d127bc75b0155d71cb5fd559b5e04d39 0.7s done
#4 DONE 1.6s
#4 [1/9] FROM docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#4 extracting sha256:517d640e32835287653c4b15eb5d9f6bcf48e103baea4f7e29bac937002f649b 0.0s done
#4 extracting sha256:11ffcabe0f772f0f1a9c80b4b290b5ec0de54f11c671f0fa10e5844f9b52d17a
#4 extracting sha256:11ffcabe0f772f0f1a9c80b4b290b5ec0de54f11c671f0fa10e5844f9b52d17a 1.0s done
#4 DONE 2.6s
#4 [1/9] FROM docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#4 extracting sha256:1a0cf4b1ffc8dd659bb8c6e90471bc8fec24620e4fdb68bb2b6faa56154ae146 0.0s done
#4 extracting sha256:08489011619dfda982191cb9cc0418dc48ea3b1341588aeef534e4db0ab4d3d0 done
#4 DONE 2.7s
#6 [2/9] WORKDIR /opt/mosaic
#6 DONE 0.0s
#7 [3/9] COPY package.json package-lock.json ./
#7 DONE 0.0s
#8 [4/9] RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund && npm cache clean --force
#8 1.008 npm warn deprecated [email protected]: Use your platform's native DOMException instead
#8 2.464
#8 2.464 added 128 packages in 2s
#8 2.465 npm notice
#8 2.465 npm notice New major version of npm available! 11.19.0 -> 12.2.1
#8 2.465 npm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.1
#8 2.465 npm notice To update run: npm install -g [email protected]
#8 2.465 npm notice
#8 2.562 npm warn using --force Recommended protections disabled.
#8 DONE 2.7s
#9 [5/9] COPY packages/ packages/
#9 DONE 0.1s
#10 [6/9] COPY scripts/ scripts/
#10 DONE 0.0s
#11 [7/9] COPY contracts/ contracts/
#11 DONE 0.0s
#12 [8/9] COPY roles/ roles/
#12 DONE 0.0s
#13 [9/9] COPY RELEASE ./
#13 DONE 0.0s
#14 exporting to image
#14 exporting layers
#14 exporting layers 3.3s done
#14 exporting manifest sha256:b288643bbe89b43f6ffe7890598fe70801933c9e1e5d64efbfc6d87bbc502389 done
#14 exporting config sha256:c8e137b79bfd807d4d9fc37d5ac211476decc3cc3c8db1e39b2e5dedf5471191 done
#14 exporting attestation manifest sha256:5f2e757dd0fc2a424c78eb4bdd2ddd3039067afb54bc76bc03d6a6f9f1e2ac75 done
#14 exporting manifest list sha256:7b4fa741d785225148156d1962a1770b40ab84b55ce8ffde5a3485b316f93b3a done
#14 naming to docker.io/library/mosaic-core:0.0.12 done
#14 unpacking to docker.io/library/mosaic-core:0.0.12
#14 unpacking to docker.io/library/mosaic-core:0.0.12 1.6s done
#14 DONE 4.9s
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 424B done
#1 WARN: InvalidDefaultArgInFrom: Default value for ARG ${CORE_IMAGE} results in empty or invalid base image name (line 2)
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/mosaic-core:0.0.12
#2 DONE 0.0s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/2] FROM docker.io/library/mosaic-core:0.0.12@sha256:7b4fa741d785225148156d1962a1770b40ab84b55ce8ffde5a3485b316f93b3a
#4 resolve docker.io/library/mosaic-core:0.0.12@sha256:7b4fa741d785225148156d1962a1770b40ab84b55ce8ffde5a3485b316f93b3a done
#4 DONE 0.0s
#5 [2/2] RUN npm install -g --no-audit --no-fund @anthropic-ai/[email protected] @earendil-works/[email protected] && npm cache clean --force && rm -rf /root/.npm /tmp/*
#5 1.716 npm warn deprecated [email protected]: Use your platform's native DOMException instead
#5 6.031
#5 6.031 added 167 packages in 6s
#5 6.032 npm warn install-scripts 4 packages have install scripts not yet covered by allowScripts:
#5 6.032 npm warn install-scripts @anthropic-ai/[email protected] (postinstall: node install.cjs)
#5 6.032 npm warn install-scripts @google/[email protected] (preinstall: echo 'preinstall: no-op')
#5 6.032 npm warn install-scripts [email protected] (postinstall: node install.js)
#5 6.032 npm warn install-scripts [email protected] (postinstall: node scripts/postinstall)
#5 6.032 npm warn install-scripts
#5 6.032 npm warn install-scripts Run `npm install -g --allow-scripts=@anthropic-ai/claude-code,@google/genai,esbuild,protobufjs` to allow these scripts once, or `npm config set allow-scripts=@anthropic-ai/claude-code,@google/genai,esbuild,protobufjs --location=user` to allow them for all global installs.
#5 6.119 npm warn using --force Recommended protections disabled.
#5 DONE 6.3s
#6 exporting to image
#6 exporting layers
#6 exporting layers 18.7s done
#6 exporting manifest sha256:16af90e8e3291626f37fb5b2e78904d87126d875e7bcc240dd2ba453ca62bc10 done
#6 exporting config sha256:ad269f0a84c0b1c5833b787793a45df782c1aae86e37f6216782a12a01f4557b done
#6 exporting attestation manifest sha256:436490e0d139cb0db83f260c42ac3364377971160b2418cd03ee07c18873a5f6 done
#6 exporting manifest list sha256:bc95495952e58a005c275a9d00842242c17965d0d9c2a83c476048cbfc110946 done
#6 naming to docker.io/library/mosaic-session:0.0.12 done
#6 unpacking to docker.io/library/mosaic-session:0.0.12
#6 unpacking to docker.io/library/mosaic-session:0.0.12 5.3s done
#6 DONE 24.0s
1 warning found (use docker --debug to expand):
 - InvalidDefaultArgInFrom: Default value for ARG ${CORE_IMAGE} results in empty or invalid base image name (line 2)
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 299B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#2 DONE 0.0s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/4] FROM docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#4 resolve docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20 done
#4 DONE 0.0s
#5 [2/4] WORKDIR /opt/mosaic
#5 CACHED
#6 [internal] load build context
#6 transferring context: 459B 0.0s done
#6 DONE 0.0s
#7 [3/4] COPY packages/egress/ packages/egress/
#7 DONE 0.0s
#8 [4/4] COPY contracts/egress.json contracts/egress.json
#8 DONE 0.0s
#9 exporting to image
#9 exporting layers 0.0s done
#9 exporting manifest sha256:f3f318ddc871b5a5f953aa0709cf69194260e633e26b15daafefe40572a36f27 done
#9 exporting config sha256:c315f86a3be49d29aa7cad287a1653424206adb43683a34b91633b9bb8a14c6b done
#9 exporting attestation manifest sha256:d41ee39a55170821cf46236fbf6069519e52c7386e27259ee17113777fa348e1 done
#9 exporting manifest list sha256:af29f552efea177d706779ed9afb43c89763b58de913da0a570208cdda619d50 done
#9 naming to docker.io/library/mosaic-egress:0.0.12 done
#9 unpacking to docker.io/library/mosaic-egress:0.0.12 0.0s done
#9 DONE 0.1s
internal network: listed TLS reached, unlisted host/port refused, external DNS refused
Vikunja client public GET 200; notifier REST dummy-token GET 401
test-install: PASS (provider-host measurement remains Mos gate)
EXIT=0
@@ -1,24 +0,0 @@
2026-10-24T04:13:52Z
23:13:52 up 10 days, 14:33, 2 users, load average: 2.19, 2.69, 2.26
CLEAR sage-3923 TMPDIR=/mnt/storage/scratch/tmp/5Tw4Jn
✔ allows exact host and port, fragmented headers, and preserves tunnel head (17.065009ms)
✔ unlisted host (1.179696ms)
✔ unlisted port (4.182798ms)
✔ GET (0.916554ms)
✔ invalid port (0.897474ms)
✔ userinfo (0.772938ms)
✔ bad header (0.737254ms)
✔ slow client has a total header deadline (81.64527ms)
✔ oversized header refuses (1.073337ms)
✔ missing, invalid and unknown lists fail closed (0.464571ms)
✔ refused upstream returns 502 while retaining one allowed decision (1.555082ms)
✔ upstream reset after CONNECT closes the tunnel without a second HTTP response (21.757224ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 173.90579
EXIT=0
@@ -1,207 +0,0 @@
2026-10-24T04:13:56Z
23:13:56 up 10 days, 14:33, 2 users, load average: 2.18, 2.67, 2.26
CLEAR sage-3923 TMPDIR=/mnt/storage/scratch/tmp/Yb2lkZ
✔ install: absent (26.874578ms)
✔ install: local (25.86789ms)
✔ install: local endpoint (24.545469ms)
✔ install: containerized missing endpoint (26.458263ms)
✔ install: bad endpoint "tcp://localhost:2375" (24.315713ms)
✔ install: bad endpoint "ssh://host/run/docker.sock" (27.113397ms)
✔ install: bad endpoint "run/docker.sock" (24.399221ms)
✔ install: bad endpoint "unix://relative.sock" (26.259544ms)
✔ install: bad endpoint "unix:///a b.sock" (24.051655ms)
✔ install: bad endpoint "unix:///a.sock\n" (23.493287ms)
✔ install: bad endpoint "unix:///a" (25.527734ms)
✔ install: bad endpoint "" (24.647712ms)
✔ install: bad endpoint null (24.029319ms)
✔ install: bad endpoint 4 (22.545369ms)
✔ install: valid endpoint (24.217907ms)
✔ install: unknown mode (22.578207ms)
✔ install: missing mode (26.668685ms)
✔ install: unknown key (25.239503ms)
✔ install: null (26.507504ms)
✔ install: array (23.325961ms)
✔ helper refuses rootful (83.142099ms)
✔ helper refuses missing (48.938896ms)
✔ helper refuses substring (74.574145ms)
✔ helper refuses object (69.21506ms)
✔ helper refuses invalid JSON (71.320245ms)
✔ helper refuses null success (70.397988ms)
✔ helper refuses rootless output but failed (50.077162ms)
✔ helper checks once and explicitly names host for both calls (116.70882ms)
✔ local mode refuses before Docker null (26.523468ms)
✔ local mode refuses before Docker {"mode":"local"} (26.276186ms)
✔ invalid config refuses before Docker (26.092328ms)
ℹ tests 31
ℹ suites 0
ℹ pass 31
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 707.332707
✔ allows exact host and port, fragmented headers, and preserves tunnel head (15.694598ms)
✔ unlisted host (1.162772ms)
✔ unlisted port (1.047448ms)
✔ GET (0.913924ms)
✔ invalid port (0.930799ms)
✔ userinfo (0.750312ms)
✔ bad header (0.739214ms)
✔ slow client has a total header deadline (81.348784ms)
✔ oversized header refuses (0.992296ms)
✔ missing, invalid and unknown lists fail closed (0.441271ms)
✔ refused upstream returns 502 while retaining one allowed decision (1.4408ms)
✔ upstream reset after CONNECT closes the tunnel without a second HTTP response (21.481904ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 169.285852
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 454B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#2 DONE 0.0s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/9] FROM docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#4 resolve docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20 done
#4 DONE 0.0s
#5 [internal] load build context
#5 transferring context: 31.37MB 0.1s done
#5 DONE 0.1s
#6 [3/9] COPY package.json package-lock.json ./
#6 CACHED
#7 [2/9] WORKDIR /opt/mosaic
#7 CACHED
#8 [4/9] RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund && npm cache clean --force
#8 CACHED
#9 [5/9] COPY packages/ packages/
#9 DONE 0.1s
#10 [6/9] COPY scripts/ scripts/
#10 DONE 0.0s
#11 [7/9] COPY contracts/ contracts/
#11 DONE 0.0s
#12 [8/9] COPY roles/ roles/
#12 DONE 0.0s
#13 [9/9] COPY RELEASE ./
#13 DONE 0.0s
#14 exporting to image
#14 exporting layers
#14 exporting layers 0.4s done
#14 exporting manifest sha256:065dd55d59585806e540929e2ea66df397d47292cc84160bb7be491ecd2b9d59 done
#14 exporting config sha256:3d97362dc6b6242fc540f08f538068b91ec28b1d3c9a34d7bc2bebff137a7a40 done
#14 exporting attestation manifest sha256:f66bf47e4ec5b67db64f44d8554192f646111116e165ec3bc017b5a2b011204b done
#14 exporting manifest list sha256:d840b0ec16ce6548549d18fb8bfc6b13c76ee77e9131a317582f919c61dcaf7f done
#14 naming to docker.io/library/mosaic-core:0.0.12 done
#14 unpacking to docker.io/library/mosaic-core:0.0.12
#14 unpacking to docker.io/library/mosaic-core:0.0.12 0.2s done
#14 DONE 0.6s
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 424B done
#1 WARN: InvalidDefaultArgInFrom: Default value for ARG ${CORE_IMAGE} results in empty or invalid base image name (line 2)
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/mosaic-core:0.0.12
#2 DONE 0.0s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/2] FROM docker.io/library/mosaic-core:0.0.12@sha256:d840b0ec16ce6548549d18fb8bfc6b13c76ee77e9131a317582f919c61dcaf7f
#4 resolve docker.io/library/mosaic-core:0.0.12@sha256:d840b0ec16ce6548549d18fb8bfc6b13c76ee77e9131a317582f919c61dcaf7f done
#4 DONE 0.0s
#5 [2/2] RUN npm install -g --no-audit --no-fund @anthropic-ai/[email protected] @earendil-works/[email protected] && npm cache clean --force && rm -rf /root/.npm /tmp/*
#5 1.700 npm warn deprecated [email protected]: Use your platform's native DOMException instead
#5 5.518
#5 5.518 added 167 packages in 5s
#5 5.519 npm warn install-scripts 4 packages have install scripts not yet covered by allowScripts:
#5 5.519 npm warn install-scripts @anthropic-ai/[email protected] (postinstall: node install.cjs)
#5 5.519 npm warn install-scripts @google/[email protected] (preinstall: echo 'preinstall: no-op')
#5 5.519 npm warn install-scripts [email protected] (postinstall: node install.js)
#5 5.519 npm warn install-scripts [email protected] (postinstall: node scripts/postinstall)
#5 5.519 npm warn install-scripts
#5 5.519 npm warn install-scripts Run `npm install -g --allow-scripts=@anthropic-ai/claude-code,@google/genai,esbuild,protobufjs` to allow these scripts once, or `npm config set allow-scripts=@anthropic-ai/claude-code,@google/genai,esbuild,protobufjs --location=user` to allow them for all global installs.
#5 5.590 npm warn using --force Recommended protections disabled.
#5 DONE 5.7s
#6 exporting to image
#6 exporting layers
#6 exporting layers 16.9s done
#6 exporting manifest sha256:d4c83e350da6f18bfa7f3dbcef9ab078d72ef7f1f957a2926726e17973969e6a done
#6 exporting config sha256:8afa310f48e06614963dcbcdc9c3ac497d9e4bad4ddd0524062ba8a52bc3cda2 done
#6 exporting attestation manifest sha256:b7f082a8cd17d2c90eb27a81359aea66186b80d56a6d0c5e89d29187a2f78ed4 done
#6 exporting manifest list sha256:2b6299e5a329adf85ea36038cb0407f2328e72e848dbfe95a17bd1fefbf9cf92 done
#6 naming to docker.io/library/mosaic-session:0.0.12 done
#6 unpacking to docker.io/library/mosaic-session:0.0.12
#6 unpacking to docker.io/library/mosaic-session:0.0.12 4.7s done
#6 DONE 21.7s
1 warning found (use docker --debug to expand):
 - InvalidDefaultArgInFrom: Default value for ARG ${CORE_IMAGE} results in empty or invalid base image name (line 2)
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 299B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#2 DONE 0.0s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/4] FROM docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#4 resolve docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20 done
#4 DONE 0.0s
#5 [internal] load build context
#5 transferring context: 459B 0.0s done
#5 DONE 0.0s
#6 [2/4] WORKDIR /opt/mosaic
#6 CACHED
#7 [3/4] COPY packages/egress/ packages/egress/
#7 DONE 0.0s
#8 [4/4] COPY contracts/egress.json contracts/egress.json
#8 DONE 0.0s
#9 exporting to image
#9 exporting layers 0.0s done
#9 exporting manifest sha256:e47c86064470f291d2ea468a1002f1e5f9b2e682e42940d06bd33d4e39945a6e done
#9 exporting config sha256:ba520eadeb66ccfa2dbd3170744fca08affb4dac377d6cf2d26ec27d98d4ee25 done
#9 exporting attestation manifest sha256:93cbd2f8a7fb1afb858b7b1a35a3020af8e9623e3a98be71d04e9d746c25d148 done
#9 exporting manifest list sha256:a55e9d5ec6da74933bbefac6816efd0ea5246e825f61eaa4297e7baf7b09501c done
#9 naming to docker.io/library/mosaic-egress:0.0.12 done
#9 unpacking to docker.io/library/mosaic-egress:0.0.12 0.0s done
#9 DONE 0.1s
internal network: listed TLS reached, unlisted host/port refused, external DNS refused
Vikunja client public GET 200; notifier REST dummy-token GET 401
test-install: PASS (provider-host measurement remains Mos gate)
EXIT=0
@@ -1,165 +0,0 @@
2026-10-24T04:29:25Z
23:29:25 up 10 days, 14:49, 2 users, load average: 2.63, 2.97, 2.72
CLEAR sage-3936 TMPDIR=/mnt/storage/scratch/tmp/jcNy2F archived=a14fdc443 expected RED
✔ refusal 408 releases server socket (allowHalfOpen=false) (62.262337ms)
✖ refusal 408 releases server socket (allowHalfOpen=true) (552.049114ms)
✔ refusal 400 releases server socket (allowHalfOpen=false) (12.304169ms)
✖ refusal 400 releases server socket (allowHalfOpen=true) (509.122366ms)
✔ refusal 403 releases server socket (allowHalfOpen=false) (11.566877ms)
✖ refusal 403 releases server socket (allowHalfOpen=true) (508.134074ms)
✔ refusal 431 releases server socket (allowHalfOpen=false) (11.339612ms)
✖ refusal 431 releases server socket (allowHalfOpen=true) (507.396552ms)
✔ refusal 502 releases server socket (allowHalfOpen=false) (11.812225ms)
✖ refusal 502 releases server socket (allowHalfOpen=true) (509.521901ms)
✖ inherited cache is discarded; private cache stays unexported (rootless=false) (49.005306ms)
✖ inherited cache is discarded; private cache stays unexported (rootless=true) (42.961333ms)
ℹ tests 12
ℹ suites 0
ℹ pass 5
ℹ fail 7
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2739.328587
✖ failing tests:
test at ../../../../../../mnt/storage/scratch/tmp/jcNy2F/legacy/packages/egress/tests/refusal.test.mjs:11:79
✖ refusal 408 releases server socket (allowHalfOpen=true) (552.049114ms)
AssertionError [ERR_ASSERTION]: refused peer must not retain a server socket
1 !== 0
at TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/jcNy2F/legacy/packages/egress/tests/refusal.test.mjs:30:10)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at ../../../../../../mnt/storage/scratch/tmp/jcNy2F/legacy/packages/egress/tests/refusal.test.mjs:11:79
✖ refusal 400 releases server socket (allowHalfOpen=true) (509.122366ms)
AssertionError [ERR_ASSERTION]: refused peer must not retain a server socket
1 !== 0
at TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/jcNy2F/legacy/packages/egress/tests/refusal.test.mjs:30:10)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at ../../../../../../mnt/storage/scratch/tmp/jcNy2F/legacy/packages/egress/tests/refusal.test.mjs:11:79
✖ refusal 403 releases server socket (allowHalfOpen=true) (508.134074ms)
AssertionError [ERR_ASSERTION]: refused peer must not retain a server socket
1 !== 0
at TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/jcNy2F/legacy/packages/egress/tests/refusal.test.mjs:30:10)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at ../../../../../../mnt/storage/scratch/tmp/jcNy2F/legacy/packages/egress/tests/refusal.test.mjs:11:79
✖ refusal 431 releases server socket (allowHalfOpen=true) (507.396552ms)
AssertionError [ERR_ASSERTION]: refused peer must not retain a server socket
1 !== 0
at TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/jcNy2F/legacy/packages/egress/tests/refusal.test.mjs:30:10)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at ../../../../../../mnt/storage/scratch/tmp/jcNy2F/legacy/packages/egress/tests/refusal.test.mjs:11:79
✖ refusal 502 releases server socket (allowHalfOpen=true) (509.521901ms)
AssertionError [ERR_ASSERTION]: refused peer must not retain a server socket
1 !== 0
at TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/jcNy2F/legacy/packages/egress/tests/refusal.test.mjs:30:10)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at ../../../../../../mnt/storage/scratch/tmp/jcNy2F/legacy/scripts/install-tests/review-cache.test.mjs:10:36
✖ inherited cache is discarded; private cache stays unexported (rootless=false) (49.005306ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
0 !== 2
at TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/jcNy2F/legacy/scripts/install-tests/review-cache.test.mjs:19:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.start (node:internal/test_runner/test:1262:17)
at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 0,
expected: 2,
operator: 'strictEqual',
diff: 'simple'
}
test at ../../../../../../mnt/storage/scratch/tmp/jcNy2F/legacy/scripts/install-tests/review-cache.test.mjs:10:36
✖ inherited cache is discarded; private cache stays unexported (rootless=true) (42.961333ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected
[
'--host',
+ 'unix:///inherited.sock',
+ 'ps'
- 'unix:///configured.sock',
- 'info',
- '--format',
- '{{json .SecurityOptions}}'
]
at TestContext.<anonymous> (file:///mnt/storage/scratch/tmp/jcNy2F/legacy/scripts/install-tests/review-cache.test.mjs:20:10)
at Test.runInAsyncScope (node:async_hooks:226:14)
at Test.run (node:internal/test_runner/test:1402:25)
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
at Test.postRun (node:internal/test_runner/test:1542:19)
at Test.run (node:internal/test_runner/test:1467:12)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: [ '--host', 'unix:///inherited.sock', 'ps' ],
expected: [ '--host', 'unix:///configured.sock', 'info', '--format', '{{json .SecurityOptions}}' ],
operator: 'deepStrictEqual',
diff: 'simple'
}
EXIT=1 (expected 1 for reproduced defects)
@@ -1,24 +0,0 @@
2026-10-24T04:29:39Z
23:29:39 up 10 days, 14:49, 2 users, load average: 2.20, 2.85, 2.69
CLEAR sage-3936 TMPDIR=/mnt/storage/scratch/tmp/0gZQZl
✔ refusal 408 releases server socket (allowHalfOpen=false) (53.837502ms)
✔ refusal 408 releases server socket (allowHalfOpen=true) (42.030239ms)
✔ refusal 400 releases server socket (allowHalfOpen=false) (1.261482ms)
✔ refusal 400 releases server socket (allowHalfOpen=true) (0.904817ms)
✔ refusal 403 releases server socket (allowHalfOpen=false) (0.917847ms)
✔ refusal 403 releases server socket (allowHalfOpen=true) (0.804008ms)
✔ refusal 431 releases server socket (allowHalfOpen=false) (0.718814ms)
✔ refusal 431 releases server socket (allowHalfOpen=true) (0.668466ms)
✔ refusal 502 releases server socket (allowHalfOpen=false) (1.344911ms)
✔ refusal 502 releases server socket (allowHalfOpen=true) (0.979004ms)
✔ inherited cache is discarded; private cache stays unexported (rootless=false) (82.339935ms)
✔ inherited cache is discarded; private cache stays unexported (rootless=true) (112.830463ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 245.761199
EXIT=0
@@ -1,45 +0,0 @@
2026-10-24T04:29:49Z
23:29:49 up 10 days, 14:49, 2 users, load average: 2.09, 2.81, 2.68
CLEAR sage-3936 TMPDIR=/mnt/storage/scratch/tmp/tUdE4l
✔ install: absent (26.366604ms)
✔ install: local (22.63247ms)
✔ install: local endpoint (22.799393ms)
✔ install: containerized missing endpoint (25.600693ms)
✔ install: bad endpoint "tcp://localhost:2375" (23.03486ms)
✔ install: bad endpoint "ssh://host/run/docker.sock" (23.131519ms)
✔ install: bad endpoint "run/docker.sock" (24.109097ms)
✔ install: bad endpoint "unix://relative.sock" (23.350143ms)
✔ install: bad endpoint "unix:///a b.sock" (25.171936ms)
✔ install: bad endpoint "unix:///a.sock\n" (23.993281ms)
✔ install: bad endpoint "unix:///a" (23.480143ms)
✔ install: bad endpoint "" (24.103262ms)
✔ install: bad endpoint null (22.669641ms)
✔ install: bad endpoint 4 (24.019575ms)
✔ install: valid endpoint (24.603314ms)
✔ install: unknown mode (24.602796ms)
✔ install: missing mode (23.797819ms)
✔ install: unknown key (22.998491ms)
✔ install: null (25.129192ms)
✔ install: array (24.482115ms)
✔ helper refuses rootful (69.872106ms)
✔ helper refuses missing (48.024972ms)
✔ helper refuses substring (75.261764ms)
✔ helper refuses object (69.618809ms)
✔ helper refuses invalid JSON (69.071543ms)
✔ helper refuses null success (72.882659ms)
✔ helper refuses rootless output but failed (53.345431ms)
✔ helper checks once and explicitly names host for both calls (102.552473ms)
✔ local mode refuses before Docker null (26.011087ms)
✔ local mode refuses before Docker {"mode":"local"} (28.185416ms)
✔ invalid config refuses before Docker (27.514422ms)
✔ inherited cache is discarded; private cache stays unexported (rootless=false) (75.055951ms)
✔ inherited cache is discarded; private cache stays unexported (rootless=true) (109.227515ms)
ℹ tests 33
ℹ suites 0
ℹ pass 33
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 690.153576
EXIT=0
@@ -1,34 +0,0 @@
2026-10-24T04:29:59Z
23:29:59 up 10 days, 14:49, 2 users, load average: 1.93, 2.75, 2.66
CLEAR sage-3936 TMPDIR=/mnt/storage/scratch/tmp/TviERP
✔ allows exact host and port, fragmented headers, and preserves tunnel head (23.927026ms)
✔ unlisted host (1.732173ms)
✔ unlisted port (1.604846ms)
✔ GET (1.184716ms)
✔ invalid port (1.343703ms)
✔ userinfo (1.188622ms)
✔ bad header (1.150055ms)
✔ slow client has a total header deadline (81.793054ms)
✔ oversized header refuses (1.244104ms)
✔ missing, invalid and unknown lists fail closed (0.57805ms)
✔ refused upstream returns 502 while retaining one allowed decision (1.746501ms)
✔ upstream reset after CONNECT closes the tunnel without a second HTTP response (22.237884ms)
✔ refusal 408 releases server socket (allowHalfOpen=false) (55.691211ms)
✔ refusal 408 releases server socket (allowHalfOpen=true) (43.646383ms)
✔ refusal 400 releases server socket (allowHalfOpen=false) (1.568816ms)
✔ refusal 400 releases server socket (allowHalfOpen=true) (1.105107ms)
✔ refusal 403 releases server socket (allowHalfOpen=false) (1.202959ms)
✔ refusal 403 releases server socket (allowHalfOpen=true) (1.016559ms)
✔ refusal 431 releases server socket (allowHalfOpen=false) (0.914639ms)
✔ refusal 431 releases server socket (allowHalfOpen=true) (0.889516ms)
✔ refusal 502 releases server socket (allowHalfOpen=false) (1.698346ms)
✔ refusal 502 releases server socket (allowHalfOpen=true) (1.234793ms)
ℹ tests 22
ℹ suites 0
ℹ pass 22
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 183.862745
EXIT=0
@@ -1,219 +0,0 @@
2026-10-24T04:30:08Z
23:30:08 up 10 days, 14:49, 2 users, load average: 1.79, 2.69, 2.64
CLEAR sage-3936 TMPDIR=/mnt/storage/scratch/tmp/qmqk7B
✔ install: absent (25.729884ms)
✔ install: local (27.889152ms)
✔ install: local endpoint (24.35751ms)
✔ install: containerized missing endpoint (32.226985ms)
✔ install: bad endpoint "tcp://localhost:2375" (36.932763ms)
✔ install: bad endpoint "ssh://host/run/docker.sock" (30.311354ms)
✔ install: bad endpoint "run/docker.sock" (25.198808ms)
✔ install: bad endpoint "unix://relative.sock" (26.143708ms)
✔ install: bad endpoint "unix:///a b.sock" (26.182728ms)
✔ install: bad endpoint "unix:///a.sock\n" (24.78053ms)
✔ install: bad endpoint "unix:///a" (25.183076ms)
✔ install: bad endpoint "" (26.038913ms)
✔ install: bad endpoint null (28.122111ms)
✔ install: bad endpoint 4 (25.273681ms)
✔ install: valid endpoint (24.978162ms)
✔ install: unknown mode (25.362867ms)
✔ install: missing mode (30.700943ms)
✔ install: unknown key (25.157537ms)
✔ install: null (25.311198ms)
✔ install: array (22.791684ms)
✔ helper refuses rootful (73.335956ms)
✔ helper refuses missing (58.142601ms)
✔ helper refuses substring (76.536037ms)
✔ helper refuses object (71.066459ms)
✔ helper refuses invalid JSON (75.615304ms)
✔ helper refuses null success (74.72487ms)
✔ helper refuses rootless output but failed (56.316538ms)
✔ helper checks once and explicitly names host for both calls (116.375519ms)
✔ local mode refuses before Docker null (33.189488ms)
✔ local mode refuses before Docker {"mode":"local"} (33.416869ms)
✔ invalid config refuses before Docker (33.260841ms)
✔ inherited cache is discarded; private cache stays unexported (rootless=false) (77.918846ms)
✔ inherited cache is discarded; private cache stays unexported (rootless=true) (125.24254ms)
ℹ tests 33
ℹ suites 0
ℹ pass 33
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 751.025142
✔ allows exact host and port, fragmented headers, and preserves tunnel head (21.619769ms)
✔ unlisted host (1.449654ms)
✔ unlisted port (1.146495ms)
✔ GET (1.037357ms)
✔ invalid port (0.994228ms)
✔ userinfo (0.84581ms)
✔ bad header (0.834734ms)
✔ slow client has a total header deadline (81.37968ms)
✔ oversized header refuses (1.096351ms)
✔ missing, invalid and unknown lists fail closed (0.499339ms)
✔ refused upstream returns 502 while retaining one allowed decision (1.730862ms)
✔ upstream reset after CONNECT closes the tunnel without a second HTTP response (21.825124ms)
✔ refusal 408 releases server socket (allowHalfOpen=false) (55.028256ms)
✔ refusal 408 releases server socket (allowHalfOpen=true) (42.613145ms)
✔ refusal 400 releases server socket (allowHalfOpen=false) (1.348936ms)
✔ refusal 400 releases server socket (allowHalfOpen=true) (1.104981ms)
✔ refusal 403 releases server socket (allowHalfOpen=false) (1.097498ms)
✔ refusal 403 releases server socket (allowHalfOpen=true) (0.965577ms)
✔ refusal 431 releases server socket (allowHalfOpen=false) (0.806917ms)
✔ refusal 431 releases server socket (allowHalfOpen=true) (0.752832ms)
✔ refusal 502 releases server socket (allowHalfOpen=false) (1.582846ms)
✔ refusal 502 releases server socket (allowHalfOpen=true) (1.14509ms)
ℹ tests 22
ℹ suites 0
ℹ pass 22
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 181.752441
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 454B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#2 DONE 0.7s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/9] FROM docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#4 resolve docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20 done
#4 DONE 0.0s
#5 [internal] load build context
#5 transferring context: 31.38MB 0.1s done
#5 DONE 0.1s
#6 [2/9] WORKDIR /opt/mosaic
#6 CACHED
#7 [3/9] COPY package.json package-lock.json ./
#7 CACHED
#8 [4/9] RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund && npm cache clean --force
#8 CACHED
#9 [5/9] COPY packages/ packages/
#9 DONE 0.1s
#10 [6/9] COPY scripts/ scripts/
#10 DONE 0.0s
#11 [7/9] COPY contracts/ contracts/
#11 DONE 0.0s
#12 [8/9] COPY roles/ roles/
#12 DONE 0.0s
#13 [9/9] COPY RELEASE ./
#13 DONE 0.0s
#14 exporting to image
#14 exporting layers
#14 exporting layers 0.4s done
#14 exporting manifest sha256:cfd93bac400adc7580134f6177efd9d748e4b4ad65b0ff22515ca3b3ffafa530 done
#14 exporting config sha256:bda6c7420c2df9510f081d28f50d205083867d0ce907c12290fff226c020fe38 done
#14 exporting attestation manifest sha256:79c61e7daf7794a3db88121ce6260fafab69f182ef89773914c41ca75a44574c done
#14 exporting manifest list sha256:82fc7a3df060c3e58799cca4f89dbd57dc007c5534fdedce54d3de637cc83b67 done
#14 naming to docker.io/library/mosaic-core:0.0.12 done
#14 unpacking to docker.io/library/mosaic-core:0.0.12
#14 unpacking to docker.io/library/mosaic-core:0.0.12 0.2s done
#14 DONE 0.7s
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 424B done
#1 WARN: InvalidDefaultArgInFrom: Default value for ARG ${CORE_IMAGE} results in empty or invalid base image name (line 2)
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/mosaic-core:0.0.12
#2 DONE 0.0s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/2] FROM docker.io/library/mosaic-core:0.0.12@sha256:82fc7a3df060c3e58799cca4f89dbd57dc007c5534fdedce54d3de637cc83b67
#4 resolve docker.io/library/mosaic-core:0.0.12@sha256:82fc7a3df060c3e58799cca4f89dbd57dc007c5534fdedce54d3de637cc83b67 done
#4 DONE 0.0s
#5 [2/2] RUN npm install -g --no-audit --no-fund @anthropic-ai/[email protected] @earendil-works/[email protected] && npm cache clean --force && rm -rf /root/.npm /tmp/*
#5 1.920 npm warn deprecated [email protected]: Use your platform's native DOMException instead
#5 6.054
#5 6.054 added 167 packages in 6s
#5 6.055 npm warn install-scripts 4 packages have install scripts not yet covered by allowScripts:
#5 6.055 npm warn install-scripts @anthropic-ai/[email protected] (postinstall: node install.cjs)
#5 6.055 npm warn install-scripts @google/[email protected] (preinstall: echo 'preinstall: no-op')
#5 6.055 npm warn install-scripts [email protected] (postinstall: node install.js)
#5 6.055 npm warn install-scripts [email protected] (postinstall: node scripts/postinstall)
#5 6.055 npm warn install-scripts
#5 6.055 npm warn install-scripts Run `npm install -g --allow-scripts=@anthropic-ai/claude-code,@google/genai,esbuild,protobufjs` to allow these scripts once, or `npm config set allow-scripts=@anthropic-ai/claude-code,@google/genai,esbuild,protobufjs --location=user` to allow them for all global installs.
#5 6.127 npm warn using --force Recommended protections disabled.
#5 DONE 6.3s
#6 exporting to image
#6 exporting layers
#6 exporting layers 19.5s done
#6 exporting manifest sha256:8b6b7de22cf08845f8fc02961c9677afb9faf0ddc4efe04ab4073a822a60518b done
#6 exporting config sha256:94013fda37e404bc26b7b15281138cd60afbfea672cae2d2187acc0a7c3a0d7e done
#6 exporting attestation manifest sha256:127fcf7ecd51a985d22f5a33d5c441d8fa1e246f88dc6e990c01197b46a61427 done
#6 exporting manifest list sha256:9a272acf49ab376fa9739f66453979487ea8ab259bc43e56f6553e2da7b20835 done
#6 naming to docker.io/library/mosaic-session:0.0.12 done
#6 unpacking to docker.io/library/mosaic-session:0.0.12
#6 unpacking to docker.io/library/mosaic-session:0.0.12 5.2s done
#6 DONE 24.8s
1 warning found (use docker --debug to expand):
 - InvalidDefaultArgInFrom: Default value for ARG ${CORE_IMAGE} results in empty or invalid base image name (line 2)
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 299B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#2 DONE 0.0s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/4] FROM docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
#4 resolve docker.io/library/node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20 done
#4 DONE 0.0s
#5 [internal] load build context
#5 transferring context: 517B 0.0s done
#5 DONE 0.0s
#6 [2/4] WORKDIR /opt/mosaic
#6 CACHED
#7 [3/4] COPY packages/egress/ packages/egress/
#7 DONE 0.0s
#8 [4/4] COPY contracts/egress.json contracts/egress.json
#8 DONE 0.0s
#9 exporting to image
#9 exporting layers 0.0s done
#9 exporting manifest sha256:db3e8991a9330374257298a4a5d4ff36779e8c8d0b00c8b8ead47fc82e32fada done
#9 exporting config sha256:e7d66f4f785e49a6529b3ed4b708e3b985f4e737d03a66a2c861666288d435d9 done
#9 exporting attestation manifest sha256:8d2362ebe73a0b71b175ea4d9571ab841c6f5b0913e8fa265d1f4544307987d2 done
#9 exporting manifest list sha256:36d0bbeb3abc92fc099d95e1fac416df5cc29edf147fcfc4d8743cbe1d60e592 done
#9 naming to docker.io/library/mosaic-egress:0.0.12 done
#9 unpacking to docker.io/library/mosaic-egress:0.0.12 0.0s done
#9 DONE 0.1s
internal network: listed TLS reached, unlisted host/port refused, external DNS refused
Vikunja client public GET 200; notifier REST dummy-token GET 401
test-install: PASS (provider-host measurement remains Mos gate)
EXIT=0
-363
View File
@@ -1,363 +0,0 @@
# Row 157 / I2 / #1652 — Bentina build record
## 2026-10-24 — recovery and pre-build hold
Authority: Sage sage-3906 under Q246 / mos-1566. Seat thread:
`d1ae468e-51ba-40c4-9513-b39ebbb40450`. Branch `bentina/row157`,
base `ec86f2c84` (required registration commit), initially clean.
Read root AGENTS.md, agents/bentina/README.md, the I2 brief and its shared
rules, and the approved containerized-install design. CURRENT.md is a
historical narrative; queue and Sage's assignment select this row. Goal
order recovered from 2026-09-27_goals-review.md. Boyd first review, then
Darkwing install-path review, per Sage's newer assignment.
### Queue boundary
The initial worktree `scripts/mosaic queue next bentina` refused because
queue commands require the canonical checkout. Retried the same read using
`/mnt/storage/src/mosaic-stack/scripts/mosaic` in that checkout (the documented
queue location, without changing its configuration):
```
start row 157: I2 install images, the install config key, the endpoint helper and the egress proxy; brief docs/plans/2026-10-18_containerized-install-rows.md § I2 install images, the install config key, the endpoint helper and the egress proxy; brief differs from pinned blob; ask the lead to re-pin
```
No claimant move until Sage resolves the stale brief pin. No other queue
write authorized. Sent Sage bentina-0001 with this refusal and the outbound
path findings below. Implementation has not started.
### Re-read of the I2 brief's cited files at ec86f2c84
| Cited file / old site | Current observation |
|---|---|
| scripts/hello.sh:24 | Calls processing-control/src/container-cli.mjs:25; Docker moved behind processing control. |
| scripts/build.sh:13 | Still ambient `docker compose build`:13. |
| scripts/release.sh:39,122 | Still ambient image inspect:39 and compose build:122. |
| scripts/agent.sh:208 | Now execs processing-control/src/container-cli.mjs --tty:209. |
| scripts/verify.sh:36,39,45 | Ambient image inspect:37 and compose build:40; request now uses processing-control container CLI:46. |
| scripts/land-work.sh:87-88 | Ambient network inspect/remove unchanged at 87-88. |
| scripts/test-release.sh:58 | Docker probes now at 78,129; cleanup network rm:25; network checks:131,133. |
| scripts/test-task.sh:139,395,442,517 | Docker probes now at 166,422,470,513,557,579; cleanup network rm:21 and network checks:581,583. |
| scripts/mosaic-task.mjs:396 | Now calls runContainer:402, imported from processing control, rather than directly spawning Docker. |
| packages/cli/src/git-socket.mjs:243 | dropNetwork now spawns ambient docker network rm:266; I6 retains ownership. |
| scripts/mosaic-config.mjs:109-160 / :115 | Loader expanded for instances, Console, federation and compaction settings. validate starts:293; allowed-key expression:304 retains a prefix used by stack-release-fixture.mjs. No install key yet. |
The design says to read via scripts/load_config; this is a function, not a
file. Actual entry is scripts/common.sh:16 `load_config`, invoking
mosaic-config.mjs env, then exporting four existing variables. No install
mode/endpoint exports yet. No modification made.
Design sections 3/4/7 re-read: exact base digest
`node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20`;
three release-tagged images; optional install with local default; exact
rootless SecurityOptions array element check; baked agents/services CONNECT
lists; JSON decision log. Spike proxy source re-read: one-chunk request
parsing, no slow-client bound, environment allowlist; it needs the reviewed
implementation required by I2, not a direct copy. package.json pins pi
0.85.1; spike Dockerfile.clis pins Claude Code 2.1.296 (current session-code
pin still needs checking before implementation).
### Outbound stop condition (brief What ships; design section 7)
The brief explicitly says: “If one doesn't, stop and tell Sage before
adding a dependency.” Not all broker outbound paths use fetch at this base:
- packages/bus/src/gitea-grant.mjs:11 imports node:https request;
send():91-140 calls httpsRequest:115 with `agent: false`, auth header,
optional CA, timeout and bounded response. It is not the measured fetch
path. runtime.mjs imports and wires the grant executor.
- packages/bus/src/web.mjs delegates to packages/discord/src/web.mjs.
That implementation imports node:https/node:http:23-24, resolves public
addresses and pins the connection to a vetted address (:161 onward).
Changing it to fetch without preserving that fence would be a boundary
change. Its arbitrary public destinations also need reconciliation with
the design's fixed services host list; do not widen the list speculatively.
- packages/tasks/src/vikunja.mjs:30,41 uses injected/default global fetch,
redirect:error, and processing cancellation/deadline.
- packages/cli/src/notifier-process.mjs:41-46 supplies global fetch to
openDirect. packages/discord/src/notify.mjs:17 builds REST only; it
explicitly does not run the gateway. packages/discord/src/rest.mjs:45
calls injected fetch. The separate Discord connector gateway uses native
WebSocket (gateway.mjs:193); do not claim the notifier needs that gateway.
- No NODE_USE_ENV_PROXY or HTTPS_PROXY setup found in host.mjs,
notifier-process.mjs or bus/process.mjs. I6 owns container startup;
I2's eventual image/process verification must demonstrate the actual path
with dummy credentials, after the transport ruling and a CLEAR slot.
Recommendation sent to Sage: re-pin the approved brief and rule on the
bounded handling/ownership of non-fetch paths before implementation. No new
dependency, host-list expansion, or replacement of DNS pinning authorized.
### Environment and evidence limits
`/run/user/1000/docker.sock` exists as a Unix socket owned by jwoltje.
Existence is not a rootless attestation. No Docker command or endpoint probe
has been run. All test execution requires Sage CLEAR; none requested yet
because there is no implementation candidate. No test, image build,
credential read, live config/data access, daemon/volume/unit change, commit
or push. Once unblocked, implement within the owned file list, add required
acceptance cases, then request a concrete CLEAR with paths and suites;
use fresh /mnt/storage/scratch/tmp/XXXXXX TMPDIR and timeout -k 10 for each
scripts/test-*.sh. Old-test changes require Sage authorization before edits.
Provider-host measurement ships with expected output for Mos; no real token
will be used here.
## sage-3907 — stop resolved; implementation draft
Sage confirms only I6 changed since the brief pin, and will re-pin row 157.
I2 must not change non-fetch security transports or add a dependency. Record
all gaps, retain each named service host and code path in the contract with
an explicit note, and build the rest. Sage owns the transport ruling before
broker rollout in later rows. This supersedes the pre-build transport hold;
the queue claim still awaits a current pin. No queue move attempted.
Additional non-fetch broker paths found:
- runtime.mjs:511 calls seat/src/working.mjs:461 publishRemote, which runs
git ls-remote/push at :479/:490 with credential helper input on fd 3.
checkout.mjs:80 gitEnv emits a fixed environment without HTTPS_PROXY.
This is Git HTTPS, not fetch; core's no-git design also prevents it from
working today. The Gitea host entry retains this caveat.
- runtime.mjs:292 wires hostGrant; host-grant.mjs:174 spawns ssh-keyscan
to read public host keys at the registered address/port. It uses fixed
PATH-only env, not fetch or HTTP CONNECT. runtime.mjs:435 wires
hostExecutor; host-run.mjs:30-45 sshArgs explicitly sets
ProxyCommand=none and ProxyJump=none, then ssh to run authorized commands
using a pinned host key and private agent. Neither routes through an env
HTTP proxy. Hosts/ports are per registry and may be SSH, so no fabricated
:443 allowlist entry or network expansion was added.
- Search-source plug-ins delegate to discord/src/web.mjs. searxng-url
sends search GETs to the operator's configured host; searxng-bundled points
at 127.0.0.1:8888. Both are non-fetch and depend on the existing DNS checks.
No fixed host can be truthfully named for arbitrary web.fetch or operator
search; no wildcard entry was added. These are documented gaps, not
claims of proxy coverage.
- Separate Discord connector gateway uses native WebSocket; retained
gateway.discord.gg:443 with a NOT PROXIED note, as Sage directs. Notifier
is REST-only; no gateway test or live connector run added.
Choices: dependency-free net proxy; exact list lookup; 8192-byte/5s total
header bounds, 10s upstream connect/120s tunnel idle bounds. JSON allowed
means the contract permitted the destination, not that upstream succeeded.
Images use exact I1 base, core has NODE_USE_ENV_PROXY=1, session globally
installs Claude 2.1.296 and package.json pi pin. Source context includes only
tracked runtime paths plus explicit new I2 runtime paths, never agent work
or host node_modules/.git. Tags derive from RELEASE. Core receives no git.
The zai host candidate api.z.ai is confirmed by the pinned dependency's
providers/zai.js:9 in canonical node_modules; real-token measurement remains
Mos's gate. OpenAI candidate is from S6 and is only measured when configured.
No live model-table config was read.
New tests cover config exit/immutability; helper rootful, null/nonzero,
substring, wrong type/JSON, exact rootless and hostile DOCKER_HOST; proxy
fragmentation/pipelined data, deny/port/malformed/slow/oversize and JSON logs.
test-install additionally builds, inspects pins and exercises an internal
network plus actual Vikunja/public GET and notifier REST/dummy GET with
proxy log evidence. The generic S5 fetch test is not repeated.
Old-suite change is prepared as test-config.patch (adds new config cases
before the existing summary). It has NOT been applied: Sage's old-test
change rule requires approval first. No old tests changed. No tests run.
## sage-3910 — first CLEAR
Authorized four serialized runs exactly as requested, plus applying the
addition-only test-config.patch. No broader suites; Sage supplies the
landing gate. Return slot on run 4 completion or an actual stop.
Additional paths retained under Sage's explicit ruling:
- scripts/install-tests/config.test.mjs: table of install schema/exit/byte
preservation cases, reused by test-config.sh and test-install.sh.
- scripts/install-tests/helper.test.mjs: fake Docker executable to assert
exact argv and rootless refusal before the requested call, without a daemon.
- scripts/install-tests/network-probe.mjs: actual internal-network tunnel,
TLS, unlisted host/port and external DNS refusal in a core image.
- scripts/install-tests/services-probe.mjs: actual Vikunja and notifier REST
client functions, public GET plus invalid dummy token, with proxy log proof.
All are new files; no old assertion changes.
## First CLEAR result and sage-3916 correction
Run 1: exit 0, config/helper 31/31, zero fail/skip/cancel,
01-install-unit.log. Run 2: exit 0, proxy 11/11, zero fail/skip/cancel,
02-egress.log. Run 3: exit 1, config selftest 55 pass / 1 fail; new install
cases 20/20, 03-config.log. Each log records UTC/load/CLEAR/fresh scratch TMPDIR.
Run 4 NOT RUN: no Docker command, probe or build. Slot returned to Sage in
bentina-0004 before any rerun.
Old red: config without console loads with unchanged output
(test-config.sh:266; log:53). Cause: this implementation unconditionally
added install:{mode:local} to resolved JSON. Sage-3916 approves the source-only
fix: omit output key when input omits it (optional-key convention), default
env export to local. Applied this fix to mosaic-config.mjs and adjusted only
the new absent-key case. No old assertion changed. No verification after fix:
Poe has the current slot, Bentina runs next only on a new CLEAR. Run order
will be 1, 3, 4; no reason to rerun unchanged proxy unit tests separately.
The approved test-config patch is addition-only: git diff --numstat reports
4 added, 0 removed lines for scripts/test-config.sh. No existing line or
assertion changed. git diff --stat before first run:
```
BUILD-LOG.md | 2 ++
README.md | 9 +++++++++
docs/SESSIONS.md | 2 ++
docs/TOOLS.md | 38 ++++++++++++++++++++++++++++++++++++++
scripts/mosaic-config.mjs | 20 +++++++++++++++++++-
scripts/test-config.sh | 4 ++++
6 files changed, 74 insertions(+), 1 deletion(-)
```
New untracked paths are not included by git diff --stat; see explicit paths
above. Claim bentina-0003 initially refused queue-lock-held; same op retried.
Sage reports re-pin rev 3808 (sage-3913).
## sage-3919 — corrected acceptance green, slot returned
Run 1 exit 0, helper/config 31/31 (04-install-unit-fixed.log); run 3 exit 0,
config selftest 56/56 plus new install 20/20 (05-config-fixed.log); run 4
exit 0 (06-install.log), helper/config 31/31 and proxy 11/11, images built
at RELEASE 0.0.12, rootless endpoint explicitly attested, image version
checks passed, internal-network TLS to listed host reached, unlisted host
and port refused, external DNS refused. Actual Vikunja public GET returned
200 and notifier REST dummy-token GET returned 401; services proxy logs
show both host decisions. No real token or broker started. Test-owned
networks/containers cleaned by suite. Slot returned bentina-0005. No broader
suites: Sage's landing gate supplies them. Claim bentina-0003 acknowledged
rev 3812 briefed -> in-progress. No queue files changed in this worktree.
Post-pass source review found one small correctness defect before handoff:
upstream errors after a successful CONNECT would append a plaintext 502 to
an established TLS tunnel. Corrected to close an established tunnel and
reserve 502 for establishment errors. Added a local reset-after-CONNECT
regression alongside the existing establishment-error test. This creates
a new verification need: request a new CLEAR for egress units and install
rebuild/probe before committing. Existing green logs above predate this fix;
do not claim them for the final proxy bytes.
## sage-3923 — final proxy and install evidence
07-egress-final.log: exit 0, 12/12, zero fail/skip/cancel, including upstream
reset after a CONNECT200 closes the tunnel without a second HTTP response.
08-install-final.log: exit 0, 31/31 config/helper and 12/12 proxy,
zero fail/skip/cancel, three images rebuilt, version checks passed,
internal-network listed TLS200/unlisted host403/unlisted port403/external
DNS refused, Vikunja public GET200 and notifier REST dummy-token GET401
through services proxy; JSON log decisions verified. Explicit endpoint
unix:///run/user/1000/docker.sock passed rootless check. Suite removed only
its named i2 containers/networks. Slot returned to Sage in bentina-0007.
No runtime source change after these final reruns. Mos-only measurement
runner was statically hardened to report a fixed message on malformed
credential JSON, avoiding JSON.parse's error snippet echoing private input.
The runner and provider script have not been executed here: Mos owns the
real-token/provider-model-table check and its output must land on #1652.
No claim of completed provider-host measurement or final host-list approval.
Acceptance accounting:
- Install validator cases and unchanged config bytes: new 20 cases green;
entire existing config suite 56/56 (05-config-fixed.log); absent output
shape preserved; env/helper default local proven by helper's absent test.
- Rootless guard: exact argv/array membership/status/type/refusal fake
cases plus real rootless check in 08-install-final.log. No ambient Docker
context used by new code. Other scripts' migration remains I6.
- Images: exact base digest in Dockerfiles, release 0.0.12 tags, core node
24.21.0/no-git/RELEASE assertion; session Claude2.1.296/pi0.85.1 assertions;
all built through the helper on the explicit rootless endpoint.
- Proxy: allow/deny/port/malformed/slow/fragmented/header-bound/upstream-error
and structured logs verified in final unit suite; actual TLS tunnel and
internal-network denials plus services client fetch paths verified by
install suite. The generic S5 fetch probe was not repeated.
- Non-fetch broker/gateway/SSH/Git/web paths remain documented gaps under
sage-3907, not modified or silently claimed to be covered. All fixed
service hosts retain entries and code-path notes. No dependency added.
- Full suites: not run here per sage-3910; Sage's landing gate supplies
broader coverage. Reviewer and Mos gates still pending.
Static source diff whitespace check passed (excluding captured .log files
and unified .patch context). Raw build logs retain Docker/npm output
including whitespace; no evidence was normalized or rewritten. Old-suite
change remains +4/-0 and every prior assertion byte-identical. No queue
file changed here. No secret read, token test, live state/unit operation,
daemon/volume/restart action, protected-branch push, or integration.
Candidate handoff: base ec86f2c84; commit only this I2 work and receipts on
bentina/row157, push through configured repository helper, then send Sage
and Boyd the tip/base/manifest for first review. Darkwing follows Boyd.
The manifest lists each changed path except its own self-reference.
## Round 2 — Boyd B1/B2 (boyd-0023; Sage sage-3933)
First candidate a14fdc443, manifest b0ed8395, CHANGES on #1652 comment31494.
Read Boyd's exact scratch harnesses and logs under /mnt/storage/scratch/tmp/IyNqMs.
B1 reproduction: normal peer control releases its socket, allowHalfOpen peer
receives408 but getConnections still reports1 after100ms (assertion1 != 0).
Root cause: refusal end sends FIN but clears the only deadline and leaves the
read side open. B2 reproduction: same-PID exec inherits exported checked_pid
and endpoint; fake rootful Docker sees ps on inherited.sock, no info; rc0 vs
expected2. Root cause: source initializes neither cache validity nor endpoint.
These findings agree with source inspection and require no design departure.
No local tests were run while Goldie holds the slot.
Added tests before fixes, adapted from the reviewer with explicit cleanup:
- packages/egress/tests/refusal.test.mjs: real loopback sockets, normal and
half-open controls for every400/403/408/431 refusal; asserts server socket
count reaches0, one denial log, expected response. Removing destruction
after flush should fail half-open cases while normal controls still pass.
- scripts/install-tests/review-cache.test.mjs: same-PID exec with inherited
exported cache, fake rootful/rootless Docker, actual loader/helper; asserts
first call is info at configured.sock, refusal or exact once-per-process
cache behavior, and no cache keys in Docker's child environment. Removing
source-time reset should fail; no real engine, config or credentials used.
No existing test file or assertion edited.
Minimal fixes: proxy uses endAndClose for all refusal responses, including
upstream establishment502: destroy after the small response flushes, plus a
fixed1s drain deadline cleared on close. Established-tunnel reset behavior
stays unchanged. Helper unsets checked_pid/endpoint/mode on every source,
clearing inherited export attributes; ordinary function calls still reuse
only an attestation obtained in the current process. No dependency, contract,
host-list or authority change. Request CLEAR for new regression red/green,
full targeted units and test-install rebuild before commit. Old-test red
still stops and returns slot. Sage's broader landing gate remains separate.
Sage-3935 approves the five-run plan including archived red-first proof;
Goldie holds the slot and Bentina is next. Still no node/docker test run.
The new refusal test also includes normal/half-open502 establishment failures
on a closed loopback port, exercising the shared termination helper on that
path (allowed log stays true for an approved destination whose upstream fails).
This stays within the named new regression file and approved commands.
## Round 2 verified — sage-3936 slot returned
Claimant move bentina-0012 acknowledged rev3825 in-review -> in-progress,
before the correction commit. Five serialized runs, each fresh short scratch
TMPDIR/UTC/load/CLEAR and timeout -k10:
| Run | Log (this directory) | Exit | Evidence |
|---|---|---|---|
| 1 old archived runtime + new tests | 09-r2-archived-red.log | 1 expected | 12 tests:5 normal-peer controls pass;7 intended failures (five half-open400/403/408/431/502 cases, two inherited-cache cases);0skip/cancel |
| 2 fixed regressions | 10-r2-regressions-green.log | 0 | 12/12,0fail/skip/cancel |
| 3 install unit suite | 11-r2-install-units.log | 0 | 33/33,0fail/skip/cancel |
| 4 egress suite | 12-r2-egress-suite.log | 0 | 22/22,0fail/skip/cancel |
| 5 install images/network/services | 13-r2-install.log | 0 | 33+22 unit tests green;three images rebuilt/version pins checked;explicit rootless guard;listed TLS200,unlisted host/port403,external DNS refused;Vikunja publicGET200/notifier REST dummyGET401;JSON decisions verified |
Run1 unpacked git archive of a14fdc443 into its own scratch directory and
overlaid only the two new tests; it did not revert or modify the builder
worktree. Failures were exact behavioral assertions, not infrastructure or
syntax errors. All test sockets cleaned in t.after; the install suite
removed only its i2-prefixed containers/networks. Slot returned to Sage in
bentina-0014. No existing test file/assertion changed for round2. No code
change after verification. Earlier config56/56 evidence remains in05 log;
loader/config unchanged for this review. No broader suites authorized here.
The round2 manifest MANIFEST-r2.sha256 covers the full candidate delta from
original base ec86f2c84, including the unchanged historical round1 manifest;
it excludes only its own self-reference (its hash is supplied in the review
message). Historical round1 manifest still describes a14fdc443 and is not
rewritten to describe later source. New manifest, candidate tip/base and
receipts go to Sage and Boyd, then Darkwing follows Boyd's approve. The
owner's automated review-request credential boundary still belongs to Sage;
no token read or identity substitution is authorized. Mos provider evidence
and Sage's broader landing gate remain required. No protected-branch push,
real-token test, live config/state/unit action or daemon/volume operation.
@@ -1,44 +0,0 @@
58e473e1783a9f64f6b1a696f747925b9a444d85dbe72e48e7a0dcbd62a2d4e5 BUILD-LOG.md
67bd811b3030ba0355af2316d02e8b2931b4b2c5a74c5be904b5311de6ef81dd README.md
6161cc0505bf0be33ba0e7c7f0f07382cca0ec2b5ace65ac54e16c4d1e3424fc agents/bentina/work/queue-157/01-install-unit.log
f13213941dab22955841108d85e62f67ede2ddbc95c25a7b351f30070b035a85 agents/bentina/work/queue-157/02-egress.log
b46c3fb5f252100f3e2de5b97bd0efc0c1fbbef26230252945f7bb309d359f5e agents/bentina/work/queue-157/03-config.log
fc76e5f9b19a31b494ebf4a128ef5f171f1f0712dc3928bebb6556815577015c agents/bentina/work/queue-157/04-install-unit-fixed.log
c9a46d969c5d28814addccd7cafe8e734d15c02a407ee80434d4e3e70a3fa4b7 agents/bentina/work/queue-157/05-config-fixed.log
1d5505a6aba0078e7cf9936fb683e06ac900af12e9e8faf224099a5e2d825fc7 agents/bentina/work/queue-157/06-install.log
c684dfdd8a460a915e15291fc0f92c9ba30f38d3d3dfa5fa190e7835fbcd448f agents/bentina/work/queue-157/07-egress-final.log
5c3efacdbcda5628882045644c32169b7020a6e49d6b89b273aba20ae042898b agents/bentina/work/queue-157/08-install-final.log
d2f7bd37b5ac8cc781fae1308a5b047e0ed0d1b63f8d5e22d56e0efa328c4883 agents/bentina/work/queue-157/09-r2-archived-red.log
4230f9fcc91d1670df0d975e9c5794edb7aa9b21b24c4f415d1594373e769da4 agents/bentina/work/queue-157/10-r2-regressions-green.log
3e5cd8d154b01ede9a9533f4ff40ac29b4e8193131fd63ba04707df51217d0bb agents/bentina/work/queue-157/11-r2-install-units.log
4d23ae4a6d3247a498bd446960c075235f632474fba9e8db374a32aaf4760219 agents/bentina/work/queue-157/12-r2-egress-suite.log
a4652f0eb6dfcc177c24c5d85d7ec08096e9f49304085f817828d7ef451acd64 agents/bentina/work/queue-157/13-r2-install.log
5b595798fa8184b15412276d070a0891fdf5cb629eabb8b7bb5f6b3d2b69b3a5 agents/bentina/work/queue-157/BUILD.md
b0ed8395108ad398f5c9c08fbac8a1b5ca02f579a5d7b709d19d3aaf0860f9ae agents/bentina/work/queue-157/MANIFEST.sha256
21f9783dcfe2cac00a49d0d7930a57134bc59ec19dd7bf2e96950f79fae5b1b1 agents/bentina/work/queue-157/PROVIDER-EXPECTED.md
6518c574291db5816057fbbc438435d0571e088c8f26d1a47ec03ff3047a5efa agents/bentina/work/queue-157/provider-hosts.sh
6dbef8d20ede98a9311ba47137b587c07cbc03e921b2a8429652fe362aeb4b85 agents/bentina/work/queue-157/provider-runner.mjs
e7cb4d0c67eadec557a25385b4ea9803970818354a9e8b0780ac4de07cf34911 agents/bentina/work/queue-157/test-config.patch
cb2b251342f0020764b7b757b22567d52380053fc15f0e1c7e665a5ec1838f24 contracts/egress.json
58107cfa274b7551ae163ea77c56d08df530e7f08e06539ddd2999c22252d749 docs/SESSIONS.md
edcaa526bf0a518716d4e87895d49920b8912295688809a25102ce6b9d6ebdc3 docs/TOOLS.md
6bd1dc7c244cfb4134d813e0b149ef1182a810eb99f01a95d548f0885b216437 images/README.md
9c6c3d5736f1ed7eb6dcdb893761b937b7108e29730daf28fe8dd0064df73740 images/core/Dockerfile
0179d84a7b9321832efb1d90c0290c8f2f6de03db3b2b6d8379672aca598c2a0 images/egress/Dockerfile
e246e3e3da1065f3c8cf24cc1f7ae15290f3e4c6508f0c6f17a4889a77f2d591 images/session/Dockerfile
3930764cf5848571507eec3e5b3eda2ebb8aa2d9642f83615b462bf5035e0e75 packages/egress/README.md
2c45bfed1f19f8a42e05552f8a10584feed8d25ccc506252d55a9a3b6c90a21c packages/egress/package.json
257058faee55a429d98955b6253d6b8a0bb058c50446764566586b06a8f76b61 packages/egress/src/cli.mjs
ce3adb0515ca7e9041f36d7a82df355a3fc97e2a96fc6811ea3b9b47e464787b packages/egress/src/proxy.mjs
5c4e778ec9ec5b93ab681d615a2240cd342d09adfe4df1634f3d6abbf16a405b packages/egress/tests/proxy.test.mjs
258e954ad7e441a7dd7d25ca9208691ea167e9cb7f34d7d689ed11005adeb4b4 packages/egress/tests/refusal.test.mjs
86e0152120b04fcfe2a4081b7686f179a139c5370027828c79572033f3cf81c3 scripts/images.sh
3647b06df02b4d9c0a74b62c774ecdff9272e68a84c06cd85861d7bf18655d5a scripts/install-tests/config.test.mjs
21e05bc3e0639427f5a9d4cf166d2991964134845272df76c712ad00ea8033b6 scripts/install-tests/helper.test.mjs
aa5328566d3e5b46fee587657dac146712735dca0aca90d3d0ad84bc20e313e1 scripts/install-tests/network-probe.mjs
45274f6745cf931f3002e6c36dcfc46a96c8f09e01e6243c18659749673752fd scripts/install-tests/review-cache.test.mjs
72ae66696ad2faaf258c832fdfdf680efc8fca7099359e58da1ee01a88541d8a scripts/install-tests/services-probe.mjs
79a8ecfc5ab279111031d5a44906450b732b517fe98511992cafd873ab7a84d3 scripts/lib/docker.sh
7398bb289d2f28ed15a2984bdeee7e30a704890a33b5e2a5c9f7554115aca8f9 scripts/mosaic-config.mjs
dcf1799238141689c9d80adc6680694a609e5fb9deef7b78d6b3f4765163e3fe scripts/test-config.sh
08be25f67d8fc6676218b1014d34a2ce36a939231f58b657972d856ee26169b2 scripts/test-install.sh
@@ -1,36 +0,0 @@
6995aa3bcdf50e2441142cc9b6fc9ec84853e353e7a408bbc877ee7e188e73c4 BUILD-LOG.md
67bd811b3030ba0355af2316d02e8b2931b4b2c5a74c5be904b5311de6ef81dd README.md
6161cc0505bf0be33ba0e7c7f0f07382cca0ec2b5ace65ac54e16c4d1e3424fc agents/bentina/work/queue-157/01-install-unit.log
f13213941dab22955841108d85e62f67ede2ddbc95c25a7b351f30070b035a85 agents/bentina/work/queue-157/02-egress.log
b46c3fb5f252100f3e2de5b97bd0efc0c1fbbef26230252945f7bb309d359f5e agents/bentina/work/queue-157/03-config.log
fc76e5f9b19a31b494ebf4a128ef5f171f1f0712dc3928bebb6556815577015c agents/bentina/work/queue-157/04-install-unit-fixed.log
c9a46d969c5d28814addccd7cafe8e734d15c02a407ee80434d4e3e70a3fa4b7 agents/bentina/work/queue-157/05-config-fixed.log
1d5505a6aba0078e7cf9936fb683e06ac900af12e9e8faf224099a5e2d825fc7 agents/bentina/work/queue-157/06-install.log
c684dfdd8a460a915e15291fc0f92c9ba30f38d3d3dfa5fa190e7835fbcd448f agents/bentina/work/queue-157/07-egress-final.log
5c3efacdbcda5628882045644c32169b7020a6e49d6b89b273aba20ae042898b agents/bentina/work/queue-157/08-install-final.log
f4755f24d626dc69b8159c6934e4213a907659dbb4d46d29d82efc0405b50152 agents/bentina/work/queue-157/BUILD.md
21f9783dcfe2cac00a49d0d7930a57134bc59ec19dd7bf2e96950f79fae5b1b1 agents/bentina/work/queue-157/PROVIDER-EXPECTED.md
6518c574291db5816057fbbc438435d0571e088c8f26d1a47ec03ff3047a5efa agents/bentina/work/queue-157/provider-hosts.sh
6dbef8d20ede98a9311ba47137b587c07cbc03e921b2a8429652fe362aeb4b85 agents/bentina/work/queue-157/provider-runner.mjs
e7cb4d0c67eadec557a25385b4ea9803970818354a9e8b0780ac4de07cf34911 agents/bentina/work/queue-157/test-config.patch
cb2b251342f0020764b7b757b22567d52380053fc15f0e1c7e665a5ec1838f24 contracts/egress.json
d6239336e48b6a78b80680c88e83ba4b67f8cbed99bf2ee3a697b1b0611e54e9 docs/SESSIONS.md
edcaa526bf0a518716d4e87895d49920b8912295688809a25102ce6b9d6ebdc3 docs/TOOLS.md
6bd1dc7c244cfb4134d813e0b149ef1182a810eb99f01a95d548f0885b216437 images/README.md
9c6c3d5736f1ed7eb6dcdb893761b937b7108e29730daf28fe8dd0064df73740 images/core/Dockerfile
0179d84a7b9321832efb1d90c0290c8f2f6de03db3b2b6d8379672aca598c2a0 images/egress/Dockerfile
e246e3e3da1065f3c8cf24cc1f7ae15290f3e4c6508f0c6f17a4889a77f2d591 images/session/Dockerfile
3930764cf5848571507eec3e5b3eda2ebb8aa2d9642f83615b462bf5035e0e75 packages/egress/README.md
2c45bfed1f19f8a42e05552f8a10584feed8d25ccc506252d55a9a3b6c90a21c packages/egress/package.json
257058faee55a429d98955b6253d6b8a0bb058c50446764566586b06a8f76b61 packages/egress/src/cli.mjs
2ec533e8a2c9ace2d2dc17ac9efe75a9930ae50e93f6700e3de8aa0475b42295 packages/egress/src/proxy.mjs
5c4e778ec9ec5b93ab681d615a2240cd342d09adfe4df1634f3d6abbf16a405b packages/egress/tests/proxy.test.mjs
86e0152120b04fcfe2a4081b7686f179a139c5370027828c79572033f3cf81c3 scripts/images.sh
3647b06df02b4d9c0a74b62c774ecdff9272e68a84c06cd85861d7bf18655d5a scripts/install-tests/config.test.mjs
21e05bc3e0639427f5a9d4cf166d2991964134845272df76c712ad00ea8033b6 scripts/install-tests/helper.test.mjs
aa5328566d3e5b46fee587657dac146712735dca0aca90d3d0ad84bc20e313e1 scripts/install-tests/network-probe.mjs
72ae66696ad2faaf258c832fdfdf680efc8fca7099359e58da1ee01a88541d8a scripts/install-tests/services-probe.mjs
e524a285dcebe2a6350e7690650c2796e54661225ce00a67b4559e519f9e3fea scripts/lib/docker.sh
7398bb289d2f28ed15a2984bdeee7e30a704890a33b5e2a5c9f7554115aca8f9 scripts/mosaic-config.mjs
dcf1799238141689c9d80adc6680694a609e5fb9deef7b78d6b3f4765163e3fe scripts/test-config.sh
08be25f67d8fc6676218b1014d34a2ce36a939231f58b657972d856ee26169b2 scripts/test-install.sh
@@ -1,37 +0,0 @@
# Mos provider-host gate (not run by Bentina)
Run only after candidate images build and Mos authorizes the real-token calls.
Use a scratch config with install.mode containerized and the rootless endpoint;
do not change the live config. Provide one JSON object on stdin with
`claudeOAuth` and `zai`, plus optional `anthropic` / `openai` keys if those
providers are in the actual model table. The script opens no host token file.
Never paste the input or raw CLI output on the issue. It prints success flags
and the proxy's host decisions only. Wrap in `timeout -k 10 600`.
From the checkout:
```
Mos-private-json-producer | MOSAIC_CONFIG=/path/to/scratch-config.json agents/bentina/work/queue-157/provider-hosts.sh
```
Expected required output (with just Claude Code and pi/zai):
```
{"case":"claude","ok":true}
{"case":"pi-zai","ok":true}
{"hosts":["api.anthropic.com:443","api.z.ai:443"],"denied":[]}
```
Optional pi/anthropic adds `{"case":"pi-anthropic","ok":true}` and no
host. Optional pi/openai adds `{"case":"pi-openai","ok":true}` and
`api.openai.com:443` to sorted hosts. All cases must return ok true, exit 0,
and denied empty. Model names are explicit candidates; Mos must reconcile
them to the actual model table without broadening a host list on failure.
A new host, refusal, failure, or different model requirement returns to Sage;
there is no wildcard or automatic discovery/update. The baked agents list
remains provisional until Mos posts matching output on #1652.
Each resource name begins i2-provider; cleanup removes only this run's two
networks and two containers. No volumes, daemon or restart operations. The
measurement uses container root as the design requires; I5 still owns the
full root filesystem re-measurement and final session limits.
@@ -1,36 +0,0 @@
#!/usr/bin/env bash
# Mos runs this explicitly with a caller-supplied scratch MOSAIC_CONFIG and
# credential JSON on stdin. This script reads no host credential file.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
cd "$REPO"
source scripts/lib/docker.sh
mosaic_docker version >/dev/null
release="$(tr -d '[:space:]' < RELEASE)"
prefix="i2-provider-$(date -u +%s)-$$"
nets=("$prefix-internal" "$prefix-egress")
cleanup(){
rc=$?; trap - EXIT
mosaic_docker container rm -f "$prefix-session" "$prefix-proxy" >/dev/null 2>&1 || true
for net in "${nets[@]}"; do mosaic_docker network rm "$net" >/dev/null 2>&1 || true; done
exit "$rc"
}
trap cleanup EXIT
mosaic_docker network create --internal "${nets[0]}" >/dev/null
mosaic_docker network create "${nets[1]}" >/dev/null
mosaic_docker create --name "$prefix-proxy" --network "${nets[1]}" --read-only --cap-drop ALL --security-opt no-new-privileges --pids-limit 64 --memory 128m --memory-swap 128m --cpus 0.5 "mosaic-egress:$release" agents >/dev/null
mosaic_docker network connect --alias proxy "${nets[0]}" "$prefix-proxy"
mosaic_docker start "$prefix-proxy" >/dev/null
ready=0
for attempt in {1..50}; do
if mosaic_docker exec "$prefix-proxy" node -e 'const s=require("node:net").connect(3128,"127.0.0.1");s.on("connect",()=>{s.destroy();process.exit(0)});s.on("error",()=>process.exit(1))'; then ready=1; break; fi
sleep 0.1
done
[ "$ready" = 1 ] || { echo proxy-not-ready >&2; exit 1; }
rc=0
# Token travels only in stdin and the CLI child's env; never an engine env or log.
mosaic_docker run --rm --init -i --name "$prefix-session" --network "${nets[0]}" --log-driver none --read-only --tmpfs /root:rw,nosuid --tmpfs /tmp:rw,noexec,nosuid --cap-drop ALL --security-opt no-new-privileges --pids-limit 512 --memory 4g --memory-swap 4g --cpus 2 \
--mount "type=bind,src=$REPO/agents/bentina/work/queue-157/provider-runner.mjs,dst=/opt/mosaic/provider-runner.mjs,readonly" \
"mosaic-session:$release" node provider-runner.mjs || rc=$?
mosaic_docker logs "$prefix-proxy" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const rows=s.trim().split("\n").filter(Boolean).map(JSON.parse);const deny=rows.filter(r=>r.host&&!r.allowed);console.log(JSON.stringify({hosts:[...new Set(rows.filter(r=>r.allowed).map(r=>r.host+":"+r.port))].sort(),denied:[...new Set(deny.map(r=>r.host+":"+r.port))].sort()}));if(deny.length)process.exitCode=1})' || rc=$?
exit "$rc"
@@ -1,17 +0,0 @@
// Mos-only real-token measurement. Reads one JSON object on stdin. Never logs CLI output.
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const exec = promisify(execFile);
let input='';for await(const chunk of process.stdin){input+=chunk;if(input.length>65536)throw new Error('measurement-input-too-large');}
let keys;try{keys=JSON.parse(input);}catch{input='';process.stderr.write('measurement-input-invalid\n');process.exit(2);}input='';
if(!keys.claudeOAuth||!keys.zai)throw new Error('measurement-needs-claudeOAuth-and-zai');
const prompt='Reply with exactly I2_PROVIDER_OK and no other text.';
const env={PATH:process.env.PATH,HOME:'/root',HTTPS_PROXY:'http://proxy:3128',HTTP_PROXY:'http://proxy:3128',NODE_USE_ENV_PROXY:'1',CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC:'1'};
const pi=['--no-session','--no-tools','--no-approve','--no-extensions','--no-skills','--no-prompt-templates','--no-themes','--no-context-files'];
const cases=[['claude','claude',['-p',prompt,'--tools',''],{CLAUDE_CODE_OAUTH_TOKEN:keys.claudeOAuth}],['pi-zai','pi',[...pi,'--provider','zai','--model','glm-5.3-flash','-p',prompt],{ZAI_API_KEY:keys.zai}]];
if(keys.anthropic)cases.push(['pi-anthropic','pi',[...pi,'--provider','anthropic','--model','claude-sonnet-5-5','-p',prompt],{ANTHROPIC_API_KEY:keys.anthropic}]);
if(keys.openai)cases.push(['pi-openai','pi',[...pi,'--provider','openai','--model','gpt-5.4','-p',prompt],{OPENAI_API_KEY:keys.openai}]);
for(const [name,bin,args,secrets] of cases){
let ok=false;try{const {stdout}=await exec(bin,args,{env:{...env,...secrets},timeout:120000,killSignal:'SIGKILL',maxBuffer:1024*1024});ok=stdout.includes('I2_PROVIDER_OK');}catch{}
console.log(JSON.stringify({case:name,ok}));if(!ok)process.exitCode=1;
}
@@ -1,12 +0,0 @@
--- a/scripts/test-config.sh
+++ b/scripts/test-config.sh
@@ -291,5 +291,9 @@
fi
echo
+# I2: each install shape asserts its exit code and config-byte preservation.
+node --test scripts/install-tests/config.test.mjs
+check "install configuration cases" $?
+
echo "selftest: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]
-8
View File
@@ -1,8 +0,0 @@
# Boyd
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 298 (the chief-of-staff grant scope, handed over from Megavolt).
No launcher in this directory. The seat runs as a T3 thread (thread 48a3ae38) and works in its own worktree. It has no
credentials of its own; it pushes only its own `boyd/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
-272
View File
@@ -1,272 +0,0 @@
# Row 298 (#1804, Q228): chief-of-staff grant scope
Owner Boyd (48a3ae38), reviewer Dewey. Branch `boyd/row298`, base `45dca69f6`.
Assignment: mos-1496/Q245. Handoff: Megavolt megavolt-0203, local `7a141269c`;
cherry-picked as `4de3cec27` (design) and `7cea739de` (UNTESTED code).
Brief: `docs/plans/2026-10-23_mvp-readiness-rows.md`, chief-of-staff grant scope.
Megavolt's original design remains in `agents/megavolt/work/queue-298/BUILD.md`.
## Authority and accepted design
Sage sage-3594, restated in sage-3612: launch.grantScope, lenient read with empty
fallback, strict human-only write. No general organization setter may write it.
Per-method agents/projects or agents/hosts/commands; host-enroll never in scope.
Host command arrays match whole tokens and exact lengths. `grants.scope.set`
from the human CLI is Approve; no Console page or chief-raised scope change.
Mos mos-1496/1502 requires the same human-shell guard as `mosaic access`.
The broker closes an in-scope request via scope and writes grant.scope.used;
missing evidence must refuse execution. Narrowing before spend fails
scope-changed. Empty scope ships; only Jason sets the live value (Q188).
No credential files read, no live grants, no deployment, no queue writes. Push
only boyd/*, additive commits, no amend/force-push or attribution trailers.
Sage owns queue, shared test slot and landing. No Node/tests before CLEAR.
## Implementation plan and ledger
Using writing-plans and executing-plans inline; the approved brief and D1-D6
are the spec. This packet is the requested plan/tracking location.
- [x] Read brief, roster, implementation and handoff; register session.
- [x] Bring Megavolt's two commits onto Boyd's branch without changing history.
- [x] Add independent CLI tests in `packages/cli/tests/grants-scope.test.mjs`:
scope read/set requests; agent-marker rejection before stdin/transport;
--tester rejection; missing revision, malformed/empty/oversize input.
- [x] Strengthen new business/bus grant-scope tests for exact commands,
scope evidence binding, host-enroll exclusion and spent scope-changed.
- [ ] On Sage CLEAR, observe targeted failures before fixes; fix inherited
implementation using the existing human proof and grant executors.
- [ ] Update bus/CLI docs, append BUILD-LOG before/after evidence, manifest.
- [ ] On Sage CLEAR run touched suites; record exact results and mutants.
- [ ] Commit UNTESTED until verified; push boyd/row298 only when slot permits;
report to Sage and Dewey, cc Mos for pushes/blockers. Dewey reviews manifest.
Pre-flight interfaces: business checkGrantScope/grantScopeOf/grantInScope feed
the instance writer and broker. Runtime supplies a fresh scope from the org
file and routes the human setter to the same organization writer. CLI callFor
supplies refuseInsideAgent and humanTransport. The grant executors consume the
broker's immutable close/evidence. Tests use scratch files/fake Gitea only.
Review focus: exact argv length, stale scope before spend, forged/missing scope
evidence, malformed fallback, and CLI agent ancestry. Tests must cover each.
Static findings before any Boyd Node/test run:
- Handoff introduced commandPrefixes beyond the accepted D2 shape, allowing
trailing args. Remove it; only exact command arrays are authorized.
- scope.used carries a digest without validating it, and no org revision.
Bind evidence to the canonical scope snapshot and grant/close sequence;
preserve historical grants when scope is later changed, recheck at spend.
- Gitea recheck throws inside the spend transaction and rolls back spend on
scope-changed. Preserve spend and log failure for that case (D6).
- The optional giteaStart?. assertion always passes when the API is absent;
use the real runGiteaGrant entry point. Fake Gitea tokens() does exist.
## Existing test changes (mos-1272)
Approved by Mos mos-1510 via Sage sage-3624; edited exactly as approved:
| File | Actual numstat | Reason |
| --- | --- | --- |
| packages/bus/tests/project-runtime.test.mjs | +1 / -1 | Vocabulary import pin adds grantScopeOf and grantInScope. |
Other test work is confined to row 298's new files. No pre-existing CLI tests
will change. Record final numstat here before review.
## Human proof and organization ownership
CLI: `packages/cli/src/cli.mjs` callFor runs refuseInsideAgent before constructing
humanTransport, as `accessCommand` does. The setter takes no --tester flag.
`packages/bus/src/human.mjs` verifyHuman checks the nonce, CLI path and every
ancestor's markers/argv; `bindHuman` and #human in broker.mjs require the
business human, via cli, outsideAgent true. Tests will pin this chain's caller
refusal. The scope receipt records by/via/outsideAgent and old/new sha256.
Org file: `packages/business/src/organization.mjs` regular() requires a regular,
non-symlink file owned by the process uid and not writable by group/other;
write() uses revision checks, locked durable receipts and atomic rename.
The OS owner uid can edit it directly; the agent sandbox does not mount the
user's config (`packages/seat/src/sandbox.mjs`). This is the existing cooperative
human/process boundary, not a hostile same-uid isolation claim.
Chief enrollment: broker instances.set calls #human and #mayHuman before
add/change; credential issue/delete use #requestCredential's human gate.
launch.by is an organization-file setting, excluded from general setters;
only that chief may auto-approve within scope. Additional chief instances
remain on the per-grant human approval path.
## Validation
UNTESTED. No Boyd Node/test run yet; waiting for Sage CLEAR.
M1: bypass in-scope check. M2: bypass invalid-value fallback. M3: remove human
setter guard. M4: drop grant.scope.used. All need named failing tests on gate.
## Session correction
The initial SESSIONS entry labels startup 19:13Z; startup was approximately
19:08Z. Message header timestamps in boyd-0001 through 0004 were estimates;
use T3's receipt timestamps as authoritative. Subsequent reports use date -u.
## Departures from Megavolt 7a141269c (Sage sage-3624)
1. Removed commandPrefixes; host scope commands match exact token arrays only.
2. grantScope callback now supplies one organization read `{value,revision}`.
grant.scope.used includes that revision, canonical scope snapshot and digest;
#approvedGrant validates the digest, shape, method, agent and close sequence.
It uses the immutable snapshot so later narrowing doesn't revoke issued grants.
3. Gitea scope-changed is caught inside the spend transaction and logs a terminal
failure without rolling back its spend. Other recheck failures keep their
existing behavior. Host already catches recheck failures after spending.
Additional handoff corrections: replaced optional nonexistent giteaStart with
runGiteaGrant; added successful host executor/revoke and enrollment-exclusion
coverage; added real runtime writer coverage and independent CLI guard tests.
Sage scheduled push at 19:41Z with MOSAIC_GITEA_CREDENTIAL_FILE unset, then T1
lane B around 19:47Z (after explicit CLEAR), four new files whole and serial,
hard stop 20:00Z. Fethry lane A is disjoint. No test was run early. Production
corrections are marked UNTESTED under that slot mandate; baseline regression
checks against the inherited implementation will run only on CLEAR.
## Candidate preparation (before T1)
The three static corrections, CLI/runtime coverage and docs are prepared but
UNTESTED. `git diff --check` is clean. The approved import-pin edit is exactly
1 addition / 1 deletion. M1-M4 are encoded in mutants.py (not run). No Node,
https git, full suite or webui build has run in Boyd's worktree.
Review package: product/test/documentation files in MANIFEST.sha256, excluding
append-only project logs and the preserved predecessor packet. The manifest
identifies exact bytes for Dewey; each later product change regenerates it and
requires a review delta. Existing tests changed: only project-runtime, 1/1.
## Dewey static review dewey-0933
N1: Added a positive control to the malformed-evidence fixture. A valid event
must pass scope authorization and reach decision-mismatch on the deliberately
absent definition binding; invalid events remain not-authorized. No spend.
N2: Additional named mutants cover spend recheck, exact argv length, Gitea spend
rollback, launch.by eligibility and accepting any evidence row (results below).
N3: At spend only grant scope membership is rechecked, not whether the raising
chief is still launch.by. This implements sage-3594 Q5; changing the launcher
later does not by itself invalidate a pending scope-approved request.
N4: The scope shortcut returns the closed decision view to the chief without
sending a separate close message; documented in the bus verb table.
N5: grant.scope is logged after the organization file commit in a separate bus
transaction. A crash between them leaves durable changes.jsonl as the receipt,
as with other organization writers. The setter does not claim cross-store
atomicity.
## T1 lane B results (sage-3642, 2026-10-23 19:23Z–19:29Z)
TMPDIR: `/mnt/storage/scratch/tmp/boyd298-4bzr6fn7`. Each whole file ran serially
as `timeout -k 10 180 node --test --test-concurrency=1 FILE`, TMPDIR set to that
scratch directory. Final restored candidate results:
| Whole file | Pass | Fail | Skip |
| --- | ---: | ---: | ---: |
| business/tests/grant-scope.test.mjs | 7 | 0 | 0 |
| bus/tests/grant-scope.test.mjs | 14 | 0 | 0 |
| bus/tests/grant-scope-runtime.test.mjs | 1 | 0 | 0 |
| cli/tests/grants-scope.test.mjs | 4 | 0 | 0 |
| bus/tests/project-runtime.test.mjs | 3 | 0 | 0 |
29/0/0 total. `final-1.log` through `final-5.log` hold the final output. The
runtime's reload-needs-restart notice is expected: the fixture's boot document
has fewer credential references than its organization document, as in the
existing instances runtime tests. No credential contents are loaded from the
real user config.
Initial run: business 7/0; bus 13/1 due to Boyd's test typo, an out-of-scope
`opts` reference in the missing-scope assertion. Corrected only that new-test
line, then bus 14/0 and the remaining files green. Dewey N1 positive control
was added and the whole bus file passed again; final rerun above followed all
mutants. No production code changed during this test window.
Mutants: all whole files, serial, with timeout -k 10 and the same TMPDIR.
`mutants.py --sage-clear sage-3642 --logs SCRATCH/review-mutants --deadline
2026-10-23T19:38:00+00:00` restores exact original source after each run.
M1/M2/M3/M4 are brief/Sage mutants; M5–M10 cover review focus and dewey-0933.
Each exited 1 with named assertion failures, no loader/syntax kill.
| Mutant | Change | Pass/fail | Named killer |
| --- | --- | --- | --- |
| M1 | skip raise scope check | 11/3 | bus out-of-scope Gitea waits; empty/bad scope waits; host argv exclusion |
| M2 | skip invalid-value fallback | 5/2 | business missing/bad scope fallback; bad file scope fallback |
| M3 | remove #human from scope verb | 13/1 | bus Q3 caller refusals |
| M4 | drop grant.scope.used write | 9/5 | bus Q4 protected evidence assertion; four executor waits also fail |
| M5 | skip scope digest check | 13/1 | bus Q4 bad evidence: decision-mismatch instead of not-authorized |
| M6 | rethrow narrowing, rolling spend back | 13/1 | bus Q5 Gitea spent assertion |
| M7 | same without length check | 6/1 | business Q2 shorter command prefix exclusion |
| M8 | no-op #scopeRecheck | 12/2 | bus Q5 Gitea and host scope-changed outcomes |
| M9 | remove launch.by check | 13/1 | bus Q6 second chief stays open |
| M10 | accept any matched scope evidence row | 13/1 | bus Q4 bad evidence, with positive control |
Mutation log sha256 (under scratch/review-mutants):
```text
55e9973af5fca0088963e2ca11fd81b2fb46dfead813a577c0e0aad2d6269b68 M1.log
abd1b784eff5ae1333a7b47085d3e199cad6d27a00e2584edf0b62417ad4c888 M2.log
1a56085e548d1e244f0f88fe12a45f77c162375880794f9ef88b241ed3a1704d M3.log
9c120f3f7128488edaf8bfd6d41bae712e69e76da4195a0d4dc0e8336b02038f M4.log
1bba88a3752823454acdaa51abde6aad66fdd5488801b4aceda02a3b42c8123d M5.log
a4ce3cacca9f7ea56b8cdfd503c76173a8e4ff1d8a7ec07032c93f0b51aa60d8 M6.log
acecd5afc93d68d96d713f8a9a2dc08351831a0b7360a13b4e30a572ad4167bf M7.log
fce7fa505ccf4f2d4190bd03c209fd19f1cf2770d3ae1246a3bd03697594019e M8.log
a830a7964890dd04a2dcc7a932a1502f1dea06419a1eae6b91a85f7878dc5681 M9.log
38ce3e7f69f6c6ffe10133397465d52538a930a830f76256a8b9280f8df78e4b M10.log
```
Full touched-package suites and every scripts/test-*.sh remain for Sage's full gate; this CLEAR authorized only the five whole files. Candidate remains
marked Untested for the full gate. No queue row has been changed by Boyd.
## Dewey final delta review (dewey-0935, 2026-10-23 19:31:55Z)
Dewey reviewed 574ece13c..bcd93dc08 with local git only, no Node: CLEAN,
nothing blocks. Verified all 13 manifest entries, exact package-file coverage,
project-runtime 1/1, M5-M10 mutation sites, all ten mutation log hashes and
counts, named assertion kills without loader/syntax errors, final five logs
29/0/0, and N1-N5 coverage. No production code delta in this review range.
Formal Gitea APPROVE on #1804 and queue record wait on the row 291 hold release
and Sage's go. Sage's touched-package and every scripts/test-*.sh gate remains
required. Boyd has released lane B and has no running test processes.
## Rebase after row 239 (sage-3794)
Current branch: `boyd/row298-r2`, rebased from published row 298 tip
`2fc27bce6bfac2debe9703782bf308132c26b393` onto
`833ec1cc8d7c11842efb82e57c1a0b256c8d22d7`, which includes row 239 through
`e0cb1db2c`. Original `boyd/row298` remains intact locally and remotely. A new
Boyd branch carries the rebased candidate so no published history is amended
or force-pushed. This section supersedes the original base/branch header for
current candidate work.
Conflicts: only the first Boyd log append in `BUILD-LOG.md` and
`docs/SESSIONS.md` conflicted while replaying 574ece13c. For each, verified the
old commit's log started with the merge base bytes, then retained the new
base's complete log bytes and appended only Boyd's suffix (434 and 281 bytes
respectively). Later Boyd appends replayed automatically. No prior entries
were removed or rewritten. Five commits replayed; the third/fourth range-diff
entries differ only in append-log context; package-only range-diff is all `=`.
The overlapping product files merged automatically: broker.mjs, runtime.mjs
and instance-settings.mjs had no textual conflicts. Inspected their diff
against the new base: only the existing row 298 scope additions remain.
Preserved row 239's injected writer, fire/delete beforeWrite callback, and
commit() passing beforeWrite into writer.write. There is no pre-lock revoke
call in commit(). `packages/business/src/organization.mjs` is byte-identical
to the new base: beforeWrite still runs inside withLock, after revision and
validation checks; post-revoke errors still carry revoked true. Broker's
row-239 error handling and runtime's lock/revoke wiring remain intact.
Static checks only: package patch range-diff all equal, organization writer
unchanged, diff whitespace clean, refreshed 13-file package manifest verifies.
The only pre-existing row-298 test change remains project-runtime.test.mjs
exactly 1/1 under mos-1510. No test files were changed for this rebase.
UNTESTED on the new base: no Node, tests, mutants, builds or queue writes run.
Earlier 29/0 and M1-M10 results apply to the old base only. Await Sage CLEAR
for whole touched-file reruns; Dewey reviews the new candidate.
Row 321 (#1817) review assignment acknowledged; waiting for Sage's request
and Gladstone's pushed docs-only F7/F10 run-sheet candidate.
@@ -1,13 +0,0 @@
612488b705853e75e6f8952e41552526f4d07dd297d7619d608d434ed75121d6 packages/bus/README.md
63fdf6c048f0994af3551510119eeb294c71f8116702772844214930b827cc56 packages/bus/src/broker.mjs
65425ead353017c99e04f1e0bcc3f85c4ba60054da293badaa0360e681e418dc packages/bus/src/runtime.mjs
e7065db949cacfd4679a1031eebf5cde09ed196ce85612e8400ae563f02a0f18 packages/bus/tests/grant-scope-runtime.test.mjs
4030cf584481a1db12d9e6f744015c48fdebd468335d065ead3e90180858d366 packages/bus/tests/grant-scope.test.mjs
bc432d8d430433b9d84c00883584dc53d4b27afb5e93f739fe3ca28e80f01efa packages/bus/tests/project-runtime.test.mjs
84d6dd15d5ca2926d8f7a34548c70c3e7a3c58e692f0b009414f1af35cb096cc packages/business/src/business.mjs
e3b425f9ad2a0d4e6e0b55bd48827e144015662d95ace4163fe6474e5f98da81 packages/business/src/instance-settings.mjs
fed4177325f65a2b9823a40a9c35c01d60c0520e3ff40b4574c3cf2c29cc096b packages/business/src/vocabulary.mjs
b7f96e45b9ffca2a7ebc18dc446f0adcaac72d3900f098c60157cf6cbf1bfc13 packages/business/tests/grant-scope.test.mjs
60fc267b7f8f9e83eeb0d0207c8abdef68d554f32b08d508e98dbf59b442a366 packages/cli/README.md
b586907ca8f351f60dc19ec2683df75d2d70ddb12955d09fb14560c9d3bed3dc packages/cli/src/grants.mjs
fb7c30b31facd887b03ff02dd6734d48a815978481b33bb21ec48604a51c5ff5 packages/cli/tests/grants-scope.test.mjs
-105
View File
@@ -1,105 +0,0 @@
#!/usr/bin/env python3
"""Row 298 mutants. Run only with Sage's explicit CLEAR in Boyd's slot.
Each mutation is applied to this worktree, runs one whole new test file,
then restores the exact original bytes, including after an interruption.
Logs go to a caller-supplied scratch directory, never the repository root.
"""
import argparse
import datetime
import pathlib
import subprocess
ROOT = pathlib.Path(__file__).resolve().parents[4]
BROKER = ROOT / 'packages/bus/src/broker.mjs'
VOCABULARY = ROOT / 'packages/business/src/vocabulary.mjs'
BUS_TEST = 'packages/bus/tests/grant-scope.test.mjs'
BUSINESS_TEST = 'packages/business/tests/grant-scope.test.mjs'
def replace_once(source, old, new):
if source.count(old) != 1:
raise RuntimeError(f'expected exactly one mutant site, found {source.count(old)}')
return source.replace(old, new, 1)
def mutate(name, source):
if name == 'M1':
return replace_once(source,
'if (!raise?.grant || !grantInScope(scope, raise.grant)) return view;',
'if (!raise?.grant) return view;')
if name == 'M2':
return replace_once(source,
'return scope === null ? { scope: GRANT_SCOPE_EMPTY, fallback: true } : { scope, fallback: false };',
'return { scope: scope ?? value, fallback: false };')
if name == 'M3':
return replace_once(source,
' #grantScopeVerb(s, verb, a) {\n this.#human(s);',
' #grantScopeVerb(s, verb, a) {')
if name == 'M4':
old = """ this.#grantEvent(s.business, {
action: 'grant.scope.used', grantDecision: d.id, decisionSeq: seq, method: raise.grant.method,
agent: raise.grant.agent, scope, revision, scopeSha256: sha256(JSON.stringify(scope)),
}, d.id);"""
return replace_once(source, old, ' // MUTANT: scope evidence omitted.')
if name == 'M5':
return replace_once(source,
"evidence.agent === grant.agent && evidence.scopeSha256 === sha256(JSON.stringify(checked.scope)) &&",
"evidence.agent === grant.agent &&")
if name == 'M6':
start = source.index(" checked = this.#store.transaction(() => {\n const consumed = this.#consumeGrant")
end = source.index('\n } catch (e) {', start)
return source[:start] + " checked = this.#store.transaction(() => this.#giteaRecheck(business, decision, this.#consumeGrant(business, decision, 'gitea')));" + source[end:]
if name == 'M7':
return replace_once(source,
'const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);',
'const same = (a, b) => a.every((x, i) => x === b[i]);')
if name == 'M8':
return replace_once(source,
" if (this.#closed(decision)?.via === 'scope' && !grantInScope(this.#scopeNow(business).scope, g)) fail('scope-changed');",
" // MUTANT: no spend-time scope recheck.")
if name == 'M9':
return replace_once(source,
' if (s.role !== this.#business(s.business).launch?.by) return view;',
' // MUTANT: any chief instance can use scope.')
if name == 'M10':
start = source.index(' const evidence = JSON.parse(row.body);', source.index(' #scopeUsed('))
end = source.index('\n }', start)
return source[:start] + ' return true;' + source[end:]
raise ValueError(name)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--sage-clear', required=True, help='CLEAR message key; required before any Node run')
parser.add_argument('--logs', type=pathlib.Path, required=True)
parser.add_argument('--mutant', choices=['M1', 'M2', 'M3', 'M4', 'M5', 'M6', 'M7', 'M8', 'M9', 'M10', 'all'], default='all')
parser.add_argument('--deadline', default='2026-10-23T19:38:00+00:00')
args = parser.parse_args()
deadline = datetime.datetime.fromisoformat(args.deadline)
args.logs.mkdir(parents=True, exist_ok=True)
names = ['M1', 'M2', 'M3', 'M4', 'M5', 'M6', 'M7', 'M8', 'M9', 'M10'] if args.mutant == 'all' else [args.mutant]
for name in names:
remaining = (deadline - datetime.datetime.now(datetime.timezone.utc)).total_seconds()
if remaining <= 15:
raise RuntimeError('Sage slot deadline reached; no new test run')
path, test = (VOCABULARY, BUSINESS_TEST) if name in ['M2', 'M7'] else (BROKER, BUS_TEST)
original = path.read_bytes()
mutated = mutate(name, original.decode())
log = args.logs / f'{name}.log'
try:
path.write_text(mutated)
with log.open('w') as output:
result = subprocess.run(['timeout', '-k', '10', str(int(min(180, remaining - 13))), 'node', '--test', '--test-concurrency=1', test],
cwd=ROOT, stdout=output, stderr=subprocess.STDOUT,
timeout=min(195, remaining - 1), check=False)
print(f'{name}: exit={result.returncode} log={log}', flush=True)
finally:
path.write_bytes(original)
# Exit 1 needs human inspection of the failing test names; errors are not kills.
if result.returncode == 0:
raise RuntimeError(f'{name} survived')
if __name__ == '__main__':
main()
-8
View File
@@ -1,8 +0,0 @@
# Bubba
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 288 (agent write notices and library revert, handed over from Honker).
No launcher in this directory. The seat runs as a T3 thread (thread 3d6672b5) and works in its own worktree. It has no
credentials of its own; it pushes only its own `bubba/*` branches through the repository credential helper (mos-0981),
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
-8
View File
@@ -1,8 +0,0 @@
# Bushroot
Builder seat, Claude Haiku 5.5, started by Sage under mos-1008 item 2 (a low-level model under Q177) for the row
that retires the control board writer on 7331 (#1696).
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `bushroot/*`
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
-38
View File
@@ -1,38 +0,0 @@
===== DARKWING NATIVE DEVELOPMENT CONTEXT =====
Your identity is Darkwing. This launch runs Pi directly on the host, in the
Mosaic Stack development repository. The injected SOUL defines your persona;
CONSTITUTION and STANDARDS supply governance, USER supplies user context,
and AGENTS.md supplies repository instructions.
You have host read, bash, edit, write, grep, find, and ls tools. This is a
development TUI with the operator's OS access, not a sandbox or a registered
managed fleet seat. Use repository scripts for Mosaic operations and inspect
their effects before running them. Container paths in skills describe worker
deployments, not your current workspace. A tool's presence is not authority
to change unrelated files, other agents' work, or the live fleet.
For an assigned improvement, inspect the implementation, reproduce the issue,
make the smallest useful change, verify it, and continue through the authorized
outcome. Run scripts/mosaic queue next darkwing for ownership, gates and your next piece;
a new user assignment does not silently resume unrelated queued work.
The local /goal extension is loaded and owns any operator-set goal lifecycle.
Use ms-proactive-agent for work selection and ms-goal for recovery guidance;
do not create a competing goal loop. Follow goal_report's actual schema and
reporting instructions. Its text format is Just Completed / Next Step /
Blocked, with '* none' for empty sections. No external reporting skill is
needed to discover that format. Native development packaging supersedes
older skill statements that this extension is unavailable.
For relocation recovery, read agents/darkwing/work/RESTART.md after the root
AGENTS.md and docs/plans/CURRENT.md. It records verified checkpoints and limits,
not a new assignment. The canonical checkout is /mnt/storage/src/mosaic-stack;
v1/ is archived legacy source. Reconcile newer owner direction before acting.
Conversation history persists across launcher restarts. Goals belong to a
single process incarnation; recover the assignment from verified records and
the operator's direction after a restart. No goal is started by this launcher.
Context is captured anew at launch; source edits do not update this process's
injected snapshot. Relaunch to load approved context changes.
-64
View File
@@ -1,64 +0,0 @@
# Darkwing development TUI
From any terminal, run:
```sh
/home/jwoltje/src/mosaic-stack-dev-test/agents/darkwing/launch.sh
```
The agent launcher is a thin shim to `scripts/agent.sh --host-dev darkwing`,
forwarding all arguments unchanged. `scripts/agent.sh` is the common entry
point; `scripts/agent-host-dev.sh` implements its native development mode.
The host launcher opens the repository as Darkwing's workspace.
It uses the repository-pinned Pi, the configured Mosaic provider/model, and
native Pi authentication (normal `~/.pi/agent`, or `PI_CODING_AGENT_DIR` if
explicitly set). It never copies credentials. Install dependencies with
`npm ci --ignore-scripts --no-audit --no-fund` if needed.
`--check` validates configuration and required inputs without opening Pi or
calling a model. `--fresh` starts a new conversation without deleting earlier
ones. Normal launches continue the latest conversation under
`.pi/state/darkwing/sessions/`; the first launch creates one. A launcher lock
rejects simultaneous launches through this script. It does not exclude Pi
processes started another way. Damaged JSONL history refuses automatic resume;
`--fresh` is an explicit escape hatch that preserves the damaged evidence.
The current files are combined into a private launch snapshot under
`.pi/state/darkwing/launches/`:
- `contracts/CONSTITUTION.md` and `contracts/STANDARDS.md`
- `agents/darkwing/SOUL.md`
- `<configured dataRoot>/user/USER.md`, the deployment's live user profile
- the repository's `AGENTS.md` and Darkwing's `CONTEXT.md`
Use `--soul FILE`, `--constitution FILE`, or `--user FILE` to select alternate
inputs, including a future `contracts/USER.md`. Relative paths resolve from
the repository root. Missing or empty inputs refuse launch. Snapshots can
contain personal context and remain local, with private file permissions.
Context edits take effect on relaunch, including when resuming a conversation.
The launcher enables coding/search tools, `goal_report`, ten explicit local
skills, and the canonical goal extension through `scripts/sync-dev-extensions.sh`.
Ambient context, skills, extensions, templates, and themes are disabled.
The normal Pi coding prompt is retained with the Mosaic context appended.
Enter `/goal <assignment and acceptance criteria>` to start continuing work;
`/goal stop`, `/goal resume`, and `/goal` pause, resume, and inspect it. A new
process does not automatically adopt a previous process's goal.
This TUI has the operator's host access, including repository edits and host
commands. Its tool list is not OS isolation. It creates no managed role or
fleet registration. Worker dispatch still uses the governed Mosaic task runner.
The user supplies the assignment; launch alone does not start self-modification.
## Deployment findings
The existing `scripts/agent.sh` launches a Docker container, defaults to the
`agent-<name>` session directory, and asks Pi to continue when that directory
is nonempty. Its default workspace is `<dataRoot>/workspaces/<name>`, not this
checkout. `src/load-contracts.sh` loads image-baked governance, an optional
seat SOUL override, live user Markdown, and mission context into a shared
prompt path. A seat override requires `agent.json`; a standalone SOUL is not
discovered. `adapters/pi/adapter.sh` disables extensions. The temporary host
launcher follows the existing native development path to provide repository
access and `/goal`, and keeps its conversations separate from container and
live fleet sessions. It does not invoke release alignment on startup.
-30
View File
@@ -1,30 +0,0 @@
# SOUL — Darkwing
You are Darkwing, Mosaic Stack's hands-on engineering collaborator, working
with Sage as project lead. Your job is to help Jason make the system
dependable by using it, finding where it falls short, and carrying authorized
improvements through verification.
Be curious, direct, and resourceful. Have a technical opinion and explain
the evidence behind it. Investigate before guessing. Distinguish a design
claim, a passing test, and behavior you have observed in the running system.
Use Mosaic's own tools and workflows where they fit. Turn a failure into a
reproducible case, make a focused correction, and test the behavior again.
Let each verified improvement inform the next one within the assignment.
Keep the human informed when the result, scope, or next decision changes.
Own the outcome while respecting other agents' work. Preserve their changes
and records, give delegated work clear boundaries, and seek independent
review where required. Self-improvement never grants new authority: changing
your instructions, permissions, or a live deployment follows the same review
and authorization rules as any other system change.
Sage leads the development team (Jason's ruling, 2026-09-26) and translates
Jason's priorities into scoped work, coordinates ownership and dependencies,
reviews results, and verifies integration. You are a collaborating seat.
Dewey owns frontend design and UX. Rocko and Filbert support general project needs,
including implementation, investigation, testing, and review. Reconcile
concurrent edits with Sage before integration. Keep Jason informed of
outcomes and decisions that require his input. Your role does not expand the
project's existing authorization or release rules.
-10
View File
@@ -1,10 +0,0 @@
#!/usr/bin/env bash
# Darkwing's native development mode through the Mosaic agent entry point.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# Register this seat with the control board (packages/seat) unless already
# registered by `mosaic launch` or only running the checks.
if [ -z "${MOSAIC_LAUNCH_REGISTERED:-}" ] && ! printf '%s\n' "$@" | grep -qx -- '--check'; then
exec "$REPO/scripts/mosaic" launch --repo "$REPO" --harness pi darkwing -- "$@"
fi
exec "$REPO/scripts/agent.sh" --host-dev darkwing "$@"
-21
View File
@@ -1,21 +0,0 @@
// Refuse damaged history before Pi's --continue can silently skip it.
import { readFileSync, lstatSync } from 'node:fs';
try {
for (const file of process.argv.slice(2)) {
if (!lstatSync(file).isFile()) throw new Error(`not a regular session file: ${file}`);
const lines = readFileSync(file, 'utf8').trim().split('\n');
const entries = lines.map((line) => JSON.parse(line));
const header = entries[0];
if (header?.type !== 'session' || typeof header.id !== 'string' || !header.id ||
typeof header.version !== 'number' || typeof header.cwd !== 'string' ||
!Number.isFinite(Date.parse(header.timestamp)) ||
entries.slice(1).some((entry) => !entry || typeof entry.type !== 'string')) {
throw new Error(`invalid session structure: ${file}`);
}
if (header.cwd !== process.cwd()) throw new Error(`session belongs to another workspace: ${file}`);
}
} catch (error) {
console.error(`darkwing: cannot safely resume: ${error.message}; inspect history or explicitly use --fresh`);
process.exit(1);
}
-108
View File
@@ -1,108 +0,0 @@
# Darkwing — relocation handoff
Recorded 2026-09-07 17:43 UTC. Jason intends to relaunch with
`/mnt/storage/src/mosaic-stack/agents/darkwing/launch.sh`.
This is a recovery note, not a new assignment or automatic goal resumption.
## Read first
1. Root `AGENTS.md` and `docs/plans/CURRENT.md`.
2. This note, then `git status --short` and `git log --oneline -5`.
3. Reconcile current owner direction and any newer declared artifacts before acting.
## Repository conversion is completed locally
Jason explicitly ordered the conversion and confirmed no work was active.
- Canonical checkout: `/mnt/storage/src/mosaic-stack`.
- Origin: `https://git.mosaicstack.dev/mosaicstack/stack`.
- Branch: `refactor`.
- Conversion commit: `127a54fdff1fe6ae56c3197edddf957481465db4`.
- New foundation is at root. `v1/` is legacy archival source, NOT current code.
- Old `/home/jwoltje/src/mosaic-stack-dev-test` is a compatibility symlink to this
same checkout. Do not recreate a second working copy there.
- Both histories retained: merge parents v2 `9a5fbdbda74b16adf488fe28138b2ba69ea5e669`
and v1 `5d2770002612a09ae0cadc129b4ea30619133e8a`.
- Exact 3,507-file v1 tracked tree imported; v1 refs under `refs/archive/v1/`.
- Original v2 refs retained; `stack-v2-archive` remote has a disabled push URL.
- Only legacy tracked tree and four conversion docs committed. All earlier
uncommitted/untracked/ignored work preserved. Index was verified clean.
- Issue https://git.mosaicstack.dev/mosaicstack/stack/issues/1495 closed explicitly
for local conversion. No push, PR/trunk merge or live-service change occurred.
Record: `docs/plans/2026-09-07_repository-consolidation-completed.md`.
Receipts: `docs/plans/reviews/2026-09-07_repository-conversion-verification.json`
and `2026-09-07_repository-conversion-postcommit-verification.json`.
Verified rollback copies, NOT development roots:
- `/mnt/storage/src/.mosaic-stack-conversion-20260907T172430Z/`
- `/home/jwoltje/src/.mosaic-stack-dev-test.pre-conversion-20260907T172430Z`
Do not delete them, launch from them or restore over newer work.
## Current unfinished foundation gate
Jason's A9 acceptance of the first offline synthetic scope/permission inspector
is pending. Code is independently approved by Filbert; no blocking code finding
remains at the reviewed r6 candidate. Owner acceptance is not inferred from tests.
- Manifest: `docs/plans/reviews/2026-09-07_foundation-inspector-rocko-build-manifest-r6.json`
SHA-256 `a4a4493000aff5905337a643886ca36e7c5377d52deed77b8aeab7174ca73dcf`.
- Report: `docs/plans/reviews/2026-09-07_foundation-inspector-rocko-build-r6.md`
SHA-256 `ee0e83efd7c71eddecf5e26f939e9a34ba85b184cfcd1cffac9ff9e56ea13c37`.
- APPROVED verdict: `docs/plans/reviews/2026-09-07_foundation-inspector-code-verdict-r6.md`
SHA-256 `ab9dd5e5c3cad5c9263e873ff82cac444da2d36040e907e4798b208fa1c08b13`.
- Guide: `docs/plans/reviews/2026-09-07_foundation-inspector-demo.md`.
All 382 approved inspector files and pinned inputs survived conversion unchanged.
Actual offline checks: Node 80/0, selftests 43/0, oracle 1,568 records / zero
schema disagreements, foundation checker PASS, config/auth/conductor 24/15/17.
Postcommit conductor 17/0 and four CLI demos passed: allowed read, allowed change
PREVIEW (no mutation), missing-registration refusal, unresolved reassignment with
original selection retained. Demo inputs are separate synthetic scenarios.
`test-task.sh` and `test-release.sh` remain NOT RUN / DEFERRED under Jason's bounded
offline-demo ruling. No deployment/native/live/provider/security certification.
Reviewer qualifications: ordering equality means structural equality, not byte
identity; auxiliary native-parser warm-run anomalies remain separate unresolved
observations, not a passing universal parser-equivalence claim. Preserve all earlier
NOT APPROVED reviews and the historical correction that r3 ran unauthorized live
branches; later deferral did not retroactively authorize them.
## Ownership and limits
- Rocko authored inspector code; Filbert independently reviewed; Darkwing coordinates
and verifies. Keep the approved candidate frozen unless a new fix is authorized.
- No automatic permission to push, merge to next/main, deploy, change live config,
grant permissions, access credentials, investigate ~/.mosaic, or start new runtime
work. Local conversion authority is not authority for those activities.
- Preserve unrelated pending work. In particular `scripts/agent.sh`, `docs/TOOLS.md`,
host launcher/context files and other untracked concepts/skills belong to existing
work. Do not blanket-stage/reset/clean. Root logs and CURRENT remain uncommitted.
- Foundation #53 in the old stack-v2 project remains a separate open issue; do not
silently close or renumber it. Accepted historical SHA/path citations remain valid.
- Rocko's Archify C1 remains HELD for owner T2/T3 decisions. No lane reassignment.
- Future durability/workflow/evidence/federation/onboarding topics are notes, not
authorization to expand the inspector.
## Communications
Use only `tools/tmux/agent-send.sh`; sender `dragon-lin:darkwing`.
Rocko: `-L mosaic-fleet -s '=rocko'`; Filbert/Dewey:
`-L default -s '=filbert'` / `'=dewey'`.
Conversion notice delivered to Rocko. Filbert/Dewey sends were unconfirmed
(input boxes not locatable); no retries, no acknowledgement claimed. Check declared
artifact paths as well as direct messages; completed reviews have existed without
transported replies. Do not inspect private panes or blindly resend.
## Relaunch and goal recovery
The project launcher continues its own latest `.pi/state/darkwing/sessions/`
conversation by default. Do NOT assume this pre-launch conversation is already in
that store or that the next launch resumes this exact conversation. This handoff
is the durable bridge. No session-tree migration or launch was performed here.
The goal extension owns lifecycle. The earlier extension goal had been paused;
no restart automatically resumes it. Reconcile the actual new process state and
Jason's direction rather than reporting progress against a guessed old goal or
creating a second goal loop. Launch alone grants no new assignment.
This handoff and its CONTEXT pointer are documentation-only. Launcher scripts,
private sessions, credentials and runtime configuration were not modified.
@@ -1,17 +0,0 @@
{
"issue": 1503,
"candidate": "/tmp/board-attention-r1-q_924ksh",
"files": [
"AGENTS.md",
"packages/control-board/src/scan.mjs",
"packages/control-board/README.md",
"packages/control-board/tests/scan.test.mjs",
"packages/control-board/tests/serve.test.mjs",
"packages/control-board/tests/attention.test.mjs",
"packages/control-board/tests/attention-flow.test.mjs",
"packages/webui/tests/fixture.mjs",
"docs/plans/2026-09-13_board-attention-status.md"
],
"manifestSha256": "e40b58ecb6844d407ba776dcde8f19ad21c0b78076ca1f9b3d96b0bb1c852405",
"testsPassed": 144
}
@@ -1,14 +0,0 @@
{
"at": "2026-09-14T00:19:32.409806+00:00",
"backendPid": 3204655,
"health": "ok",
"researcher": {
"agent": "researcher",
"project": "mosaic-stack",
"alive": true,
"state": "idle",
"waitingOnYou": false,
"lastActivity": "2026-09-13T21:19:57.102Z"
},
"fiveAgentProcessesUnchanged": true
}
@@ -1,46 +0,0 @@
{
"at": "2026-09-14T00:19:11.579013+00:00",
"ownerAuthorized": true,
"oldPid": 1265952,
"newPid": 3204655,
"command": [
"/usr/bin/node",
"packages/control-board/src/cli.mjs",
"serve"
],
"cwd": "/mnt/storage/src/mosaic-stack",
"log": "/tmp/board-attention-backend-ag9xvks2.log",
"agentProcessesBefore": {
"default/darkwing": [
[
"2733924",
"12863634"
]
],
"default/dewey": [
[
"934346",
"466065"
]
],
"default/filbert": [
[
"72183",
"100870"
]
],
"default/researcher": [
[
"173699",
"66404285"
]
],
"mosaic-fleet/rocko": [
[
"90599",
"128275"
]
]
},
"gracefulExitObserved": true
}
@@ -1,54 +0,0 @@
# CHAT-02 board routes: Darkwing's review (#1507)
Reviewer: Darkwing, 2026-09-26, per Sage's D3. Requested by Dewey. Scope: the
two read-only routes only. Filbert reviews `packages/conversation` in full.
Candidate, base 34777c56, uncommitted. I verified both hashes:
- `packages/control-board/src/serve.mjs` afc95bdb…c540d
- `packages/control-board/tests/serve.test.mjs` e60aa14b…ecbc
**Verdict: approve**, with one commit condition and two nonblocking notes.
## What I checked
- Order. `foreignRequest` runs first on every request, then the POST routes,
then the GET/HEAD check (405 otherwise), then these routes. A POST to
either path is 405, and a foreign Host or Origin is 403 before any read.
- Query validation. `/api/conversations` refuses any parameter.
`/api/conversation` accepts only `id`, `branch` and `cursor`, one value
each, each matching `QUERY_VALUE`. That is the same pattern as
`parts.mjs` `ID`, so every id the reader issues (`safeId`, `root`, `c-`
cursors, `pi-` conversations) passes. No path comes from the request.
- Responses. JSON with `no-store` and `nosniff`, no CORS headers. A thrown
error gives a fixed 500 body and logs to stderr only.
- Refusal bodies. Every `Refusal` message in `packages/conversation/src` is
a fixed string. The one interpolated message (`denied`, safe-fs.mjs:34)
interpolates only "session root" or "session file". No path or content
reaches the client through `error`.
- Status map. It covers every code the route can reach. `unknown-actor` and
`unsupported-purpose` are absent, and the route can't produce them because
it always passes the default actor and purpose.
- Tests: `serve.test.mjs` plus `packages/conversation/tests/`, 61/61 on the
pinned files.
## Commit condition
`serve.mjs` imports `../../conversation/src/reader.mjs` at module load, and
`packages/conversation/` is untracked. Committing the routes without that
package breaks the board's start, not only these routes. The package must
land in the same commit or an earlier one, after Filbert's review.
## Notes (nonblocking)
1. **A cursor needs its branch.** The header comment says `branch` and
`cursor` are optional. But `next()` compares `branch !== record.branch`,
and every cursor record carries a string branch (`safeId` or `root`). So
`?id=X&cursor=C` without `branch` is always 409 `cursor-foreign`, with
`reconcile: true`. That is safe, but a client that follows `nextCursor`
alone gets a refusal that reads like a stale view. The test passes the
page's branch, so it doesn't show this. Either say in the comment that a
cursor call must repeat `page.branch`, or answer 400 "cursor requires
branch". I'd take the comment now and let CHAT-03's client decide.
2. **New codes fall to 422.** A code the reader adds later maps to 422
without a test failing. A test that runs the reader's refusal codes
through `REFUSAL_STATUS` would catch that. That's optional.
@@ -1,69 +0,0 @@
# CHAT-02 board routes, revision 2: Darkwing's review (#1507)
Reviewer: Darkwing, 2026-09-26, at Sage's request. Scope: the route delta
since my R1 approval (`chat-02-routes-review-2026-09-26.md`, 07b10ad1). Filbert
reviewed the backend (packet `agents/dewey/work/chat-02/BACKEND.md`, 0cf177b1).
Candidate, base 34777c56, uncommitted. I verified both hashes:
- `packages/control-board/src/serve.mjs` d62720dc…a2f3
- `packages/control-board/tests/serve.test.mjs` d38aa2b2…3f4a
**Verdict: approve.** The R1 commit condition still holds, and I have two
new nonblocking notes.
## What I checked
I kept no copy of the R1 files, so I read the whole route change against the
base (`git diff 34777c56 -- packages/control-board`) instead of only the
delta. It covers every item the packet's §0 lists and nothing else in the
route path.
- **Cursor needs its branch.** This was my R1 note 1. `conversationQuery` now
answers 400 "a cursor call repeats the page's branch" when `cursor` comes
without `branch`. The check runs after the per-key validation, so a
malformed value still gets its own 400 first. The header comment says the
same. A test covers it, and removing the line fails it.
- **Status map.** My R1 note 2. `REFUSAL_STATUS` is exported and now has 16
entries. I listed every `new Refusal("<code>"` in
`packages/conversation/src` myself and got 15 codes plus
`unsupported-harness`, which reader.mjs:352 raises by value. That matches the
map exactly. `parts.mjs` raises none. `unavailable`, which safe-fs.mjs:58
raises when a session root doesn't exist, is 404. That fits the rest of the
map, where not-found is 404. `unknown-actor` 403 and `unsupported-purpose` 422
are explicit now.
- **Order and guards** are unchanged from R1. The foreign Host or Origin check
comes first, then the POST routes, then 405, then these routes. No path comes
from the request, and responses carry `no-store` and `nosniff` with no CORS
headers.
- **Tests.** `serve.test.mjs` plus `packages/conversation/tests/` pass
67/67 on the pinned files.
- **Mutations** on a scratch clone of HEAD with the conversation package and
the two pinned files:
| Mutation | Result |
|---|---|
| cursor-without-branch check removed | 1 fails |
| `unknown-actor` entry dropped | 1 fails (the scan test) |
| `nosniff` removed | 1 fails |
| repeated-parameter check removed | 1 fails |
| catalogue parameter check removed | 1 fails |
| `unavailable` changed from 404 to 422 | nothing fails |
The last row is note 1 below.
## Commit condition (unchanged)
`serve.mjs` imports `../../conversation/src/reader.mjs` at module load, and
`packages/conversation/` is still untracked. The package must land in the
same commit as the routes or an earlier one. Otherwise the board fails to
start.
## Notes (nonblocking)
1. **The scan test checks keys, not values.** It proves every code has an
entry. No test proves `unavailable` is 404. If someone edits that value, or
any status no route test exercises, nothing fails. A table test that
asserts the whole `REFUSAL_STATUS` object would pin them. That's optional.
2. **The scan reads a fixed list of three files.** If a refusal is added to
`parts.mjs` or a new file, the scan won't see it, and that code falls to 422.
Reading every `.mjs` in `packages/conversation/src` would close the gap.
@@ -1,160 +0,0 @@
# Queue row 5, CHAT-03 increment 1, round 1 review (#1508)
Darkwing, 2026-10-04. Request: #1508 comment 26671. My part is the binding
and the extension-load refusal (lead decisions 31 and 36). Brief:
`agents/dewey/work/chat-03/BRIEF.md` at 1ef15ac0. Candidate:
`agents/dewey/work/chat-03/I1-manifest.sha256`, digest
`1404341eaeaf7d1e684c9f27e76718061ed08c25a52da5f190425feb1274ba69`,
27 files, uncommitted.
Verdict: changes requested. Two blocking findings, B1 and B2.
## Checks
- The manifest hashes to 1404341e. All 27 files match it in the canonical
working tree.
- Export: `git archive` of 1c724958 plus the 27 candidate files in
`/tmp/r5-exp`, manifest OK there too. `node --test
packages/conversation/tests/` passes 141/141, 0 skipped.
`node --test packages/control-board/tests/` passes 124/124 (the board
reads `ControlRefusal` codes). The CHAT-00, CHAT-01 and CHAT-01c checks
still pass.
## B1 (blocking). The seal is a deny-list over an argv the caller builds
`checkSeal` (pi-pin.mjs 59–66) refuses `-e`/`--extension`, a missing seal
flag, or a first pair other than `--mode rpc`. Everything else in
`engine.extraArgs` passes, and `buildPiArgs` appends extraArgs after the
controller's own `--session`. Pi's parser keeps the last `--mode` and the
last `--session`. The controller is a public export (`./controller` in
package.json), so this is reachable without touching the source.
(a) `extraArgs: ["--session", <file outside the fixture root>]`. The guard
refuses that same path when it is passed as `sessionFile`, but it never
sees extraArgs. With the real pinned Pi under a scratch HOME and agent dir
(`/tmp/r5/seal-escape.mjs session`):
```
guard on sessionFile: live-session-refused
constructed with extraArgs ["--session",".../outside/proj/.pi/state/other/sessions/s1.jsonl"]
-> piArgs ["--mode","rpc","--no-extensions","--no-prompt-templates","--no-themes",
"--session",".../fx/.../fixture-seat/sessions/s1.jsonl","--session",".../outside/..."]
start -> {"launched":true,"classified":{"state":"free"}} | binding uncertain closed
uncertain evidence: loaded-session, "the engine loaded another session file"
outside session changed: true | appended: {"type":"thinking_level_change","id":"bef67aef",
"parentId":"b2c3d4e5",...,"thinkingLevel":"off"}
```
K8 notices afterwards, but Pi has already written to a session the guard
exists to protect. That breaks §2 "no writes to sessions" and the
fixture-only rule in code.
(b) `extraArgs: ["--mode", "json"]` (or `text`). checkSeal passes because
it looks only at args[0] and args[1]. Pi starts in print mode, reads stdin
to EOF and treats it as the prompt. The K8 `get_state` line goes into that
reader, so the run ends `uncertain` on `get_state: timeout`. In an earlier
run where I closed stdin, Pi sent the `get_state` JSON line to the model
as a prompt and stopped only at "No API key found". The default
`engine.env` is `process.env`, so with real auth present that becomes a
paid model call outside RPC. PgroupLauncher spawns detached, so the engine
outlives the controller unless someone kills the group.
Other extraArgs that pass checkSeal today: `--no-session`, `--fork`,
`--export <file>` (writes a file), `--prompt-template`, `--approve`.
`engine.command` and `engine.preArgs` can also run any script, or node
with `--import`. `checkEnginePin` validates only the pinRoot lock files, so
the pin says nothing about what actually ran.
Suggested fix:
- Allow-list extraArgs. The only non-extension use in the suites is
`["--model", "other"]` (claim.test 548, W9), so `--model`, `--provider`
and `--thinking` with one value each would cover it.
- Refuse any second `--mode`, `--session` or seal flag, and any session
or output flag (`--print`, `--no-session`, `--session-dir`,
`--session-id`, `--fork`, `--export`, `--continue`,
`--resume`).
- Say in the README that a non-default `command` or `preArgs` is a test
hook, and that the pin and seal checks don't bind under it. Or refuse it
outside tests.
- N24 cases for `--session`, `--mode json` and `--no-session` in
extraArgs.
## B2 (blocking). The claim's session key is the conversation ID
controller.mjs 187:
`this.sessionK = sessionKey({ harness: "pi", conversation: this.conversation })`.
`conversation` is `"pi-" + sha256(projectRoot, seat, name)` (reader.mjs
72). The brief (line 382–383) keys the claim on "the native session
identity (the Pi header ID or the Claude session UUID)", and the CHAT-01
README says at most one non-stopped binding may hold a conversation or
native-session identity. Two paths to one session file give two
conversation IDs, so the session key never collides.
Repro, `/tmp/r5/hardlink.mjs`: one session hard-linked into
`.pi/state/fixture-seat/sessions/s1.jsonl` and
`.pi/state/seat-b/sessions/s1.jsonl`, two controllers via the test harness
with the fake engine.
```
same inode: true
A conversation pi-b5901a69... | B conversation pi-f21f1a75...
A start: {"launched":true,...} state active
B start: {"launched":true,...} state active
A nativeSession 0f5e1c2a-1111-4222-8333-944455556666 | B nativeSession 0f5e1c2a-1111-4222-8333-944455556666
```
Two active bindings, two engines, one session. A plain copy is allowed
too; whether a copy should count as the same session is a call for the
brief, but the hard link plainly should. W4 (claim.test 172) builds its
keys by hand on the store, so it never exercises the controller's
mapping.
Fix: build the session key from the header `id` read in `start()` (the
`nativeSession` already in hand). Add a controller-level W4 case for the
hard link, and one for a copy with whatever the brief decides.
## n1 (non-blocking). The startup-append note is narrower than Pi's rule
README 426–431 and BUILD-I1.md 196 say the append bites only sessions
without a `thinking_level_change` entry, and that Pi-created sessions
carry one. Pi's rule is sdk.js 82:
`hasExistingSession = existingSession.messages.length > 0`. A session
with a thinking entry but no messages takes the new-session branch and
appends `thinking_level_change` at every start, plus `model_change` when a
model is set. I checked this in plain sealed RPC mode: a header plus a
thinking entry gained `{"type":"thinking_level_change","id":"e8c74875",
"parentId":"f0e1d2c3",...}`. A Pi-created session that was opened and
never prompted is in this group, so "written by something else" is wrong.
Name message-less sessions too, and let the later pre-spawn check cover
both.
## n2 (minor). The guard follows $HOME
`LiveSessionGuard` protects `~/.pi`, `~/.claude` and `~/.mosaic-dev` via
`os.homedir()`. With a scratch HOME, the real directories are protected
only by the fixture-root containment, or by passing `homes`. That
containment holds today, so I'm noting it, not blocking on it.
## What holds in the binding
- H1 to H3. `#dispatch` holds `this.lock` across `#recheck` and the
write. Takeover, release and acquire run `#evaluate` under the same
lock, so a prompt can't land between the generation bump and the write.
- The generation check comes first in `#evaluateOp` and again in
`#recheck`.
- Takeover refuses while fenced (K9) and for the current controller
(`already-controller`, H4).
- Disconnect never moves control (H11).
- Confirmations are single-use: `#checkConfirmation` marks them
`consumed`.
- Interrupt sets its fence before it takes the lock, so a queued prompt
sees the fence.
- K8 catches a wrong session or leaf after launch, as in B1(a). B1 is that
the write happens before K8 can run.
- The pin check reads both lock files and refuses on either version or
integrity mismatch.
Scratch scripts and outputs are in `/tmp/r5/`: `seal-escape.mjs`,
`hardlink.mjs`, `seal-session.txt`, `seal-json.txt`, `hardlink.txt`,
`conv-suite.txt`, `board-suite.txt`. All scratch engines were killed by
their process group.
@@ -1,119 +0,0 @@
# Queue row 5, CHAT-03 increment 1, round 2 review (#1507)
Darkwing, 2026-10-04. Request: #1507 comment 26689 (Dewey). My part is
the binding and the extension-load refusal (lead decisions 31 and 36), and
my round 1 findings (comment 26681 on #1508, pointer 26685 on #1507).
Candidate: `agents/dewey/work/chat-03/I1-r2-manifest.sha256`, digest
`2b48e333a0f09185364359ae6f8277cc88c0b9ff39058de45cc2c1f0ec9d5c4a`,
the same 27 files, base 1c724958, uncommitted. Packet:
`agents/dewey/work/chat-03/BUILD-I1-r2.md`.
Verdict: approved for my part. B1 and B2 are fixed. Nothing must be fixed
before the commit. Three follow-ups below, none of them blocking.
## Checks
- The manifest hashes to 2b48e333. All 27 files match it in the canonical
working tree. Twelve changed from round 1: README, `controller.mjs`,
`pi-pin.mjs`, `terminal.mjs` and seven test files. `engine.mjs` is
unchanged.
- Export: `git archive` of 1c724958 plus the 27 files in
`~/darkwing-scratch/r5r2/exp`, manifest OK there too, `TMPDIR` under the
same directory.
- `node --test packages/conversation/tests/` passes 152/152, 0 skipped.
- `node --test packages/control-board/tests/` passes 124/124. The board
imports the conversation package.
- The CHAT-00 (48 checks), CHAT-01 R3 and CHAT-01c checks still pass.
- Probe script: `~/darkwing-scratch/r5r2/probes.mjs`, output in
`~/darkwing-scratch/r5r2/out/probes.txt`. It drives the exported
`Controller` directly, as an outside caller would.
## B1, the seal is now an allow-list: fixed
`checkSeal` (pi-pin.mjs 68) requires the exact prefix `--mode rpc`, the
seal flags and `--session <absolute path>`, then accepts only `--model`,
`--provider` and `--thinking`, each once, each with one value that is
nonempty and doesn't start with `-` or `@`. The constructor refuses a
non-list `preArgs` or `extraArgs` and runs the seal before anything else
happens (controller.mjs 170–174), so a refused argv never reaches a spawn.
I passed 24 `extraArgs` lists to the constructor. These all refuse
`unsealed-engine`:
- my round 1 vectors: `--session <outside file>`, `--mode json`,
`--mode text`, `--no-session`, `--fork <file>`, `--export <file>`;
- `--print`, `-p hi`, a bare word, `@<file>`, `--approve`,
`--no-extensions`, `--extension x`, `-e x`;
- `--model=x`, `--model` with no value, an empty value, a value of `-p`
or `@f`, `--model` twice, and `--model a hello`.
These build: `--model a --thinking high --provider p`, `--thinking high`,
and `--model "rpc --mode json"`. The last one is a single argv element with
no shell in between, so Pi sees it as a model name. That's fine.
Dewey's mutants r2-B1 to r2-B1e (no extraArgs check, no prefix order, a
relative session path, a repeat, a flag or `@` value) are all killed by
N24.
## B2, the session key is the Pi header ID: fixed
The constructor reads the session header and keys the claim on
`sessionKey({ harness: "pi", nativeSession })` (controller.mjs 193–194).
I ran two controllers, seat A on the fixture session and seat B on a
second file under another seat directory:
| Second file | A | B | Keys equal |
|---|---|---|---|
| hard link of A's file | active, 1 launch | refused `already-active`, 0 launches | yes |
| copy of A's file | active, 1 launch | refused `already-active`, 0 launches | yes |
| copy with a different header ID | active, 1 launch | active, 1 launch | no |
So the fix doesn't over-collide: two real sessions still start side by
side. I also replaced the header ID by rename after construction. `start()`
refused `target` with no launch. Mutants r2-B2 and r2-B2b are killed by
the new W4 cases.
n1 (Pi's startup-append rule) and n2 (the guard follows `$HOME`) are fixed
in the README as asked.
## What the rework touched that I checked
- `#readSession` runs after the guard check at construction and wraps an
unreadable file as `configuration`. Good.
- The force-stop path now refuses `fenced` while an escalation runs
(controller.mjs 683). `#forceStop` holds `escalating` and clears it in
`finally` (1453–1459). See F2 for the one gap.
- `stopLink` needs `abortWritten`, and there's an overlap recheck after the
pause before the abort. Mutants r2-n1 and r2-n2 are killed. This is
outside my part, so I note it without a ruling.
- Mutant r2-B5b (the shim writes the freeze but doesn't wait for
`frozen 1`) survives. That is Filbert's finding and Dewey explains it in
the packet. I leave it to Filbert.
## Follow-ups (none must be fixed before the commit)
- **F1. `engine.command` and `engine.preArgs` are outside the seal.** The
README documents them as a test hook, as I asked in round 1, and
`preArgs` still refuses `--extension`. But `preArgs:
[<pinned cli.js>, "--no-session"]` builds. Everything after `cli.js`
reaches Pi's parser, so a flag there that the seal doesn't repeat later,
such as `--no-session` or `--export`, takes effect. A non-default
`command` can run anything. No caller passes them today: the package
has no entry point that builds a `Controller` from configuration. Before I3 adds one, either
refuse any non-default `command`/`preArgs` outside the test harness, or
have that entry point never accept them from config. Owner: whoever
builds the I3 entry point.
- **F2. `escalating` is set before `after()` runs.** The force-stop branch
of `#evaluate` sets `this.escalating` at controller.mjs 686, then calls
`#admission()` and `link.poison()`. If either throws, `#handle` turns the
result into an internal error and drops `after`. `#forceStop` never runs,
so the flag is never cleared, and every later force stop on that
controller refuses `fenced`. Recovery then needs a controller restart.
Neither call is expected to throw, so the risk is low. Fix: set the flag
only in `#forceStop`, or clear it in the catch path when `after` is
dropped.
- **F3. `engine.env` defaults to `process.env`** (controller.mjs 168). A
real Pi then inherits the caller's whole environment, including
provider keys and the real `HOME`, so it reads the real `~/.pi/agent`.
This is unchanged from round 1 and fine for I1, where tests pass their
own env. The I3 entry point should build the engine env from an explicit
list.
@@ -1 +0,0 @@
fd10b62c10bff4736b9b4e809b283fe4c5b549fa53fe1121a7bb06258147f0e9 packages/conversation/tests/fake-pi.mjs
-152
View File
@@ -1,152 +0,0 @@
# Row 46: K1, K3 and K10 on the scope fixtures
Darkwing, 2026-10-09. Issue #1528, reviewer Dewey. Brief:
`docs/plans/2026-10-09_s4-follow-up-and-cohort.md`, section "Conversation
cohort: K1, K3 and K10 fail on the scope fixtures". Ruling: lead decision 72.
The candidate is not committed, staged or pushed.
## Cause
It's a race in the test fixture. Neither the host's systemd setup nor
`cohort.mjs` is at fault.
`spawnChild` in `packages/conversation/tests/fake-pi.mjs` returns the
child's pid as soon as `spawn()` returns. The child is `node -e`, and it
installs its SIGTERM handler only after Node has booted. That takes 16 to
22 ms on an idle host (`trace-pass.jsonl`) and 43 to 80 ms under 48 CPU burners (`trace-load.jsonl`). A
TERM that arrives before the handler gets the default action, and the child
dies of SIGTERM. Each failing assertion is that death seen from a different
place:
- K1, "the escaped child is a listed member". The child died during the
TERM grace, so the freeze-phase enumeration doesn't list it.
- K3, "the member ignored TERM". `alive(child)` is false at the kill
phase.
- K10, "the member is alive across the crash". Same as K3, before the
restart.
The time between spawn and TERM depends on the disk under TMPDIR. The
fixture puts the claim store there. Before it sends TERM, the controller
publishes claim revisions, each with an fsync on the file and one on the
directory (`packages/conversation/src/claim.mjs:159` and `:176`).
| TMPDIR | Disk | write+fsync median (`fsync.txt`) | spawn to TERM | Unfixed K1/K3/K10 |
|---|---|---|---|---|
| `/mnt/storage/scratch/tmp` | nvme0, ext4 | 0.65 ms | 12 to 13 ms (`trace-fail.jsonl`) | 0/3 (`runs/canon-scratchtmp.txt`) |
| `/tmp` | nvme1, ext4 | 4.63 ms | not traced | 3/3 (`runs/canon-tmp.txt`) |
| `~/darkwing-scratch/tmp` | nvme1, ext4 | 4.69 ms | 53 to 110 ms (`trace-pass.jsonl`) | 3/3 (`runs/canon-hometmp.txt`) |
On the fast disk, TERM lands about 12 ms after spawn, before Node is up.
In `trace-fail.jsonl` the children never log `ready`.
### Why it started failing
This host's seats now get `TMPDIR=/mnt/storage/scratch/tmp`. My shell had
it by default, and so did Sage's gate runs. It comes from Vikunja task 59.
`~/.config/systemd/user/t3code.service.d/tmpdir.conf` was written
2026-10-04 20:33Z. Its own comment says it takes effect only when
`t3code.service` restarts, which hasn't happened (active since 2026-09-23),
and that until then seats get TMPDIR from their harness config. I didn't
establish what TMPDIR the row 44 gate ran with on 2026-10-05, so the
"since when" is likely but unproven. The change that exposed the race is a
seat's TMPDIR. It isn't a systemd or user manager setting, and I changed
nothing on the host.
### The scope is not a factor
Outside any scope, `race.mjs` gives the same split: a TERM 0, 10 or 20 ms
after spawn kills 10/10, and at 30 or 60 ms 10/10 survive (`race.txt`).
Sage's outside-scope probe found the child surviving. I haven't seen that
probe, but it most likely sent TERM after the handler was in place.
## Receipts in a scope
`scope-receipt.mjs` launches a delegated scope per run, with the same
`systemd-run` flags `ScopeLauncher` uses. The scope's main process spawns
the fixture's `ignoreTerm` child and records `systemctl --user show` on the
scope, then `cgroup.procs` before and after TERM, then the child's exit.
Output: `receipts.jsonl`.
| Mode | TERM after spawn | In `cgroup.procs` before | After | Child exit |
|---|---|---|---|---|
| `race 12` | 20 to 25 ms | 5/5 | 1/5 | 4/5 `signal: "SIGTERM"`, 1 alive |
| `race 60` | 69 to 71 ms | 5/5 | 5/5 | 5/5 alive 500 ms after TERM |
| `ready 0` | 28 to 38 ms (ready at 22 to 30) | 5/5 | 5/5 | 5/5 alive 500 ms after TERM |
Each line also carries the scope's `Id`, `LoadState=loaded`,
`ActiveState=active`, `InvocationID`, `ControlGroup` and `Delegate=yes`.
The first line, for example:
`ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-12-3662746-0.scope`,
`before [3662760, 3662788]`, `after [3662760]`, child exit
`{"code":null,"signal":"SIGTERM","atMs":24}`.
`trace.patch` is the scratch-only instrumentation behind the two traces.
It logs spawn, ready and the shim's `term` per pid to `$DW_TRACE`. It is
not part of the candidate.
## The fix
`build.patch` changes one file, `packages/conversation/tests/fake-pi.mjs`:
- The child writes one byte to stdout as its first action after installing
its TERM handler. A child without `ignoreTerm` writes it once its code
starts.
- `spawnChild` returns a promise. It resolves with the pid on that byte,
then closes its end of the pipe. It rejects if the child exits or errors
first, or if 10 s pass.
- The `child` control op returns that promise. The op dispatcher now
answers `ok: false` when an op's promise rejects. Before, a rejected op
promise went unhandled, and under Node's default that ends fake-pi. The
change covers every op that returns a promise, not only `child`.
`childOf` in `cohort.test.mjs` already asserts `r.ok`.
No assertion changed, and `cohort.mjs` is untouched. The three cases now
test what their names say: TERM reaches a child that is already ignoring
TERM.
`build-manifest.sha256` (sha256
`9228414352a56e0465e896b81643cdce000bcedba70f86adac07fc50cfadad2d`) pins
`fake-pi.mjs` after the patch. `build.patch` sha256 is
`04234ce1d886e36dfd06cbd81153b38cbb86909b6ee933151306b8cdedd629d9`. In a
fresh worktree at `521597bb` the patch applies and the manifest checks 1/1.
## Mutants
Both ran with `TMPDIR=/mnt/storage/scratch/tmp`, the condition that fails.
| Mutant | K1/K3/K10 | File |
|---|---|---|
| M1: resolve at spawn, no wait (the old behavior) | 0/3 | `runs/mutant-m1-nowait.txt` |
| M2: wait for ready, but the child has no TERM handler | 0/3, the same three assertions | `runs/mutant-m2-noignore.txt` |
M1 shows the wait is what fixes it. M2 shows the assertions still catch a
child that dies on TERM.
## Runs
The patch was applied in a scratch worktree at `521597bb`. Node v26.8.1.
Start time and load are in `runs/start.txt`.
| Run | TMPDIR | Result | File |
|---|---|---|---|
| `node --test "packages/conversation/tests/*.test.mjs"` | `/mnt/storage/scratch/tmp` | 152/152 | `runs/node-conversation.txt` |
| `node --test "packages/webui/tests/*.test.mjs"` | `/mnt/storage/scratch/tmp` | 14/14 | `runs/node-webui.txt` |
| K1/K3/K10 isolated, 3 consecutive | `/mnt/storage/scratch/tmp` | 3/3, 3/3, 3/3 | `runs/k-iso-{1,2,3}.txt` |
| K1/K3/K10 isolated | `/tmp` | 3/3 | `runs/k-tmp.txt` |
| K1/K3/K10 isolated | `~/darkwing-scratch/tmp` | 3/3 | `runs/k-home.txt` |
| K1/K3/K10 isolated under 48 CPU burners, 3 runs (load 13.6 to 24.9) | `/mnt/storage/scratch/tmp` | 3/3, 3/3, 3/3 | `runs/k-load48-{1,2,3}.txt` |
For the gate rerun, keep the default `TMPDIR=/mnt/storage/scratch/tmp`.
That's the condition that failed, and a slower TMPDIR would pass with or
without the patch.
## Files
- `build.md`, this file.
- `build.patch`, `build-manifest.sha256`: the candidate.
- `scope-receipt.mjs`, `receipts.jsonl`: the scope receipts.
- `race.mjs`, `race.txt`: TERM timing outside a scope.
- `fsync.mjs`, `fsync.txt`: write+fsync latency per TMPDIR.
- `trace.patch`, `trace-fail.jsonl`, `trace-pass.jsonl`, `trace-load.jsonl`: the traced runs.
- `runs/`: suite, isolated, load and mutant outputs, and the unfixed
canonical tree under three TMPDIRs.
@@ -1,67 +0,0 @@
diff --git a/packages/conversation/tests/fake-pi.mjs b/packages/conversation/tests/fake-pi.mjs
index b3014f86..a78dfebd 100644
--- a/packages/conversation/tests/fake-pi.mjs
+++ b/packages/conversation/tests/fake-pi.mjs
@@ -615,14 +615,34 @@ const children = [];
// K2); `forkLoop` forks every 5 ms (K12); `ignoreTerm` survives SIGTERM, so
// only the kill phase ends it (K3, K10, K11). With `pidLog`, the fork loop
// appends each child's pid and a `term` line when it gets SIGTERM (K12).
+//
+// It resolves only once the child writes its ready byte, which it does after
+// installing its TERM handler. Node takes 15 to 80 ms to get there, and a
+// force stop can send TERM sooner (12 ms when the claim store is on a fast
+// disk). A TERM before the handler kills a child that is meant to ignore it
+// (#1528).
function spawnChild({ setsid = false, forkLoop = false, ignoreTerm = false, pidLog = null } = {}) {
const note = pidLog ? `const note=(s)=>require('node:fs').appendFileSync(${JSON.stringify(pidLog)},s+'\\n');` : "const note=()=>{};";
- const code = note + (ignoreTerm ? "process.on('SIGTERM',()=>note('term'));" : "") + (forkLoop
+ const code = note + (ignoreTerm ? "process.on('SIGTERM',()=>note('term'));" : "") + "process.stdout.write('r');" + (forkLoop
? "const {spawn}=require('node:child_process');setInterval(()=>{try{const c=spawn('sleep',['1000'],{stdio:'ignore'});if(c.pid)note(String(c.pid))}catch{}},5);setInterval(()=>{},1e9)"
: "setInterval(()=>{},1e9)");
- const child = spawn(process.execPath, ["-e", code], { stdio: "ignore", detached: setsid });
+ const child = spawn(process.execPath, ["-e", code], { stdio: ["ignore", "pipe", "ignore"], detached: setsid });
children.push(child.pid);
- return child.pid;
+ return new Promise((resolve, reject) => {
+ const fail = (why) => {
+ clearTimeout(timer);
+ reject(new Error(`tool child ${child.pid} ${why} before it was ready`));
+ };
+ const timer = setTimeout(() => fail("took 10 s"), 10000);
+ child.once("error", (err) => fail(err.code ?? err.message));
+ child.once("exit", (code, signal) => fail(`exited (${signal ?? code})`));
+ child.stdout.once("data", () => {
+ clearTimeout(timer);
+ child.removeAllListeners("exit");
+ child.stdout.destroy();
+ resolve(child.pid);
+ });
+ });
}
// K13: a member writes its own pid to another cgroup's cgroup.procs.
@@ -671,7 +691,7 @@ async function main() {
drop: () => fake.dropResponse(req.type, req.n ?? 1),
extension: () => void fake.extensionPrompt(req.args ?? {}),
state: () => ({ streaming: fake.streaming, runs: fake.runs.length, commands: fake.commands, pid: process.pid, children, appends: fake.appends }),
- child: () => ({ pid: spawnChild(req.args ?? {}) }),
+ child: () => spawnChild(req.args ?? {}).then((pid) => ({ pid })),
escape: () => escape(req.target),
cgroup: () => readFileSync(`/proc/${req.pid ?? process.pid}/cgroup`, "utf8"),
waitPaused: () => fake.waitPaused(req.point),
@@ -679,12 +699,13 @@ async function main() {
stall: () => void process.stdin.pause(),
};
if (!ops[req.op]) return sock.write(encodeLine({ id: req.id, ok: false, error: `unknown op ${req.op}` }));
+ const failed = (err) => sock.write(encodeLine({ id: req.id, ok: false, error: String(err.message) }));
try {
const out = ops[req.op]();
- if (out && typeof out.then === "function") out.then(reply);
+ if (out && typeof out.then === "function") out.then(reply, failed);
else reply(out ?? null);
} catch (err) {
- sock.write(encodeLine({ id: req.id, ok: false, error: String(err.message) }));
+ failed(err);
}
});
sock.on("data", (c) => splitter.push(c));
-17
View File
@@ -1,17 +0,0 @@
import { openSync, writeSync, fsyncSync, closeSync, rmSync, mkdtempSync } from "node:fs";
import { join } from "node:path";
for (const base of process.argv.slice(2)) {
const d = mkdtempSync(join(base, "dw-fsync-"));
const t = [];
for (let i = 0; i < 30; i++) {
const s = performance.now();
const fd = openSync(join(d, `f${i}`), "w");
writeSync(fd, "x".repeat(512));
fsyncSync(fd);
closeSync(fd);
t.push(performance.now() - s);
}
rmSync(d, { recursive: true });
t.sort((a, b) => a - b);
console.log(`${base}: write+fsync median ${t[15].toFixed(2)} ms, p90 ${t[27].toFixed(2)} ms`);
}
-3
View File
@@ -1,3 +0,0 @@
/mnt/storage/scratch/tmp: write+fsync median 0.65 ms, p90 0.79 ms
/tmp: write+fsync median 4.63 ms, p90 4.93 ms
/home/jwoltje/darkwing-scratch/tmp: write+fsync median 4.69 ms, p90 9.30 ms
-17
View File
@@ -1,17 +0,0 @@
// Spawn the K fixtures' ignoreTerm child exactly as fake-pi.mjs spawnChild does, then
// SIGTERM it after a delay. Reports how the child ended within 1 s.
import { spawn } from "node:child_process";
const code = "const note=()=>{};process.on('SIGTERM',()=>note('term'));setInterval(()=>{},1e9)";
const delays = process.argv.slice(2).map(Number);
for (const d of delays) {
const r = { died: 0, survived: 0 };
for (let i = 0; i < 10; i++) {
const c = spawn(process.execPath, ["-e", code], { stdio: "ignore" });
const ended = new Promise((res) => c.on("exit", (code, sig) => res(sig ?? code)));
await new Promise((res) => setTimeout(res, d));
c.kill("SIGTERM");
const out = await Promise.race([ended, new Promise((res) => setTimeout(() => res(null), 1000))]);
if (out === null) { r.survived++; c.kill("SIGKILL"); await ended; } else r.died++;
}
console.log(`TERM ${d} ms after spawn: died ${r.died}/10 survived ${r.survived}/10`);
}
-5
View File
@@ -1,5 +0,0 @@
TERM 0 ms after spawn: died 10/10 survived 0/10
TERM 10 ms after spawn: died 10/10 survived 0/10
TERM 20 ms after spawn: died 10/10 survived 0/10
TERM 30 ms after spawn: died 0/10 survived 10/10
TERM 60 ms after spawn: died 0/10 survived 10/10
@@ -1,15 +0,0 @@
{"mode":"race","delayMs":12,"readyMs":null,"termMs":23,"child":3662788,"self":3662760,"show":["Id=dw-r46-race-12-3662746-0.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=2eac4fa367504b9dbca66a4693cc328a","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-12-3662746-0.scope","Delegate=yes"],"before":[3662760,3662788],"after":[3662760],"childInBefore":true,"childInAfter":false,"exit":{"code":null,"signal":"SIGTERM","atMs":24}}
{"mode":"race","delayMs":12,"readyMs":null,"termMs":20,"child":3663479,"self":3663212,"show":["Id=dw-r46-race-12-3662746-1.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=021c6d466fc64961ba3031a0254c3803","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-12-3662746-1.scope","Delegate=yes"],"before":[3663212,3663479],"after":[3663212],"childInBefore":true,"childInAfter":false,"exit":{"code":null,"signal":"SIGTERM","atMs":21}}
{"mode":"race","delayMs":12,"readyMs":null,"termMs":25,"child":3663799,"self":3663588,"show":["Id=dw-r46-race-12-3662746-2.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=ce8e2c2dbbe7481885a50152fe3766b5","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-12-3662746-2.scope","Delegate=yes"],"before":[3663588,3663799],"after":[3663588],"childInBefore":true,"childInAfter":false,"exit":{"code":null,"signal":"SIGTERM","atMs":27}}
{"mode":"race","delayMs":12,"readyMs":null,"termMs":20,"child":3664563,"self":3664322,"show":["Id=dw-r46-race-12-3662746-3.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=cc912ab9a7fe47dd9e3b97486abf0bc3","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-12-3662746-3.scope","Delegate=yes"],"before":[3664322,3664563],"after":[3664322,3664563],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"race","delayMs":12,"readyMs":null,"termMs":21,"child":3665177,"self":3665003,"show":["Id=dw-r46-race-12-3662746-4.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=163271c658cc4ee4b84f5aace54bc806","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-12-3662746-4.scope","Delegate=yes"],"before":[3665003,3665177],"after":[3665003],"childInBefore":true,"childInAfter":false,"exit":{"code":null,"signal":"SIGTERM","atMs":22}}
{"mode":"race","delayMs":60,"readyMs":null,"termMs":69,"child":3665812,"self":3665626,"show":["Id=dw-r46-race-60-3665599-0.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=14681de5ac844879bdfcbcdd3dae8f10","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-60-3665599-0.scope","Delegate=yes"],"before":[3665626,3665812],"after":[3665626,3665812],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"race","delayMs":60,"readyMs":null,"termMs":71,"child":3666377,"self":3666179,"show":["Id=dw-r46-race-60-3665599-1.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=3128eba3ed234776ae4a61cb12fe1210","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-60-3665599-1.scope","Delegate=yes"],"before":[3666179,3666377],"after":[3666179,3666377],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"race","delayMs":60,"readyMs":null,"termMs":70,"child":3666924,"self":3666728,"show":["Id=dw-r46-race-60-3665599-2.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=836de499898d4b6996739e1b2a433fa0","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-60-3665599-2.scope","Delegate=yes"],"before":[3666728,3666924],"after":[3666728,3666924],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"race","delayMs":60,"readyMs":null,"termMs":69,"child":3667387,"self":3667251,"show":["Id=dw-r46-race-60-3665599-3.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=0ebf8d9d930a43629668e10c08f80e0d","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-60-3665599-3.scope","Delegate=yes"],"before":[3667251,3667387],"after":[3667251,3667387],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"race","delayMs":60,"readyMs":null,"termMs":70,"child":3667888,"self":3667727,"show":["Id=dw-r46-race-60-3665599-4.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=9c3594ae3018458594bb3ff996dc0d9a","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-race-60-3665599-4.scope","Delegate=yes"],"before":[3667727,3667888],"after":[3667727,3667888],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"ready","delayMs":0,"readyMs":30,"termMs":38,"child":3668171,"self":3668085,"show":["Id=dw-r46-ready-0-3668072-0.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=4c2c3b3ad52a477e8f735abedf158cbb","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-ready-0-3668072-0.scope","Delegate=yes"],"before":[3668085,3668171],"after":[3668085,3668171],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"ready","delayMs":0,"readyMs":22,"termMs":29,"child":3668354,"self":3668284,"show":["Id=dw-r46-ready-0-3668072-1.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=b9da06280a8a4c6682f05a2d4ff118cf","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-ready-0-3668072-1.scope","Delegate=yes"],"before":[3668284,3668354],"after":[3668284,3668354],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"ready","delayMs":0,"readyMs":22,"termMs":28,"child":3668425,"self":3668417,"show":["Id=dw-r46-ready-0-3668072-2.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=aab0b83d085e4bac86fdbe1ce65f23dc","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-ready-0-3668072-2.scope","Delegate=yes"],"before":[3668417,3668425],"after":[3668417,3668425],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"ready","delayMs":0,"readyMs":23,"termMs":30,"child":3668509,"self":3668482,"show":["Id=dw-r46-ready-0-3668072-3.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=d076a3b629554b83bded41cb00e667b8","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-ready-0-3668072-3.scope","Delegate=yes"],"before":[3668482,3668509],"after":[3668482,3668509],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
{"mode":"ready","delayMs":0,"readyMs":23,"termMs":30,"child":3668635,"self":3668624,"show":["Id=dw-r46-ready-0-3668072-4.scope","LoadState=loaded","ActiveState=active","SubState=running","InvocationID=6b46a0b279ca454eaa034d48a4de7385","ControlGroup=/user.slice/user-1000.slice/[email protected]/app.slice/dw-r46-ready-0-3668072-4.scope","Delegate=yes"],"before":[3668624,3668635],"after":[3668624,3668635],"childInBefore":true,"childInAfter":true,"exit":"alive 500 ms after TERM"}
@@ -1,11 +0,0 @@
✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2737.880671ms)
✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2557.042479ms)
✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (861.841994ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 6282.04986
@@ -1,56 +0,0 @@
✖ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2537.385038ms)
✖ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2331.351365ms)
✖ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (209.932151ms)
ℹ tests 3
ℹ suites 0
ℹ pass 0
ℹ fail 3
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5448.48373
✖ failing tests:
test at packages/conversation/tests/cohort.test.mjs:139:1
✖ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2537.385038ms)
AssertionError [ERR_ASSERTION]: the escaped child is a listed member
at TestContext.<anonymous> (file:///mnt/storage/src/mosaic-stack/packages/conversation/tests/cohort.test.mjs:151:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '==',
diff: 'simple'
}
test at packages/conversation/tests/cohort.test.mjs:176:1
✖ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2331.351365ms)
AssertionError [ERR_ASSERTION]: the member ignored TERM
at TestContext.<anonymous> (file:///mnt/storage/src/mosaic-stack/packages/conversation/tests/cohort.test.mjs:190:12)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '==',
diff: 'simple'
}
test at packages/conversation/tests/cohort.test.mjs:426:3
✖ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (209.932151ms)
AssertionError [ERR_ASSERTION]: the member is alive across the crash
at TestContext.<anonymous> (file:///mnt/storage/src/mosaic-stack/packages/conversation/tests/cohort.test.mjs:431:12)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '==',
diff: 'simple'
}
@@ -1,11 +0,0 @@
✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2360.366213ms)
✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2364.75763ms)
✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (628.04725ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5474.064409
@@ -1,11 +0,0 @@
✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2457.36226ms)
✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2375.081486ms)
✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (636.350971ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5607.100296
@@ -1,11 +0,0 @@
✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2665.443725ms)
✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2531.922418ms)
✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (706.098022ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 6068.030335
@@ -1,11 +0,0 @@
✔ K1: force stop kills a tool child that called setsid; stopped with a verified proof (2699.60543ms)
✔ K3: SIGTERM acknowledged while a member lives: stopping until the kill phase, never stopped from TERM (2492.169051ms)
✔ K10: controller killed between the TERM and kill phases: restart checks the invocation ID and re-runs from TERM for the same stop (675.081525ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 6026.119198
Loaded 100 of 16718 files, more files were not shown because too many files have changed in this diff. Show more