Jason Woltje 1afe2b36dc fix(git): #1007 suite hermeticity — pin repo-local mosaic.gitIdentity in four test suites
CENSUS CORRECTION: FOUR suites, not the three my own #1007 audit named. The
fourth (test-pr-metadata-gitea.sh) was outside the candidate set that audit
worked from and was found only by sweeping the discriminator across all 16
tools/git/test-*.sh suites. Recording that as a correction to my finding, not
as part of the original claim.

THE DEFECT. get_gitea_token() (detect-platform.sh:502-599) resolves a per-agent
identity at STEP 0, from `git config --get mosaic.gitIdentity`, BEFORE both the
Mosaic credential loader (step 1) and the GITEA_TOKEN env check (step 2). On a
provisioned agent seat that value is set GLOBALLY in ~/.gitconfig and is
inherited by any freshly-`git init`ed repo, so step 0 reads a REAL per-slot
token out of $HOME and returns it without ever consulting the suite's own
MOSAIC_CREDENTIALS_FILE / GITEA_TOKEN fixtures. The suites were running against
production credentials, and the fixture credential each one carefully
constructs was inert.

THE FIX: an empty repo-local `mosaic.gitIdentity`. An empty local value shadows
the global one and reads back empty at rc=0, so step 0 declines. The env route
does NOT work: detect-platform.sh reads "${MOSAIC_GIT_IDENTITY:-}", and `:-`
treats set-but-empty identically to unset.

OPERATIVE vs CONTAINMENT — the two mechanisms are not interchangeable and the
comment in each suite says so. The pin is operative: it prevents the resolution.
The sandboxed HOME each suite now also gets is containment: it bounds a failure
the pin should already have prevented. Conflating them is how this class stays
invisible, because a decoy HOME REMOVES the trigger (~/.gitconfig is where the
global identity lives), so any suite audited under one reads clean however
vulnerable it is. To MEASURE, replicate a seat: a decoy HOME whose .gitconfig
sets mosaic.gitIdentity with no per-slot token, so step 0 reaches its fail-loud
branch. That note is in each file for the next auditor.

SECOND, INDEPENDENT DEFECT in test-pr-metadata-gitea.sh. Applying the pin alone
turned that suite RED — and a control at baseline 826a8b3 under a plain HOME
reproduced the same failure, so it is pre-existing, not introduced. Its
`GITEA_TOKEN="stub-token"` / `GITEA_URL="https://git.example.test"` pair can
never satisfy step 2, because step 2 accepts GITEA_TOKEN only when GITEA_URL
matches the remote host and this repo's origin is git.uscllc.com. The suite had
therefore only ever passed by resolving a REAL credential — step 0 on a seat, or
step 1 from the operator's own credentials.json. A MOSAIC_CREDENTIALS_FILE
fixture is added rather than leaning on the sandboxed HOME making step 1 find
nothing: a test that passes because production configuration is ABSENT fails the
moment it is present. Shipping the pin without this would have moved the failure
rather than removed it.

NO CI ARM. .woodpecker/ci.yml does not run these suites; packages/mosaic/
package.json:28 (test:framework-shell) runs an ENUMERATED list that excludes all
four. They run only by hand — i.e. exclusively on a provisioned seat, the one
environment where the defect is live. "Passes in CI, fails on a seat" does not
apply here; there is no CI observation at all.

VERIFICATION (seat replica = decoy HOME with mosaic.gitIdentity set, no per-slot
token; canary = same plus a marked non-credential at both per-slot paths; plain
= empty HOME; real = ordinary invocation):
  - bash -n clean on all four.
  - Sweep of all 16 suites at baseline 826a8b3 under the seat replica:
    test-gitea-login-resolution rc=1 REACHES-STEP0; test-issue-create-
    interactive-auth rc=1 REACHES-STEP0; test-pr-merge-gitea-empty-uid rc=1
    REACHES-STEP0; test-pr-metadata-gitea rc=1 REACHES-STEP0.
  - Same sweep after: every row rc=0 with step0 absent.
  - test-gitea-token-identity flags REACHES-STEP0 in BOTH arms and is NOT a
    defect: it runs under `env -i HOME="$FAKE_HOME"` (line 77) and its hit is
    its own deliberate assert_failloud fixtures (lines 158-171). The fail-loud
    grep matches the intended behaviour as well as the defect, so it needs the
    second discriminator; recorded here so the next sweep does not re-file it.
  - Durable-argv assay (a PATH shim that tees argv out of each suite's own mock
    curl, because test-pr-merge-gitea-empty-uid truncates its log between phases
    and its EXIT trap removes the sandbox — a post-hoc read of that suite is a
    non-measurement, and "no trace" there is not a clearance):
      test-pr-merge-gitea-empty-uid  before: canary token in argv, fixture never
        used. after: fixture token in argv, canary absent. 5 curl calls both arms.
      test-pr-metadata-gitea         before: canary in argv. after: both calls
        carry the fixture token against git.uscllc.com.
  - test-pr-metadata-gitea across seat/canary/plain HOMEs after the fix: rc=0,
    rc=0, rc=0.
  - All four under the real HOME: rc=0. No regression to ordinary invocation.

The comment block is duplicated across the four files rather than pointing at a
shared note. Deliberate, and matching the merged #1006 precedent
(test-pr-review-gitea-comment.sh:87-95): the reader who needs it is auditing one
file.

TWO FINDINGS DELIBERATELY NOT FIXED HERE (out of this branch's scope, to be
filed):
  1. pr-metadata.sh:89-92 — the anonymous curl fallback does not check ^2, so an
     HTTP 200 carrying valid JSON is reported as "unknown API error" at rc=1.
  2. test-issue-comment-readback.sh exits 1 with ZERO bytes on stdout AND
     stderr, dying at its first seed_state python3 heredoc. Reproduces at
     baseline 826a8b3 under both a seat replica and the real HOME. Silently red
     at main for everyone; unrelated to #1007.

Refs #1007
2026-07-31 07:13:13 -05:00

Mosaic Stack

Self-hosted, multi-user AI agent platform. One config, every runtime, same standards.

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.

Quick Install

curl -fsSL https://mosaicstack.dev/install.sh | bash

Or use the direct URL:

bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)

The installer auto-launches the setup wizard, which walks you through gateway install and verification. Flags for non-interactive use:

bash <(curl -fsSL …) --yes               # Accept all defaults
bash <(curl -fsSL …) --yes --no-auto-launch  # Install only, skip wizard

This installs both components:

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/

After install, the wizard runs automatically or you can invoke it manually:

mosaic wizard        # Full guided setup (gateway install → verify)

Requirements

Usage

Launching Agent Sessions

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

The launcher verifies your config, checks for SOUL.md, injects your AGENTS.md standards into the runtime, and forwards all arguments.

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.

TUI & Gateway

mosaic tui                   # Interactive TUI connected to the gateway
mosaic gateway login         # Authenticate with a gateway instance
mosaic sessions list         # List active agent sessions

Gateway Management

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 for multi-user and production deployments, or Migrating to Federated to upgrade from existing tiers.

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

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:

# 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

# 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 ≥ 20
  • pnpm 10.6+
  • Docker & Docker Compose

Setup

git clone [email protected]:mosaicstack/stack.git
cd stack

# Install dependencies. The local tier uses in-process PGlite; leave DATABASE_URL unset.
pnpm install

# 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 --runmosaic-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

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:

curl -fsSL https://mosaicstack.dev/install.sh | bash

Or use the direct URL:

bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)

Or use the CLI:

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 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 --ref v1.0        # Install from a specific git ref
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

# 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.

S
Description
Self-hosted multi-user AI agent platform — web dashboard, TUI, remote control, shared memory, mission orchestration
Readme MIT
14 MiB
2026-06-11 20:14:20 +00:00
Languages
TypeScript 67.4%
Shell 25.7%
Python 4.1%
JavaScript 1.3%
PowerShell 1.2%