Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ae1411d5f | ||
|
|
13968e9a8b | ||
|
|
ad21ad7ac5 | ||
|
|
9aa4983cf2 | ||
|
|
736b0affc1 | ||
|
|
e18d13d36f | ||
|
|
60bc5d2022 | ||
|
|
ea91cfc421 | ||
|
|
acf640d00f | ||
|
|
431ead3a18 |
+15
-155
@@ -1,156 +1,16 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 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.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Mosaic Stack standalone deployment (compose `stack` profile)
|
||||
# Copy to .env and adjust. Port overrides exist because the defaults
|
||||
# collide with common host services (and with the dev compose itself).
|
||||
PG_HOST_PORT=5433
|
||||
VALKEY_HOST_PORT=6380
|
||||
GATEWAY_HOST_PORT=14242
|
||||
# Registry image override (defaults to a local build of docker/gateway.Dockerfile):
|
||||
# GATEWAY_IMAGE=git.mosaicstack.dev/mosaicstack/stack/gateway:sha-acf640d
|
||||
|
||||
|
||||
# ─── 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
|
||||
|
||||
# Docker Compose host-port override for the PostgreSQL container (default: 5433)
|
||||
# PG_HOST_PORT=5433
|
||||
|
||||
|
||||
# ─── 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 (SPA) ───────────────────────────────────────────────────────────
|
||||
# Directory holding the built SPA bundle (vite build output). When set, the
|
||||
# gateway serves the SPA same-origin; when unset (dev), run the Vite dev
|
||||
# server (pnpm --filter @mosaicstack/web dev), which proxies to the gateway.
|
||||
# safe-default: unset in dev — SPA serving is an opt-in production concern
|
||||
#WEB_DIST_DIR=apps/web/dist
|
||||
|
||||
|
||||
# ─── 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.
|
||||
# Optional explicit dogfood overlay (docker-compose.dogfood.yml).
|
||||
# All three paths are required when that overlay is used. Use a dedicated
|
||||
# next-based worktree, its canonical clone's .git directory, and the external
|
||||
# home of the unprivileged code-dogfood-01 functional seat.
|
||||
# MOSAIC_DOGFOOD_WORKTREE=/home/example/src/mosaic-stack-worktrees/dogfood-1487
|
||||
# MOSAIC_DOGFOOD_COMMON_GIT_DIR=/home/example/src/mosaic-stack/.git
|
||||
# MOSAIC_DOGFOOD_SEAT_HOME=/home/example/.mosaic/fleet/agents/code-dogfood-01
|
||||
|
||||
@@ -208,6 +208,51 @@ mosaic telemetry upload # Dry-run unless opted in
|
||||
|
||||
Consent state is persisted in config. Remote upload is a no-op until you run `mosaic telemetry opt-in`.
|
||||
|
||||
## Standalone container deployment
|
||||
|
||||
The `stack` profile runs PostgreSQL, Valkey, the gateway, and the bundled webUI. Copy
|
||||
`.env.example` to `.env`, generate `BETTER_AUTH_SECRET`, then start the profile:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
printf 'BETTER_AUTH_SECRET=%s\n' "$(openssl rand -hex 32)" >> .env
|
||||
docker compose --profile stack up -d
|
||||
```
|
||||
|
||||
The optional dogfood overlay gives one dedicated in-stack agent a writable stack
|
||||
worktree and its own read-only credential slot. It does not mount the fleet brain or
|
||||
any other seat. Prepare a `next`-based worktree and an unprivileged
|
||||
`code-dogfood-01` functional seat outside the container, then set these paths in
|
||||
`.env`:
|
||||
|
||||
```dotenv
|
||||
MOSAIC_DOGFOOD_WORKTREE=/path/to/mosaic-stack-worktrees/dogfood-1487
|
||||
MOSAIC_DOGFOOD_COMMON_GIT_DIR=/path/to/mosaic-stack/.git
|
||||
MOSAIC_DOGFOOD_SEAT_HOME=/path/to/.mosaic/fleet/agents/code-dogfood-01
|
||||
```
|
||||
|
||||
The common Git directory must match the worktree's `.git` pointer. The seat home
|
||||
must contain only that seat's credential at
|
||||
`secrets/gitea-mosaicstack-code-dogfood-01.token`. Never place the token value in
|
||||
`.env`. Start the overlay with:
|
||||
|
||||
```bash
|
||||
docker compose \
|
||||
-f docker-compose.yml \
|
||||
-f docker-compose.dogfood.yml \
|
||||
--profile stack up -d
|
||||
```
|
||||
|
||||
The overlay removes the general shell tool for every session, including admins.
|
||||
File tools stay inside the mounted checkout. Two dedicated delivery tools stage
|
||||
explicit paths, run the CI queue guard, push through `git-credential-mosaic`, and
|
||||
open PRs through `pr-create.sh`. They resolve only the `code-dogfood-01` slot and fail
|
||||
if it is absent. The overlay enables Docker's init process so the R4 helper can
|
||||
establish the gateway's seat lineage below PID 1.
|
||||
|
||||
This deployment route is separate from the local source-development restrictions
|
||||
below.
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
@@ -27,10 +27,11 @@ import { McpClientService } from '../mcp-client/mcp-client.service.js';
|
||||
import { SkillLoaderService } from './skill-loader.service.js';
|
||||
import { createBrainTools } from './tools/brain-tools.js';
|
||||
import { createCoordTools } from './tools/coord-tools.js';
|
||||
import { createDeliveryTools } from './tools/delivery-tools.js';
|
||||
import { createMemoryTools } from './tools/memory-tools.js';
|
||||
import { createFileTools } from './tools/file-tools.js';
|
||||
import { createGitTools } from './tools/git-tools.js';
|
||||
import { createShellTools } from './tools/shell-tools.js';
|
||||
import { createShellToolsIfEnabled } from './tools/shell-tools.js';
|
||||
import { createWebTools } from './tools/web-tools.js';
|
||||
import { createSearchTools } from './tools/search-tools.js';
|
||||
import type { SessionInfoDto, SessionMetrics } from './session.dto.js';
|
||||
@@ -167,7 +168,8 @@ export class AgentService implements OnModuleDestroy {
|
||||
),
|
||||
...createFileTools(sandboxDir),
|
||||
...createGitTools(sandboxDir),
|
||||
...createShellTools(sandboxDir),
|
||||
...createShellToolsIfEnabled(sandboxDir),
|
||||
...createDeliveryTools(sandboxDir),
|
||||
...createWebTools(),
|
||||
...createSearchTools(),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||
import { createFileTools } from './file-tools.js';
|
||||
import { createShellTools, createShellToolsIfEnabled } from './shell-tools.js';
|
||||
import {
|
||||
createDeliveryTools,
|
||||
type DeliveryToolEnvironment,
|
||||
type ProcessResult,
|
||||
type ProcessRunner,
|
||||
} from './delivery-tools.js';
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function tempDir(prefix: string): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function textOf(result: unknown): string {
|
||||
const typed = result as { content: Array<{ text: string }> };
|
||||
return typed.content.map((item) => item.text).join('\n');
|
||||
}
|
||||
|
||||
async function execute(tool: ToolDefinition, params: Record<string, unknown>): Promise<unknown> {
|
||||
return (
|
||||
tool.execute as unknown as (id: string, input: Record<string, unknown>) => Promise<unknown>
|
||||
)('test-call', params);
|
||||
}
|
||||
|
||||
function ok(stdout = ''): ProcessResult {
|
||||
return { exitCode: 0, stdout, stderr: '', timedOut: false };
|
||||
}
|
||||
|
||||
function deliveryEnv(extra: Partial<DeliveryToolEnvironment> = {}): DeliveryToolEnvironment {
|
||||
return {
|
||||
AGENT_DELIVERY_ENABLED: 'true',
|
||||
MOSAIC_GIT_TOOLS_DIR: '/opt/mosaic/tools/git',
|
||||
MOSAIC_GIT_IDENTITY: 'code-dogfood-01',
|
||||
MOSAIC_AGENT_NAME: 'code-dogfood-01',
|
||||
MOSAIC_BRAIN_HOME: '/opt/mosaic/brain',
|
||||
MOSAIC_INTEGRATION_TRUNK: 'next',
|
||||
HOME: '/home/node',
|
||||
PATH: '/usr/bin:/bin',
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('dogfood execution boundary', () => {
|
||||
it('removes shell_exec mechanically while its first-token bypass red control stays live', async () => {
|
||||
const sandbox = tempDir('mosaic-shell-boundary-');
|
||||
expect(createShellToolsIfEnabled(sandbox, { AGENT_SHELL_ENABLED: 'false' })).toEqual([]);
|
||||
|
||||
const redControl = createShellTools(sandbox)[0]!;
|
||||
const result = await execute(redControl, { command: 'env printf FIRST_TOKEN_BYPASS' });
|
||||
expect(textOf(result)).toContain('FIRST_TOKEN_BYPASS');
|
||||
});
|
||||
|
||||
it('refuses an outside-sandbox token-shaped read and proves the path guard is the enforcement', async () => {
|
||||
const root = tempDir('mosaic-file-boundary-');
|
||||
const sandbox = path.join(root, 'workspace', 'stack');
|
||||
const token = path.join(
|
||||
root,
|
||||
'brain',
|
||||
'fleet',
|
||||
'agents',
|
||||
'code-dogfood-01',
|
||||
'secrets',
|
||||
'gitea-mosaicstack-code-dogfood-01.token',
|
||||
);
|
||||
fs.mkdirSync(sandbox, { recursive: true });
|
||||
fs.mkdirSync(path.dirname(token), { recursive: true });
|
||||
fs.writeFileSync(token, 'OUTSIDE_SANDBOX_SENTINEL');
|
||||
|
||||
const read = createFileTools(sandbox).find((tool) => tool.name === 'fs_read_file')!;
|
||||
const refused = await execute(read, { path: token });
|
||||
expect(textOf(refused)).toContain('Path escape attempt blocked');
|
||||
expect(textOf(refused)).not.toContain('OUTSIDE_SANDBOX_SENTINEL');
|
||||
|
||||
fs.symlinkSync(token, path.join(sandbox, 'credential.token'));
|
||||
const symlinkRefused = await execute(read, { path: 'credential.token' });
|
||||
expect(textOf(symlinkRefused)).toContain('Path escape attempt blocked');
|
||||
expect(textOf(symlinkRefused)).not.toContain('OUTSIDE_SANDBOX_SENTINEL');
|
||||
|
||||
const redRead = createFileTools(root).find((tool) => tool.name === 'fs_read_file')!;
|
||||
const redControl = await execute(redRead, { path: token });
|
||||
expect(textOf(redControl)).toContain('OUTSIDE_SANDBOX_SENTINEL');
|
||||
});
|
||||
});
|
||||
|
||||
describe('delivery tools', () => {
|
||||
it('stay absent unless explicitly enabled and reject identity mismatch', () => {
|
||||
const sandbox = tempDir('mosaic-delivery-disabled-');
|
||||
expect(createDeliveryTools(sandbox, {})).toEqual([]);
|
||||
expect(() =>
|
||||
createDeliveryTools(sandbox, deliveryEnv({ MOSAIC_AGENT_NAME: 'another-seat' })),
|
||||
).toThrow('matching safe MOSAIC agent and git identities');
|
||||
});
|
||||
|
||||
it('publishes through execFile-only git and queue operations with a scrubbed environment', async () => {
|
||||
const sandbox = tempDir('mosaic-delivery-publish-');
|
||||
fs.writeFileSync(path.join(sandbox, 'change.md'), 'change');
|
||||
const calls: Array<{ file: string; args: readonly string[]; env: NodeJS.ProcessEnv }> = [];
|
||||
const runner: ProcessRunner = async (file, args, options) => {
|
||||
calls.push({ file, args, env: options.env });
|
||||
if (args[0] === 'branch') return ok('feat/1487-dogfood-proof\n');
|
||||
return ok();
|
||||
};
|
||||
const hostile = {
|
||||
...deliveryEnv(),
|
||||
BASH_ENV: '/tmp/injected',
|
||||
'BASH_FUNC_read%%': '() { :; }',
|
||||
GITEA_TOKEN: 'must-not-cross',
|
||||
} as DeliveryToolEnvironment;
|
||||
const publish = createDeliveryTools(sandbox, hostile, runner).find(
|
||||
(tool) => tool.name === 'git_publish_branch',
|
||||
)!;
|
||||
|
||||
const result = await execute(publish, {
|
||||
issue: 1487,
|
||||
paths: ['change.md'],
|
||||
commitMessage: 'docs: dogfood proof (#1487)',
|
||||
});
|
||||
expect(textOf(result)).toBe('Published branch feat/1487-dogfood-proof as code-dogfood-01.');
|
||||
|
||||
expect(calls.map((call) => call.file)).toEqual([
|
||||
'/usr/bin/git',
|
||||
'/usr/bin/git',
|
||||
'/usr/bin/git',
|
||||
'/opt/mosaic/tools/git/ci-queue-wait.sh',
|
||||
'/usr/bin/git',
|
||||
]);
|
||||
expect(calls[3]!.args).toEqual(['--purpose', 'push', '-B', 'feat/1487-dogfood-proof']);
|
||||
expect(calls[4]!.args).toEqual(['push', '--set-upstream', 'origin', 'feat/1487-dogfood-proof']);
|
||||
for (const call of calls) {
|
||||
expect(call.file).not.toMatch(/(?:^|\/)sh$/);
|
||||
expect(call.env).not.toHaveProperty('BASH_ENV');
|
||||
expect(Object.keys(call.env).some((key) => key.startsWith('BASH_FUNC_'))).toBe(false);
|
||||
expect(call.env).not.toHaveProperty('GITEA_TOKEN');
|
||||
expect(call.env.MOSAIC_GIT_IDENTITY).toBe('code-dogfood-01');
|
||||
}
|
||||
});
|
||||
|
||||
it('opens PRs only through pr-create.sh against next', async () => {
|
||||
const sandbox = tempDir('mosaic-delivery-pr-');
|
||||
const calls: Array<{ file: string; args: readonly string[] }> = [];
|
||||
const runner: ProcessRunner = async (file, args) => {
|
||||
calls.push({ file, args });
|
||||
if (args[0] === 'branch') return ok('feat/1487-dogfood-proof\n');
|
||||
return ok('https://git.mosaicstack.dev/mosaicstack/stack/pulls/999\n');
|
||||
};
|
||||
const openPr = createDeliveryTools(sandbox, deliveryEnv(), runner).find(
|
||||
(tool) => tool.name === 'git_open_pull_request',
|
||||
)!;
|
||||
|
||||
const result = await execute(openPr, {
|
||||
issue: 1487,
|
||||
title: 'docs: dogfood proof',
|
||||
body: 'Measured from the in-stack agent.',
|
||||
});
|
||||
expect(textOf(result)).toContain('/pulls/999');
|
||||
expect(calls[1]!.file).toBe('/opt/mosaic/tools/git/pr-create.sh');
|
||||
expect(calls[1]!.args).toEqual([
|
||||
'-t',
|
||||
'docs: dogfood proof',
|
||||
'-b',
|
||||
'Measured from the in-stack agent.',
|
||||
'-B',
|
||||
'next',
|
||||
'-H',
|
||||
'feat/1487-dogfood-proof',
|
||||
'-i',
|
||||
'1487',
|
||||
]);
|
||||
});
|
||||
|
||||
it('blocks publish paths outside the sandbox before staging', async () => {
|
||||
const root = tempDir('mosaic-delivery-path-');
|
||||
const sandbox = path.join(root, 'sandbox');
|
||||
const outside = path.join(root, 'outside.md');
|
||||
fs.mkdirSync(sandbox);
|
||||
fs.writeFileSync(outside, 'OUTSIDE_DELIVERY_SENTINEL');
|
||||
const calls: Array<{ file: string; args: readonly string[] }> = [];
|
||||
const runner: ProcessRunner = async (file, args) => {
|
||||
calls.push({ file, args });
|
||||
return args[0] === 'branch' ? ok('feat/1487-dogfood-proof\n') : ok();
|
||||
};
|
||||
const publish = createDeliveryTools(sandbox, deliveryEnv(), runner).find(
|
||||
(tool) => tool.name === 'git_publish_branch',
|
||||
)!;
|
||||
|
||||
const result = await execute(publish, {
|
||||
issue: 1487,
|
||||
paths: [outside],
|
||||
commitMessage: 'docs: must not publish',
|
||||
});
|
||||
expect(textOf(result)).toContain('Path escape attempt blocked');
|
||||
expect(textOf(result)).not.toContain('OUTSIDE_DELIVERY_SENTINEL');
|
||||
expect(calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||
import { spawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { guardPath, SandboxEscapeError } from './path-guard.js';
|
||||
|
||||
const PROCESS_TIMEOUT_MS = 120_000;
|
||||
const MAX_OUTPUT_BYTES = 100 * 1024;
|
||||
const SAFE_IDENTITY = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
||||
const SAFE_BRANCH = /^(?:feat|fix|docs|test)\/[a-z0-9][a-z0-9._/-]*$/i;
|
||||
|
||||
export interface ProcessResult {
|
||||
exitCode: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
timedOut: boolean;
|
||||
}
|
||||
|
||||
export type ProcessRunner = (
|
||||
file: string,
|
||||
args: readonly string[],
|
||||
options: { cwd: string; env: NodeJS.ProcessEnv; timeoutMs: number },
|
||||
) => Promise<ProcessResult>;
|
||||
|
||||
export interface DeliveryToolEnvironment {
|
||||
AGENT_DELIVERY_ENABLED?: string;
|
||||
MOSAIC_GIT_TOOLS_DIR?: string;
|
||||
MOSAIC_GIT_IDENTITY?: string;
|
||||
MOSAIC_AGENT_NAME?: string;
|
||||
MOSAIC_BRAIN_HOME?: string;
|
||||
MOSAIC_CREDENTIAL_SPOOL?: string;
|
||||
MOSAIC_CREDENTIAL_LINEAGE_FENCE?: string;
|
||||
MOSAIC_INTEGRATION_TRUNK?: string;
|
||||
HOME?: string;
|
||||
PATH?: string;
|
||||
LANG?: string;
|
||||
LC_ALL?: string;
|
||||
}
|
||||
|
||||
function runProcess(
|
||||
file: string,
|
||||
args: readonly string[],
|
||||
options: { cwd: string; env: NodeJS.ProcessEnv; timeoutMs: number },
|
||||
): Promise<ProcessResult> {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(file, [...args], {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
shell: false,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let timedOut = false;
|
||||
let outputBytes = 0;
|
||||
|
||||
const append = (current: string, chunk: Buffer): string => {
|
||||
const remaining = MAX_OUTPUT_BYTES - outputBytes;
|
||||
if (remaining <= 0) return current;
|
||||
outputBytes += chunk.length;
|
||||
return current + chunk.subarray(0, remaining).toString();
|
||||
};
|
||||
child.stdout.on('data', (chunk: Buffer) => {
|
||||
stdout = append(stdout, chunk);
|
||||
});
|
||||
child.stderr.on('data', (chunk: Buffer) => {
|
||||
stderr = append(stderr, chunk);
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill('SIGTERM');
|
||||
}, options.timeoutMs);
|
||||
|
||||
child.on('error', (error) => {
|
||||
clearTimeout(timer);
|
||||
resolve({ exitCode: null, stdout, stderr: `${stderr}${String(error)}`, timedOut });
|
||||
});
|
||||
child.on('close', (exitCode) => {
|
||||
clearTimeout(timer);
|
||||
resolve({ exitCode, stdout, stderr, timedOut });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cleanEnvironment(env: DeliveryToolEnvironment): NodeJS.ProcessEnv {
|
||||
const clean: NodeJS.ProcessEnv = {
|
||||
GIT_TERMINAL_PROMPT: '0',
|
||||
};
|
||||
for (const key of [
|
||||
'HOME',
|
||||
'PATH',
|
||||
'LANG',
|
||||
'LC_ALL',
|
||||
'MOSAIC_GIT_IDENTITY',
|
||||
'MOSAIC_AGENT_NAME',
|
||||
'MOSAIC_BRAIN_HOME',
|
||||
'MOSAIC_CREDENTIAL_SPOOL',
|
||||
'MOSAIC_CREDENTIAL_LINEAGE_FENCE',
|
||||
] as const) {
|
||||
const value = env[key];
|
||||
if (value !== undefined) clean[key] = value;
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
function textResult(text: string): {
|
||||
content: Array<{ type: 'text'; text: string }>;
|
||||
details: undefined;
|
||||
} {
|
||||
return { content: [{ type: 'text', text }], details: undefined };
|
||||
}
|
||||
|
||||
function describeFailure(label: string, result: ProcessResult): string {
|
||||
if (result.timedOut) return `${label} timed out`;
|
||||
const diagnostic = result.stderr.trim() || result.stdout.trim() || 'no diagnostic output';
|
||||
return `${label} failed (exit ${result.exitCode ?? 'null'}): ${diagnostic}`;
|
||||
}
|
||||
|
||||
function currentBranchPattern(issue: number): RegExp {
|
||||
return new RegExp(`^(?:feat|fix|docs|test)/${issue}(?:[-/].+)$`, 'i');
|
||||
}
|
||||
|
||||
export function createDeliveryTools(
|
||||
sandboxDir: string,
|
||||
sourceEnv: DeliveryToolEnvironment = process.env,
|
||||
runner: ProcessRunner = runProcess,
|
||||
): ToolDefinition[] {
|
||||
if (sourceEnv.AGENT_DELIVERY_ENABLED !== 'true') return [];
|
||||
|
||||
const identity = sourceEnv.MOSAIC_GIT_IDENTITY ?? '';
|
||||
const agentName = sourceEnv.MOSAIC_AGENT_NAME ?? '';
|
||||
const toolsDir = sourceEnv.MOSAIC_GIT_TOOLS_DIR ?? '';
|
||||
const baseBranch = sourceEnv.MOSAIC_INTEGRATION_TRUNK ?? 'next';
|
||||
if (!SAFE_IDENTITY.test(identity) || identity !== agentName) {
|
||||
throw new Error('Delivery tools require matching safe MOSAIC agent and git identities');
|
||||
}
|
||||
if (!path.isAbsolute(toolsDir)) {
|
||||
throw new Error('Delivery tools require an absolute MOSAIC_GIT_TOOLS_DIR');
|
||||
}
|
||||
if (!SAFE_BRANCH.test(`feat/${baseBranch}`) || baseBranch.includes('/')) {
|
||||
throw new Error('Delivery tools require a safe integration branch name');
|
||||
}
|
||||
|
||||
const env = cleanEnvironment(sourceEnv);
|
||||
const queueGuard = path.join(toolsDir, 'ci-queue-wait.sh');
|
||||
const prCreate = path.join(toolsDir, 'pr-create.sh');
|
||||
|
||||
const run = (file: string, args: readonly string[], timeoutMs = PROCESS_TIMEOUT_MS) =>
|
||||
runner(file, args, { cwd: sandboxDir, env, timeoutMs });
|
||||
|
||||
const readBranch = async (): Promise<{ branch?: string; error?: string }> => {
|
||||
const result = await run('/usr/bin/git', ['branch', '--show-current'], 15_000);
|
||||
if (result.exitCode !== 0) return { error: describeFailure('git branch', result) };
|
||||
const branch = result.stdout.trim();
|
||||
if (!SAFE_BRANCH.test(branch))
|
||||
return { error: `Unsafe delivery branch: ${branch || '<empty>'}` };
|
||||
if (branch === baseBranch || branch === 'main') {
|
||||
return { error: `Refusing delivery from protected branch ${branch}` };
|
||||
}
|
||||
return { branch };
|
||||
};
|
||||
|
||||
const publish: ToolDefinition = {
|
||||
name: 'git_publish_branch',
|
||||
label: 'Publish Git Branch',
|
||||
description:
|
||||
'Stage explicit files in the current sandbox branch, commit them as the dedicated dogfood identity, run the CI queue guard, and push the branch. No shell or raw provider API is used.',
|
||||
parameters: Type.Object({
|
||||
issue: Type.Integer({ minimum: 1, description: 'Tracking issue number' }),
|
||||
paths: Type.Array(Type.String(), {
|
||||
minItems: 1,
|
||||
maxItems: 100,
|
||||
description: 'Files to stage, relative to the sandbox root',
|
||||
}),
|
||||
commitMessage: Type.String({ minLength: 1, maxLength: 4000 }),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { issue, paths, commitMessage } = params as {
|
||||
issue: number;
|
||||
paths: string[];
|
||||
commitMessage: string;
|
||||
};
|
||||
const branchResult = await readBranch();
|
||||
if (!branchResult.branch) return textResult(`Error: ${branchResult.error}`);
|
||||
const branch = branchResult.branch;
|
||||
if (!currentBranchPattern(issue).test(branch)) {
|
||||
return textResult(`Error: branch ${branch} does not carry issue ${issue}`);
|
||||
}
|
||||
|
||||
const relativePaths: string[] = [];
|
||||
try {
|
||||
const sandboxRoot = guardPath('.', sandboxDir);
|
||||
for (const candidate of paths) {
|
||||
const resolved = guardPath(candidate, sandboxDir);
|
||||
const relative = path.relative(sandboxRoot, resolved);
|
||||
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
||||
throw new SandboxEscapeError(candidate, sandboxDir, resolved);
|
||||
}
|
||||
relativePaths.push(relative);
|
||||
}
|
||||
} catch (error) {
|
||||
return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
const add = await run('/usr/bin/git', ['add', '--', ...relativePaths], 30_000);
|
||||
if (add.exitCode !== 0) return textResult(`Error: ${describeFailure('git add', add)}`);
|
||||
|
||||
const commit = await run(
|
||||
'/usr/bin/git',
|
||||
[
|
||||
'-c',
|
||||
`user.name=${identity}`,
|
||||
'-c',
|
||||
`user.email=${identity}@mosaic.invalid`,
|
||||
'commit',
|
||||
'-m',
|
||||
commitMessage,
|
||||
'--',
|
||||
...relativePaths,
|
||||
],
|
||||
60_000,
|
||||
);
|
||||
if (commit.exitCode !== 0)
|
||||
return textResult(`Error: ${describeFailure('git commit', commit)}`);
|
||||
|
||||
const queue = await run(queueGuard, ['--purpose', 'push', '-B', branch]);
|
||||
if (queue.exitCode !== 0) {
|
||||
return textResult(`Error: ${describeFailure('CI queue guard', queue)}`);
|
||||
}
|
||||
|
||||
const push = await run(
|
||||
'/usr/bin/git',
|
||||
['push', '--set-upstream', 'origin', branch],
|
||||
PROCESS_TIMEOUT_MS,
|
||||
);
|
||||
if (push.exitCode !== 0) return textResult(`Error: ${describeFailure('git push', push)}`);
|
||||
|
||||
return textResult(`Published branch ${branch} as ${identity}.`);
|
||||
},
|
||||
};
|
||||
|
||||
const openPr: ToolDefinition = {
|
||||
name: 'git_open_pull_request',
|
||||
label: 'Open Pull Request',
|
||||
description:
|
||||
'Open a pull request from the current sandbox branch through the Mosaic pr-create wrapper. The wrapper targets the configured integration branch and links the tracking issue.',
|
||||
parameters: Type.Object({
|
||||
issue: Type.Integer({ minimum: 1, description: 'Tracking issue number' }),
|
||||
title: Type.String({ minLength: 1, maxLength: 240 }),
|
||||
body: Type.String({ maxLength: 20_000 }),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { issue, title, body } = params as { issue: number; title: string; body: string };
|
||||
const branchResult = await readBranch();
|
||||
if (!branchResult.branch) return textResult(`Error: ${branchResult.error}`);
|
||||
const branch = branchResult.branch;
|
||||
if (!currentBranchPattern(issue).test(branch)) {
|
||||
return textResult(`Error: branch ${branch} does not carry issue ${issue}`);
|
||||
}
|
||||
|
||||
const result = await run(prCreate, [
|
||||
'-t',
|
||||
title,
|
||||
'-b',
|
||||
body,
|
||||
'-B',
|
||||
baseBranch,
|
||||
'-H',
|
||||
branch,
|
||||
'-i',
|
||||
String(issue),
|
||||
]);
|
||||
if (result.exitCode !== 0) {
|
||||
return textResult(`Error: ${describeFailure('pr-create wrapper', result)}`);
|
||||
}
|
||||
return textResult(result.stdout.trim() || `Pull request opened from ${branch}.`);
|
||||
},
|
||||
};
|
||||
|
||||
return [publish, openPr];
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||
import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
|
||||
import { guardPath, guardPathUnsafe, SandboxEscapeError } from './path-guard.js';
|
||||
import { guardPath, guardWritePath, SandboxEscapeError } from './path-guard.js';
|
||||
|
||||
const MAX_READ_BYTES = 512 * 1024; // 512 KB read limit
|
||||
const MAX_WRITE_BYTES = 1024 * 1024; // 1 MB write limit
|
||||
@@ -92,7 +92,7 @@ export function createFileTools(baseDir: string): ToolDefinition[] {
|
||||
};
|
||||
let safePath: string;
|
||||
try {
|
||||
safePath = guardPathUnsafe(path, baseDir);
|
||||
safePath = guardWritePath(path, baseDir);
|
||||
} catch (err) {
|
||||
if (err instanceof SandboxEscapeError) {
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export { createBrainTools } from './brain-tools.js';
|
||||
export { createCoordTools } from './coord-tools.js';
|
||||
export { createDeliveryTools } from './delivery-tools.js';
|
||||
export { createFileTools } from './file-tools.js';
|
||||
export { createGitTools } from './git-tools.js';
|
||||
export { createSearchTools } from './search-tools.js';
|
||||
export { createShellTools } from './shell-tools.js';
|
||||
export { createShellTools, createShellToolsIfEnabled } from './shell-tools.js';
|
||||
export { createWebTools } from './web-tools.js';
|
||||
export { createSkillTools } from './skill-tools.js';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { guardPath, guardPathUnsafe, SandboxEscapeError } from './path-guard.js';
|
||||
import { guardPath, guardPathUnsafe, guardWritePath, SandboxEscapeError } from './path-guard.js';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import fs from 'node:fs';
|
||||
@@ -101,4 +101,55 @@ describe('guardPath', () => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects a symlink inside the sandbox that resolves outside it', () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'path-guard-test-'));
|
||||
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'path-guard-outside-'));
|
||||
try {
|
||||
const target = path.join(outside, 'credential.token');
|
||||
fs.writeFileSync(target, 'OUTSIDE_SYMLINK_SENTINEL');
|
||||
fs.symlinkSync(target, path.join(tmpDir, 'credential.token'));
|
||||
expect(() => guardPath('credential.token', tmpDir)).toThrow(SandboxEscapeError);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
fs.rmSync(outside, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('guardWritePath', () => {
|
||||
it('allows a new file under an existing real sandbox directory', () => {
|
||||
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'path-write-test-'));
|
||||
try {
|
||||
expect(guardWritePath('new.txt', sandbox)).toBe(path.join(sandbox, 'new.txt'));
|
||||
} finally {
|
||||
fs.rmSync(sandbox, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects writes through a file symlink that resolves outside the sandbox', () => {
|
||||
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'path-write-test-'));
|
||||
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'path-write-outside-'));
|
||||
try {
|
||||
const target = path.join(outside, 'credential.token');
|
||||
fs.writeFileSync(target, 'OUTSIDE_WRITE_SENTINEL');
|
||||
fs.symlinkSync(target, path.join(sandbox, 'credential.token'));
|
||||
expect(() => guardWritePath('credential.token', sandbox)).toThrow(SandboxEscapeError);
|
||||
} finally {
|
||||
fs.rmSync(sandbox, { recursive: true, force: true });
|
||||
fs.rmSync(outside, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects new files under a directory symlink that leaves the sandbox', () => {
|
||||
const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), 'path-write-test-'));
|
||||
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'path-write-outside-'));
|
||||
try {
|
||||
fs.symlinkSync(outside, path.join(sandbox, 'outside'));
|
||||
expect(() => guardWritePath('outside/new.txt', sandbox)).toThrow(SandboxEscapeError);
|
||||
} finally {
|
||||
fs.rmSync(sandbox, { recursive: true, force: true });
|
||||
fs.rmSync(outside, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,47 +1,63 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
/**
|
||||
* Resolves a user-provided path and verifies it is inside the allowed sandbox directory.
|
||||
* Throws SandboxEscapeError if the resolved path is outside the sandbox.
|
||||
*
|
||||
* Uses realpathSync to resolve symlinks in the sandbox root. The user-supplied path
|
||||
* is checked for containment AFTER lexical resolution but BEFORE resolving any symlinks
|
||||
* within the user path — so symlink escape attempts are caught too.
|
||||
*
|
||||
* @param userPath - The path provided by the agent (may be relative or absolute)
|
||||
* @param sandboxDir - The allowed root directory (already validated on session creation)
|
||||
* @returns The resolved absolute path, guaranteed to be within sandboxDir
|
||||
*/
|
||||
export function guardPath(userPath: string, sandboxDir: string): string {
|
||||
const resolved = path.resolve(sandboxDir, userPath);
|
||||
const sandboxResolved = fs.realpathSync.native(sandboxDir);
|
||||
function isContained(candidate: string, root: string): boolean {
|
||||
return candidate === root || candidate.startsWith(root + path.sep);
|
||||
}
|
||||
|
||||
// Normalize both paths to resolve any symlinks in the sandbox root itself.
|
||||
// For the user path, we check containment BEFORE resolving symlinks in the path
|
||||
// (so we catch symlink escape attempts too — the resolved path must still be under sandbox)
|
||||
if (!resolved.startsWith(sandboxResolved + path.sep) && resolved !== sandboxResolved) {
|
||||
function assertLexicalContainment(userPath: string, sandboxDir: string): string {
|
||||
const resolved = path.resolve(sandboxDir, userPath);
|
||||
const sandboxAbsolute = path.resolve(sandboxDir);
|
||||
if (!isContained(resolved, sandboxAbsolute)) {
|
||||
throw new SandboxEscapeError(userPath, sandboxDir, resolved);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a path without resolving symlinks in the user-provided portion.
|
||||
* Use for paths that may not exist yet (creates, writes).
|
||||
*
|
||||
* Performs a lexical containment check only using path.resolve.
|
||||
* Resolve an existing path and verify both its lexical path and real symlink
|
||||
* target remain inside the sandbox.
|
||||
*/
|
||||
export function guardPath(userPath: string, sandboxDir: string): string {
|
||||
const resolved = assertLexicalContainment(userPath, sandboxDir);
|
||||
const sandboxReal = fs.realpathSync.native(sandboxDir);
|
||||
const resolvedReal = fs.realpathSync.native(resolved);
|
||||
if (!isContained(resolvedReal, sandboxReal)) {
|
||||
throw new SandboxEscapeError(userPath, sandboxDir, resolvedReal);
|
||||
}
|
||||
return resolvedReal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a writable file path whose parent already exists. Existing targets
|
||||
* are resolved fully. New targets use the real parent directory, which blocks
|
||||
* writes through a parent symlink that leaves the sandbox.
|
||||
*/
|
||||
export function guardWritePath(userPath: string, sandboxDir: string): string {
|
||||
const resolved = assertLexicalContainment(userPath, sandboxDir);
|
||||
const sandboxReal = fs.realpathSync.native(sandboxDir);
|
||||
let writableReal: string;
|
||||
try {
|
||||
writableReal = fs.realpathSync.native(resolved);
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code !== 'ENOENT') throw error;
|
||||
const parentReal = fs.realpathSync.native(path.dirname(resolved));
|
||||
writableReal = path.join(parentReal, path.basename(resolved));
|
||||
}
|
||||
if (!isContained(writableReal, sandboxReal)) {
|
||||
throw new SandboxEscapeError(userPath, sandboxDir, writableReal);
|
||||
}
|
||||
return writableReal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lexical-only validation for non-filesystem pathspecs such as `git diff --`
|
||||
* targets, where the path may name a deleted file and Git does not dereference
|
||||
* a tracked symlink.
|
||||
*/
|
||||
export function guardPathUnsafe(userPath: string, sandboxDir: string): string {
|
||||
const resolved = path.resolve(sandboxDir, userPath);
|
||||
const sandboxAbs = path.resolve(sandboxDir);
|
||||
|
||||
if (!resolved.startsWith(sandboxAbs + path.sep) && resolved !== sandboxAbs) {
|
||||
throw new SandboxEscapeError(userPath, sandboxDir, resolved);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
return assertLexicalContainment(userPath, sandboxDir);
|
||||
}
|
||||
|
||||
export class SandboxEscapeError extends Error {
|
||||
|
||||
@@ -128,6 +128,14 @@ function runCommand(
|
||||
});
|
||||
}
|
||||
|
||||
export function createShellToolsIfEnabled(
|
||||
sandboxDir: string | undefined,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): ToolDefinition[] {
|
||||
if (env['AGENT_SHELL_ENABLED'] === 'false') return [];
|
||||
return createShellTools(sandboxDir);
|
||||
}
|
||||
|
||||
export function createShellTools(sandboxDir?: string): ToolDefinition[] {
|
||||
const defaultCwd = sandboxDir ?? process.cwd();
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { HarnessModule } from './harness/harness.module.js';
|
||||
import { ReloadModule } from './reload/reload.module.js';
|
||||
import { WorkspaceModule } from './workspace/workspace.module.js';
|
||||
import { HierarchyModule } from './hierarchy/hierarchy.module.js';
|
||||
import { EnrollmentModule } from './enrollment/enrollment.module.js';
|
||||
import { QueueModule } from './queue/queue.module.js';
|
||||
import { FederationModule } from './federation/federation.module.js';
|
||||
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
||||
@@ -67,6 +68,7 @@ const federationEnabled = loadConfig(resolveGatewayConfigPath()).tier === 'feder
|
||||
ReloadModule,
|
||||
WorkspaceModule,
|
||||
HierarchyModule,
|
||||
EnrollmentModule,
|
||||
...(federationEnabled ? [FederationModule] : []),
|
||||
],
|
||||
controllers: [HealthController],
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { Logger, ValidationPipe, type ExecutionContext } from '@nestjs/common';
|
||||
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||
import supertest from 'supertest';
|
||||
import { unseal } from '@mosaicstack/auth';
|
||||
import {
|
||||
agentAuditEvents,
|
||||
agentIdempotencyFence,
|
||||
agentOutbox,
|
||||
agents,
|
||||
and,
|
||||
createPgliteDb,
|
||||
eq,
|
||||
providerCredentials,
|
||||
runPgliteMigrations,
|
||||
sql,
|
||||
users,
|
||||
type DbHandle,
|
||||
} from '@mosaicstack/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { HarnessRegistry } from '../harness/harness.registry.js';
|
||||
import { HARNESS_REGISTRY } from '../harness/harness.tokens.js';
|
||||
import { FakeHarnessAdapter } from '../harness/testing/fake-harness.adapter.js';
|
||||
import { EnrollmentController } from './enrollment.controller.js';
|
||||
import {
|
||||
EnrollmentRepository,
|
||||
type EnrollAgentInput,
|
||||
type EnrollmentResult,
|
||||
type EnrolledAgentView,
|
||||
} from './enrollment.repository.js';
|
||||
import { EnrollmentService } from './enrollment.service.js';
|
||||
|
||||
/**
|
||||
* Command-level witnesses for the agent enrollment family (M4-4b) — design
|
||||
* docs/plans/2026-08-29-agent-enrollment-command-design.md §5 items 1–9 and
|
||||
* 11 (item 10, CLI parity, lives in packages/mosaic). Schema-level
|
||||
* constraints are witnessed in packages/db/src/agent-enrollment.witness.test.ts.
|
||||
*
|
||||
* The suite runs the REAL repository/service/controller graph over PGlite,
|
||||
* with only AuthGuard overridden (a session store is out of scope; the
|
||||
* override binds request.user exactly as the real guard does). The §6.3
|
||||
* static companions — no `any`-typed boundary pass-through, a single audit
|
||||
* emitter (EnrollmentRepository.appendEvent) — are code-surface properties
|
||||
* reviewed on the PR, not runtime probes.
|
||||
*/
|
||||
describe('enrollment commands integration', (): void => {
|
||||
let dataDir: string;
|
||||
let handle: DbHandle;
|
||||
let moduleRef: TestingModule;
|
||||
let app: NestFastifyApplication;
|
||||
let http: ReturnType<typeof supertest>;
|
||||
let repo: EnrollmentRepository;
|
||||
let previousAuthSecret: string | undefined;
|
||||
|
||||
const OWNER = 'enr-owner';
|
||||
const ADMIN = 'enr-admin';
|
||||
const STRANGER = 'enr-stranger';
|
||||
const HARNESS = 'fake-harness';
|
||||
/** Never-echo probe value (§5.1). Unique enough that any leak is unambiguous. */
|
||||
const SECRET = `enr-secret-value-${randomUUID()}`;
|
||||
|
||||
/** The HTTP-leg acting user; the overridden guard binds it per request. */
|
||||
let currentUserId = OWNER;
|
||||
|
||||
const enrollInput = (overrides: Partial<EnrollAgentInput> = {}): EnrollAgentInput => ({
|
||||
actorId: OWNER,
|
||||
harness: HARNESS,
|
||||
name: `Agent ${randomUUID().slice(0, 8)}`,
|
||||
persona: null,
|
||||
model: 'anthropic/claude-test',
|
||||
provider: `prov-${randomUUID().slice(0, 8)}`,
|
||||
credential: { mode: 'intake', type: 'api_key', value: SECRET },
|
||||
idempotencyKey: randomUUID(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
function expectOk<T>(result: EnrollmentResult<T>): { ok: true; correlationId: string } & T {
|
||||
if (!result.ok) throw new Error(`expected ok, got ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function expectFail<T>(
|
||||
result: EnrollmentResult<T>,
|
||||
error: string,
|
||||
): { ok: false; error: string; message: string; correlationId: string } {
|
||||
if (result.ok) throw new Error(`expected ${error}, got ok`);
|
||||
expect(result.error).toBe(error);
|
||||
return result;
|
||||
}
|
||||
|
||||
const fenceForKey = (key: string) =>
|
||||
handle.db
|
||||
.select()
|
||||
.from(agentIdempotencyFence)
|
||||
.where(eq(agentIdempotencyFence.idempotencyKey, key));
|
||||
|
||||
const eventsForAgent = (agentId: string) =>
|
||||
handle.db.select().from(agentAuditEvents).where(eq(agentAuditEvents.agentId, agentId));
|
||||
|
||||
const agentsNamed = (name: string) =>
|
||||
handle.db.select().from(agents).where(eq(agents.name, name));
|
||||
|
||||
const credentialsFor = (userId: string, provider: string) =>
|
||||
handle.db
|
||||
.select()
|
||||
.from(providerCredentials)
|
||||
.where(
|
||||
and(eq(providerCredentials.userId, userId), eq(providerCredentials.provider, provider)),
|
||||
);
|
||||
|
||||
const allOutbox = () => handle.db.select().from(agentOutbox);
|
||||
|
||||
beforeAll(async (): Promise<void> => {
|
||||
previousAuthSecret = process.env['BETTER_AUTH_SECRET'];
|
||||
process.env['BETTER_AUTH_SECRET'] = 'enrollment-witness-sealing-key';
|
||||
|
||||
dataDir = await mkdtemp(join(tmpdir(), 'mosaic-gateway-enrollment-commands-'));
|
||||
handle = createPgliteDb(dataDir);
|
||||
await runPgliteMigrations(handle);
|
||||
|
||||
const registry = new HarnessRegistry();
|
||||
registry.register(new FakeHarnessAdapter({ id: HARNESS }));
|
||||
|
||||
moduleRef = await Test.createTestingModule({
|
||||
controllers: [EnrollmentController],
|
||||
providers: [
|
||||
EnrollmentRepository,
|
||||
EnrollmentService,
|
||||
{ provide: DB, useValue: handle.db },
|
||||
{ provide: HARNESS_REGISTRY, useValue: registry },
|
||||
],
|
||||
})
|
||||
.overrideGuard(AuthGuard)
|
||||
.useValue({
|
||||
canActivate: (ctx: ExecutionContext): boolean => {
|
||||
const request = ctx.switchToHttp().getRequest<{ user?: unknown }>();
|
||||
request.user = { id: currentUserId };
|
||||
return true;
|
||||
},
|
||||
})
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());
|
||||
// Mirror main.ts exactly — the closure witnesses depend on these options.
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
|
||||
);
|
||||
await app.init();
|
||||
await app.getHttpAdapter().getInstance().ready();
|
||||
http = supertest(app.getHttpServer());
|
||||
repo = moduleRef.get(EnrollmentRepository);
|
||||
|
||||
await handle.db.insert(users).values([
|
||||
{ id: OWNER, name: 'Owner', email: `${OWNER}@example.com` },
|
||||
{ id: ADMIN, name: 'Admin', email: `${ADMIN}@example.com`, role: 'admin' },
|
||||
{ id: STRANGER, name: 'Stranger', email: `${STRANGER}@example.com` },
|
||||
]);
|
||||
});
|
||||
|
||||
afterAll(async (): Promise<void> => {
|
||||
await app?.close();
|
||||
await handle.close();
|
||||
await rm(dataDir, { recursive: true, force: true });
|
||||
if (previousAuthSecret === undefined) delete process.env['BETTER_AUTH_SECRET'];
|
||||
else process.env['BETTER_AUTH_SECRET'] = previousAuthSecret;
|
||||
});
|
||||
|
||||
// ── §5.7 wizard-facing zero-mutation (runs FIRST: no call → zero rows) ────
|
||||
|
||||
it('zero-mutation: with no enrollment invocation the family tables hold zero rows', async () => {
|
||||
expect(await handle.db.select().from(agents)).toHaveLength(0);
|
||||
expect(await handle.db.select().from(agentAuditEvents)).toHaveLength(0);
|
||||
expect(await handle.db.select().from(agentOutbox)).toHaveLength(0);
|
||||
expect(await handle.db.select().from(agentIdempotencyFence)).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ── §5.1 never-echo + §5.2 sealed single-copy ─────────────────────────────
|
||||
|
||||
it('never echoes the intake credential value: HTTP result, audit, outbox, fence, and logs are clean', async () => {
|
||||
const logSink: string[] = [];
|
||||
const logSpies = (['log', 'error', 'warn', 'debug', 'verbose'] as const).map((method) =>
|
||||
vi.spyOn(Logger.prototype, method).mockImplementation((...args: unknown[]) => {
|
||||
logSink.push(args.map(String).join(' '));
|
||||
}),
|
||||
);
|
||||
try {
|
||||
currentUserId = OWNER;
|
||||
const provider = `prov-echo-${randomUUID().slice(0, 8)}`;
|
||||
const res = await http.post('/api/enrollment/agents').send({
|
||||
harness: HARNESS,
|
||||
name: 'Echo Probe',
|
||||
persona: 'a persona',
|
||||
model: 'anthropic/claude-test',
|
||||
provider,
|
||||
credential: { mode: 'intake', type: 'api_key', value: SECRET },
|
||||
idempotencyKey: randomUUID(),
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.text).not.toContain(SECRET);
|
||||
const agentId = (res.body as { agent: EnrolledAgentView }).agent.id;
|
||||
|
||||
const events = await eventsForAgent(agentId);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(JSON.stringify(events)).not.toContain(SECRET);
|
||||
expect(JSON.stringify(await allOutbox())).not.toContain(SECRET);
|
||||
const fences = await handle.db
|
||||
.select()
|
||||
.from(agentIdempotencyFence)
|
||||
.where(eq(agentIdempotencyFence.outcomeAgentId, agentId));
|
||||
expect(fences).toHaveLength(1);
|
||||
expect(JSON.stringify(fences)).not.toContain(SECRET);
|
||||
expect(logSink.join('\n')).not.toContain(SECRET);
|
||||
|
||||
// §5.2 sealed single-copy: exactly one provider_credentials row, sealed
|
||||
// at rest, and it round-trips through unseal — no plaintext column.
|
||||
const creds = await credentialsFor(OWNER, provider);
|
||||
expect(creds).toHaveLength(1);
|
||||
expect(creds[0]?.encryptedValue).not.toBe(SECRET);
|
||||
expect(creds[0]?.encryptedValue).not.toContain(SECRET);
|
||||
expect(unseal(creds[0]?.encryptedValue as string)).toBe(SECRET);
|
||||
} finally {
|
||||
logSpies.forEach((spy) => spy.mockRestore());
|
||||
}
|
||||
});
|
||||
|
||||
it('the agents table itself has no credential-bearing column (§5.2)', async () => {
|
||||
const result = (await handle.db.execute(
|
||||
sql`select column_name from information_schema.columns where table_name = 'agents'`,
|
||||
)) as unknown as { rows?: Array<{ column_name: string }> } & Array<{ column_name: string }>;
|
||||
const names = (result.rows ?? result).map((row) => row.column_name);
|
||||
expect(names.length).toBeGreaterThan(0);
|
||||
for (const name of names) {
|
||||
expect(name).not.toMatch(/credential|secret|token|api_key/i);
|
||||
}
|
||||
});
|
||||
|
||||
// ── §5.3 reference resolution ─────────────────────────────────────────────
|
||||
|
||||
it('refuses an unresolvable credential reference with precondition_failed and creates nothing', async () => {
|
||||
const input = enrollInput({ credential: { mode: 'reference' } });
|
||||
const result = await repo.enroll(input);
|
||||
expectFail(result, 'precondition_failed');
|
||||
expect(await agentsNamed(input.name)).toHaveLength(0);
|
||||
expect(await fenceForKey(input.idempotencyKey)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('resolves a reference credential stored earlier for (actor, provider)', async () => {
|
||||
const provider = `prov-ref-${randomUUID().slice(0, 8)}`;
|
||||
const seeded = expectOk(await repo.enroll(enrollInput({ provider })));
|
||||
const result = expectOk(
|
||||
await repo.enroll(enrollInput({ provider, credential: { mode: 'reference' } })),
|
||||
);
|
||||
expect(result.agent.id).not.toBe(seeded.agent.id);
|
||||
expect(await credentialsFor(OWNER, provider)).toHaveLength(1);
|
||||
});
|
||||
|
||||
// ── §5.4 harness refusals, both codes ────────────────────────────────────
|
||||
|
||||
it('refuses a syntactically invalid harness as validation_failed and a registry miss as precondition_failed', async () => {
|
||||
const blank = await repo.enroll(enrollInput({ harness: ' ' }));
|
||||
expectFail(blank, 'validation_failed');
|
||||
const miss = await repo.enroll(enrollInput({ harness: 'well-formed-but-unregistered' }));
|
||||
expectFail(miss, 'precondition_failed');
|
||||
|
||||
currentUserId = OWNER;
|
||||
const httpBlank = await http.post('/api/enrollment/agents').send({
|
||||
harness: '',
|
||||
name: 'H',
|
||||
model: 'm',
|
||||
provider: 'p',
|
||||
credential: { mode: 'reference' },
|
||||
idempotencyKey: randomUUID(),
|
||||
});
|
||||
expect(httpBlank.status).toBe(400);
|
||||
});
|
||||
|
||||
// ── §5.5 idempotency set (contract 3 §4.3) ───────────────────────────────
|
||||
|
||||
it('actor-bound replay returns the recorded outcome and executes nothing new', async () => {
|
||||
const input = enrollInput();
|
||||
const first = expectOk(await repo.enroll(input));
|
||||
const replay = expectOk(await repo.enroll({ ...input, correlationId: randomUUID() }));
|
||||
expect(replay.agent.id).toBe(first.agent.id);
|
||||
|
||||
expect(await agentsNamed(input.name)).toHaveLength(1);
|
||||
expect(await fenceForKey(input.idempotencyKey)).toHaveLength(1);
|
||||
const events = await eventsForAgent(first.agent.id);
|
||||
expect(events.filter((e) => e.eventType === 'agent.enrolled')).toHaveLength(1);
|
||||
// A passing replay appends exactly the non-mutation access event.
|
||||
const replayed = events.filter((e) => e.eventType === 'agent.enrollment.replayed');
|
||||
expect(replayed).toHaveLength(1);
|
||||
expect((replayed[0]?.payload as { fenceId?: string }).fenceId).toBeDefined();
|
||||
});
|
||||
|
||||
it('payload-digest mismatch on a recorded key refuses with the single bounded conflict shape', async () => {
|
||||
const input = enrollInput();
|
||||
expectOk(await repo.enroll(input));
|
||||
const mismatch = await repo.enroll({ ...input, name: `${input.name} CHANGED` });
|
||||
const failure = expectFail(mismatch, 'conflict');
|
||||
expect(failure.message).toBe('idempotency conflict');
|
||||
});
|
||||
|
||||
it('replay-mode and scope mismatches on the recorded fence each refuse as the same constant conflict', async () => {
|
||||
const modeInput = enrollInput();
|
||||
expectOk(await repo.enroll(modeInput));
|
||||
await handle.db
|
||||
.update(agentIdempotencyFence)
|
||||
.set({ replayMode: 'shared' })
|
||||
.where(eq(agentIdempotencyFence.idempotencyKey, modeInput.idempotencyKey));
|
||||
const modeFailure = expectFail(await repo.enroll(modeInput), 'conflict');
|
||||
|
||||
const scopeInput = enrollInput();
|
||||
expectOk(await repo.enroll(scopeInput));
|
||||
await handle.db
|
||||
.update(agentIdempotencyFence)
|
||||
.set({ authorizationScope: 'some-other-scope' })
|
||||
.where(eq(agentIdempotencyFence.idempotencyKey, scopeInput.idempotencyKey));
|
||||
const scopeFailure = expectFail(await repo.enroll(scopeInput), 'conflict');
|
||||
|
||||
expect(modeFailure.message).toBe(scopeFailure.message);
|
||||
});
|
||||
|
||||
it('a different actor replaying an actor-bound key is refused conflict, learning nothing', async () => {
|
||||
const input = enrollInput();
|
||||
expectOk(await repo.enroll(input));
|
||||
const failure = expectFail(await repo.enroll({ ...input, actorId: STRANGER }), 'conflict');
|
||||
expect(failure.message).toBe('idempotency conflict');
|
||||
});
|
||||
|
||||
it('a replay is re-authorized fresh: revoked target authority refuses instead of replaying', async () => {
|
||||
const input = enrollInput();
|
||||
const first = expectOk(await repo.enroll(input));
|
||||
// Simulate the legacy CRUD DELETE path removing the outcome agent: the
|
||||
// submitter no longer holds read authority on the referenced row.
|
||||
await handle.db.delete(agents).where(eq(agents.id, first.agent.id));
|
||||
expectFail(await repo.enroll(input), 'conflict');
|
||||
});
|
||||
|
||||
it('a shared replay-mode declaration is refused validation_failed with nothing executed and no fence row', async () => {
|
||||
const input = enrollInput({ replayMode: 'shared' });
|
||||
expectFail(await repo.enroll(input), 'validation_failed');
|
||||
expect(await agentsNamed(input.name)).toHaveLength(0);
|
||||
expect(await fenceForKey(input.idempotencyKey)).toHaveLength(0);
|
||||
|
||||
currentUserId = OWNER;
|
||||
const key = randomUUID();
|
||||
const res = await http.post('/api/enrollment/agents').send({
|
||||
harness: HARNESS,
|
||||
name: 'Shared Probe',
|
||||
model: 'm',
|
||||
provider: 'p',
|
||||
credential: { mode: 'reference' },
|
||||
idempotencyKey: key,
|
||||
replayMode: 'shared',
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect(await fenceForKey(key)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('two concurrent same-key submissions produce exactly one mutation, the loser resolving as a replay', async () => {
|
||||
const input = enrollInput();
|
||||
const [a, b] = await Promise.all([
|
||||
repo.enroll(input),
|
||||
repo.enroll({ ...input, correlationId: randomUUID() }),
|
||||
]);
|
||||
const okA = expectOk(a);
|
||||
const okB = expectOk(b);
|
||||
expect(okA.agent.id).toBe(okB.agent.id);
|
||||
expect(await agentsNamed(input.name)).toHaveLength(1);
|
||||
expect(await fenceForKey(input.idempotencyKey)).toHaveLength(1);
|
||||
const events = await eventsForAgent(okA.agent.id);
|
||||
expect(events.filter((e) => e.eventType === 'agent.enrolled')).toHaveLength(1);
|
||||
expect(events.filter((e) => e.eventType === 'agent.enrollment.replayed')).toHaveLength(1);
|
||||
});
|
||||
|
||||
// ── §5.6 same-tx atomicity fault injection ───────────────────────────────
|
||||
|
||||
it('rolls everything back on failure at each write point — no orphan credential survives', async () => {
|
||||
const injectionPoints = [
|
||||
'writeSealedCredential',
|
||||
'insertAgentRow',
|
||||
'insertFenceRow',
|
||||
'appendEvent',
|
||||
'insertOutboxRow',
|
||||
] as const;
|
||||
|
||||
for (const point of injectionPoints) {
|
||||
const input = enrollInput();
|
||||
const spy = vi.spyOn(repo, point).mockImplementationOnce(() => {
|
||||
throw new Error(`injected ${point} fault`);
|
||||
});
|
||||
try {
|
||||
const result = await repo.enroll(input);
|
||||
expectFail(result, 'internal_fault');
|
||||
expect(await agentsNamed(input.name)).toHaveLength(0);
|
||||
expect(await fenceForKey(input.idempotencyKey)).toHaveLength(0);
|
||||
// Injection at fence/audit/outbox fires AFTER the sealed credential
|
||||
// write's statement ran — the rollback must leave no orphan row.
|
||||
expect(await credentialsFor(OWNER, input.provider)).toHaveLength(0);
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── §5.8 is_system closure ───────────────────────────────────────────────
|
||||
|
||||
it('rejects an is_system injection attempt at the DTO boundary', async () => {
|
||||
currentUserId = OWNER;
|
||||
const key = randomUUID();
|
||||
const res = await http.post('/api/enrollment/agents').send({
|
||||
harness: HARNESS,
|
||||
name: 'System Probe',
|
||||
model: 'm',
|
||||
provider: 'p',
|
||||
credential: { mode: 'reference' },
|
||||
idempotencyKey: key,
|
||||
isSystem: true,
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect(await fenceForKey(key)).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ── §5.9 correlation + no-existence-oracle ───────────────────────────────
|
||||
|
||||
it('carries a submitted correlation id into the result, the audit event, and the outbox record', async () => {
|
||||
const correlationId = randomUUID();
|
||||
const input = enrollInput({ correlationId });
|
||||
const result = expectOk(await repo.enroll(input));
|
||||
expect(result.correlationId).toBe(correlationId);
|
||||
const events = await eventsForAgent(result.agent.id);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0]?.correlationId).toBe(correlationId);
|
||||
const outboxRows = await handle.db
|
||||
.select()
|
||||
.from(agentOutbox)
|
||||
.where(eq(agentOutbox.eventId, events[0]?.id as string));
|
||||
expect(outboxRows).toHaveLength(1);
|
||||
expect(outboxRows[0]?.correlationId).toBe(correlationId);
|
||||
|
||||
// Refusals carry the correlation envelope too (contract 5 §4.3).
|
||||
const refusal = expectFail(
|
||||
await repo.enroll({ ...input, name: 'changed name', correlationId }),
|
||||
'conflict',
|
||||
);
|
||||
expect(refusal.correlationId).toBe(correlationId);
|
||||
});
|
||||
|
||||
it('agent.enrollment.get returns owner and admin reads with the correlation envelope, no idempotency key', async () => {
|
||||
const enrolled = expectOk(await repo.enroll(enrollInput()));
|
||||
const correlationId = randomUUID();
|
||||
const asOwner = expectOk(await repo.getEnrollment(OWNER, enrolled.agent.id, correlationId));
|
||||
expect(asOwner.correlationId).toBe(correlationId);
|
||||
expect(asOwner.agent.id).toBe(enrolled.agent.id);
|
||||
const asAdmin = expectOk(await repo.getEnrollment(ADMIN, enrolled.agent.id));
|
||||
expect(asAdmin.correlationId).toMatch(/^[0-9a-f-]{36}$/);
|
||||
|
||||
currentUserId = OWNER;
|
||||
const wire = randomUUID();
|
||||
const res = await http.get(`/api/enrollment/agents/${enrolled.agent.id}?correlationId=${wire}`);
|
||||
expect(res.status).toBe(200);
|
||||
expect((res.body as { correlationId: string }).correlationId).toBe(wire);
|
||||
});
|
||||
|
||||
it('no existence oracle: unauthorized get of a real agent and get of a missing id are indistinguishable', async () => {
|
||||
const enrolled = expectOk(await repo.enroll(enrollInput()));
|
||||
|
||||
currentUserId = STRANGER;
|
||||
const unauthorized = await http.get(`/api/enrollment/agents/${enrolled.agent.id}`);
|
||||
const missing = await http.get(`/api/enrollment/agents/${randomUUID()}`);
|
||||
expect(unauthorized.status).toBe(404);
|
||||
expect(missing.status).toBe(404);
|
||||
const strip = (body: Record<string, unknown>): Record<string, unknown> =>
|
||||
Object.fromEntries(Object.entries(body).filter(([key]) => key !== 'correlationId'));
|
||||
expect(strip(unauthorized.body as Record<string, unknown>)).toEqual(
|
||||
strip(missing.body as Record<string, unknown>),
|
||||
);
|
||||
});
|
||||
|
||||
// ── §5.11 fail-closed ────────────────────────────────────────────────────
|
||||
|
||||
it('fails closed as internal_fault when the store is unreachable, with no fallback write', async () => {
|
||||
const before = (await handle.db.select().from(agents)).length;
|
||||
const txSpy = vi.spyOn(handle.db, 'transaction').mockImplementationOnce(() => {
|
||||
throw new Error('injected store outage');
|
||||
});
|
||||
try {
|
||||
expectFail(await repo.enroll(enrollInput()), 'internal_fault');
|
||||
} finally {
|
||||
txSpy.mockRestore();
|
||||
}
|
||||
const selectSpy = vi.spyOn(handle.db, 'select').mockImplementationOnce(() => {
|
||||
throw new Error('injected store outage');
|
||||
});
|
||||
try {
|
||||
expectFail(await repo.getEnrollment(OWNER, randomUUID()), 'internal_fault');
|
||||
} finally {
|
||||
selectSpy.mockRestore();
|
||||
}
|
||||
expect((await handle.db.select().from(agents)).length).toBe(before);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
import { EnrollAgentDto, GetEnrollmentQueryDto } from './enrollment.dto.js';
|
||||
import { EnrollmentRepository } from './enrollment.repository.js';
|
||||
import { EnrollmentService } from './enrollment.service.js';
|
||||
|
||||
/**
|
||||
* The agent enrollment command family's closed HTTP surface (design
|
||||
* docs/plans/2026-08-29-agent-enrollment-command-design.md §3): one command,
|
||||
* one query. Authentication failures are the guard's (401); everything else
|
||||
* is the repository's closed enum mapped by EnrollmentService.
|
||||
*/
|
||||
@Controller('api/enrollment')
|
||||
@UseGuards(AuthGuard)
|
||||
export class EnrollmentController {
|
||||
constructor(
|
||||
private readonly repository: EnrollmentRepository,
|
||||
private readonly service: EnrollmentService,
|
||||
) {}
|
||||
|
||||
/** agent.enroll (§3.1). */
|
||||
@Post('agents')
|
||||
async enroll(@CurrentUser() user: { id: string }, @Body() dto: EnrollAgentDto) {
|
||||
return this.service.unwrap(
|
||||
await this.repository.enroll({
|
||||
actorId: user.id,
|
||||
harness: dto.harness,
|
||||
name: dto.name,
|
||||
persona: dto.persona ?? null,
|
||||
model: dto.model,
|
||||
provider: dto.provider,
|
||||
credential: dto.credential,
|
||||
idempotencyKey: dto.idempotencyKey,
|
||||
correlationId: dto.correlationId,
|
||||
replayMode: dto.replayMode,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** agent.enrollment.get (§3.2): owner-or-admin; unauthorized and missing fold to one not_found. */
|
||||
@Get('agents/:id')
|
||||
async getEnrollment(
|
||||
@CurrentUser() user: { id: string },
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Query() query: GetEnrollmentQueryDto,
|
||||
) {
|
||||
return this.service.unwrap(
|
||||
await this.repository.getEnrollment(user.id, id, query.correlationId),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsIn,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
/**
|
||||
* Agent enrollment command DTOs (design
|
||||
* docs/plans/2026-08-29-agent-enrollment-command-design.md §3.1/§3.2,
|
||||
* contract 5 §4.1 typed boundary).
|
||||
*
|
||||
* The global ValidationPipe runs with whitelist + forbidNonWhitelisted, so
|
||||
* closure is contract surface here exactly as in the hierarchy DTOs:
|
||||
* - EnrollAgentDto declares NO isSystem field — `is_system` is never
|
||||
* settable through this command (design §3.1 rule 4); the pipe refuses it.
|
||||
* - replayMode admits ONLY 'actor-bound': `shared` is seed-only (contract 3
|
||||
* §4.3), so a shared declaration is refused `validation_failed` at the
|
||||
* boundary, executes nothing, and records no fence row (design §3.1).
|
||||
* Every class here must be registered in PIPE_GUARDED_DTOS so the boot-time
|
||||
* assertion proves the pipe sees the decorators.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Credential input, discriminated on `mode` (design §3.1):
|
||||
* - `{ mode: 'reference' }` — a stored credential for (actor, provider)
|
||||
* must already exist; `type`/`value` must be ABSENT (the repository
|
||||
* refuses a reference that smuggles a value).
|
||||
* - `{ mode: 'intake', type: 'api_key', value }` — the value is sealed
|
||||
* into the credential store inside the enrollment transaction and is
|
||||
* never echoed anywhere (§3.1 rule 1).
|
||||
*/
|
||||
export class EnrollCredentialDto {
|
||||
@IsIn(['reference', 'intake'])
|
||||
mode!: 'reference' | 'intake';
|
||||
|
||||
@ValidateIf((o: EnrollCredentialDto) => o.mode === 'intake')
|
||||
@IsIn(['api_key'])
|
||||
type?: 'api_key';
|
||||
|
||||
@ValidateIf((o: EnrollCredentialDto) => o.mode === 'intake')
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(4096)
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export class EnrollAgentDto {
|
||||
/** Registered harness name; a well-formed name missing from the registry is `precondition_failed`. */
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
harness!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
name!: string;
|
||||
|
||||
/** Stored as the agent's system prompt; null/absent leaves it unset. */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(20000)
|
||||
persona?: string | null;
|
||||
|
||||
/** Provider-qualified model id. */
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
model!: string;
|
||||
|
||||
/** Names the credential's provider. */
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(200)
|
||||
provider!: string;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => EnrollCredentialDto)
|
||||
credential!: EnrollCredentialDto;
|
||||
|
||||
/** REQUIRED — contract 3 §4.3, ratified into contract 5 §4 via §7 item 4. */
|
||||
@IsUUID()
|
||||
idempotencyKey!: string;
|
||||
|
||||
/** Optional; generated when absent (contract 5 §4.3). */
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
correlationId?: string;
|
||||
|
||||
/** Only 'actor-bound' is admissible on this family — see module doc. */
|
||||
@IsOptional()
|
||||
@IsIn(['actor-bound'])
|
||||
replayMode?: 'actor-bound';
|
||||
}
|
||||
|
||||
/** Query envelope for agent.enrollment.get (design §3.2): correlation only, no idempotency key. */
|
||||
export class GetEnrollmentQueryDto {
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
correlationId?: string;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HarnessModule } from '../harness/harness.module.js';
|
||||
import { EnrollmentController } from './enrollment.controller.js';
|
||||
import { EnrollmentRepository } from './enrollment.repository.js';
|
||||
import { EnrollmentService } from './enrollment.service.js';
|
||||
|
||||
/**
|
||||
* Agent enrollment command family (M4-4b; design
|
||||
* docs/plans/2026-08-29-agent-enrollment-command-design.md). Imports
|
||||
* HarnessModule for the live harness registry — the validation source for
|
||||
* the `harness` field (a well-formed name the registry does not know is a
|
||||
* precondition failure). EnrollmentRepository is the family's sole writer;
|
||||
* every mutation runs fence-check → mutate → audit + outbox in one
|
||||
* transaction.
|
||||
*/
|
||||
@Module({
|
||||
imports: [HarnessModule],
|
||||
controllers: [EnrollmentController],
|
||||
providers: [EnrollmentRepository, EnrollmentService],
|
||||
exports: [EnrollmentRepository],
|
||||
})
|
||||
export class EnrollmentModule {}
|
||||
@@ -0,0 +1,538 @@
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { seal } from '@mosaicstack/auth';
|
||||
import {
|
||||
agentAuditEvents,
|
||||
agentIdempotencyFence,
|
||||
agentOutbox,
|
||||
agents,
|
||||
and,
|
||||
eq,
|
||||
providerCredentials,
|
||||
users,
|
||||
type Db,
|
||||
} from '@mosaicstack/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import type { HarnessRegistry } from '../harness/harness.registry.js';
|
||||
import { HARNESS_REGISTRY } from '../harness/harness.tokens.js';
|
||||
|
||||
/**
|
||||
* Agent enrollment command repository (design
|
||||
* docs/plans/2026-08-29-agent-enrollment-command-design.md §3; contract 5 §4
|
||||
* envelope; contract 3 §4.3 idempotency fence, ratified via §7 item 4).
|
||||
*
|
||||
* The ONLY writer of the enrollment family's tables (`agent_audit_events`,
|
||||
* `agent_outbox`, `agent_idempotency_fence`) and the only path that sets
|
||||
* `agents.harness`/`agents.enrolled_at`. Every enroll runs one transaction:
|
||||
* fence check → (replay | credential handling → agent insert → fence insert →
|
||||
* audit event + outbox), so state, fence, event, and outbox commit or roll
|
||||
* back together (§3.1 rule 6).
|
||||
*
|
||||
* Authorization (v1, §3.1 rule 4) is the AuthGuard-authenticated actor — no
|
||||
* hierarchy grant is consulted because v1 enrollment binds no hierarchy node.
|
||||
* The recorded fence authorization scope is therefore the constant
|
||||
* platform-user identity domain (§3.1 rule 5).
|
||||
*
|
||||
* Never-echo (§3.1 rule 1): the credential value reaches exactly one sink —
|
||||
* the sealed store write — and appears in no result, audit payload, outbox
|
||||
* row, or log line. Log lines here carry correlation ids and error names
|
||||
* only, never request fields.
|
||||
*
|
||||
* The single-write helper methods (writeSealedCredential, insertAgentRow,
|
||||
* insertFenceRow, appendEvent, insertOutboxRow) are ordinary decomposition;
|
||||
* the atomicity witnesses (§5.6) spy on them to inject faults at each write
|
||||
* point without any test-only production switch.
|
||||
*/
|
||||
|
||||
export const ENROLLMENT_OPERATION = 'agent.enroll';
|
||||
/** §3.1 rule 5: v1 authorization is grant-free, so the scope is the authenticated-user identity domain. */
|
||||
const AUTHORIZATION_SCOPE = 'platform-user';
|
||||
/** The single bounded collision shape (§3.1 rule 5): constant, identifying no record. */
|
||||
const CONFLICT_MESSAGE = 'idempotency conflict';
|
||||
/** One fixed message for every not_found cause — missing and unauthorized are indistinguishable (§3.2). */
|
||||
const NOT_FOUND_MESSAGE = 'agent not found';
|
||||
|
||||
/** Closed per-family error enum (§3.3). 401 is produced by AuthGuard; 403 folds to not_found (§3.2). */
|
||||
export type EnrollmentErrorCode =
|
||||
| 'validation_failed'
|
||||
| 'authentication_failed'
|
||||
| 'authorization_refused'
|
||||
| 'not_found'
|
||||
| 'conflict'
|
||||
| 'precondition_failed'
|
||||
| 'internal_fault';
|
||||
|
||||
export interface EnrollmentFailure {
|
||||
readonly ok: false;
|
||||
readonly error: EnrollmentErrorCode;
|
||||
readonly message: string;
|
||||
/** Refusals carry the correlation id too (contract 5 §4.3 end-to-end traceability). */
|
||||
readonly correlationId: string;
|
||||
}
|
||||
|
||||
export type EnrollmentResult<T> =
|
||||
| ({ readonly ok: true; readonly correlationId: string } & T)
|
||||
| EnrollmentFailure;
|
||||
|
||||
/** The persisted agent row; the table stores no credential material (§3.1 rule 1). */
|
||||
export interface EnrolledAgentView {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly provider: string;
|
||||
readonly model: string;
|
||||
readonly status: string;
|
||||
readonly harness: string | null;
|
||||
readonly persona: string | null;
|
||||
readonly ownerId: string | null;
|
||||
readonly enrolledAt: string | null;
|
||||
readonly createdAt: string;
|
||||
}
|
||||
|
||||
export interface EnrollCredentialInput {
|
||||
readonly mode: 'reference' | 'intake';
|
||||
readonly type?: 'api_key';
|
||||
readonly value?: string;
|
||||
}
|
||||
|
||||
export interface EnrollAgentInput {
|
||||
readonly actorId: string;
|
||||
readonly harness: string;
|
||||
readonly name: string;
|
||||
readonly persona?: string | null;
|
||||
readonly model: string;
|
||||
readonly provider: string;
|
||||
readonly credential: EnrollCredentialInput;
|
||||
readonly idempotencyKey: string;
|
||||
readonly correlationId?: string;
|
||||
/** Defense in depth below the DTO: anything but 'actor-bound' is refused (seed-only rule). */
|
||||
readonly replayMode?: string;
|
||||
}
|
||||
|
||||
type Tx = Pick<Db, 'insert' | 'select' | 'update' | 'delete'>;
|
||||
type AgentRow = typeof agents.$inferSelect;
|
||||
type FenceRow = typeof agentIdempotencyFence.$inferSelect;
|
||||
|
||||
/** Raised inside the transaction when the fence insert lost a same-key race (§3.1 rule 5 concurrency). */
|
||||
class ConcurrentEnrollmentError extends Error {
|
||||
constructor() {
|
||||
super('concurrent enrollment lost the fence race');
|
||||
this.name = 'ConcurrentEnrollmentError';
|
||||
}
|
||||
}
|
||||
|
||||
function agentView(row: AgentRow): EnrolledAgentView {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
provider: row.provider,
|
||||
model: row.model,
|
||||
status: row.status,
|
||||
harness: row.harness,
|
||||
persona: row.systemPrompt,
|
||||
ownerId: row.ownerId,
|
||||
enrolledAt: row.enrolledAt?.toISOString() ?? null,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Key-order-independent serialization (jsonb precedent in hierarchy-audit). */
|
||||
function canonicalJson(value: unknown): string {
|
||||
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
||||
if (value !== null && typeof value === 'object') {
|
||||
const record = value as Record<string, unknown>;
|
||||
const body = Object.keys(record)
|
||||
.sort()
|
||||
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
||||
.join(',');
|
||||
return `{${body}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
interface NormalizedEnrollment {
|
||||
readonly actorId: string;
|
||||
readonly harness: string;
|
||||
readonly name: string;
|
||||
readonly persona: string | null;
|
||||
readonly model: string;
|
||||
readonly provider: string;
|
||||
readonly credential: EnrollCredentialInput;
|
||||
readonly idempotencyKey: string;
|
||||
readonly correlationId: string;
|
||||
readonly digest: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalized-payload digest (§3.1 rule 5). The input EXCLUDES the
|
||||
* credential value by construction: it covers mode and declared type only —
|
||||
* plaintext never reaches the hash.
|
||||
*/
|
||||
function digestOf(
|
||||
input: Omit<NormalizedEnrollment, 'actorId' | 'idempotencyKey' | 'correlationId' | 'digest'>,
|
||||
): string {
|
||||
const canonical = canonicalJson({
|
||||
harness: input.harness,
|
||||
name: input.name,
|
||||
persona: input.persona,
|
||||
model: input.model,
|
||||
provider: input.provider,
|
||||
credential: { mode: input.credential.mode, type: input.credential.type ?? null },
|
||||
});
|
||||
return createHash('sha256').update(canonical).digest('hex');
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EnrollmentRepository {
|
||||
private readonly logger = new Logger(EnrollmentRepository.name);
|
||||
|
||||
constructor(
|
||||
@Inject(DB) private readonly db: Db,
|
||||
@Inject(HARNESS_REGISTRY) private readonly registry: HarnessRegistry,
|
||||
) {}
|
||||
|
||||
async enroll(input: EnrollAgentInput): Promise<EnrollmentResult<{ agent: EnrolledAgentView }>> {
|
||||
const correlationId = input.correlationId ?? randomUUID();
|
||||
const fail = (error: EnrollmentErrorCode, message: string): EnrollmentFailure => ({
|
||||
ok: false,
|
||||
error,
|
||||
message,
|
||||
correlationId,
|
||||
});
|
||||
|
||||
const harness = input.harness.trim();
|
||||
const name = input.name.trim();
|
||||
if (harness.length === 0) return fail('validation_failed', 'harness must be non-empty');
|
||||
if (name.length === 0 || name.length > 200) {
|
||||
return fail('validation_failed', 'name must be non-empty and at most 200 characters');
|
||||
}
|
||||
if (input.replayMode !== undefined && input.replayMode !== 'actor-bound') {
|
||||
// Seed-only rule (contract 3 §4.3): refused with nothing executed and no fence row.
|
||||
return fail('validation_failed', 'replayMode must be actor-bound');
|
||||
}
|
||||
if (input.credential.mode === 'reference') {
|
||||
if (input.credential.type !== undefined || input.credential.value !== undefined) {
|
||||
return fail('validation_failed', 'a reference credential carries no type or value');
|
||||
}
|
||||
} else if (
|
||||
input.credential.type !== 'api_key' ||
|
||||
typeof input.credential.value !== 'string' ||
|
||||
input.credential.value.length === 0
|
||||
) {
|
||||
return fail('validation_failed', 'an intake credential requires type api_key and a value');
|
||||
}
|
||||
// Syntactic validity ends above; a well-formed name the live registry
|
||||
// does not know is a precondition failure (§3.1 table).
|
||||
if (!this.registry.has(harness)) {
|
||||
return fail('precondition_failed', 'harness is not registered');
|
||||
}
|
||||
|
||||
const normalized: NormalizedEnrollment = {
|
||||
actorId: input.actorId,
|
||||
harness,
|
||||
name,
|
||||
persona: input.persona ?? null,
|
||||
model: input.model,
|
||||
provider: input.provider,
|
||||
credential: input.credential,
|
||||
idempotencyKey: input.idempotencyKey,
|
||||
correlationId,
|
||||
digest: digestOf({
|
||||
harness,
|
||||
name,
|
||||
persona: input.persona ?? null,
|
||||
model: input.model,
|
||||
provider: input.provider,
|
||||
credential: input.credential,
|
||||
}),
|
||||
};
|
||||
|
||||
// Two attempts: a fence-race loser's transaction rolls back and the retry
|
||||
// resolves through the replay path against the winner's committed row —
|
||||
// or executes afresh if the winner aborted (§3.1 rule 5 concurrency). A
|
||||
// unique-violation race never surfaces as an unhandled internal fault.
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
try {
|
||||
return await this.db.transaction(async (tx) => this.enrollTx(tx, normalized));
|
||||
} catch (error) {
|
||||
if (error instanceof ConcurrentEnrollmentError && attempt === 0) continue;
|
||||
if (error instanceof ConcurrentEnrollmentError) {
|
||||
return fail('conflict', CONFLICT_MESSAGE);
|
||||
}
|
||||
// §4.4 fail-closed: whatever broke, the transaction rolled back and
|
||||
// the refusal is the internal-fault class — no fallback write or read.
|
||||
this.logger.error(
|
||||
`agent.enroll failed closed (correlation=${correlationId}): ${
|
||||
error instanceof Error ? error.name : 'unknown error'
|
||||
}`,
|
||||
);
|
||||
return fail('internal_fault', 'internal fault');
|
||||
}
|
||||
}
|
||||
return fail('internal_fault', 'internal fault');
|
||||
}
|
||||
|
||||
private async enrollTx(
|
||||
tx: Tx,
|
||||
input: NormalizedEnrollment,
|
||||
): Promise<EnrollmentResult<{ agent: EnrolledAgentView }>> {
|
||||
const fence = await this.fenceFor(tx, input.idempotencyKey);
|
||||
if (fence) return this.replay(tx, fence, input);
|
||||
|
||||
if (input.credential.mode === 'reference') {
|
||||
// §3.1 rule 3: the reference must resolve for (actor, provider).
|
||||
const existing = await tx
|
||||
.select({ id: providerCredentials.id })
|
||||
.from(providerCredentials)
|
||||
.where(
|
||||
and(
|
||||
eq(providerCredentials.userId, input.actorId),
|
||||
eq(providerCredentials.provider, input.provider),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'precondition_failed',
|
||||
message: 'credential reference does not resolve',
|
||||
correlationId: input.correlationId,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// §3.1 rule 2: sealed-store write inside THIS transaction — a later
|
||||
// failure rolls it back, leaving no orphan credential.
|
||||
await this.writeSealedCredential(
|
||||
tx,
|
||||
input.actorId,
|
||||
input.provider,
|
||||
input.credential.value as string,
|
||||
);
|
||||
}
|
||||
|
||||
const agentRow = await this.insertAgentRow(tx, input);
|
||||
const fenceRow = await this.insertFenceRow(tx, input, agentRow.id);
|
||||
if (!fenceRow) {
|
||||
// A same-(operation, key) winner committed first; abandon our writes.
|
||||
throw new ConcurrentEnrollmentError();
|
||||
}
|
||||
await this.appendEvent(tx, {
|
||||
eventType: 'agent.enrolled',
|
||||
actorId: input.actorId,
|
||||
agentId: agentRow.id,
|
||||
correlationId: input.correlationId,
|
||||
// §3.1 rule 6 payload: harness, provider, name, credentialMode — no credential material.
|
||||
payload: {
|
||||
harness: input.harness,
|
||||
provider: input.provider,
|
||||
name: input.name,
|
||||
credentialMode: input.credential.mode,
|
||||
},
|
||||
});
|
||||
return { ok: true, correlationId: input.correlationId, agent: agentView(agentRow) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay path (§3.1 rule 5): a fresh submission of a recorded
|
||||
* (operation, key). The actor is re-authorized exactly as a fresh
|
||||
* submission (v1: authenticated actor — the guard already ran); then mode,
|
||||
* scope, digest, and recorded-actor equality; then target-result read
|
||||
* authority (owner or admin) on the referenced agent. ANY failure refuses
|
||||
* with the single bounded conflict shape — constant, identifying no record.
|
||||
* A passing replay executes nothing and appends only the non-mutation
|
||||
* access event (with its outbox record — one outbox row per event).
|
||||
*/
|
||||
private async replay(
|
||||
tx: Tx,
|
||||
fence: FenceRow,
|
||||
input: NormalizedEnrollment,
|
||||
): Promise<EnrollmentResult<{ agent: EnrolledAgentView }>> {
|
||||
const collision: EnrollmentFailure = {
|
||||
ok: false,
|
||||
error: 'conflict',
|
||||
message: CONFLICT_MESSAGE,
|
||||
correlationId: input.correlationId,
|
||||
};
|
||||
if (fence.replayMode !== 'actor-bound') return collision;
|
||||
if (fence.authorizationScope !== AUTHORIZATION_SCOPE) return collision;
|
||||
if (fence.payloadDigest !== input.digest) return collision;
|
||||
if (fence.actorId !== input.actorId) return collision;
|
||||
|
||||
const rows = await tx.select().from(agents).where(eq(agents.id, fence.outcomeAgentId)).limit(1);
|
||||
const agentRow = rows[0];
|
||||
if (!agentRow) return collision;
|
||||
const authorized =
|
||||
agentRow.ownerId === input.actorId || (await this.isPlatformAdmin(tx, input.actorId));
|
||||
if (!authorized) return collision;
|
||||
|
||||
await this.appendEvent(tx, {
|
||||
eventType: 'agent.enrollment.replayed',
|
||||
actorId: input.actorId,
|
||||
agentId: agentRow.id,
|
||||
correlationId: input.correlationId,
|
||||
payload: { fenceId: fence.id },
|
||||
});
|
||||
return { ok: true, correlationId: input.correlationId, agent: agentView(agentRow) };
|
||||
}
|
||||
|
||||
/**
|
||||
* agent.enrollment.get (§3.2): owner-or-admin read. Unauthorized and
|
||||
* missing fold to the same not_found wire shape (no existence oracle).
|
||||
*/
|
||||
async getEnrollment(
|
||||
actorId: string,
|
||||
agentId: string,
|
||||
correlationId?: string,
|
||||
): Promise<EnrollmentResult<{ agent: EnrolledAgentView }>> {
|
||||
const resolvedCorrelation = correlationId ?? randomUUID();
|
||||
try {
|
||||
const rows = await this.db.select().from(agents).where(eq(agents.id, agentId)).limit(1);
|
||||
const row = rows[0];
|
||||
if (row) {
|
||||
const authorized =
|
||||
row.ownerId === actorId || (await this.isPlatformAdmin(this.db, actorId));
|
||||
if (authorized) {
|
||||
return { ok: true, correlationId: resolvedCorrelation, agent: agentView(row) };
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: 'not_found',
|
||||
message: NOT_FOUND_MESSAGE,
|
||||
correlationId: resolvedCorrelation,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`agent.enrollment.get failed closed (correlation=${resolvedCorrelation}): ${
|
||||
error instanceof Error ? error.name : 'unknown error'
|
||||
}`,
|
||||
);
|
||||
return {
|
||||
ok: false,
|
||||
error: 'internal_fault',
|
||||
message: 'internal fault',
|
||||
correlationId: resolvedCorrelation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async fenceFor(tx: Tx, idempotencyKey: string): Promise<FenceRow | null> {
|
||||
const rows = await tx
|
||||
.select()
|
||||
.from(agentIdempotencyFence)
|
||||
.where(
|
||||
and(
|
||||
eq(agentIdempotencyFence.operation, ENROLLMENT_OPERATION),
|
||||
eq(agentIdempotencyFence.idempotencyKey, idempotencyKey),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
private async isPlatformAdmin(tx: Tx, actorId: string): Promise<boolean> {
|
||||
const rows = await tx
|
||||
.select({ role: users.role })
|
||||
.from(users)
|
||||
.where(eq(users.id, actorId))
|
||||
.limit(1);
|
||||
return rows[0]?.role === 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealed intake write, mirroring ProviderCredentialsService.store semantics
|
||||
* (seal-at-rest, one row per (userId, provider)) but on the enrollment
|
||||
* transaction (§3.1 rule 2). The plaintext exists only in this frame.
|
||||
*/
|
||||
async writeSealedCredential(
|
||||
tx: Tx,
|
||||
userId: string,
|
||||
provider: string,
|
||||
value: string,
|
||||
): Promise<void> {
|
||||
const encryptedValue = seal(value);
|
||||
await tx
|
||||
.insert(providerCredentials)
|
||||
.values({ userId, provider, credentialType: 'api_key', encryptedValue, metadata: null })
|
||||
.onConflictDoUpdate({
|
||||
target: [providerCredentials.userId, providerCredentials.provider],
|
||||
set: {
|
||||
credentialType: 'api_key',
|
||||
encryptedValue,
|
||||
metadata: null,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async insertAgentRow(tx: Tx, input: NormalizedEnrollment): Promise<AgentRow> {
|
||||
const rows = await tx
|
||||
.insert(agents)
|
||||
.values({
|
||||
name: input.name,
|
||||
provider: input.provider,
|
||||
model: input.model,
|
||||
harness: input.harness,
|
||||
systemPrompt: input.persona,
|
||||
// §3.1 rule 4: owner is the authenticated actor; is_system stays default false.
|
||||
ownerId: input.actorId,
|
||||
enrolledAt: new Date(),
|
||||
})
|
||||
.returning();
|
||||
const row = rows[0];
|
||||
if (!row) throw new Error('agent insert returned no row');
|
||||
return row;
|
||||
}
|
||||
|
||||
async insertFenceRow(
|
||||
tx: Tx,
|
||||
input: NormalizedEnrollment,
|
||||
outcomeAgentId: string,
|
||||
): Promise<FenceRow | null> {
|
||||
const rows = await tx
|
||||
.insert(agentIdempotencyFence)
|
||||
.values({
|
||||
operation: ENROLLMENT_OPERATION,
|
||||
idempotencyKey: input.idempotencyKey,
|
||||
actorId: input.actorId,
|
||||
authorizationScope: AUTHORIZATION_SCOPE,
|
||||
payloadDigest: input.digest,
|
||||
replayMode: 'actor-bound',
|
||||
outcomeAgentId,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
.returning();
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
/** Append one audit event and its outbox record on the caller's transaction (one outbox row per event). */
|
||||
async appendEvent(
|
||||
tx: Tx,
|
||||
input: {
|
||||
eventType: 'agent.enrolled' | 'agent.enrollment.replayed';
|
||||
actorId: string;
|
||||
agentId: string;
|
||||
correlationId: string;
|
||||
payload: Record<string, unknown>;
|
||||
causationId?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const inserted = await tx
|
||||
.insert(agentAuditEvents)
|
||||
.values({
|
||||
eventType: input.eventType,
|
||||
actorId: input.actorId,
|
||||
agentId: input.agentId,
|
||||
correlationId: input.correlationId,
|
||||
causationId: input.causationId ?? null,
|
||||
payload: input.payload,
|
||||
})
|
||||
.returning();
|
||||
const event = inserted[0];
|
||||
if (!event) throw new Error('agent audit event insert returned no row');
|
||||
await this.insertOutboxRow(tx, event.id, input.correlationId);
|
||||
}
|
||||
|
||||
async insertOutboxRow(tx: Tx, eventId: string, correlationId: string): Promise<void> {
|
||||
await tx.insert(agentOutbox).values({ eventId, correlationId });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import type {
|
||||
EnrollmentErrorCode,
|
||||
EnrollmentFailure,
|
||||
EnrollmentResult,
|
||||
} from './enrollment.repository.js';
|
||||
|
||||
/**
|
||||
* Maps enrollment result unions onto the closed HTTP status set (design
|
||||
* docs/plans/2026-08-29-agent-enrollment-command-design.md §3.3, contract 5
|
||||
* §4.2). Every refusal body carries the correlation id (contract 5 §4.3
|
||||
* end-to-end traceability) alongside the enum code. `not_found` carries one
|
||||
* fixed message for every cause — missing agent and unauthorized caller are
|
||||
* indistinguishable on the wire (§3.2).
|
||||
*/
|
||||
const HTTP_STATUS: Record<EnrollmentErrorCode, HttpStatus> = {
|
||||
validation_failed: HttpStatus.BAD_REQUEST,
|
||||
authentication_failed: HttpStatus.UNAUTHORIZED,
|
||||
authorization_refused: HttpStatus.FORBIDDEN,
|
||||
not_found: HttpStatus.NOT_FOUND,
|
||||
conflict: HttpStatus.CONFLICT,
|
||||
precondition_failed: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
internal_fault: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class EnrollmentService {
|
||||
unwrap<T>(result: EnrollmentResult<T>): { ok: true; correlationId: string } & T {
|
||||
if (result.ok) return result;
|
||||
throw this.toException(result);
|
||||
}
|
||||
|
||||
private toException(failure: EnrollmentFailure): HttpException {
|
||||
const status = HTTP_STATUS[failure.error];
|
||||
return new HttpException(
|
||||
{
|
||||
statusCode: status,
|
||||
error: failure.error,
|
||||
message: failure.message,
|
||||
correlationId: failure.correlationId,
|
||||
},
|
||||
status,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,11 @@ import {
|
||||
TransferEstateDto,
|
||||
TransferPlatformProjectDto,
|
||||
} from './hierarchy/hierarchy.dto.js';
|
||||
import {
|
||||
EnrollAgentDto,
|
||||
EnrollCredentialDto,
|
||||
GetEnrollmentQueryDto,
|
||||
} from './enrollment/enrollment.dto.js';
|
||||
|
||||
/**
|
||||
* Boot-time self-check: the global ValidationPipe must be able to SEE the
|
||||
@@ -105,6 +110,31 @@ export const PIPE_GUARDED_DTOS: Array<{
|
||||
target: ChangeGrantDto,
|
||||
properties: ['role', 'idempotencyKey'],
|
||||
},
|
||||
{
|
||||
name: 'EnrollAgentDto',
|
||||
target: EnrollAgentDto,
|
||||
properties: [
|
||||
'harness',
|
||||
'name',
|
||||
'persona',
|
||||
'model',
|
||||
'provider',
|
||||
'credential',
|
||||
'idempotencyKey',
|
||||
'correlationId',
|
||||
'replayMode',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'EnrollCredentialDto',
|
||||
target: EnrollCredentialDto,
|
||||
properties: ['mode', 'type', 'value'],
|
||||
},
|
||||
{
|
||||
name: 'GetEnrollmentQueryDto',
|
||||
target: GetEnrollmentQueryDto,
|
||||
properties: ['correlationId'],
|
||||
},
|
||||
];
|
||||
|
||||
export class PipeMetatypeCheckError extends Error {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Explicit, single-seat dogfood mode for stack-containerization B2.
|
||||
# Use with docker-compose.yml. The base stack remains credential-free.
|
||||
services:
|
||||
gateway:
|
||||
# The R4 credential helper establishes ownership from process ancestry and
|
||||
# intentionally does not trust PID 1. Keep gateway Node below Docker's init.
|
||||
init: true
|
||||
environment:
|
||||
# Identity and credential layout match a fleet seat. This fixed name prevents
|
||||
# an operator from mounting one seat while attributing actions to another.
|
||||
MOSAIC_AGENT_NAME: code-dogfood-01
|
||||
MOSAIC_GIT_IDENTITY: code-dogfood-01
|
||||
MOSAIC_BRAIN_HOME: /opt/mosaic/brain
|
||||
AGENT_FILE_SANDBOX_DIR: /workspace/stack
|
||||
# Disable the general shell before admin/user allowlist resolution. Delivery
|
||||
# uses execFile-only tools bound to the queue and PR wrappers below.
|
||||
AGENT_SHELL_ENABLED: 'false'
|
||||
AGENT_DELIVERY_ENABLED: 'true'
|
||||
MOSAIC_GIT_TOOLS_DIR: /opt/mosaic/tools/git
|
||||
MOSAIC_INTEGRATION_TRUNK: next
|
||||
AGENT_USER_TOOLS: fs_read_file,fs_write_file,fs_list_directory,fs_edit_file,git_status,git_log,git_diff,git_publish_branch,git_open_pull_request
|
||||
volumes:
|
||||
# Mount a dedicated worktree, never the canonical clone or divergent local main.
|
||||
- type: bind
|
||||
source: ${MOSAIC_DOGFOOD_WORKTREE:?set to a dedicated next-based stack worktree}
|
||||
target: /workspace/stack
|
||||
# A Git worktree's .git file points into the canonical clone's common Git
|
||||
# directory. Mount that directory at its original absolute path so Git can
|
||||
# resolve the pointer. File tools cannot traverse outside /workspace/stack.
|
||||
- type: bind
|
||||
source: ${MOSAIC_DOGFOOD_COMMON_GIT_DIR:?set to the canonical stack clone .git directory}
|
||||
target: ${MOSAIC_DOGFOOD_COMMON_GIT_DIR:?set to the canonical stack clone .git directory}
|
||||
# Only this seat home enters the container. Other fleet credentials stay outside.
|
||||
- type: bind
|
||||
source: ${MOSAIC_DOGFOOD_SEAT_HOME:?set to the external code-dogfood-01 seat directory}
|
||||
target: /opt/mosaic/brain/fleet/agents/code-dogfood-01
|
||||
read_only: true
|
||||
@@ -47,6 +47,44 @@ services:
|
||||
environment:
|
||||
COLLECTOR_OTLP_ENABLED: 'true'
|
||||
|
||||
|
||||
gateway:
|
||||
# Standalone-tier application service (compose `stack` profile).
|
||||
# Default image = local build of docker/gateway.Dockerfile (works with
|
||||
# no registry auth); override GATEWAY_IMAGE to a CI-published sha tag
|
||||
# for registry deploys (git.mosaicstack.dev/mosaicstack/stack/gateway:sha-XXXXXXX).
|
||||
profiles: [stack]
|
||||
image: ${GATEWAY_IMAGE:-mosaic-gateway:dev}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/gateway.Dockerfile
|
||||
ports:
|
||||
- '${GATEWAY_HOST_PORT:-14242}:14242'
|
||||
environment:
|
||||
GATEWAY_PORT: '14242'
|
||||
DATABASE_URL: postgresql://mosaic:mosaic@postgres:5432/mosaic
|
||||
VALKEY_URL: valkey://valkey:6379
|
||||
# The compose IS the standalone tier by declaration (mode contract:
|
||||
# mode chosen at install); pinning skips cross-container probe races.
|
||||
MOSAIC_STORAGE_TIER: standalone
|
||||
# Standalone-tier secrets: generated at install (see .env.example).
|
||||
# Enterprise tier replaces these with Vault/Openbao plumbing.
|
||||
BETTER_AUTH_SECRET: '${BETTER_AUTH_SECRET:?set in .env — openssl rand -hex 32}'
|
||||
volumes:
|
||||
- gateway_workspaces:/opt/mosaic/.workspaces
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:14242/health || exit 1']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 20s
|
||||
|
||||
volumes:
|
||||
gateway_workspaces:
|
||||
pg_data:
|
||||
valkey_data:
|
||||
|
||||
@@ -29,11 +29,29 @@ ENV NODE_ENV=production
|
||||
# $MOSAIC_ROOT/.workspaces (apps/gateway/src/workspace/workspace.service.ts);
|
||||
# mount a volume over /opt/mosaic to persist workspaces across container restarts.
|
||||
# Intentionally unpinned: Alpine's signed repository is the trust anchor; pinning
|
||||
# git was declined so routine base-image security updates remain maintainable.
|
||||
RUN apk add --no-cache git \
|
||||
# packages was declined so routine base-image security updates remain maintainable.
|
||||
# bash/curl/python3 are runtime dependencies of the provider-neutral Mosaic git
|
||||
# wrappers. jq supports wrapper discovery for non-canonical Gitea hosts.
|
||||
RUN apk add --no-cache bash curl git jq python3 \
|
||||
&& mkdir -p /opt/mosaic/.workspaces \
|
||||
&& chown -R node:node /opt/mosaic /app
|
||||
ENV MOSAIC_ROOT=/opt/mosaic
|
||||
# Dogfood agents use the same fail-closed credential helper, queue guard, and
|
||||
# PR-create wrapper as fleet seats. Copy only those operations and their shared
|
||||
# dependencies. Merge and infrastructure tools stay out of the image.
|
||||
COPY --from=builder /app/packages/mosaic/framework/tools/git/pr-create.sh /opt/mosaic/tools/git/pr-create.sh
|
||||
COPY --from=builder /app/packages/mosaic/framework/tools/git/ci-queue-wait.sh /opt/mosaic/tools/git/ci-queue-wait.sh
|
||||
COPY --from=builder /app/packages/mosaic/framework/tools/git/detect-platform.sh /opt/mosaic/tools/git/detect-platform.sh
|
||||
COPY --from=builder /app/packages/mosaic/framework/tools/git/repo-decl.sh /opt/mosaic/tools/git/repo-decl.sh
|
||||
COPY --from=builder /app/packages/mosaic/framework/tools/git/git-credential-mosaic /opt/mosaic/tools/git/git-credential-mosaic
|
||||
# R4 hardening (P0-SEC, brain 15f6979a): the credential helper is a pair.
|
||||
# python entrypoint (allowlist envp, execve boundary) + the bash implementation
|
||||
# it execs. The entrypoint derives the .impl path from its own directory, so the
|
||||
# pair sits side by side; system gitconfig keeps pointing at the entrypoint.
|
||||
COPY --from=builder /app/packages/mosaic/framework/tools/git/git-credential-mosaic.impl /opt/mosaic/tools/git/git-credential-mosaic.impl
|
||||
COPY --from=builder /app/packages/mosaic/framework/tools/_lib/credentials.sh /opt/mosaic/tools/_lib/credentials.sh
|
||||
COPY --from=builder /app/packages/mosaic/framework/tools/structure/validate-repo-json.sh /opt/mosaic/tools/structure/validate-repo-json.sh
|
||||
RUN git config --system credential.helper /opt/mosaic/tools/git/git-credential-mosaic
|
||||
# Use the pnpm deploy output — resolves all deps into a flat, self-contained node_modules
|
||||
COPY --chown=node:node --from=builder /deploy/node_modules ./node_modules
|
||||
COPY --chown=node:node --from=builder /deploy/package.json ./package.json
|
||||
|
||||
@@ -81,4 +81,4 @@ The page may be promoted to an operative runbook only after deny-all is intentio
|
||||
## Related contract
|
||||
|
||||
- [M1 logical identity and fencing decision](../../DEVELOPER-GUIDE/architecture/decisions/mos-runtime-portability-m1.md)
|
||||
- [MOS-PORT requirements](../../PRD.md#mos-runtime-portability-workstream-mos-port)
|
||||
- [MOS-PORT requirements](../../PRDs/2026-08-31_PRD_rev1/GOV.4-workstream-contracts.md#mos-runtime-portability-workstream-mos-port)
|
||||
|
||||
@@ -98,4 +98,4 @@ A valid lease or grant is therefore not a claim of exactly-once delivery, produc
|
||||
## Related contract
|
||||
|
||||
- [M1 connector lease operations — held/non-operative](../../../ADMIN-GUIDE/operations/mos-connector-lease-operations.md)
|
||||
- [MOS-PORT requirements](../../../PRD.md#mos-runtime-portability-workstream-mos-port)
|
||||
- [MOS-PORT requirements](../../../PRDs/2026-08-31_PRD_rev1/GOV.4-workstream-contracts.md#mos-runtime-portability-workstream-mos-port)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
kind: tracking
|
||||
status: active
|
||||
status: superseded
|
||||
---
|
||||
|
||||
> **Superseded (2026-09-01, PRD rev1 ratification).** This document's federated-tier-as-canonical-MVP-deployment-topology and Federation-v1-as-top-priority framing is historical. Federation M1–M3 are shipped but **frozen** (dormant since 2026-06-25, excluded from the v1 bar, security re-audit gate before any resumption); the canonical v1 deployment topology is the compose standalone tier (PRD rev1, D15). Authority: `docs/PRD.md` → `docs/PRDs/2026-08-31_PRD_rev1/` (decision D3 as amended, GOV.5 Q-T1). Tracking: `docs/fleet/NORTH_STAR.yaml` (dormant federation workstream). Content below is preserved verbatim as a record — do not edit it.
|
||||
|
||||
# Mission Manifest — MVP
|
||||
|
||||
> Top-level rollup tracking Mosaic Stack MVP execution.
|
||||
|
||||
+19
-888
@@ -1,893 +1,24 @@
|
||||
---
|
||||
kind: spec
|
||||
kind: shim
|
||||
status: active
|
||||
source_of_truth: true
|
||||
current_rev: docs/PRDs/2026-08-31_PRD_rev1/
|
||||
---
|
||||
|
||||
# PRD: Mosaic Stack — North Star
|
||||
|
||||
This document is the product source of truth for Mosaic Stack.
|
||||
|
||||
- **Part I** defines the product north star. It is written from the ratified
|
||||
decision set D1–D14 (operator decision session, 2026-08-25; decision owner
|
||||
Jason Woltje). Each section cites the decisions it implements.
|
||||
- **Part II** preserves the active workstream contracts unchanged. Open issues
|
||||
bind to them; this rewrite does not alter a single normative word in them.
|
||||
- The previous v0.1.0 beta PRD body is archived verbatim at
|
||||
[docs/archive/PRD-v0.1.md](./archive/PRD-v0.1.md) and is no longer authority.
|
||||
- The delivery roadmap lives in [docs/ROADMAP.md](./ROADMAP.md). Per D11, every
|
||||
planned phase appears there from day one, even as a placeholder.
|
||||
|
||||
## Metadata
|
||||
|
||||
- **Owner / decision authority:** Jason Woltje
|
||||
- **Status:** active (supersedes the v0.1.0 PRD as product authority)
|
||||
- **Date:** 2026-08-26
|
||||
- **Decision registry:** D1–D14, recorded in Part I §12
|
||||
- **SSOT rule:** this repository's `docs/` tree is the product source of truth
|
||||
(D5). Estate brains hold operational records, not product canon; only
|
||||
product-relevant material migrates here (D6).
|
||||
|
||||
---
|
||||
|
||||
## Part I — Product north star
|
||||
|
||||
### 1. What Mosaic Stack is (D1)
|
||||
|
||||
Mosaic Stack is an **open-source, AI-first platform for people who want a
|
||||
self-hosted environment for agentic management and a life operating system.**
|
||||
It serves personal, business, and employee needs from one deployment, and the
|
||||
work is offered freely.
|
||||
|
||||
"AI-first" means agents are first-class operators of the system, not a bolted-on
|
||||
chat box: the platform exists to let humans direct fleets of agents over their
|
||||
projects, tasks, communications, and infrastructure, with the same tools and
|
||||
the same guarantees whether a human or an agent is acting.
|
||||
|
||||
### 2. Who it is for (D1, D9)
|
||||
|
||||
The operator of a deployment is its user. Mosaic Stack is **not a hosted
|
||||
business**: running the system as a service for external customers is outside
|
||||
the north star. Multi-tenancy exists WITHIN a deployment so that one operator
|
||||
can separate their world — for example, several LLCs plus a personal domain —
|
||||
while every deployment is self-hosted by its own operator.
|
||||
|
||||
"Company" in the hierarchy is organizational separation for one operator's
|
||||
world, not a customer account.
|
||||
|
||||
### 3. Deployment modes (D3)
|
||||
|
||||
Two modes, chosen at install time:
|
||||
|
||||
| | Standalone / personal | Enterprise |
|
||||
| ------------------- | -------------------------------------- | ----------------------------------------------------- |
|
||||
| Brains | one mosaic-brain (system + user files) | system brain for config + one brain per user |
|
||||
| User-data isolation | single user | no user-data leakage between users; sharing is opt-in |
|
||||
| Secrets | OpenBao/Vault or flat files | OpenBao/Vault REQUIRED |
|
||||
| Conversion | Standalone → Enterprise, **one-way** | terminal state |
|
||||
|
||||
Brains are configurable as external git repositories (recommended, not
|
||||
required); git tracking is always on locally.
|
||||
|
||||
**Federation** (connecting deployments: system-level config, assigned users,
|
||||
rights and data-access control, trusts with boundaries, exfiltration
|
||||
monitoring) is intentionally not fully designed. It is deferred, appears on the
|
||||
roadmap as a placeholder phase per D11, and nothing in v1 may foreclose it.
|
||||
|
||||
### 4. Structure and tenancy (D2, D9, D13)
|
||||
|
||||
The hierarchy:
|
||||
|
||||
```
|
||||
company/organization (N per deployment)
|
||||
└─ estate (each in exactly one company)
|
||||
└─ project (each in exactly one estate)
|
||||
└─ workspace (project-specific; carries the Kanban)
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Users can create N companies, N estates, N projects.
|
||||
- Tasks bubble UP the hierarchy so whole-system status is visible at every
|
||||
level. Bubble-up is **read-only aggregation**, never a cross-workspace write.
|
||||
- Granular RBAC: admins restrict access per company, estate, and project;
|
||||
grants are evaluated down the chain. Assets are transferable subject to the
|
||||
structure.
|
||||
- **`workspace_id` remains the hard mechanical isolation unit** exactly as
|
||||
ratified in
|
||||
[docs/requirements/native-kanban-sot.md](./requirements/native-kanban-sot.md)
|
||||
(#751): PostgreSQL sole writable SOT, cross-workspace relationships rejected,
|
||||
fail-closed mutations. The hierarchy is parent structure ABOVE workspaces,
|
||||
used for RBAC evaluation and read-only roll-ups. The kanban SOT carries this
|
||||
as Amendment A1, added by reviewed PR — an amendment, not a rewrite (D13).
|
||||
|
||||
### 5. Identity (D10)
|
||||
|
||||
Built-in auth (better-auth) is the **account system of record**. Authentik and
|
||||
other external IdPs federate in via OIDC as login methods; they never become
|
||||
the system of record. Perimeter shims (forward-auth in front of a web host) are
|
||||
deployment workarounds, not the design.
|
||||
|
||||
### 6. Onboarding (D4)
|
||||
|
||||
Onboarding is a **wizard that differs by mode, is re-runnable (no lock-in), and
|
||||
is extensible** — new wizards attach as tabs.
|
||||
|
||||
Standalone flow captures: system and company name; component choices (Mosaic
|
||||
Comms/Matrix vs external; Mosaic SSO/Authentik vs external; Mosaic
|
||||
DB/PostgreSQL vs external; vector DB); the initial user
|
||||
(email/password/name/SSO); comms setup (Matrix/Discord/Slack); agent enrollment
|
||||
(harness choice and install, OAuth or API-key login, multi-account, model
|
||||
choice with recommendation, agent name and persona, account assignment,
|
||||
optional comms auto-enroll); a user onboarding profile (disabilities including
|
||||
ADHD/autism/PDA/vision, professional background, education, desired agent
|
||||
communication style, optional voice-matching interview, family/pets/friends/
|
||||
hobbies/likes-dislikes); email and drive connectors (Gmail/IMAP, Google
|
||||
Drive/OneDrive/Dropbox) with granular agentic-access consent; SSO/OIDC
|
||||
configuration; an initial estate, an initial project, and seeded example data.
|
||||
|
||||
Enterprise uses the same skeleton with personal data optional; the focus moves
|
||||
to business structure, org chart, RBAC, M365 and external systems, immediate
|
||||
OIDC, SSO prominent.
|
||||
|
||||
Profile answers feed `USER.md` and/or the user's data store subject to the
|
||||
custody rule in §7.
|
||||
|
||||
### 7. Data custody (D6, D14)
|
||||
|
||||
- **Sensitive profile categories** (disabilities, family, communication style,
|
||||
and similar) live in the **user's own brain ONLY**. PostgreSQL holds
|
||||
structural data, consent records, and pointers — never the content. "User
|
||||
data does not leak" is enforced by architecture, not policy (D14).
|
||||
- Standalone (one user, one brain) **may** keep the same split — D14 makes it
|
||||
optional in Standalone, not required. Keeping it is the recommended default
|
||||
because it preserves forward-compatibility with the one-way Enterprise
|
||||
conversion (D3).
|
||||
- Estate brains hold operational records. Only product-relevant material
|
||||
migrates into this repository's docs; operational records stay in their
|
||||
brains and are linked (D6).
|
||||
|
||||
### 8. Architecture gate — the webUI sits OVER official tooling (D8, D12)
|
||||
|
||||
**HARD RULE:** every webUI operation goes through the Gateway API backed by the
|
||||
same official framework tooling the CLI uses. The CLI remains the primary
|
||||
execution method; the webUI uses the tools to operate and configure the
|
||||
system. The webUI never bypasses tooling to reach the database or filesystem
|
||||
directly.
|
||||
|
||||
Consequence for planning: when a desired webUI operation has no backing tool,
|
||||
the gap is scored **"blocked on tooling"** and the tool is built first. The
|
||||
product baseline therefore always includes all three D8 inputs: the tool
|
||||
inventory (what exists and what is missing), the webUI→tool mapping, and the
|
||||
measured current state of the `next` branch.
|
||||
|
||||
### 9. v1 slice (D11)
|
||||
|
||||
v1 is deliberately small:
|
||||
|
||||
1. **Standalone onboarding wizard** — system/company name, component choices,
|
||||
initial user, initial estate + project, seeded examples, re-runnable.
|
||||
2. **Hierarchy core** — company → estate → project → workspace → kanban, with
|
||||
read-only task bubble-up.
|
||||
3. **Basic RBAC** on the hierarchy.
|
||||
4. **Minimal agent enrollment** — one harness, API key, name/persona.
|
||||
|
||||
Deferred beyond v1: connectors, comms integrations, voice-matching, M365,
|
||||
Enterprise conversion, federation. Every deferred item appears in
|
||||
[docs/ROADMAP.md](./ROADMAP.md) per the D11 rule: nothing exists only in heads.
|
||||
|
||||
### 10. Relationship to the fleet north star
|
||||
|
||||
[docs/fleet/NORTH_STAR.md](./fleet/NORTH_STAR.md) (generated from
|
||||
`docs/fleet/NORTH_STAR.yaml`) is the **delivery-fleet** north star: how the
|
||||
agent fleet that builds and operates the system should run (NS-1..NS-10,
|
||||
workstreams A–L). This PRD is the **product** north star. They are not
|
||||
competitors: the fleet north star is subordinate product-wise — its workstream
|
||||
J ("Web control plane") is one consumer of this PRD's D8/D12 gate — and this
|
||||
PRD does not redefine fleet invariants. The subordination rule is ratified in
|
||||
the frozen audit-input baseline (T2 operator freeze, 2026-08-25: "the PRD must
|
||||
cite and subordinate it, never fork it"). A change that would put the two in
|
||||
conflict must amend one of them explicitly, never fork a third document
|
||||
(drafting addition — see §12.1).
|
||||
|
||||
### 11. Explicit non-goals
|
||||
|
||||
- Hosted/SaaS operation for external customers (D9).
|
||||
- A webUI that writes to the database or filesystem around the tooling (D12).
|
||||
- A second writable task store beside PostgreSQL (native-kanban-sot invariants).
|
||||
- Fully-designed federation in v1 (D3 — roadmap placeholder only).
|
||||
|
||||
### 12. Decision registry
|
||||
|
||||
| ID | Decision (short form) |
|
||||
| --- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| D1 | Open-source, AI-first, self-hosted platform for agentic management + life OS |
|
||||
| D2 | Hierarchy company→estate→project→workspace→kanban; bubble-up; granular RBAC |
|
||||
| D3 | Standalone vs Enterprise; one-way conversion; per-user brains + Vault required in Enterprise; federation deferred |
|
||||
| D4 | Re-runnable, extensible, per-mode onboarding wizards |
|
||||
| D5 | North star = this rewrite of docs/PRD.md; stack docs/ = product SSOT |
|
||||
| D6 | Only product-relevant material migrates from brains; operational records stay and link |
|
||||
| D7 | Spec-inventory sweep launched immediately (executed; INPUTS baseline frozen by operator ruling T2, 2026-08-25) |
|
||||
| D8 | webUI sits over official framework tooling; CLI primary |
|
||||
| D9 | Not a hosted business; company = organizational separation for one operator |
|
||||
| D10 | better-auth is the account system of record; external IdPs via OIDC |
|
||||
| D11 | Small v1 slice; ALL phases on the documented roadmap from day one |
|
||||
| D12 | HARD RULE: webUI never bypasses tooling; missing tool ⇒ build the tool first |
|
||||
| D13 | workspace_id stays the hard isolation unit; hierarchy is parent structure above; kanban SOT amended, not rewritten |
|
||||
| D14 | Sensitive profile data in the user's own brain only; postgres holds structure/consent/pointers |
|
||||
|
||||
The full decision texts are recorded in the operator decision log (USC estate
|
||||
brain, webui-audit lane, `GRILL.md`).
|
||||
|
||||
### 12.1 Drafting additions beyond D1–D14
|
||||
|
||||
Independent review of this rewrite identified rules in this document that are
|
||||
not present in the D1–D14 record or the frozen T2 baseline. They are listed
|
||||
here so their ratification is explicit: approval of the PR that introduces
|
||||
this document, by the decision owner, ratifies them. If any is rejected it is
|
||||
removed, not silently kept.
|
||||
|
||||
1. **Federation forward-compatibility gate:** "nothing in v1 may foreclose
|
||||
federation" (§3), and scoping federation later requires its own PRD plus
|
||||
threat model ([ROADMAP](./ROADMAP.md) P5). D3 defers federation; these
|
||||
protective gates are additions.
|
||||
2. **North-star amendment rule:** a product/fleet north-star conflict must be
|
||||
resolved by amending one of the two documents explicitly, never by forking
|
||||
a third (§10). The subordination itself is T2-ratified; this amendment
|
||||
procedure is an addition.
|
||||
|
||||
---
|
||||
|
||||
## Part II — Active workstream contracts (preserved unchanged)
|
||||
|
||||
The sections below are normative, in-flight workstream contracts carried over
|
||||
verbatim from the previous revision of this file. Open issues bind to them.
|
||||
This rewrite moved no text and changed no requirement in them; they are
|
||||
governed by their own issues and review gates, and they graduate out of this
|
||||
file individually when their workstreams close.
|
||||
|
||||
## Current addendum: #1194 — Installed framework-tool drift detection
|
||||
|
||||
- Compare the framework tools shipped with the executing Mosaic package against the deployed `$MOSAIC_HOME/tools` tree by content hash.
|
||||
- Treat every shipped `tools/**` file as framework-owned/required according to `framework-manifest.txt`, while excluding the explicit operator-owned credential carve-out and preserving installed-only operator/unknown files.
|
||||
- Distinguish and count `IN_SYNC`, `STALE`, `NOT_INSTALLED`, and installed-only classifications; fail non-zero when shipped tools are stale or absent and refuse self-comparison that would make drift unobservable.
|
||||
- Surface the observational check through `mosaic doctor`; do not refresh files, restart seats, or mutate live tooling.
|
||||
- Document identity/messaging/gate behavior changes in the current stale set, the reviewed quiet-window keep-mode refresh command, and post-refresh probes against the installed path.
|
||||
- Prove by construction that a stale and missing deployed tool are detected; that regression must fail before this checker exists.
|
||||
|
||||
## Compaction Refresh Trust Lifecycle (M1, #827–#830)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
Context compaction, session replacement, and same-PID runtime reloads can leave a previously VERIFIED runtime lease attached to stale directives. M1 must revoke that authority mechanically for Claude (including Claudex) and Pi without trusting caller-asserted identity or forking the external broker state machine.
|
||||
|
||||
### Requirements
|
||||
|
||||
1. `CR-REQ-01`: Claude `PreCompact` and `SessionStart` with matcher `compact`, plus Pi `session_before_compact` and the first post-`session_compact` `context`, SHALL independently revoke the active broker lease.
|
||||
2. `CR-REQ-02`: Runtime generation increases—including same-PID Pi reload/new/resume/fork and Claude resume/clear—SHALL monotonically replace the prior broker incarnation and inherit no VERIFIED lease.
|
||||
3. `CR-REQ-03`: A fired observer that cannot confirm broker revocation SHALL fail closed through lifecycle cancellation, a private local generation fence, and/or a runtime-local tool latch. The existing all-tools broker gate remains authoritative.
|
||||
4. `CR-REQ-04`: The lease TTL SHALL remain monotonic and capped at 300 seconds. If both observers are missed, within-TTL consequential actions remain allowed and after-TTL actions are denied. This named bounded residual stale window SHALL be documented without claiming a mutator-action bound inside the window.
|
||||
5. `CR-REQ-05`: Hook descendants SHALL use the broker-minted session and owner-only current-generation state inherited from register-before-exec. Caller-minted sessions and parallel lease state machines remain forbidden.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-CR-01`: Real-socket tests prove each Claude observer revokes, Pi lifecycle tests prove both observer paths, and Claudex isolated settings preserve and install the mandatory hooks.
|
||||
2. `AC-CR-02`: A same-PID generation test proves the old generation is stale and the replacement generation is UNVERIFIED across reload/resume/fork-equivalent lifecycle events.
|
||||
3. `AC-CR-03`: RED-first T12b/T30 evidence explicitly reports dual-hook miss within TTL as **ALLOWED** and after TTL as **DENIED**.
|
||||
4. `AC-CR-04`: Attributable executable coverage is at least 85%, the full repository suite is green on deterministic main, and independent code/security review completes before merge.
|
||||
|
||||
---
|
||||
|
||||
## Pi Persistent Goal Loop (#1150)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
A Pi agent can stop after a plausible-looking answer even when the operator's broader objective is
|
||||
not complete, and ordinary compaction can weaken or omit the original objective. Mosaic needs an
|
||||
optional, operator-controlled goal loop that keeps a Pi session oriented, checks progress at native
|
||||
lifecycle boundaries, and resumes work until completion is verified or a bounded safety state is
|
||||
reached.
|
||||
|
||||
The objective is a Mosaic-owned Pi extension deployed from the framework into
|
||||
`~/.config/mosaic/runtime/pi/`. It must not install into or depend on `~/.pi/agent/extensions/`.
|
||||
|
||||
### Scope
|
||||
|
||||
#### In scope
|
||||
|
||||
1. `PGL-REQ-01`: The framework SHALL ship a dedicated Pi goal extension under
|
||||
`packages/mosaic/framework/runtime/pi/`, seed it under `$MOSAIC_HOME/runtime/pi/`, and make
|
||||
`mosaic pi` load it alongside the core Mosaic extension when present.
|
||||
2. `PGL-REQ-02`: `/goal` SHALL support setting a goal plus status, pause, resume, cancel, and help
|
||||
operations without silently replacing an active goal.
|
||||
3. `PGL-REQ-03`: Active branch-specific goal state SHALL be persisted in Pi custom session entries,
|
||||
restored on session start and tree navigation, and never rely on a compaction summary as its
|
||||
source of truth.
|
||||
4. `PGL-REQ-04`: A hidden goal contract SHALL be injected through Pi's `context` event before every
|
||||
model request so it remains effective across tool turns, retries, and post-compaction requests.
|
||||
5. `PGL-REQ-05`: The harness SHALL inspect every `turn_end` and successful `session_compact` event.
|
||||
A structured terminating goal-report tool SHALL capture `continue`, evidence-bearing `achieved`,
|
||||
or `blocked` status without requiring a redundant model turn.
|
||||
6. `PGL-REQ-06`: An achievement claim SHALL remain provisional until a second consecutive
|
||||
evidence-bearing verification report. Any continuation report or successful compaction during
|
||||
verification SHALL reset the verification sequence.
|
||||
7. `PGL-REQ-07`: Continuation SHALL be initiated at safe lifecycle boundaries, primarily
|
||||
`agent_settled`; manual compaction and restored active sessions may schedule a deferred idle
|
||||
continuation without re-entering compaction handlers.
|
||||
8. `PGL-REQ-08`: The loop SHALL have operator cancellation plus bounded turn and repeated-no-progress
|
||||
limits. Exhausted or blocked goals pause rather than continuing indefinitely.
|
||||
9. `PGL-REQ-09`: Framework installation and update SHALL preserve normal manifest ownership: the
|
||||
goal extension is framework-owned under `runtime/**`, while no goal extension or configuration
|
||||
asset is created or modified under the operator's main Pi configuration. Pi remains the owner of
|
||||
its native session files used by `appendEntry()`.
|
||||
|
||||
#### Out of scope
|
||||
|
||||
1. A mathematical guarantee that an arbitrary natural-language goal is semantically complete.
|
||||
2. Automatically executing user-supplied shell predicates or accepting executable validation code in
|
||||
`/goal` arguments.
|
||||
3. Restarting Pi after process, host, or supervisor failure; the existing Mosaic fleet/runtime
|
||||
supervisor owns process durability.
|
||||
4. Gateway, database, web UI, Discord, or cross-harness goal orchestration in this slice.
|
||||
|
||||
### User and stakeholder requirements
|
||||
|
||||
- An operator can start a goal from Pi and see its current phase, evidence, limits, and latest report.
|
||||
- The agent remains oriented after each turn and compaction until verified, paused, blocked,
|
||||
exhausted, or cancelled.
|
||||
- Local testing uses a file under `~/.config/mosaic/runtime/pi/`; the feature never writes an
|
||||
extension asset to `~/.pi/agent/extensions/`.
|
||||
- Framework updates deploy the same reviewed extension source through Mosaic's existing manifest
|
||||
sync path.
|
||||
|
||||
### Non-functional requirements
|
||||
|
||||
1. **Safety:** bounded continuation, explicit cancellation, no arbitrary command execution, and no
|
||||
completion without non-empty reported evidence.
|
||||
2. **Reliability:** serialized continuation scheduling, branch-aware restoration, compaction-safe
|
||||
context injection, and stale-timer cancellation on session shutdown.
|
||||
3. **Performance:** no extra nested judge-model request on every turn; structured reporting uses the
|
||||
active agent's final terminating tool call.
|
||||
4. **Observability:** Pi status/notifications expose phase and bounded counters without recording
|
||||
credentials or hidden model reasoning.
|
||||
5. **Maintainability:** the state machine is deterministic and behavior-tested independently from Pi
|
||||
provider/network access.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-PGL-01`: A framework-sync fixture installs the extension at
|
||||
`$MOSAIC_HOME/runtime/pi/goal-extension.ts`, and launcher tests prove both Mosaic Pi extensions are
|
||||
emitted in deterministic order while absent optional files remain backward-compatible.
|
||||
2. `AC-PGL-02`: Command tests prove set/status/pause/resume/cancel behavior, active-goal replacement
|
||||
refusal, and bounded input handling.
|
||||
3. `AC-PGL-03`: Lifecycle tests prove every turn is recorded, active context is injected on every
|
||||
request, two evidence-bearing achievement reports are required, and `agent_settled` continues an
|
||||
unmet goal without duplicate scheduling.
|
||||
4. `AC-PGL-04`: Compaction and restoration tests prove goal state survives, verification is reset and
|
||||
rechecked after compaction, manual compaction continuation is deferred until idle, and tree/session
|
||||
branch state is reconstructed correctly.
|
||||
5. `AC-PGL-05`: Limit tests prove max-turn and repeated-no-progress exhaustion stop autonomous
|
||||
continuation, while pause/cancel/blocked states do not restart.
|
||||
6. `AC-PGL-06`: Focused tests, package typecheck/lint/test, repository quality gates, a local Pi load
|
||||
smoke test from `~/.config/mosaic/runtime/pi/`, independent review, and terminal-green CI pass before
|
||||
issue #1150 closes.
|
||||
|
||||
### Constraints, risks, and assumptions
|
||||
|
||||
- Dependency: Pi's extension API must continue to provide `registerCommand`, `registerTool`,
|
||||
`context`, `turn_end`, `agent_settled`, `session_compact`, session custom entries, and terminating
|
||||
tool results.
|
||||
- Risk: the working agent can overstate completion. Mitigation: structured evidence, a mandatory
|
||||
second verification pass, explicit semantic limitations, and operator-visible reports.
|
||||
- Risk: an impossible goal can consume unbounded resources. Mitigation: hard turn/no-progress bounds
|
||||
and paused terminal states.
|
||||
- Risk: automatic continuation can race compaction or session replacement. Mitigation: drive from
|
||||
`agent_settled`, defer idle restarts, generation-check timers, and clear timers on shutdown.
|
||||
- `ASSUMPTION:` Two consecutive evidence-bearing reports are the initial local verification policy;
|
||||
rationale: it provides a real recheck without doubling every turn's model cost. Future policy may
|
||||
add independent or deterministic validators.
|
||||
- `ASSUMPTION:` Default limits are 40 turns and 6 repeated no-progress reports, configurable only by
|
||||
bounded Mosaic environment settings; rationale: useful persistence with a finite autonomous budget.
|
||||
- `ASSUMPTION:` Documentation remains canonical in-repo for this slice; no external docs publication
|
||||
is requested.
|
||||
|
||||
### Testing and delivery intent
|
||||
|
||||
Use TDD for the deterministic controller and lifecycle invariants. Test with fake Pi lifecycle
|
||||
objects first, then run a local load/smoke test from the deployed Mosaic path. Deliver source, tests,
|
||||
launcher wiring, framework/runtime documentation, user/developer guides, and sitemap updates in one
|
||||
reviewed squash PR to `main` with terminal-green CI.
|
||||
|
||||
---
|
||||
|
||||
## Fleet Declarative Configuration Management Workstream (FCM, #758)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
The local Mosaic fleet has a roster, generated agent environment files, user-systemd units, tmux
|
||||
sessions, heartbeat files, examples, profiles, and separate gateway-backed agent records. These
|
||||
planes have drifted and are not one safe operator lifecycle. The objective is one **local fleet
|
||||
roster** as the desired-state SSOT, with generated environment, systemd, tmux, and heartbeat
|
||||
artifacts as rebuildable projections; it does not merge the local fleet control plane with the
|
||||
gateway-backed agent catalog.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
| ID | Requirement |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `FCM-REQ-01` | The roster SHALL be the sole writable desired-state source for local fleet membership, launch policy, and persisted lifecycle target. Generated environment files, systemd enablement, tmux sessions, and heartbeat state SHALL be non-authoritative projections. |
|
||||
| `FCM-REQ-02` | The implementation SHALL provide one executable structural contract for YAML/JSON input and one shared semantic validator. Roster load, profile validation, provision, migration, and apply SHALL reuse the existing baseline-plus-`roles.local` profile/persona resolver; a parallel role resolver is forbidden. |
|
||||
| `FCM-REQ-03` | The local fleet CLI SHALL expose documented programmatic validate, show, plan, apply/reconcile, create, inspect, update, delete, start, stop, restart, status, verify, and doctor operations with stable JSON and exit-code behavior. Existing `fleet add/remove` compatibility aliases may remain during the stated deprecation window. |
|
||||
| `FCM-REQ-04` | A fresh create SHALL persist `enabled:true` and `desired_state:stopped` unless an explicit persisted start is requested. The model SHALL distinguish enabled state, persisted desired state, and observed state. Migration, apply, reboot, and rollback SHALL not start an agent that was observed stopped before cutover. |
|
||||
| `FCM-REQ-05` | The launch chain SHALL consume deterministic, digest-stamped generated input only. Optional local overrides SHALL be parsed as strict data, may not shadow authoritative generated keys, and may not contain arbitrary commands, credential values, channels, or unknown `MOSAIC_AGENT_*` keys. Forbidden legacy keys, including `MOSAIC_AGENT_COMMAND`, SHALL be privately quarantined before launch and reported only by key name and content hash. |
|
||||
| `FCM-REQ-06` | Mutations and apply SHALL validate before mutation, use an expected generation/lock, write projections atomically, produce a deterministic plan, and emit recovery information on partial failure. Reconciliation SHALL act only on local, enabled, roster-owned projections and SHALL not kill unmanaged tmux sessions by fuzzy name. |
|
||||
| `FCM-REQ-07` | Canonical required classes are `code`, `review`, `validator`, `orchestrator`, `team-leader`, `enhancer`, and `interaction`. `validator` issues an independent final certificate but has no merge authority; `merge-gate` remains sole approve-to-land/merge authority. Team-leader capacity is bounded by an orchestrator-issued lease, and interaction is request/status only. Tess and Ultron are configurable instance/display names, not required machine identities. |
|
||||
| `FCM-REQ-08` | v1 migration SHALL be field-complete, reversible, and explicit about aliases, unresolved classes, lifecycle inference, generated-file regeneration, local override quarantine, schema-only remote/connector fields, and rollback. Every shipped example, profile, and service preset SHALL be migrated and executable, retained as an explicitly versioned v1 fixture, or retired with a replacement and deprecation note. |
|
||||
| `FCM-REQ-09` | M1–M5 SHALL remain local tmux/systemd control-plane work. Remote/SSH reconciliation, connector mutation, secret references, arbitrary command/channel overrides, gateway/API convergence, and UI configuration storage are excluded and require a separate PRD/threat model. |
|
||||
| `FCM-REQ-10` | Documentation and examples are delivery gates. The M0 checklist at [docs/fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) and the baseline disposition inventory at [docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md) SHALL be maintained as acceptance evidence. |
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-FCM-01`: A valid local v2 roster can be parsed from YAML or JSON, validated structurally and semantically through the shared resolver, and rendered canonically; invalid fields, duplicate names, unresolved classes, unsupported runtime/model combinations, socket ambiguity, and incompatible options fail closed.
|
||||
2. `AC-FCM-02`: `plan` reports deterministic desired-versus-observed differences for roster, generated environment, systemd enablement, tmux/session, heartbeat, installed-asset revision, and provable orphans without mutation; `apply --check` reports drift without mutation.
|
||||
3. `AC-FCM-03`: Local create/update/delete is generation-guarded, atomic, idempotent, and safe by default; it permits supported runtime/model/harness/effort/workdir/role changes without direct editing of generated environment files and does not start a newly created agent unless explicitly persisted.
|
||||
4. `AC-FCM-04`: The generated-env/local-override launch chain rejects generated-key shadowing, arbitrary command override, unknown keys, shell evaluation, and sensitive-value diagnostics before any agent starts; known-safe legacy input is regenerated or strictly relocated, and forbidden input is quarantined.
|
||||
5. `AC-FCM-05`: Local lifecycle reconciliation implements the persisted/transient start-stop rules, exact default/named tmux socket targeting, systemd/tmux status, stale generated state, unmanaged-session reporting, and rollback without surprise restarts or fuzzy destructive targeting.
|
||||
6. `AC-FCM-06`: A v1 roster migration previews field-by-field disposition, preserves observed stopped/running state, inventories rather than reconciles remote/schema-only entries, supports a canary and rollback, and classifies every shipped example, profile, and service preset according to the M0 inventory.
|
||||
7. `AC-FCM-07`: Required role authority is validated: validator certificate is consumed but does not merge, merge-gate is the sole merge authority, team-leader leases do not change roster/credentials/authority, and interaction/Tess cannot claim orchestration or merge powers.
|
||||
8. `AC-FCM-08`: Documentation, examples, migration, troubleshooting, operational recovery, package/update asset drift, schema/example/profile validation, independent code/security review, validator certificate, and terminal-green CI are complete before #758 closes.
|
||||
|
||||
### M0 implementation gate
|
||||
|
||||
No source, schema, role, example, profile, systemd, or live-fleet change is authorized before M0
|
||||
lands. M0 consists only of these normative requirements, the complete task DAG, the scoped
|
||||
documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards
|
||||
are defined in [docs/TASKS.md](./TASKS.md) and must remain one card/one PR.
|
||||
|
||||
### Fleet git identity launch propagation (#1043)
|
||||
|
||||
#### Problem and objective
|
||||
|
||||
A fleet seat can have a registered per-agent Git credential while its launched runtime process lacks
|
||||
`MOSAIC_GIT_IDENTITY`. The credential resolver then cannot select the seat identity reliably, which
|
||||
blocks repository operations on fail-closed estates and can fall through to an unrelated identity on
|
||||
estates where that refusal is not active. The objective is to make Git identity a deterministic,
|
||||
roster-derived part of the generated launch projection and prove it reaches the launched process.
|
||||
|
||||
#### Normative requirements
|
||||
|
||||
1. `FGI-REQ-01`: Every generated fleet agent projection SHALL declare
|
||||
`MOSAIC_GIT_IDENTITY=<MOSAIC_AGENT_NAME>`; a differing or unsafe identity SHALL fail closed before
|
||||
tmux launch.
|
||||
2. `FGI-REQ-02`: The clean `/usr/bin/env -i` pane boundary SHALL pass every variable declared by the
|
||||
generated projection, including `MOSAIC_GIT_IDENTITY`, to the launched runtime process.
|
||||
3. `FGI-REQ-03`: A behavioral integration test SHALL set-compare the complete generated projection
|
||||
against the launched process environment. Source-text/string-presence assertions are insufficient.
|
||||
4. `FGI-REQ-04`: Verification SHALL include RED-first evidence and a delete-the-subject mutation that
|
||||
removes Git-identity pane propagation and makes the behavioral test fail.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
1. `AC-FGI-01`: A launched seat process contains every key/value pair declared by its generated
|
||||
environment projection, including the roster-derived Git identity.
|
||||
2. `AC-FGI-02`: Missing, unsafe, or split Git identity is rejected before a tmux session is created.
|
||||
3. `AC-FGI-03`: Focused launcher and generated-environment tests, repository quality gates,
|
||||
independent review, and the required RED/green/R7 evidence are recorded before push.
|
||||
|
||||
### Framework shell assertion portability (#1098)
|
||||
|
||||
#### Problem and objective
|
||||
|
||||
The blocking framework-shell chain can report that a pane command omitted `/usr/bin/env -i` even when
|
||||
`-i` matched successfully. A short-circuiting `grep -q` under `set -o pipefail` may close its pipe after
|
||||
the match and cause an upstream producer to exit with SIGPIPE, turning a valid semantic result into a
|
||||
nonzero aggregate pipeline. The objective is to inspect the captured NUL-delimited argv directly and
|
||||
make failures carry the observed records needed for diagnosis.
|
||||
|
||||
#### Normative requirements
|
||||
|
||||
1. `FSP-REQ-01`: The pane-boundary test SHALL validate an adjacent `/usr/bin/env`, `-i` argv pair from
|
||||
the authoritative NUL-delimited tmux capture without a short-circuit pipeline whose upstream status
|
||||
can override a successful match.
|
||||
2. `FSP-REQ-02`: Missing, reversed, or non-adjacent boundary tokens SHALL fail, while valid boundaries
|
||||
SHALL remain valid regardless of trailing argv size, pipe capacity, process scheduling, or host/CI
|
||||
utility implementation.
|
||||
3. `FSP-REQ-03`: A failed boundary check SHALL print stable indexed, shell-escaped observed argv records
|
||||
before exiting nonzero; the fixture SHALL continue to contain generated non-secret launch data only.
|
||||
4. `FSP-REQ-04`: Verification SHALL include RED-first large-payload evidence, negative token-order
|
||||
controls, the complete focused launcher suite, canonical Woodpecker CI, and independent review.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
1. `AC-FSP-01`: A large captured argv with adjacent `/usr/bin/env`, `-i` passes even when the former
|
||||
`grep -q` pipeline returns nonzero from an upstream SIGPIPE.
|
||||
2. `AC-FSP-02`: Missing executable, missing flag, and detached/reversed flag fixtures return nonzero and
|
||||
emit the indexed observed argv.
|
||||
3. `AC-FSP-03`: The focused suite passes on the development host and CI image, and the merged-main
|
||||
Woodpecker pipeline is terminal green before #1098 closes.
|
||||
|
||||
---
|
||||
|
||||
## Exact Cross-Harness Fleet Communications Contract (#766)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
Fleet runtime contracts currently combine exact peer rows with generic operational metavariables and
|
||||
independently parsed roster data. Non-Claude harnesses can mistake those metavariables for values to
|
||||
infer, producing incorrect host, session, socket, or helper targets. The objective is one
|
||||
roster-resolved communications contract that every supported harness receives unchanged.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
1. `FCOM-REQ-01`: Fleet commands and runtime composition SHALL use one shared v1 roster structural
|
||||
resolver. A second lenient communications parser is forbidden.
|
||||
2. `FCOM-REQ-02`: The composed contract SHALL render the local roster member's authoritative host,
|
||||
exact agent/session name, resolved tmux socket, exact helper path, and deterministic communications
|
||||
generation.
|
||||
3. `FCOM-REQ-03`: Every known peer SHALL have one exact executable command. Same-host commands SHALL
|
||||
omit `-H`; cross-host commands SHALL use only that peer's explicit roster `ssh` target; the one
|
||||
supported fleet-wide named socket SHALL use `-L` with its exact value. A per-agent socket declaration
|
||||
must equal that fleet-wide value; unsupported independent sockets and missing cross-host SSH data SHALL
|
||||
fail closed.
|
||||
4. `FCOM-REQ-04`: Operational fleet examples SHALL not contain unresolved host, session, socket, or
|
||||
helper-path metavariables. Agents SHALL select an exact rendered peer row and SHALL NOT infer,
|
||||
substitute, or fuzzy-match targeting values.
|
||||
5. `FCOM-REQ-05`: An unknown local member or requested peer SHALL fail closed with exact-name discovery
|
||||
guidance. Runtime composition SHALL not silently omit a requested fleet member's communications
|
||||
contract.
|
||||
6. `FCOM-REQ-06`: Claude Code, Codex, OpenCode, and Pi SHALL receive equivalent authoritative
|
||||
communications data through the common runtime composer.
|
||||
7. `FCOM-REQ-07`: Tests SHALL prove the contract from framework-source `TOOLS.md`, through a fresh
|
||||
installed `TOOLS.md`, to final runtime composition and helper executability. User-owned installed
|
||||
`TOOLS.md` content SHALL remain preserved.
|
||||
8. `FCOM-REQ-08`: Stale installed or active composed context SHALL be reported with deterministic
|
||||
generation/repair/relaunch guidance. Currency requires the expected source and installed contract
|
||||
marker/version plus bounded byte equality. The supported current-version repair SHALL run independently
|
||||
of package updates, preserve divergent `TOOLS.md` bytes in a digest-qualified no-clobber backup, restore
|
||||
a regular executable helper without following symlinks, and be idempotent. Detection and reporting SHALL
|
||||
NOT rewrite active context, restart a session, or mutate a live fleet.
|
||||
9. `FCOM-REQ-09`: The shared resolver SHALL preserve and strictly validate every schema-supported v1
|
||||
connector kind (`tmux`, `discord`, and `matrix`) from YAML and JSON. Every accepted snake/camel alias
|
||||
pair SHALL reject differing dual declarations and accept identical declarations. JSON roster fallback
|
||||
SHALL occur only when `roster.yaml` is absent; all other YAML access failures SHALL fail closed.
|
||||
10. `FCOM-REQ-10`: The communications generation SHALL cover the complete canonical rendered semantic
|
||||
contract, including identity, role/class, resolved host/socket/helper, peer metadata, and exact commands.
|
||||
Installed helpers SHALL be validated with no-follow filesystem inspection as regular executable files.
|
||||
Keep-mode reseed and relaunch discovery SHALL preserve and support both YAML and JSON rosters.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-FCOM-01`: Contract fixtures contain no unresolved operational targeting metavariables; local
|
||||
identity contains exact host/session/socket/helper values.
|
||||
2. `AC-FCOM-02`: Same-host, cross-host, named-socket, literal-default-socket, and missing-SSH tests prove
|
||||
exact targeting and fail-closed behavior.
|
||||
3. `AC-FCOM-03`: Unknown identities and peers report known exact names plus an exact self-scoped
|
||||
discovery command; no fuzzy session selection is emitted.
|
||||
4. `AC-FCOM-04`: Four-harness tests prove byte-equal authoritative communications sections.
|
||||
5. `AC-FCOM-05`: Source, fresh-install, preserved-custom-install, stale-installed, composed-generation,
|
||||
helper executable, agent-send socket isolation, and exact-target tests pass.
|
||||
6. `AC-FCOM-06`: Documentation defines non-mutating stale-context detection and operator-authorized,
|
||||
exact-agent relaunch; no implementation path performs automatic session mutation.
|
||||
7. `AC-FCOM-07`: YAML and JSON fixtures cover every connector kind; all snake/camel aliases cover
|
||||
identical acceptance and conflicting rejection; non-`ENOENT` YAML failures do not fall back.
|
||||
8. `AC-FCOM-08`: Missing, directory, symlink, and non-executable installed helpers fail closed. Explicit
|
||||
current-version repair proves partial-deletion recovery, digest-qualified backup collision safety,
|
||||
symlink-target safety, and repeated-run idempotence.
|
||||
9. `AC-FCOM-09`: Markerless-equal and wrong-version source/installed contracts are stale, and a rendered
|
||||
role/class change produces a different communications generation.
|
||||
|
||||
---
|
||||
|
||||
## KBN-101 Database Runtime/Migration Role Split (#771)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
PostgreSQL Gateway/storage currently uses one `DATABASE_URL` for runtime queries and migrations. That makes the deployed application identity an owner and prevents certification that KBN immutable event, artifact, checkpoint, and evidence relations reject runtime `UPDATE`/`DELETE`. KBN-101 freezes a least-privilege runtime/migration split before KBN-100 schema work.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
1. `K101-REQ-01`: `DATABASE_URL` SHALL be the non-owner PostgreSQL runtime connection and `DATABASE_MIGRATION_URL` SHALL be the migration-only owner/migrator connection. They are required respectively for runtime and the dedicated `mosaic-db-migrator --run|--verify` phase in `standalone`/`federated`; local PGlite is the explicit exception. The published `@mosaicstack/db` bin maps exactly `mosaic-db-migrator` to `./dist/cli.js`, its image entrypoint is exactly `mosaic-db-migrator`, accepts no URL/SQL/schema/role argv, and returns stable sanitized exits. Every current/future PostgreSQL DDL entrypoint SHALL route to that runner or be denied, and SHALL reject `DATABASE_URL`-only execution before connection/DDL. Data migration may connect only after the runner prepares and verifies the PostgreSQL target, through dedicated non-DDL `mosaic_data_importer` and exactly `--target-url-file /run/secrets/mosaic-migrate-target-url`, its fixed paired authenticated provider-version file `/run/secrets/mosaic-migrate-target-version`, plus `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`. KBN-101-05 obtains URL key `url` and version only from the same successful Vault KV-v2 response at `secret-{env}/mosaic-stack/database/importer` (`data.metadata.version`), renders them as one immutable generation into separate consumer copies, and never infers a provider version from DSN bytes. The trusted runner verifies TLS/identity/manifest, reads its fixed importer URL/version copies only for binding through safe no-follow fd checks, and signs a credential-free JCS/Ed25519 attestation using its runner-only fixed root-owned private-key file; no signing key reaches importer/runtime. The artifact binds secret version and SHA-256 of exact high-entropy credential-file bytes, canonical TLS host/port/database, CA/SPKI, PostgreSQL system identifier/database OID, importer role, manifest/schema fingerprints, producer invocation/build/image digest, issued/expires/nonce, and correlation. Before target connection the importer validates URL/version/attestation/public-key files, signature/key/expiry/replay/authenticated provider version/digest/generation/bindings and the importer-only CA at exact `DATABASE_TLS_CA_CERT_PATH`; after verified TLS and before DML it validates server/database/role/CA/schema identity, with same-fd/in-memory-byte TOCTOU protection, rotation/revocation, a privileged producer-only-to-importer-only artifact handoff controller that verifies/copies/fsyncs/atomically renames/seals before importer start, consumer isolation/no logging-oracle, and sanitized errors. Raw `--target-url`, `DATABASE_URL` fallback, runtime-owner use, missing/unsafe/substituted files, stale/replayed/tampered/wrong-key attestation, wrong binding, and DDL attempt fail before target connection/DDL; post-connect mismatch closes with zero DML/DDL. A reviewed finite classifier inventories executable current source/scripts/package bins, operator docs, deploy manifests, and exact normative contracts by path; active secure records pin both options/files, producer/key/bindings/tests, while normative contracts cannot mask instructions. Unknown active commands, duplicate-owner, ownerless, missing-path, and historical/status-only masking hits fail. `db:push` is forbidden outside an explicitly disposable local developer database and cannot accept a production-like URL.
|
||||
2. `K101-REQ-02`: Gateway runtime/replicas SHALL not execute migrations or DDL. The runner SHALL hold one `max:1` session and fixed two-int advisory namespace `1297044289` (`MOSA`), `1262636593` (`KBN1`) across preflight, reconciliation, migration, verification, and release. It SHALL compare the versioned canonical manifest v1 tuple (journal logical index/tag plus exact SQL-byte SHA-256) to the complete observed ledger mapping; count/set-only, timestamps, and physical insertion order are non-normative and insufficient.
|
||||
3. `K101-REQ-03`: PostgreSQL SHALL separate non-login platform database owner, non-login schema owner, dedicated `NOLOGIN SUPERUSER` `mosaic_extension_owner`, login migrator, dedicated login non-DDL data importer, non-login runtime capability, and login runtime roles. For PostgreSQL 17 + pgvector 0.8.2, `vector` is untrusted (`trusted` is absent and `relocatable=true`): only an externally controlled audited platform-bootstrap superuser session may `SET ROLE mosaic_extension_owner` for CREATE/UPDATE/SET SCHEMA, then `RESET ROLE`; the role has `rolcanlogin=false`, `rolsuper=true`, zero members, no runtime credential/Vault secret, and is never provided to app containers. It owns `mosaic_extensions`, fresh `vector`, and owner-bearing extension members, while `mosaic_schema_owner` receives only `USAGE` for type resolution and never ownership/`CREATE`/`ALTER`/`DROP`/member-change/default-privilege authority there. Superuser cannot be constrained by `GRANT`/`REVOKE`; this is identity/non-login/no-membership/external-control/audit isolation, not a false least-privilege claim. Extension operations require control-plane change, independent review, backup/rollback, maintenance window, and audit evidence. Managed targets that cannot establish this exact role are ineligible until an independently approved versioned provider-owned extension-owner profile exists; app/migrator ownership is never silently retained. Existing approved-owner extension relocation validates exact `pg_namespace.nspowner`, `pg_extension.extowner`, member ownership/schema/version, while legacy runtime-owned extension fails closed to a controlled shadow-database migration—never unsupported ownership alteration, catalog mutation, ownership adoption, or `DROP CASCADE`. Runtime, migrator, schema owner, importer, and all service roles must fail `SET ROLE`, catalog/direct `ALTER`/`UPDATE`/`DROP`/membership-change denial, role ownership, superuser/role-creation/schema-creation/TEMPORARY, unsafe membership, untrusted search path, missing grants, unauthenticated TLS, and immutable privilege drift checks. Application schema is fixed `mosaic` with exact `pg_catalog,mosaic` session path; historical public migrations remain byte-immutable legacy bootstrap only, every future Drizzle application declaration targets `mosaic`, and `vector` is explicitly qualified from non-writable `mosaic_extensions`. No config-derived SQL identifier is permitted.
|
||||
4. `K101-REQ-04`: `mosaicstack/stack` KBN-101-00 SHALL exclusively own `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, `infra/pg-bootstrap/README.md`, and bootstrap tests; KBN-101-05 SHALL exclusively own `tools/db/render-postgres-secrets.ts`, its tests, and current Compose/Portainer/two-gateway deployment declarations, consuming the versioned bootstrap interface without overlap. Environment IaC/Vault is named input and Mosaic deployment control plane/Jason is activation authority. Distinct runtime/migrator/importer URL, importer authenticated provider-version, DB-client CA, Gateway leaf, and PostgreSQL server key/certificate materials are provisioned before a production-like database starts. Importer and migrator have separate immutable URL/version copies at fixed `10002:10002`/`10003:10003` identities; runtime/unrelated containers receive neither importer material, attestation private key, or importer artifact. Runtime, migrator, and importer require their mounted CA plus `sslmode=verify-full`. Exact UID/GID/mode/rendering, service-DNS SANs, Vault/compose/Swarm consumer isolation, two-gateway pair ordering, server activation, pre-enforcement legacy-client drain and `hostssl` zero-plaintext-session proof, fresh/existing transition, CA-overlap rotation, TLS-only rollback, and standalone/federated/Swarm/two-gateway positive/negative TLS evidence are required. No application-generated production certificate or plaintext bootstrap exception is permitted.
|
||||
5. `K101-REQ-05`: KBN immutable relations SHALL permit the real runtime role INSERT/SELECT only and deny UPDATE/DELETE; parent retention remains RESTRICT/no-cascade. Role/password/Vault creation is external platform control, never application migration/source.
|
||||
6. `K101-REQ-06`: N-1 single-URL compatibility, rollout/rollback, Vault ownership/rotation/redaction, CI, installer, compose/Portainer, observability, and deployment handoffs SHALL be separately bounded one-card/one-PR work. Prepared slices remain inactive while current owner-runtime deployments stay N-1; Mosaic control plane/Jason alone authorizes one final atomic activation or rollback, with no force-on-red/bypass. KBN-101 planning itself SHALL not mutate production.
|
||||
7. `K101-REQ-07`: KBN-100 SHALL begin only after the KBN-101 foundation role/schema-boundary certificate; it SHALL rebase on that main head, restore generated Drizzle declaration/snapshot/journal consistency, and bound procedural immutable-table grant/trigger/backfill additions to its schema slice. KBN-101 real deployed-role immutable-operation certification SHALL complete after KBN-100 creates those relations and before KBN-105.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-K101-01`: DTO/command-matrix tests prove required modes, PGlite exception, `mosaic-db-migrator --help|--run|--verify`/stable exits/argv refusal, public-import negative, every finite classified DDL/static-bypass inventory path and both harness pairs reject `DATABASE_URL`-only before connection/DDL, no migration-to-runtime fallback, and `db:push` refusal outside an allowlisted disposable DB. Before inventory, ownership, or status masking, the semantic fixture fails README's exact former commented code-fence generic-wrapper form and the user guide's exact former executable generic-wrapper form; source-consistency proves current `packages/storage/src/cli.ts` directly `execSync`s `pnpm --filter @mosaicstack/db db:migrate` and no `mosaic-db-migrator` bin exists, so runner-delegation documentation fails. The active `docs/guides/migrate-tier.md` route is inventoried to KBN-101-07 and proves runner-produced `--target-url-file /run/secrets/mosaic-migrate-target-url`, fixed paired provider-version file, and `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`; runner-only signing/private-key isolation; Vault KV-v2 same-response version provenance, separate immutable generation mounts, importer CA, JCS/Ed25519 signature/key rotation/revocation, atomic artifact, expiry/replay, safe-fd secret-version/digest, canonical TLS/CA/server/database/role/manifest/schema bindings, dedicated non-DDL importer, consumer isolation/no log-oracle, and exact no-connection versus zero-DML rejection for missing/wrong/stale/replayed/tampered/wrong-key/substituted/generation-mismatched inputs. The full current non-normative docs inventory—including user guide, federation historical task/MILESTONES status, and non-operative SETUP—has an exact safe disposition. Scanner semantic checks reject automatic first-boot/startup extension/schema/migration wording, Compose-up-before-runner, init-script authority, production `.env`/monorepo auto-load/`EnvironmentFile=`/credential-export-or-argv/restart-as-secret-activation routes, and every unqualified operator-document `mosaic-db-migrator --run|--verify` hit regardless of named/normative/status classification. The exact former README/dev/deployment Compose-first sequences, former SETUP wording, exact former MILESTONES wording `pgvector extension installed + verified on startup`, former architecture-plan/PERFORMANCE/backlog runner routes, and any unqualified runner fixture fail before inventory masking. Only one `Held future procedure` Markdown section—bounded through the next equal-or-higher heading—may contain the explicit non-operative/no-current-command-authority form that names KBN-101-00/-03/-05 and preserves external bootstrap → TLS/roles → `mosaic-db-migrator --run` → `mosaic-db-migrator --verify` → Gateway/Compose readiness; every runner hit outside that section fails. The README assertion for the checked-in direct CI `pnpm --filter @mosaicstack/db run db:migrate` with `DATABASE_URL` passes only as active legacy N-1, uncertified, non-authorizing-as-an-operator-route status against an isolated disposable CI database pending KBN-101-06 removal—not as an ordinary operator or approved DDL-authority route. Only local PGlite data-layer work or non-PostgreSQL Compose is current (Gateway/Web local startup is held pending daemon/inherited/project-DSN rejection).
|
||||
2. `AC-K101-02`: Fixed namespace lock contention/crash/readiness/non-interference and exact manifest-v1 reconciliation tests prove no replica race/runtime auto-migration and fail closed on every missing/unknown/duplicate/ambiguous/corrupt/stale ledger state.
|
||||
3. `AC-K101-03`: Actual PostgreSQL 17 + pgvector 0.8.2 control-file, catalog, Drizzle-generation, vector-query/operator, fresh/approved-owner/legacy-shadow/partial/resume/rollback/N-1, and real deployed-role tests prove `trusted` absent/untrusted plus relocatability, external-superuser `SET ROLE` create/update/`RESET ROLE` audit, exact `rolcanlogin=false`/`rolsuper=true`/zero-membership/no-runtime-secret state, platform/schema/extension-owner/migrator/importer/runtime separation, `pg_extension.extowner` plus owner-bearing extension-member/schema/version assertions, and runtime/migrator/schema-owner/importer/all-service-role `SET ROLE`/ALTER/DROP/member-update denial. They also prove `pg_catalog,mosaic` per-session pool safety, `mosaic_extensions` qualification, identifier injection denial, ownership/membership/ledger-read/TEMP/default grants, and unsafe privilege denial.
|
||||
4. `AC-K101-04`: Disposable standalone, federated/Swarm, and two-gateway verified-TLS positives plus for both pairs missing CA/wrong CA/wrong SAN/sslmode downgrade, server/Gateway key mode, UID/GID, secret-consumer isolation, and legacy-drain/`hostssl` negatives prove server bootstrap, ordering, and readiness; PGlite is expressly excluded from this PostgreSQL evidence.
|
||||
5. `AC-K101-05`: Real runtime-role evidence proves INSERT/SELECT succeeds and UPDATE/DELETE fails for every frozen immutable KBN relation.
|
||||
6. `AC-K101-06`: N-1/atomic activation/rollback, Vault/CA-overlap rotation/redaction, health/operator behavior, CI/deployment handoff, independent exact-head security review, and terminal-green CI evidence the foundation before KBN-100; after KBN-100, the real deployed-role immutable-operation certificate and Ultron approval release KBN-105.
|
||||
|
||||
**Normative implementation contract:** [`docs/native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md`](./native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md). `ASSUMPTION:` existing `standalone` and `federated` are all PostgreSQL production-like modes; any new PostgreSQL tier inherits these requirements until an explicit versioned amendment.
|
||||
|
||||
---
|
||||
|
||||
## Tess Interaction Agent Workstream (TESS)
|
||||
|
||||
### Problem and Objective
|
||||
|
||||
Jason needs one durable, operator-facing Mosaic agent outside Hermes that is reachable through a dedicated Discord channel and CLI, can attach to and operate the Mosaic fleet and transitional Hermes agents, and preserves context across restarts and compaction. Mos remains the coding/general fleet orchestrator; Tess is the complementary human interaction, visibility, control, and migration agent.
|
||||
|
||||
The objective is to ship **Tess** (from _tessera_, a piece of a mosaic) as a Pi-native, GPT-5.6 Sol agent with high reasoning. Tess must use Mosaic-owned contracts and plugins so Hermes can be replaced incrementally rather than becoming a permanent architectural dependency.
|
||||
|
||||
### Scope
|
||||
|
||||
#### In Scope
|
||||
|
||||
1. `TESS-ARP-001`: A runtime-neutral `AgentRuntimeProvider` contract supporting `listSessions`, `streamSession`, `sendMessage`, `terminate`, `getSessionTree`, `attach`, health, capability discovery, and normalized events/errors.
|
||||
2. `TESS-PI-001`: A long-running Pi-native Tess agent profile/service pinned to GPT-5.6 Sol with high reasoning, explicit tool policy, lifecycle hooks, durable checkpoints, and restart recovery.
|
||||
3. `TESS-DSC-001`: Dedicated Discord channel binding to Tess through the Mosaic gateway, with allowlists/RBAC, thread/reply policy, streaming, attachments, approvals, and correlation IDs.
|
||||
4. `TESS-CLI-001`: `mosaic tess` CLI commands for chat, status, session listing, attach/detach, send/steer/stop, provider health, and recovery.
|
||||
5. `TESS-FLT-001`: Fleet plugin capabilities for roster/status/heartbeat inspection, message delivery, session hierarchy, safe attach, and controlled restart/recovery.
|
||||
6. `TESS-MOS-001`: Explicit Mos coordination boundary and tools: hand off orchestration requests, observe mission/task state, receive results, and never silently compete for orchestration authority.
|
||||
7. `TESS-HRM-001`: Transitional Hermes adapter for profiles/agents, sessions, streaming/messages, Kanban, skills, memory, tools, cron, and health, using capability negotiation and fail-closed unsupported operations.
|
||||
8. `TESS-MEM-001`: Unified memory/retrieval plugin with scoped search/recent/capture/stats, startup context injection, provenance, redaction, namespace isolation, and flat-file/project truth precedence.
|
||||
9. `TESS-STA-001`: Durable agent state, inbox, handoff, compaction-recovery, and resume reconstruction.
|
||||
10. `TESS-PLG-001`: Plugin/tool catalog covering runtime bootstrap, repository/PR workflow, fleet diagnostics, incident-safe read operations, Discord interaction, and extensible MCP/skill discovery.
|
||||
11. `TESS-TRN-001`: Replaceable transport providers: tmux/fleet now, Matrix/native Mosaic transport later, with no Discord/CLI business logic coupled to transport details.
|
||||
12. `TESS-SEC-001`: RBAC, per-operation authorization, explicit approval for destructive/privileged/customer-visible actions, audit events, secret/PII redaction, tenant isolation, and bounded command execution.
|
||||
13. `TESS-SEC-002`: Command execution SHALL enforce declared scope/role server-side; admin/system and destructive operations SHALL require policy-bound durable approval.
|
||||
14. `TESS-SEC-003`: Every session list/read/attach/send/terminate operation SHALL enforce server-derived owner and tenant scope; guessed or client-supplied IDs SHALL grant no authority.
|
||||
15. `TESS-SEC-004`: MCP tools SHALL derive actor/tenant from authenticated context and SHALL NOT accept caller-controlled identity fields.
|
||||
16. `TESS-SEC-005`: Discord plugin ingress SHALL authenticate service identity, enforce guild/channel/user allowlists, propagate correlation/message IDs, and reject replay.
|
||||
17. `TESS-SEC-006`: Secret/PII classification and redaction SHALL occur before persistence and before channel egress, including tool metadata and authentication flows.
|
||||
18. `TESS-SEC-007`: Approvals SHALL be one-time, expiring, actor/tenant-bound, and cryptographically bound to the exact structured action digest.
|
||||
19. `TESS-SEC-008`: Ingress, provider sends, tool side effects, and responses SHALL use durable inbox/outbox/checkpoints and idempotency records for restart-safe replay.
|
||||
20. `TESS-SEC-009`: Garbage collection and retention SHALL be session/tenant scoped unless executed as a separately authorized and audited system-wide job.
|
||||
21. `TESS-OBS-001`: Structured logs, traces, health/readiness, provider latency/errors, session lifecycle, tool audit, and actionable recovery diagnostics.
|
||||
22. `TESS-MIG-001`: Capability inventory and staged Hermes-to-Mosaic migration matrix with coexistence, cutover, rollback, and deprecation gates.
|
||||
|
||||
#### Out of Scope
|
||||
|
||||
1. Replacing Mos as coding/general fleet orchestrator.
|
||||
2. Making Hermes the Mosaic core or coupling Mosaic domain logic to Hermes schemas.
|
||||
3. Migrating every historical chat verbatim; only policy-compliant indexed summaries and user-selected sessions are migrated.
|
||||
4. Unrestricted shell execution from Discord.
|
||||
5. Full web UI parity in the first Tess operational milestone; gateway contracts must remain web-consumable.
|
||||
6. Replacing tmux before Matrix/native transport reaches operational parity.
|
||||
|
||||
### Stakeholder and User Requirements
|
||||
|
||||
- Jason must be able to converse with the same Tess session from Discord and CLI.
|
||||
- Jason must be able to see what is running, stale, blocked, or unhealthy without attaching manually to every session.
|
||||
- Jason must be able to attach to Tess and authorized fleet sessions through supported CLI controls.
|
||||
- Tess must collaborate with Mos and the fleet while preserving a single clear orchestration authority.
|
||||
- The system must migrate useful Hermes/OpenClaw capabilities intentionally, with evidence, instead of copying implementations wholesale.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
1. **Security:** default-deny provider/tool capabilities, least privilege, no secrets in logs/prompts/commits, Discord user/channel authorization, and auditable approvals.
|
||||
2. **Reliability:** durable inbox/checkpoints; idempotent message handling; reconnect with bounded backoff; no message loss or duplicate execution across gateway restart.
|
||||
3. **Performance:** first acknowledgement within 2 seconds when connected; streamed agent output begins within 5 seconds excluding model/provider delay; status reads return within 2 seconds under nominal local conditions.
|
||||
4. **Observability:** every ingress message and resulting provider/tool operation carries a correlation ID across Discord, gateway, Tess, provider, and audit events.
|
||||
5. **Maintainability:** channel, runtime, transport, memory, and external-agent integrations remain adapter-based with contract tests.
|
||||
6. **Privacy:** only scoped context enters external runtimes; persisted messages/memories follow retention and redaction policy.
|
||||
7. **Portability:** Tess runs through Pi/Mosaic contracts and does not require Hermes to start or serve native Mosaic operations.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
1. `AC-TESS-01`: A dedicated Discord channel and `mosaic tess chat` connect to one durable Tess session and stream responses bidirectionally.
|
||||
2. `AC-TESS-02`: `mosaic tess status|sessions|tree|attach|send|stop` operate against authorized provider capabilities with stable typed outputs and actionable errors.
|
||||
3. `AC-TESS-03`: Tess runs GPT-5.6 Sol at high reasoning and its effective runtime/model/tool policy is visible through status without exposing credentials.
|
||||
4. `AC-TESS-04`: Tess can inspect and message the Mosaic fleet, hand orchestration work to Mos, and demonstrate that Tess does not independently claim Mos-owned orchestration work.
|
||||
5. `AC-TESS-05`: Hermes adapter demonstrates session listing, streaming/message delivery, hierarchy mapping, and at least one approved capability in each of Kanban, skills, memory, tools, and cron—or reports unsupported capabilities fail-closed.
|
||||
6. `AC-TESS-06`: Restart/compaction test preserves session identity, pending inbox, last durable checkpoint, and a resumable handoff without duplicate side effects.
|
||||
7. `AC-TESS-07`: Unauthorized Discord users/channels, cross-tenant access, unsafe tool calls, forged approvals, and sensitive-output cases are denied and audited.
|
||||
8. `AC-TESS-08`: tmux/fleet and Matrix/native transport implementations pass the same provider contract suite; Matrix may remain non-default until readiness gates pass.
|
||||
9. `AC-TESS-09`: Baseline quality gates, unit/integration/contract tests, Discord+CLI E2E, restart/recovery tests, independent code review, and security review are green.
|
||||
10. `AC-TESS-10`: Migration matrix documents every audited Hermes/OpenClaw capability as native, adapted, deferred, or rejected, with cutover and rollback evidence.
|
||||
11. `AC-TESS-11`: User, admin, developer, API/OpenAPI, operations/recovery, and plugin-authoring documentation is current and linked from the sitemap.
|
||||
|
||||
### Constraints, Dependencies, Risks, and Assumptions
|
||||
|
||||
- Dependency: Mosaic gateway remains the single API surface; Pi is the native runtime; Valkey/PostgreSQL provide canonical durable state where required.
|
||||
- Dependency: Discord bot credentials and dedicated channel ID are deployment secrets provisioned outside source control.
|
||||
- Risk: Tess could drift into a second orchestrator. Mitigation: explicit role policy, Mos handoff contract, authority checks, and E2E boundary tests.
|
||||
- Risk: broad Hermes compatibility can freeze legacy semantics into Mosaic. Mitigation: Mosaic-owned normalized contracts and capability negotiation.
|
||||
- Risk: Discord creates a privileged remote-control surface. Mitigation: pairing/allowlists, RBAC, approvals, rate limits, audit, and safe tool classes.
|
||||
- Risk: transcript ingestion can violate privacy or overload memory. Mitigation: scoped opt-in import, redacted summaries, provenance, retention, and deduplication.
|
||||
- Risk: current root filesystem has limited headroom. Mitigation: isolated worktrees, no duplicated dependency installation unless required, and cleanup only after active-lane verification.
|
||||
- `ASSUMPTION:` The public name is **Tess**, because the user requested a name and the tessera/Mosaic relationship is distinctive; config must permit later display-name changes without renaming APIs or storage keys.
|
||||
- `ASSUMPTION:` The dedicated Discord channel ID and final guild policy will be supplied/provisioned during deployment, so implementation uses explicit configuration and fail-fast startup validation.
|
||||
- `ASSUMPTION:` tmux/fleet is the production transport for the first operational milestone; Matrix/native transport is implemented behind the same contract and promoted only after parity/reliability verification.
|
||||
- `ASSUMPTION:` Project/task truth remains in canonical Mosaic/project stores; semantic memory systems are retrieval/mirror layers, not hidden authorities.
|
||||
|
||||
### Testing and Delivery Intent
|
||||
|
||||
Delivery uses five gated milestones: runtime contracts/security; Pi service/state; Discord/CLI; fleet/Hermes/plugin suite; migration/Matrix/recovery/qualification. Every source-code task requires tests, independent review, a PR to `main`, terminal-green CI, and issue/task closure. Production activation additionally requires a clean-host Pi launch, dedicated Discord channel smoke test, CLI attach test, restart/recovery drill, and rollback procedure.
|
||||
|
||||
---
|
||||
|
||||
## Official Channel Plugin Workstream (#756)
|
||||
|
||||
### Problem and Objective
|
||||
|
||||
The Discord plugin currently couples Discord event handling, gateway bridging, and reply routing in one implementation and activates only on mentions. Mosaic needs an official channel adapter that behaves the same no matter whether the bound logical agent currently runs through Claude, Codex, Pi, OpenCode, or a future harness. The Discord connection and conversation address must remain stable while the gateway changes the runtime provider behind that logical session.
|
||||
|
||||
The objective is to make Discord the first implementation of a transport-neutral official channel contract, with explicit authorization and deterministic channel/thread routing that future Matrix, Slack, and other adapters can share.
|
||||
|
||||
### Scope
|
||||
|
||||
#### In Scope
|
||||
|
||||
1. `CHN-001`: Transport-neutral channel adapter, route, message, attachment, authorization-principal, response-target, and health contracts in `@mosaicstack/types`, including trusted per-binding logical-agent configuration selection.
|
||||
2. `CHN-002`: Stable channel conversation addresses based on logical agent plus channel/thread identity; harness, model, and runtime-provider IDs are forbidden from channel session keys.
|
||||
3. `DSC-001`: An authorized untagged message in a configured agent-bound channel routes to the agent and receives its response in that channel.
|
||||
4. `DSC-002`: A bot mention in a configured parent channel creates a Discord thread, or reuses the thread already attached to that same native message; the mentioned turn and subsequent thread turns route and respond in that thread.
|
||||
5. `DSC-003`: A message already inside an authorized thread inherits authorization from its configured parent and never attempts a nested thread.
|
||||
6. `DSC-004`: Guild, parent channel, user, pairing, and role authorization remains default-deny before thread creation or gateway dispatch.
|
||||
7. `DSC-005`: Discord service authentication, HMAC envelope integrity, replay protection, attachments, approvals, response chunking, and correlation behavior remain intact.
|
||||
8. `DSC-006`: The Discord adapter exposes lifecycle and health behavior through the shared channel contract without importing a harness SDK.
|
||||
|
||||
#### Out of Scope
|
||||
|
||||
1. The logical-agent lease, fencing epoch, execution grant, checkpoint, or cross-harness takeover implementation tracked by #754/#755.
|
||||
2. Dynamic Discord authorization administration in the web UI.
|
||||
3. Multi-guild tenant isolation, DMs, slash commands, voice, reactions, or production bot deployment.
|
||||
4. Implementing Matrix or Slack adapters in this slice.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
1. **Security:** no thread or dispatch side effect occurs until guild, parent channel, user, pairing, role, and bounded per-user/channel rate checks pass; attachment metadata is shape- and size-bounded; credentials never enter source, messages, session keys, or logs.
|
||||
2. **Portability:** channel contracts and stable conversation IDs contain no Claude, Codex, Pi, OpenCode, model, process, or provider-specific field; each configuration-owned binding selects its trusted logical agent without changing the channel identity.
|
||||
3. **Reliability:** repeated messages for one channel/thread resolve the same conversation handle; reconnecting the adapter does not require a harness-specific rebinding.
|
||||
4. **Maintainability:** Discord-specific API translation stays in the Discord package; gateway and future adapters depend on transport-neutral contracts.
|
||||
5. **Observability:** thread creation or routing failure is reported without message content or credential material.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
1. `AC-CHN-01`: Contract and behavior tests prove the plugin route contains only logical agent plus channel/thread identity and produces the same stable conversation handle regardless of underlying harness selection.
|
||||
2. `AC-CHN-02`: A mentioned authorized parent-channel message creates a thread (or reuses its already-attached thread), dispatches to the thread conversation, and targets the response to that thread.
|
||||
3. `AC-CHN-03`: An untagged authorized parent-channel message dispatches to the parent conversation and targets the response to the parent channel.
|
||||
4. `AC-CHN-04`: Untagged follow-ups inside an authorized thread dispatch and respond in that same thread without creating a nested thread.
|
||||
5. `AC-CHN-05`: Unauthorized guilds, channels, users, unpaired users, insufficient roles, and rate-limited senders produce no thread and no gateway dispatch.
|
||||
6. `AC-CHN-06`: Shared channel contracts are exported from `@mosaicstack/types`, Discord implements the lifecycle/health seam, and no harness SDK is imported by the plugin.
|
||||
7. `AC-CHN-07`: Focused routing/auth tests, package tests, typecheck, lint, formatting, coverage, independent code/security review, and terminal-green CI pass.
|
||||
|
||||
### Constraints, Risks, and Assumptions
|
||||
|
||||
- Dependency: Mosaic gateway remains the policy, durable-session, audit, and runtime-provider boundary.
|
||||
- Constraint: This work must not modify orchestrator-to-Pi migration or #754/#755 lease/fencing files.
|
||||
- Risk: accepting untagged messages could create noisy or unintended agent input. Mitigation: only explicitly configured channels and paired, role-authorized users are accepted, with bounded per-user/channel message and thread rates.
|
||||
- Risk: Discord thread creation can fail because of channel permissions, archived state, or API rate limits. Mitigation: fail without dispatching a turn whose response destination cannot be honored, and emit sanitized diagnostics.
|
||||
- `ASSUMPTION:` Configured channels are dedicated agent interaction surfaces, so authorized untagged human messages are intentional agent input.
|
||||
- `ASSUMPTION:` Mention in a parent channel selects a public thread; messages already in a thread remain there because Discord has no nested threads.
|
||||
- `ASSUMPTION:` One Discord bot may serve multiple configuration-owned logical-agent bindings.
|
||||
- `ASSUMPTION:` Static allowlists and paired-user roles are the authorization administration surface for this slice.
|
||||
|
||||
### Testing and Delivery Intent
|
||||
|
||||
Use TDD for remote-ingress routing and permission boundaries. Required evidence includes parent-channel mention, untagged parent message, existing-thread follow-up, existing-thread mention, thread reuse, unauthorized side-effect denial, stable harness-neutral conversation identity, adapter health, and regression coverage for signed envelopes and approvals. Deliver through issue #756, a reviewed squash PR to `main`, terminal-green CI, and issue closure.
|
||||
|
||||
---
|
||||
|
||||
## Mos Runtime Portability Workstream (MOS-PORT)
|
||||
|
||||
### Problem and Objective
|
||||
|
||||
Mos is currently identified partly by a harness-native session and communication process. Replacement/rebinding exists, but no gateway-enforced logical identity or fencing prevents a stale harness from continuing to reply or execute effects after takeover.
|
||||
|
||||
The objective is to make Mos a server-derived logical Mosaic identity whose authority can move safely among runtime connectors. The gateway owns identity, lease, policy, and audit; harnesses remain replaceable adapters.
|
||||
|
||||
### M1 Requirements
|
||||
|
||||
1. `MOS-PORT-ID-001`: Define a normalized logical-agent identity independent of Claude Code, Pi, Codex, tmux, Matrix, and provider-native session IDs.
|
||||
2. `MOS-PORT-LEASE-001`: Persist one exclusive connector lease per tenant/logical-agent/binding with CAS acquisition, monotonic fencing epoch, TTL, heartbeat, explicit release, and takeover.
|
||||
3. `MOS-PORT-FENCE-001`: Bind every connector dispatch/execution grant to the current server-derived tenant, logical identity, binding, connector, scopes, expiry, and lease epoch.
|
||||
4. `MOS-PORT-FENCE-002`: Reject and audit stale, expired, forged, cross-tenant, cross-binding, and unauthorized grants before connector, channel, provider, or tool side effects.
|
||||
5. `MOS-PORT-OBS-001`: Emit credential-safe correlation/audit events for lease acquire, renew, takeover, reject, release, and expiry.
|
||||
6. `MOS-PORT-ARCH-001`: Runtime/provider adapters consume normalized lease context without adding harness-native schemas to Mosaic core.
|
||||
|
||||
### M1 Acceptance Criteria
|
||||
|
||||
1. `AC-MOS-PORT-01`: Two contenders for one binding cannot simultaneously hold current authority under concurrency.
|
||||
2. `AC-MOS-PORT-02`: Successful takeover increments the fencing epoch and every operation from the old epoch fails closed before side effects.
|
||||
3. `AC-MOS-PORT-03`: Gateway/database restart preserves lease and epoch state; expired leases can be recovered only through the authorized takeover path.
|
||||
4. `AC-MOS-PORT-04`: Cross-tenant, cross-agent, cross-binding, forged, and expired lease/grant cases are denied and audited.
|
||||
5. `AC-MOS-PORT-05`: Unit, migration, repository close/reopen, concurrency, abuse, gateway integration, independent security review, CI, and documentation gates pass.
|
||||
|
||||
### Deferred to Later #754 Milestones
|
||||
|
||||
Canonical checkpoint/handoff payloads, exactly-once connector receipts, concrete Claude/Pi/Codex adapters, channel cutover, and full cross-harness failover/rollback E2E are explicitly out of M1 scope.
|
||||
|
||||
---
|
||||
|
||||
## Workspace placement guard hardening (#1174)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
The Bash pre-tool guard must prevent Git checkouts and repository state from being placed under
|
||||
`$HOME` without refusing ordinary Git commands merely because a source, option value, branch name,
|
||||
or metadata mentions `$HOME`. A guard that over-blocks routine work is unsafe because operators
|
||||
will route around it.
|
||||
|
||||
### Scope and requirements
|
||||
|
||||
1. `WPG-REQ-01`: `git clone` and `git worktree add` placement SHALL be judged from their placement
|
||||
operands, not from every HOME-shaped word in the command.
|
||||
2. `WPG-REQ-02`: Clone sources, references, templates, environment assignments, and non-placement
|
||||
worktree metadata MAY resolve under HOME when all placement operands resolve elsewhere.
|
||||
3. `WPG-REQ-03`: Both attached and separate-value `--separate-git-dir` forms SHALL remain placement
|
||||
operands and SHALL be refused when they resolve under HOME.
|
||||
4. `WPG-REQ-04`: Option classification SHALL account for Git's rule-generated boolean negations
|
||||
without relying on an enumerable allowlist of flag spellings.
|
||||
5. `WPG-REQ-05`: Quote removal, escapes, shell command boundaries, redirections, and end-of-options
|
||||
handling SHALL preserve existing fail-closed checkout coverage.
|
||||
6. `WPG-REQ-06`: Absolute placement aliases SHALL resolve shell-known HOME spellings, dot segments,
|
||||
repeated separators, and existing symlink parents before the HOME boundary comparison.
|
||||
7. Relative targets whose effective path depends on the shell cwd are out of scope and tracked by
|
||||
#1197.
|
||||
|
||||
### Acceptance and verification
|
||||
|
||||
1. Git's own option parser accepts each tested flag, including generated `--no-*` forms, while the
|
||||
guard allows a HOME-valued source with an explicit safe destination.
|
||||
2. Equivalent clone and worktree fixtures cover rule-generated negations and remain discriminating
|
||||
against the prior head where the defect existed.
|
||||
3. Real HOME destinations and both `--separate-git-dir` forms remain blocked, including placements
|
||||
after shell command boundaries.
|
||||
4. The full hermetic guard suite, syntax/static checks, adversarial probes, independent review, and
|
||||
terminal-green CI pass before merge.
|
||||
5. Any option-classification residual is documented with its deliberate failure direction.
|
||||
|
||||
### Constraints, risks, and assumptions
|
||||
|
||||
- Security and usability are co-equal: neither a placement bypass nor routine over-block is an
|
||||
acceptable repair.
|
||||
- `ASSUMPTION:` The value-taking option surface exposed by the installed Git version is closed and
|
||||
measurable through Git's own parser/help output; rationale: boolean flags are rule-generated,
|
||||
while separate-value options have explicit grammar and must be classified as such.
|
||||
- Risk: a future Git release may add a new value-taking placement option. Mitigation: document the
|
||||
chosen residual direction and pin every currently supported placement option in behavior tests.
|
||||
- Risk: a symlink can be replaced after pre-execution canonicalization. Mitigation: resolve every
|
||||
existing parent physically and document the remaining inherent TOCTOU window; the worktree helper
|
||||
remains the authoritative path-derivation mechanism, with atomic closure tracked by #1199.
|
||||
|
||||
---
|
||||
|
||||
## Release Integrity Workstream (RI, #1275)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
At `next` 476db12b (review of 2026-08-17), publication from `next` is not bound to the full verification pipeline for the same commit: the publish pipeline's publish steps depend on `build` only, while ordinary push CI excludes `next`. Public Forge/MACP paths contain false-success placeholders: a stub executor that reports `completed` with exit zero, planning/remediation gates that execute literal `true`, a review gate that echoes an approving verdict, and a gate runner that treats empty commands and unimplemented CI-provider gates as passing. Shipping UI surfaces can render a failed fetch as an empty, healthy collection.
|
||||
|
||||
Objective: for alpha 0.0.50, the release cannot publish, report, or display work state that the repository has not actually verified. Decisions SDLC-D-033 through SDLC-D-038 (Jason, 2026-08-17) scope this floor; full decision text and required-behavior lists live in jarvis-brain `docs/plans/2026-08-16_mosaic-stack-sdlc-protocol.md` and `data/decisions/mosaic-stack-sdlc-protocol.json`. This section restates only the normative requirements.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
1. **RI-N1 Exact-commit publication verification (SDLC-D-034).** One canonical terminal verification command performs self-contained re-verification in the publish pipeline against the job's checked-out commit before any external publication effect. The command contains or invokes the complete mandatory verification set (semantic parity with the PR merge gate, including sanitization, upgrade-guard, typecheck, lint, format check, tests, and build); CI and publication do not maintain separate semantic checklists. Every publish step depends on the verification step in the executable pipeline DAG. Provider commit identity and `git rev-parse HEAD` must identify the same commit. Missing, skipped, cancelled, stale, or inconclusive checks fail closed. Documentation-only runs may skip publication but cannot bypass verification when a publication effect will occur. A negative control must prove that a broken check blocks every publish step.
|
||||
|
||||
2. **RI-N2 Fail-closed Forge/MACP with explicit simulation (SDLC-D-035).** Simulation requires explicit caller intent (e.g. `--simulate`) and produces a distinct typed `simulated` state that can never satisfy dependencies, acceptance criteria, gates, merge, or release. Normal execution exits nonzero with a typed capability failure when a required executor, reviewer, command, or CI provider is absent — no stub completion, no literal-`true` gates, no synthetic approvals, no empty-command passes. A manual gate with no automation enters a waiting state; it does not pass. Positive tests prove explicit simulation still works; negative controls prove simulation and every missing-provider case cannot advance lifecycle state.
|
||||
|
||||
3. **RI-N3 One transitional PRD authority (SDLC-D-036).** `@mosaicstack/prdy` structured storage under `docs/prdy/`, driven by `mosaic mission --plan`, is the authoritative PRD representation for the alpha. `mosaic prdy` either routes through the same application service or operates only as an explicit, named Markdown import/export adapter; `docs/PRD.md` is not a peer authority. `mission --plan` must persist the mission↔PRD linkage (mission id/version, PRD id/version, selected requirements). Markdown output is a generated view carrying source identity; editing it cannot mutate authority silently. Import is explicit, validated, and conflict-aware (proposed successor, never overwrite). Structural validity is separate from approval.
|
||||
|
||||
4. **RI-N4 One quality-rails evaluator (SDLC-D-037).** The TypeScript quality-rails package is the sole authoritative evaluator. A complete probe inventory maps every current TypeScript and shell check to one canonical check with disposition (preserve/strengthen/retire, each named). Effective shell enforcement probes are absorbed before their independent paths retire; expected-file presence alone is not parity. The evaluator returns typed results (`passed`/`failed`/`blocked`/`error`/`not-applicable`) with check version, subject, and reason; missing implementation, missing input, unknown check, process error, timeout, or malformed output can never become `passed` or an unqualified skip. Check definitions and policy are versioned and digested. Shell commands become thin adapters with no separate verdict logic. The canonical terminal verification command (RI-N1) invokes this evaluator rather than duplicating its logic. Contract, parity, and negative-control tests are required, plus independent review of probe equivalence.
|
||||
|
||||
5. **RI-N5 Consequence-aware stale UI (SDLC-D-038).** Mission Control distinguishes typed freshness states (`current`, `stale`, `partial`, `unknown`, `unavailable`) rather than inferring from empty arrays or null. A failed fetch never renders as an empty healthy collection. Last-known data may display for situational awareness only with source identity, version, and age visibly labeled; any derived completion/assurance/release verdict whose inputs are stale becomes `unknown`; all state-changing actions are disabled until fresh state loads and is revalidated. With no verified snapshot, surfaces show an explicit unavailable state. Cache corruption, cross-workspace data, schema mismatch, and version regression invalidate the snapshot. Tests cover the failure matrix (network, auth, malformed, partial, corruption, stale age, schema mismatch, recovery, stale-action rejection) with negative controls proving no case yields a current green verdict or enabled mutation.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- AC-RI-1: A push to `next` that fails any mandatory verification step publishes nothing (no npm package, no image), demonstrated by a checked-in negative control and by pipeline evidence on a real `next` publish run where the verification step is green and every publish step depends on it.
|
||||
- AC-RI-2: With no executor/reviewer/CI provider wired, Forge and MACP normal runs exit nonzero with typed capability failures; with `--simulate`, runs complete but every result is typed `simulated` and cannot satisfy any gate, dependency, or completion state — proven by unit tests including negative controls.
|
||||
- AC-RI-3: A PRD created or revised through either `mosaic mission --plan` or `mosaic prdy` resolves to one authority under `docs/prdy/` with stable identities and versions; the mission↔PRD linkage survives restart; a Markdown export is labeled as generated and cannot silently become a second writer; divergent legacy content blocks baseline claims until explicitly resolved — proven by contract tests.
|
||||
- AC-RI-4: `quality-rails check` through any entry point (TS CLI, framework shell adapter) returns the same typed verdict for the same subject; the probe inventory names every legacy check's disposition; a deliberately broken probe fails closed — proven by contract/parity/negative-control tests and independent review of probe equivalence.
|
||||
- AC-RI-5: No shipping surface renders a failed fetch as an empty healthy state; stale/partial/unavailable states are typed, labeled, and mutation-disabled — proven by the failure-matrix tests.
|
||||
- AC-RI-6: All cards merged to `next` via squash PR with terminal-green CI; release evidence for 0.0.50 records commit, verification run, and published artifacts.
|
||||
|
||||
### Out of scope
|
||||
|
||||
The canonical dispatcher/control-plane vertical slice (work graph, execution attempts, fenced leases, typed check-in, independent verifier dispatch) is decided post-alpha (SDLC-D-033, option B). Multi-pipeline verification certificates (SDLC-D-034 option B) are post-alpha. Full AF-1..AF-4 objective matrices and Mission Control portfolio surfaces are post-alpha.
|
||||
|
||||
## Official CLI Capability and Tool Migration Workstream (T78)
|
||||
|
||||
Normative contract on integration trunk `next`:
|
||||
[docs/requirements/cli-capability-migration.md](./requirements/cli-capability-migration.md):
|
||||
migrates agent-facing operations from directly invoked scripts into documented, first-class
|
||||
`mosaic` CLI command groups, together with the central-registry resolver, capability catalog,
|
||||
adapter boundary, and phased legacy-tool-tree decommission the migration requires. The contract
|
||||
carries its own implementation hold and delivery stages.
|
||||
# PRD: Mosaic Stack
|
||||
|
||||
This file is a permanent shim, not the PRD body (GOV.1 lifecycle rule). The
|
||||
project source of truth is the **current revision bundle**:
|
||||
|
||||
**[docs/PRDs/2026-08-31_PRD_rev1/](./PRDs/2026-08-31_PRD_rev1/PRD.md)** —
|
||||
rev1, ratified 2026-09-01 (Jason Woltje). It consolidates the 2026-08-26 North
|
||||
Star (D1–D15), the fleet north star, the agent-runtime L1/L2 contracts, and the
|
||||
control-plane-surfaces findings into sectioned documents (AUTHN, AUTHZ, CLI,
|
||||
DATA, GOV.1–5, HARN, PROV, ROLE, SEAT, SESS, UI, VIS) with a single decision
|
||||
map and a closed open-questions list.
|
||||
|
||||
Revision bundles are frozen at ratification and never deleted. The prior
|
||||
revision, rev0 (2026-08-26 North Star), is archived verbatim at
|
||||
[docs/PRDs/2026-08-26_PRD_rev0/PRD.md](./PRDs/2026-08-26_PRD_rev0/PRD.md).
|
||||
Updating the PRD means ratifying a new bundle under `docs/PRDs/` and repointing
|
||||
`current_rev:` here; this path never changes.
|
||||
|
||||
@@ -0,0 +1,910 @@
|
||||
---
|
||||
kind: spec
|
||||
status: active
|
||||
source_of_truth: true
|
||||
---
|
||||
|
||||
# PRD: Mosaic Stack — North Star
|
||||
|
||||
This document is the product source of truth for Mosaic Stack.
|
||||
|
||||
- **Part I** defines the product north star. It is written from the ratified
|
||||
decision set D1–D14 (operator decision session, 2026-08-25; decision owner
|
||||
Jason Woltje). Each section cites the decisions it implements.
|
||||
- **Part II** preserves the active workstream contracts unchanged. Open issues
|
||||
bind to them; this rewrite does not alter a single normative word in them.
|
||||
- The previous v0.1.0 beta PRD body is archived verbatim at
|
||||
[docs/archive/PRD-v0.1.md](./archive/PRD-v0.1.md) and is no longer authority.
|
||||
- The delivery roadmap lives in [docs/ROADMAP.md](./ROADMAP.md). Per D11, every
|
||||
planned phase appears there from day one, even as a placeholder.
|
||||
|
||||
## Metadata
|
||||
|
||||
- **Owner / decision authority:** Jason Woltje
|
||||
- **Status:** active (supersedes the v0.1.0 PRD as product authority)
|
||||
- **Date:** 2026-08-26
|
||||
- **Decision registry:** D1–D14, recorded in Part I §12
|
||||
- **SSOT rule:** this repository's `docs/` tree is the product source of truth
|
||||
(D5). Estate brains hold operational records, not product canon; only
|
||||
product-relevant material migrates here (D6).
|
||||
|
||||
---
|
||||
|
||||
## Part I — Product north star
|
||||
|
||||
### 1. What Mosaic Stack is (D1)
|
||||
|
||||
Mosaic Stack is an **open-source, AI-first platform for people who want a
|
||||
self-hosted environment for agentic management and a life operating system.**
|
||||
It serves personal, business, and employee needs from one deployment, and the
|
||||
work is offered freely.
|
||||
|
||||
"AI-first" means agents are first-class operators of the system, not a bolted-on
|
||||
chat box: the platform exists to let humans direct fleets of agents over their
|
||||
projects, tasks, communications, and infrastructure, with the same tools and
|
||||
the same guarantees whether a human or an agent is acting.
|
||||
|
||||
### 2. Who it is for (D1, D9)
|
||||
|
||||
The operator of a deployment is its user. Mosaic Stack is **not a hosted
|
||||
business**: running the system as a service for external customers is outside
|
||||
the north star. Multi-tenancy exists WITHIN a deployment so that one operator
|
||||
can separate their world — for example, several LLCs plus a personal domain —
|
||||
while every deployment is self-hosted by its own operator.
|
||||
|
||||
"Company" in the hierarchy is organizational separation for one operator's
|
||||
world, not a customer account.
|
||||
|
||||
### 3. Deployment modes (D3)
|
||||
|
||||
Two modes, chosen at install time:
|
||||
|
||||
| | Standalone / personal | Enterprise |
|
||||
| ------------------- | -------------------------------------- | ----------------------------------------------------- |
|
||||
| Brains | one mosaic-brain (system + user files) | system brain for config + one brain per user |
|
||||
| User-data isolation | single user | no user-data leakage between users; sharing is opt-in |
|
||||
| Secrets | OpenBao/Vault or flat files | OpenBao/Vault REQUIRED |
|
||||
| Conversion | Standalone → Enterprise, **one-way** | terminal state |
|
||||
|
||||
Brains are configurable as external git repositories (recommended, not
|
||||
required); git tracking is always on locally.
|
||||
|
||||
**Federation** (connecting deployments: system-level config, assigned users,
|
||||
rights and data-access control, trusts with boundaries, exfiltration
|
||||
monitoring) is intentionally not fully designed. It is deferred, appears on the
|
||||
roadmap as a placeholder phase per D11, and nothing in v1 may foreclose it.
|
||||
|
||||
### 4. Structure and tenancy (D2, D9, D13)
|
||||
|
||||
The hierarchy:
|
||||
|
||||
```
|
||||
company/organization (N per deployment)
|
||||
└─ estate (each in exactly one company)
|
||||
└─ project (each in exactly one estate)
|
||||
└─ workspace (project-specific; carries the Kanban)
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Users can create N companies, N estates, N projects.
|
||||
- Tasks bubble UP the hierarchy so whole-system status is visible at every
|
||||
level. Bubble-up is **read-only aggregation**, never a cross-workspace write.
|
||||
- Granular RBAC: admins restrict access per company, estate, and project;
|
||||
grants are evaluated down the chain. Assets are transferable subject to the
|
||||
structure.
|
||||
- **`workspace_id` remains the hard mechanical isolation unit** exactly as
|
||||
ratified in
|
||||
[docs/requirements/native-kanban-sot.md](./requirements/native-kanban-sot.md)
|
||||
(#751): PostgreSQL sole writable SOT, cross-workspace relationships rejected,
|
||||
fail-closed mutations. The hierarchy is parent structure ABOVE workspaces,
|
||||
used for RBAC evaluation and read-only roll-ups. The kanban SOT carries this
|
||||
as Amendment A1, added by reviewed PR — an amendment, not a rewrite (D13).
|
||||
|
||||
### 5. Identity (D10)
|
||||
|
||||
Built-in auth (better-auth) is the **account system of record**. Authentik and
|
||||
other external IdPs federate in via OIDC as login methods; they never become
|
||||
the system of record. Perimeter shims (forward-auth in front of a web host) are
|
||||
deployment workarounds, not the design.
|
||||
|
||||
### 6. Onboarding (D4)
|
||||
|
||||
Onboarding is a **wizard that differs by mode, is re-runnable (no lock-in), and
|
||||
is extensible** — new wizards attach as tabs.
|
||||
|
||||
Standalone flow captures: system and company name; component choices (Mosaic
|
||||
Comms/Matrix vs external; Mosaic SSO/Authentik vs external; Mosaic
|
||||
DB/PostgreSQL vs external; vector DB); the initial user
|
||||
(email/password/name/SSO); comms setup (Matrix/Discord/Slack); agent enrollment
|
||||
(harness choice and install, OAuth or API-key login, multi-account, model
|
||||
choice with recommendation, agent name and persona, account assignment,
|
||||
optional comms auto-enroll); a user onboarding profile (disabilities including
|
||||
ADHD/autism/PDA/vision, professional background, education, desired agent
|
||||
communication style, optional voice-matching interview, family/pets/friends/
|
||||
hobbies/likes-dislikes); email and drive connectors (Gmail/IMAP, Google
|
||||
Drive/OneDrive/Dropbox) with granular agentic-access consent; SSO/OIDC
|
||||
configuration; an initial estate, an initial project, and seeded example data.
|
||||
|
||||
Enterprise uses the same skeleton with personal data optional; the focus moves
|
||||
to business structure, org chart, RBAC, M365 and external systems, immediate
|
||||
OIDC, SSO prominent.
|
||||
|
||||
Profile answers feed `USER.md` and/or the user's data store subject to the
|
||||
custody rule in §7.
|
||||
|
||||
### 7. Data custody (D6, D14)
|
||||
|
||||
- **Sensitive profile categories** (disabilities, family, communication style,
|
||||
and similar) live in the **user's own brain ONLY**. PostgreSQL holds
|
||||
structural data, consent records, and pointers — never the content. "User
|
||||
data does not leak" is enforced by architecture, not policy (D14).
|
||||
- Standalone (one user, one brain) **may** keep the same split — D14 makes it
|
||||
optional in Standalone, not required. Keeping it is the recommended default
|
||||
because it preserves forward-compatibility with the one-way Enterprise
|
||||
conversion (D3).
|
||||
- Estate brains hold operational records. Only product-relevant material
|
||||
migrates into this repository's docs; operational records stay in their
|
||||
brains and are linked (D6).
|
||||
|
||||
### 8. Architecture gate — the webUI sits OVER official tooling (D8, D12)
|
||||
|
||||
**HARD RULE:** every webUI operation goes through the Gateway API backed by the
|
||||
same official framework tooling the CLI uses. The CLI remains the primary
|
||||
execution method; the webUI uses the tools to operate and configure the
|
||||
system. The webUI never bypasses tooling to reach the database or filesystem
|
||||
directly.
|
||||
|
||||
Consequence for planning: when a desired webUI operation has no backing tool,
|
||||
the gap is scored **"blocked on tooling"** and the tool is built first. The
|
||||
product baseline therefore always includes all three D8 inputs: the tool
|
||||
inventory (what exists and what is missing), the webUI→tool mapping, and the
|
||||
measured current state of the `next` branch.
|
||||
|
||||
### 9. v1 slice (D11)
|
||||
|
||||
v1 is deliberately small:
|
||||
|
||||
1. **Standalone onboarding wizard** — system/company name, component choices,
|
||||
initial user, initial estate + project, seeded examples, re-runnable.
|
||||
2. **Hierarchy core** — company → estate → project → workspace → kanban, with
|
||||
read-only task bubble-up.
|
||||
3. **Basic RBAC** on the hierarchy.
|
||||
4. **Minimal agent enrollment** — one harness, API key, name/persona.
|
||||
|
||||
Deferred beyond v1: connectors, comms integrations, voice-matching, M365,
|
||||
Enterprise conversion, federation. Every deferred item appears in
|
||||
[docs/ROADMAP.md](./ROADMAP.md) per the D11 rule: nothing exists only in heads.
|
||||
|
||||
### 10. Relationship to the fleet north star
|
||||
|
||||
[docs/fleet/NORTH_STAR.md](./fleet/NORTH_STAR.md) (generated from
|
||||
`docs/fleet/NORTH_STAR.yaml`) is the **delivery-fleet** north star: how the
|
||||
agent fleet that builds and operates the system should run (NS-1..NS-10,
|
||||
workstreams A–L). This PRD is the **product** north star. They are not
|
||||
competitors: the fleet north star is subordinate product-wise — its workstream
|
||||
J ("Web control plane") is one consumer of this PRD's D8/D12 gate — and this
|
||||
PRD does not redefine fleet invariants. The subordination rule is ratified in
|
||||
the frozen audit-input baseline (T2 operator freeze, 2026-08-25: "the PRD must
|
||||
cite and subordinate it, never fork it"). A change that would put the two in
|
||||
conflict must amend one of them explicitly, never fork a third document
|
||||
(drafting addition — see §12.1).
|
||||
|
||||
### 11. Explicit non-goals
|
||||
|
||||
- Hosted/SaaS operation for external customers (D9).
|
||||
- A webUI that writes to the database or filesystem around the tooling (D12).
|
||||
- A second writable task store beside PostgreSQL (native-kanban-sot invariants).
|
||||
- Fully-designed federation in v1 (D3 — roadmap placeholder only).
|
||||
|
||||
### D15 — Tiered containerized deployment (2026-08-30, containerization lane)
|
||||
|
||||
The stack ships a tiered deployment target, additive to the architecture
|
||||
gate (D8): (1) Standalone tier — docker compose is the canonical
|
||||
single-host deployment: postgres, valkey, openbao, gateway, appservice
|
||||
and the served webUI in one composition, with migrations, health checks,
|
||||
and a documented install/upgrade path; the registry (CI-published
|
||||
images) is the only deployment source. (2) Enterprise tier — Kubernetes
|
||||
manifests for the same service set, phase-gated on the standalone tier
|
||||
holding its acceptance bar. The v1 acceptance bar for the standalone
|
||||
tier: compose-up healthy; webUI hosts agent chat; an in-stack agent can
|
||||
open a PR to this repo; CI validates it; the running deployment adopts
|
||||
the merged change (pull + restart). Federation (D3 clause) remains
|
||||
deferred and unforeclosed. Implementation plan:
|
||||
docs/plans/2026-08-30_containerization.md.
|
||||
|
||||
## 12. Decision registry
|
||||
|
||||
| ID | Decision (short form) |
|
||||
| --- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
|
||||
| D1 | Open-source, AI-first, self-hosted platform for agentic management + life OS |
|
||||
| D2 | Hierarchy company→estate→project→workspace→kanban; bubble-up; granular RBAC |
|
||||
| D3 | Standalone vs Enterprise; one-way conversion; per-user brains + Vault required in Enterprise; federation deferred |
|
||||
| D4 | Re-runnable, extensible, per-mode onboarding wizards |
|
||||
| D5 | North star = this rewrite of docs/PRD.md; stack docs/ = product SSOT |
|
||||
| D6 | Only product-relevant material migrates from brains; operational records stay and link |
|
||||
| D7 | Spec-inventory sweep launched immediately (executed; INPUTS baseline frozen by operator ruling T2, 2026-08-25) |
|
||||
| D8 | webUI sits over official framework tooling; CLI primary |
|
||||
| D9 | Not a hosted business; company = organizational separation for one operator |
|
||||
| D10 | better-auth is the account system of record; external IdPs via OIDC |
|
||||
| D11 | Small v1 slice; ALL phases on the documented roadmap from day one |
|
||||
| D12 | HARD RULE: webUI never bypasses tooling; missing tool ⇒ build the tool first |
|
||||
| D13 | workspace_id stays the hard isolation unit; hierarchy is parent structure above; kanban SOT amended, not rewritten |
|
||||
| D14 | Sensitive profile data in the user's own brain only; postgres holds structure/consent/pointers |
|
||||
| D15 | Tiered containerized deployment: compose standalone tier (five-point v1 bar) + phase-gated k8s enterprise tier; registry-only image source | 2026-08-30 containerization lane; plan docs/plans/2026-08-30_containerization.md |
|
||||
|
||||
The full decision texts are recorded in the operator decision log (USC estate
|
||||
brain, webui-audit lane, `GRILL.md`).
|
||||
|
||||
### 12.1 Drafting additions beyond D1–D14
|
||||
|
||||
Independent review of this rewrite identified rules in this document that are
|
||||
not present in the D1–D14 record or the frozen T2 baseline. They are listed
|
||||
here so their ratification is explicit: approval of the PR that introduces
|
||||
this document, by the decision owner, ratifies them. If any is rejected it is
|
||||
removed, not silently kept.
|
||||
|
||||
1. **Federation forward-compatibility gate:** "nothing in v1 may foreclose
|
||||
federation" (§3), and scoping federation later requires its own PRD plus
|
||||
threat model ([ROADMAP](./ROADMAP.md) P5). D3 defers federation; these
|
||||
protective gates are additions.
|
||||
2. **North-star amendment rule:** a product/fleet north-star conflict must be
|
||||
resolved by amending one of the two documents explicitly, never by forking
|
||||
a third (§10). The subordination itself is T2-ratified; this amendment
|
||||
procedure is an addition.
|
||||
|
||||
---
|
||||
|
||||
## Part II — Active workstream contracts (preserved unchanged)
|
||||
|
||||
The sections below are normative, in-flight workstream contracts carried over
|
||||
verbatim from the previous revision of this file. Open issues bind to them.
|
||||
This rewrite moved no text and changed no requirement in them; they are
|
||||
governed by their own issues and review gates, and they graduate out of this
|
||||
file individually when their workstreams close.
|
||||
|
||||
## Current addendum: #1194 — Installed framework-tool drift detection
|
||||
|
||||
- Compare the framework tools shipped with the executing Mosaic package against the deployed `$MOSAIC_HOME/tools` tree by content hash.
|
||||
- Treat every shipped `tools/**` file as framework-owned/required according to `framework-manifest.txt`, while excluding the explicit operator-owned credential carve-out and preserving installed-only operator/unknown files.
|
||||
- Distinguish and count `IN_SYNC`, `STALE`, `NOT_INSTALLED`, and installed-only classifications; fail non-zero when shipped tools are stale or absent and refuse self-comparison that would make drift unobservable.
|
||||
- Surface the observational check through `mosaic doctor`; do not refresh files, restart seats, or mutate live tooling.
|
||||
- Document identity/messaging/gate behavior changes in the current stale set, the reviewed quiet-window keep-mode refresh command, and post-refresh probes against the installed path.
|
||||
- Prove by construction that a stale and missing deployed tool are detected; that regression must fail before this checker exists.
|
||||
|
||||
## Compaction Refresh Trust Lifecycle (M1, #827–#830)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
Context compaction, session replacement, and same-PID runtime reloads can leave a previously VERIFIED runtime lease attached to stale directives. M1 must revoke that authority mechanically for Claude (including Claudex) and Pi without trusting caller-asserted identity or forking the external broker state machine.
|
||||
|
||||
### Requirements
|
||||
|
||||
1. `CR-REQ-01`: Claude `PreCompact` and `SessionStart` with matcher `compact`, plus Pi `session_before_compact` and the first post-`session_compact` `context`, SHALL independently revoke the active broker lease.
|
||||
2. `CR-REQ-02`: Runtime generation increases—including same-PID Pi reload/new/resume/fork and Claude resume/clear—SHALL monotonically replace the prior broker incarnation and inherit no VERIFIED lease.
|
||||
3. `CR-REQ-03`: A fired observer that cannot confirm broker revocation SHALL fail closed through lifecycle cancellation, a private local generation fence, and/or a runtime-local tool latch. The existing all-tools broker gate remains authoritative.
|
||||
4. `CR-REQ-04`: The lease TTL SHALL remain monotonic and capped at 300 seconds. If both observers are missed, within-TTL consequential actions remain allowed and after-TTL actions are denied. This named bounded residual stale window SHALL be documented without claiming a mutator-action bound inside the window.
|
||||
5. `CR-REQ-05`: Hook descendants SHALL use the broker-minted session and owner-only current-generation state inherited from register-before-exec. Caller-minted sessions and parallel lease state machines remain forbidden.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-CR-01`: Real-socket tests prove each Claude observer revokes, Pi lifecycle tests prove both observer paths, and Claudex isolated settings preserve and install the mandatory hooks.
|
||||
2. `AC-CR-02`: A same-PID generation test proves the old generation is stale and the replacement generation is UNVERIFIED across reload/resume/fork-equivalent lifecycle events.
|
||||
3. `AC-CR-03`: RED-first T12b/T30 evidence explicitly reports dual-hook miss within TTL as **ALLOWED** and after TTL as **DENIED**.
|
||||
4. `AC-CR-04`: Attributable executable coverage is at least 85%, the full repository suite is green on deterministic main, and independent code/security review completes before merge.
|
||||
|
||||
---
|
||||
|
||||
## Pi Persistent Goal Loop (#1150)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
A Pi agent can stop after a plausible-looking answer even when the operator's broader objective is
|
||||
not complete, and ordinary compaction can weaken or omit the original objective. Mosaic needs an
|
||||
optional, operator-controlled goal loop that keeps a Pi session oriented, checks progress at native
|
||||
lifecycle boundaries, and resumes work until completion is verified or a bounded safety state is
|
||||
reached.
|
||||
|
||||
The objective is a Mosaic-owned Pi extension deployed from the framework into
|
||||
`~/.config/mosaic/runtime/pi/`. It must not install into or depend on `~/.pi/agent/extensions/`.
|
||||
|
||||
### Scope
|
||||
|
||||
#### In scope
|
||||
|
||||
1. `PGL-REQ-01`: The framework SHALL ship a dedicated Pi goal extension under
|
||||
`packages/mosaic/framework/runtime/pi/`, seed it under `$MOSAIC_HOME/runtime/pi/`, and make
|
||||
`mosaic pi` load it alongside the core Mosaic extension when present.
|
||||
2. `PGL-REQ-02`: `/goal` SHALL support setting a goal plus status, pause, resume, cancel, and help
|
||||
operations without silently replacing an active goal.
|
||||
3. `PGL-REQ-03`: Active branch-specific goal state SHALL be persisted in Pi custom session entries,
|
||||
restored on session start and tree navigation, and never rely on a compaction summary as its
|
||||
source of truth.
|
||||
4. `PGL-REQ-04`: A hidden goal contract SHALL be injected through Pi's `context` event before every
|
||||
model request so it remains effective across tool turns, retries, and post-compaction requests.
|
||||
5. `PGL-REQ-05`: The harness SHALL inspect every `turn_end` and successful `session_compact` event.
|
||||
A structured terminating goal-report tool SHALL capture `continue`, evidence-bearing `achieved`,
|
||||
or `blocked` status without requiring a redundant model turn.
|
||||
6. `PGL-REQ-06`: An achievement claim SHALL remain provisional until a second consecutive
|
||||
evidence-bearing verification report. Any continuation report or successful compaction during
|
||||
verification SHALL reset the verification sequence.
|
||||
7. `PGL-REQ-07`: Continuation SHALL be initiated at safe lifecycle boundaries, primarily
|
||||
`agent_settled`; manual compaction and restored active sessions may schedule a deferred idle
|
||||
continuation without re-entering compaction handlers.
|
||||
8. `PGL-REQ-08`: The loop SHALL have operator cancellation plus bounded turn and repeated-no-progress
|
||||
limits. Exhausted or blocked goals pause rather than continuing indefinitely.
|
||||
9. `PGL-REQ-09`: Framework installation and update SHALL preserve normal manifest ownership: the
|
||||
goal extension is framework-owned under `runtime/**`, while no goal extension or configuration
|
||||
asset is created or modified under the operator's main Pi configuration. Pi remains the owner of
|
||||
its native session files used by `appendEntry()`.
|
||||
|
||||
#### Out of scope
|
||||
|
||||
1. A mathematical guarantee that an arbitrary natural-language goal is semantically complete.
|
||||
2. Automatically executing user-supplied shell predicates or accepting executable validation code in
|
||||
`/goal` arguments.
|
||||
3. Restarting Pi after process, host, or supervisor failure; the existing Mosaic fleet/runtime
|
||||
supervisor owns process durability.
|
||||
4. Gateway, database, web UI, Discord, or cross-harness goal orchestration in this slice.
|
||||
|
||||
### User and stakeholder requirements
|
||||
|
||||
- An operator can start a goal from Pi and see its current phase, evidence, limits, and latest report.
|
||||
- The agent remains oriented after each turn and compaction until verified, paused, blocked,
|
||||
exhausted, or cancelled.
|
||||
- Local testing uses a file under `~/.config/mosaic/runtime/pi/`; the feature never writes an
|
||||
extension asset to `~/.pi/agent/extensions/`.
|
||||
- Framework updates deploy the same reviewed extension source through Mosaic's existing manifest
|
||||
sync path.
|
||||
|
||||
### Non-functional requirements
|
||||
|
||||
1. **Safety:** bounded continuation, explicit cancellation, no arbitrary command execution, and no
|
||||
completion without non-empty reported evidence.
|
||||
2. **Reliability:** serialized continuation scheduling, branch-aware restoration, compaction-safe
|
||||
context injection, and stale-timer cancellation on session shutdown.
|
||||
3. **Performance:** no extra nested judge-model request on every turn; structured reporting uses the
|
||||
active agent's final terminating tool call.
|
||||
4. **Observability:** Pi status/notifications expose phase and bounded counters without recording
|
||||
credentials or hidden model reasoning.
|
||||
5. **Maintainability:** the state machine is deterministic and behavior-tested independently from Pi
|
||||
provider/network access.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-PGL-01`: A framework-sync fixture installs the extension at
|
||||
`$MOSAIC_HOME/runtime/pi/goal-extension.ts`, and launcher tests prove both Mosaic Pi extensions are
|
||||
emitted in deterministic order while absent optional files remain backward-compatible.
|
||||
2. `AC-PGL-02`: Command tests prove set/status/pause/resume/cancel behavior, active-goal replacement
|
||||
refusal, and bounded input handling.
|
||||
3. `AC-PGL-03`: Lifecycle tests prove every turn is recorded, active context is injected on every
|
||||
request, two evidence-bearing achievement reports are required, and `agent_settled` continues an
|
||||
unmet goal without duplicate scheduling.
|
||||
4. `AC-PGL-04`: Compaction and restoration tests prove goal state survives, verification is reset and
|
||||
rechecked after compaction, manual compaction continuation is deferred until idle, and tree/session
|
||||
branch state is reconstructed correctly.
|
||||
5. `AC-PGL-05`: Limit tests prove max-turn and repeated-no-progress exhaustion stop autonomous
|
||||
continuation, while pause/cancel/blocked states do not restart.
|
||||
6. `AC-PGL-06`: Focused tests, package typecheck/lint/test, repository quality gates, a local Pi load
|
||||
smoke test from `~/.config/mosaic/runtime/pi/`, independent review, and terminal-green CI pass before
|
||||
issue #1150 closes.
|
||||
|
||||
### Constraints, risks, and assumptions
|
||||
|
||||
- Dependency: Pi's extension API must continue to provide `registerCommand`, `registerTool`,
|
||||
`context`, `turn_end`, `agent_settled`, `session_compact`, session custom entries, and terminating
|
||||
tool results.
|
||||
- Risk: the working agent can overstate completion. Mitigation: structured evidence, a mandatory
|
||||
second verification pass, explicit semantic limitations, and operator-visible reports.
|
||||
- Risk: an impossible goal can consume unbounded resources. Mitigation: hard turn/no-progress bounds
|
||||
and paused terminal states.
|
||||
- Risk: automatic continuation can race compaction or session replacement. Mitigation: drive from
|
||||
`agent_settled`, defer idle restarts, generation-check timers, and clear timers on shutdown.
|
||||
- `ASSUMPTION:` Two consecutive evidence-bearing reports are the initial local verification policy;
|
||||
rationale: it provides a real recheck without doubling every turn's model cost. Future policy may
|
||||
add independent or deterministic validators.
|
||||
- `ASSUMPTION:` Default limits are 40 turns and 6 repeated no-progress reports, configurable only by
|
||||
bounded Mosaic environment settings; rationale: useful persistence with a finite autonomous budget.
|
||||
- `ASSUMPTION:` Documentation remains canonical in-repo for this slice; no external docs publication
|
||||
is requested.
|
||||
|
||||
### Testing and delivery intent
|
||||
|
||||
Use TDD for the deterministic controller and lifecycle invariants. Test with fake Pi lifecycle
|
||||
objects first, then run a local load/smoke test from the deployed Mosaic path. Deliver source, tests,
|
||||
launcher wiring, framework/runtime documentation, user/developer guides, and sitemap updates in one
|
||||
reviewed squash PR to `main` with terminal-green CI.
|
||||
|
||||
---
|
||||
|
||||
## Fleet Declarative Configuration Management Workstream (FCM, #758)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
The local Mosaic fleet has a roster, generated agent environment files, user-systemd units, tmux
|
||||
sessions, heartbeat files, examples, profiles, and separate gateway-backed agent records. These
|
||||
planes have drifted and are not one safe operator lifecycle. The objective is one **local fleet
|
||||
roster** as the desired-state SSOT, with generated environment, systemd, tmux, and heartbeat
|
||||
artifacts as rebuildable projections; it does not merge the local fleet control plane with the
|
||||
gateway-backed agent catalog.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
| ID | Requirement |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `FCM-REQ-01` | The roster SHALL be the sole writable desired-state source for local fleet membership, launch policy, and persisted lifecycle target. Generated environment files, systemd enablement, tmux sessions, and heartbeat state SHALL be non-authoritative projections. |
|
||||
| `FCM-REQ-02` | The implementation SHALL provide one executable structural contract for YAML/JSON input and one shared semantic validator. Roster load, profile validation, provision, migration, and apply SHALL reuse the existing baseline-plus-`roles.local` profile/persona resolver; a parallel role resolver is forbidden. |
|
||||
| `FCM-REQ-03` | The local fleet CLI SHALL expose documented programmatic validate, show, plan, apply/reconcile, create, inspect, update, delete, start, stop, restart, status, verify, and doctor operations with stable JSON and exit-code behavior. Existing `fleet add/remove` compatibility aliases may remain during the stated deprecation window. |
|
||||
| `FCM-REQ-04` | A fresh create SHALL persist `enabled:true` and `desired_state:stopped` unless an explicit persisted start is requested. The model SHALL distinguish enabled state, persisted desired state, and observed state. Migration, apply, reboot, and rollback SHALL not start an agent that was observed stopped before cutover. |
|
||||
| `FCM-REQ-05` | The launch chain SHALL consume deterministic, digest-stamped generated input only. Optional local overrides SHALL be parsed as strict data, may not shadow authoritative generated keys, and may not contain arbitrary commands, credential values, channels, or unknown `MOSAIC_AGENT_*` keys. Forbidden legacy keys, including `MOSAIC_AGENT_COMMAND`, SHALL be privately quarantined before launch and reported only by key name and content hash. |
|
||||
| `FCM-REQ-06` | Mutations and apply SHALL validate before mutation, use an expected generation/lock, write projections atomically, produce a deterministic plan, and emit recovery information on partial failure. Reconciliation SHALL act only on local, enabled, roster-owned projections and SHALL not kill unmanaged tmux sessions by fuzzy name. |
|
||||
| `FCM-REQ-07` | Canonical required classes are `code`, `review`, `validator`, `orchestrator`, `team-leader`, `enhancer`, and `interaction`. `validator` issues an independent final certificate but has no merge authority; `merge-gate` remains sole approve-to-land/merge authority. Team-leader capacity is bounded by an orchestrator-issued lease, and interaction is request/status only. Tess and Ultron are configurable instance/display names, not required machine identities. |
|
||||
| `FCM-REQ-08` | v1 migration SHALL be field-complete, reversible, and explicit about aliases, unresolved classes, lifecycle inference, generated-file regeneration, local override quarantine, schema-only remote/connector fields, and rollback. Every shipped example, profile, and service preset SHALL be migrated and executable, retained as an explicitly versioned v1 fixture, or retired with a replacement and deprecation note. |
|
||||
| `FCM-REQ-09` | M1–M5 SHALL remain local tmux/systemd control-plane work. Remote/SSH reconciliation, connector mutation, secret references, arbitrary command/channel overrides, gateway/API convergence, and UI configuration storage are excluded and require a separate PRD/threat model. |
|
||||
| `FCM-REQ-10` | Documentation and examples are delivery gates. The M0 checklist at [docs/fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) and the baseline disposition inventory at [docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md) SHALL be maintained as acceptance evidence. |
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-FCM-01`: A valid local v2 roster can be parsed from YAML or JSON, validated structurally and semantically through the shared resolver, and rendered canonically; invalid fields, duplicate names, unresolved classes, unsupported runtime/model combinations, socket ambiguity, and incompatible options fail closed.
|
||||
2. `AC-FCM-02`: `plan` reports deterministic desired-versus-observed differences for roster, generated environment, systemd enablement, tmux/session, heartbeat, installed-asset revision, and provable orphans without mutation; `apply --check` reports drift without mutation.
|
||||
3. `AC-FCM-03`: Local create/update/delete is generation-guarded, atomic, idempotent, and safe by default; it permits supported runtime/model/harness/effort/workdir/role changes without direct editing of generated environment files and does not start a newly created agent unless explicitly persisted.
|
||||
4. `AC-FCM-04`: The generated-env/local-override launch chain rejects generated-key shadowing, arbitrary command override, unknown keys, shell evaluation, and sensitive-value diagnostics before any agent starts; known-safe legacy input is regenerated or strictly relocated, and forbidden input is quarantined.
|
||||
5. `AC-FCM-05`: Local lifecycle reconciliation implements the persisted/transient start-stop rules, exact default/named tmux socket targeting, systemd/tmux status, stale generated state, unmanaged-session reporting, and rollback without surprise restarts or fuzzy destructive targeting.
|
||||
6. `AC-FCM-06`: A v1 roster migration previews field-by-field disposition, preserves observed stopped/running state, inventories rather than reconciles remote/schema-only entries, supports a canary and rollback, and classifies every shipped example, profile, and service preset according to the M0 inventory.
|
||||
7. `AC-FCM-07`: Required role authority is validated: validator certificate is consumed but does not merge, merge-gate is the sole merge authority, team-leader leases do not change roster/credentials/authority, and interaction/Tess cannot claim orchestration or merge powers.
|
||||
8. `AC-FCM-08`: Documentation, examples, migration, troubleshooting, operational recovery, package/update asset drift, schema/example/profile validation, independent code/security review, validator certificate, and terminal-green CI are complete before #758 closes.
|
||||
|
||||
### M0 implementation gate
|
||||
|
||||
No source, schema, role, example, profile, systemd, or live-fleet change is authorized before M0
|
||||
lands. M0 consists only of these normative requirements, the complete task DAG, the scoped
|
||||
documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards
|
||||
are defined in [docs/TASKS.md](./TASKS.md) and must remain one card/one PR.
|
||||
|
||||
### Fleet git identity launch propagation (#1043)
|
||||
|
||||
#### Problem and objective
|
||||
|
||||
A fleet seat can have a registered per-agent Git credential while its launched runtime process lacks
|
||||
`MOSAIC_GIT_IDENTITY`. The credential resolver then cannot select the seat identity reliably, which
|
||||
blocks repository operations on fail-closed estates and can fall through to an unrelated identity on
|
||||
estates where that refusal is not active. The objective is to make Git identity a deterministic,
|
||||
roster-derived part of the generated launch projection and prove it reaches the launched process.
|
||||
|
||||
#### Normative requirements
|
||||
|
||||
1. `FGI-REQ-01`: Every generated fleet agent projection SHALL declare
|
||||
`MOSAIC_GIT_IDENTITY=<MOSAIC_AGENT_NAME>`; a differing or unsafe identity SHALL fail closed before
|
||||
tmux launch.
|
||||
2. `FGI-REQ-02`: The clean `/usr/bin/env -i` pane boundary SHALL pass every variable declared by the
|
||||
generated projection, including `MOSAIC_GIT_IDENTITY`, to the launched runtime process.
|
||||
3. `FGI-REQ-03`: A behavioral integration test SHALL set-compare the complete generated projection
|
||||
against the launched process environment. Source-text/string-presence assertions are insufficient.
|
||||
4. `FGI-REQ-04`: Verification SHALL include RED-first evidence and a delete-the-subject mutation that
|
||||
removes Git-identity pane propagation and makes the behavioral test fail.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
1. `AC-FGI-01`: A launched seat process contains every key/value pair declared by its generated
|
||||
environment projection, including the roster-derived Git identity.
|
||||
2. `AC-FGI-02`: Missing, unsafe, or split Git identity is rejected before a tmux session is created.
|
||||
3. `AC-FGI-03`: Focused launcher and generated-environment tests, repository quality gates,
|
||||
independent review, and the required RED/green/R7 evidence are recorded before push.
|
||||
|
||||
### Framework shell assertion portability (#1098)
|
||||
|
||||
#### Problem and objective
|
||||
|
||||
The blocking framework-shell chain can report that a pane command omitted `/usr/bin/env -i` even when
|
||||
`-i` matched successfully. A short-circuiting `grep -q` under `set -o pipefail` may close its pipe after
|
||||
the match and cause an upstream producer to exit with SIGPIPE, turning a valid semantic result into a
|
||||
nonzero aggregate pipeline. The objective is to inspect the captured NUL-delimited argv directly and
|
||||
make failures carry the observed records needed for diagnosis.
|
||||
|
||||
#### Normative requirements
|
||||
|
||||
1. `FSP-REQ-01`: The pane-boundary test SHALL validate an adjacent `/usr/bin/env`, `-i` argv pair from
|
||||
the authoritative NUL-delimited tmux capture without a short-circuit pipeline whose upstream status
|
||||
can override a successful match.
|
||||
2. `FSP-REQ-02`: Missing, reversed, or non-adjacent boundary tokens SHALL fail, while valid boundaries
|
||||
SHALL remain valid regardless of trailing argv size, pipe capacity, process scheduling, or host/CI
|
||||
utility implementation.
|
||||
3. `FSP-REQ-03`: A failed boundary check SHALL print stable indexed, shell-escaped observed argv records
|
||||
before exiting nonzero; the fixture SHALL continue to contain generated non-secret launch data only.
|
||||
4. `FSP-REQ-04`: Verification SHALL include RED-first large-payload evidence, negative token-order
|
||||
controls, the complete focused launcher suite, canonical Woodpecker CI, and independent review.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
1. `AC-FSP-01`: A large captured argv with adjacent `/usr/bin/env`, `-i` passes even when the former
|
||||
`grep -q` pipeline returns nonzero from an upstream SIGPIPE.
|
||||
2. `AC-FSP-02`: Missing executable, missing flag, and detached/reversed flag fixtures return nonzero and
|
||||
emit the indexed observed argv.
|
||||
3. `AC-FSP-03`: The focused suite passes on the development host and CI image, and the merged-main
|
||||
Woodpecker pipeline is terminal green before #1098 closes.
|
||||
|
||||
---
|
||||
|
||||
## Exact Cross-Harness Fleet Communications Contract (#766)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
Fleet runtime contracts currently combine exact peer rows with generic operational metavariables and
|
||||
independently parsed roster data. Non-Claude harnesses can mistake those metavariables for values to
|
||||
infer, producing incorrect host, session, socket, or helper targets. The objective is one
|
||||
roster-resolved communications contract that every supported harness receives unchanged.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
1. `FCOM-REQ-01`: Fleet commands and runtime composition SHALL use one shared v1 roster structural
|
||||
resolver. A second lenient communications parser is forbidden.
|
||||
2. `FCOM-REQ-02`: The composed contract SHALL render the local roster member's authoritative host,
|
||||
exact agent/session name, resolved tmux socket, exact helper path, and deterministic communications
|
||||
generation.
|
||||
3. `FCOM-REQ-03`: Every known peer SHALL have one exact executable command. Same-host commands SHALL
|
||||
omit `-H`; cross-host commands SHALL use only that peer's explicit roster `ssh` target; the one
|
||||
supported fleet-wide named socket SHALL use `-L` with its exact value. A per-agent socket declaration
|
||||
must equal that fleet-wide value; unsupported independent sockets and missing cross-host SSH data SHALL
|
||||
fail closed.
|
||||
4. `FCOM-REQ-04`: Operational fleet examples SHALL not contain unresolved host, session, socket, or
|
||||
helper-path metavariables. Agents SHALL select an exact rendered peer row and SHALL NOT infer,
|
||||
substitute, or fuzzy-match targeting values.
|
||||
5. `FCOM-REQ-05`: An unknown local member or requested peer SHALL fail closed with exact-name discovery
|
||||
guidance. Runtime composition SHALL not silently omit a requested fleet member's communications
|
||||
contract.
|
||||
6. `FCOM-REQ-06`: Claude Code, Codex, OpenCode, and Pi SHALL receive equivalent authoritative
|
||||
communications data through the common runtime composer.
|
||||
7. `FCOM-REQ-07`: Tests SHALL prove the contract from framework-source `TOOLS.md`, through a fresh
|
||||
installed `TOOLS.md`, to final runtime composition and helper executability. User-owned installed
|
||||
`TOOLS.md` content SHALL remain preserved.
|
||||
8. `FCOM-REQ-08`: Stale installed or active composed context SHALL be reported with deterministic
|
||||
generation/repair/relaunch guidance. Currency requires the expected source and installed contract
|
||||
marker/version plus bounded byte equality. The supported current-version repair SHALL run independently
|
||||
of package updates, preserve divergent `TOOLS.md` bytes in a digest-qualified no-clobber backup, restore
|
||||
a regular executable helper without following symlinks, and be idempotent. Detection and reporting SHALL
|
||||
NOT rewrite active context, restart a session, or mutate a live fleet.
|
||||
9. `FCOM-REQ-09`: The shared resolver SHALL preserve and strictly validate every schema-supported v1
|
||||
connector kind (`tmux`, `discord`, and `matrix`) from YAML and JSON. Every accepted snake/camel alias
|
||||
pair SHALL reject differing dual declarations and accept identical declarations. JSON roster fallback
|
||||
SHALL occur only when `roster.yaml` is absent; all other YAML access failures SHALL fail closed.
|
||||
10. `FCOM-REQ-10`: The communications generation SHALL cover the complete canonical rendered semantic
|
||||
contract, including identity, role/class, resolved host/socket/helper, peer metadata, and exact commands.
|
||||
Installed helpers SHALL be validated with no-follow filesystem inspection as regular executable files.
|
||||
Keep-mode reseed and relaunch discovery SHALL preserve and support both YAML and JSON rosters.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-FCOM-01`: Contract fixtures contain no unresolved operational targeting metavariables; local
|
||||
identity contains exact host/session/socket/helper values.
|
||||
2. `AC-FCOM-02`: Same-host, cross-host, named-socket, literal-default-socket, and missing-SSH tests prove
|
||||
exact targeting and fail-closed behavior.
|
||||
3. `AC-FCOM-03`: Unknown identities and peers report known exact names plus an exact self-scoped
|
||||
discovery command; no fuzzy session selection is emitted.
|
||||
4. `AC-FCOM-04`: Four-harness tests prove byte-equal authoritative communications sections.
|
||||
5. `AC-FCOM-05`: Source, fresh-install, preserved-custom-install, stale-installed, composed-generation,
|
||||
helper executable, agent-send socket isolation, and exact-target tests pass.
|
||||
6. `AC-FCOM-06`: Documentation defines non-mutating stale-context detection and operator-authorized,
|
||||
exact-agent relaunch; no implementation path performs automatic session mutation.
|
||||
7. `AC-FCOM-07`: YAML and JSON fixtures cover every connector kind; all snake/camel aliases cover
|
||||
identical acceptance and conflicting rejection; non-`ENOENT` YAML failures do not fall back.
|
||||
8. `AC-FCOM-08`: Missing, directory, symlink, and non-executable installed helpers fail closed. Explicit
|
||||
current-version repair proves partial-deletion recovery, digest-qualified backup collision safety,
|
||||
symlink-target safety, and repeated-run idempotence.
|
||||
9. `AC-FCOM-09`: Markerless-equal and wrong-version source/installed contracts are stale, and a rendered
|
||||
role/class change produces a different communications generation.
|
||||
|
||||
---
|
||||
|
||||
## KBN-101 Database Runtime/Migration Role Split (#771)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
PostgreSQL Gateway/storage currently uses one `DATABASE_URL` for runtime queries and migrations. That makes the deployed application identity an owner and prevents certification that KBN immutable event, artifact, checkpoint, and evidence relations reject runtime `UPDATE`/`DELETE`. KBN-101 freezes a least-privilege runtime/migration split before KBN-100 schema work.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
1. `K101-REQ-01`: `DATABASE_URL` SHALL be the non-owner PostgreSQL runtime connection and `DATABASE_MIGRATION_URL` SHALL be the migration-only owner/migrator connection. They are required respectively for runtime and the dedicated `mosaic-db-migrator --run|--verify` phase in `standalone`/`federated`; local PGlite is the explicit exception. The published `@mosaicstack/db` bin maps exactly `mosaic-db-migrator` to `./dist/cli.js`, its image entrypoint is exactly `mosaic-db-migrator`, accepts no URL/SQL/schema/role argv, and returns stable sanitized exits. Every current/future PostgreSQL DDL entrypoint SHALL route to that runner or be denied, and SHALL reject `DATABASE_URL`-only execution before connection/DDL. Data migration may connect only after the runner prepares and verifies the PostgreSQL target, through dedicated non-DDL `mosaic_data_importer` and exactly `--target-url-file /run/secrets/mosaic-migrate-target-url`, its fixed paired authenticated provider-version file `/run/secrets/mosaic-migrate-target-version`, plus `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`. KBN-101-05 obtains URL key `url` and version only from the same successful Vault KV-v2 response at `secret-{env}/mosaic-stack/database/importer` (`data.metadata.version`), renders them as one immutable generation into separate consumer copies, and never infers a provider version from DSN bytes. The trusted runner verifies TLS/identity/manifest, reads its fixed importer URL/version copies only for binding through safe no-follow fd checks, and signs a credential-free JCS/Ed25519 attestation using its runner-only fixed root-owned private-key file; no signing key reaches importer/runtime. The artifact binds secret version and SHA-256 of exact high-entropy credential-file bytes, canonical TLS host/port/database, CA/SPKI, PostgreSQL system identifier/database OID, importer role, manifest/schema fingerprints, producer invocation/build/image digest, issued/expires/nonce, and correlation. Before target connection the importer validates URL/version/attestation/public-key files, signature/key/expiry/replay/authenticated provider version/digest/generation/bindings and the importer-only CA at exact `DATABASE_TLS_CA_CERT_PATH`; after verified TLS and before DML it validates server/database/role/CA/schema identity, with same-fd/in-memory-byte TOCTOU protection, rotation/revocation, a privileged producer-only-to-importer-only artifact handoff controller that verifies/copies/fsyncs/atomically renames/seals before importer start, consumer isolation/no logging-oracle, and sanitized errors. Raw `--target-url`, `DATABASE_URL` fallback, runtime-owner use, missing/unsafe/substituted files, stale/replayed/tampered/wrong-key attestation, wrong binding, and DDL attempt fail before target connection/DDL; post-connect mismatch closes with zero DML/DDL. A reviewed finite classifier inventories executable current source/scripts/package bins, operator docs, deploy manifests, and exact normative contracts by path; active secure records pin both options/files, producer/key/bindings/tests, while normative contracts cannot mask instructions. Unknown active commands, duplicate-owner, ownerless, missing-path, and historical/status-only masking hits fail. `db:push` is forbidden outside an explicitly disposable local developer database and cannot accept a production-like URL.
|
||||
2. `K101-REQ-02`: Gateway runtime/replicas SHALL not execute migrations or DDL. The runner SHALL hold one `max:1` session and fixed two-int advisory namespace `1297044289` (`MOSA`), `1262636593` (`KBN1`) across preflight, reconciliation, migration, verification, and release. It SHALL compare the versioned canonical manifest v1 tuple (journal logical index/tag plus exact SQL-byte SHA-256) to the complete observed ledger mapping; count/set-only, timestamps, and physical insertion order are non-normative and insufficient.
|
||||
3. `K101-REQ-03`: PostgreSQL SHALL separate non-login platform database owner, non-login schema owner, dedicated `NOLOGIN SUPERUSER` `mosaic_extension_owner`, login migrator, dedicated login non-DDL data importer, non-login runtime capability, and login runtime roles. For PostgreSQL 17 + pgvector 0.8.2, `vector` is untrusted (`trusted` is absent and `relocatable=true`): only an externally controlled audited platform-bootstrap superuser session may `SET ROLE mosaic_extension_owner` for CREATE/UPDATE/SET SCHEMA, then `RESET ROLE`; the role has `rolcanlogin=false`, `rolsuper=true`, zero members, no runtime credential/Vault secret, and is never provided to app containers. It owns `mosaic_extensions`, fresh `vector`, and owner-bearing extension members, while `mosaic_schema_owner` receives only `USAGE` for type resolution and never ownership/`CREATE`/`ALTER`/`DROP`/member-change/default-privilege authority there. Superuser cannot be constrained by `GRANT`/`REVOKE`; this is identity/non-login/no-membership/external-control/audit isolation, not a false least-privilege claim. Extension operations require control-plane change, independent review, backup/rollback, maintenance window, and audit evidence. Managed targets that cannot establish this exact role are ineligible until an independently approved versioned provider-owned extension-owner profile exists; app/migrator ownership is never silently retained. Existing approved-owner extension relocation validates exact `pg_namespace.nspowner`, `pg_extension.extowner`, member ownership/schema/version, while legacy runtime-owned extension fails closed to a controlled shadow-database migration—never unsupported ownership alteration, catalog mutation, ownership adoption, or `DROP CASCADE`. Runtime, migrator, schema owner, importer, and all service roles must fail `SET ROLE`, catalog/direct `ALTER`/`UPDATE`/`DROP`/membership-change denial, role ownership, superuser/role-creation/schema-creation/TEMPORARY, unsafe membership, untrusted search path, missing grants, unauthenticated TLS, and immutable privilege drift checks. Application schema is fixed `mosaic` with exact `pg_catalog,mosaic` session path; historical public migrations remain byte-immutable legacy bootstrap only, every future Drizzle application declaration targets `mosaic`, and `vector` is explicitly qualified from non-writable `mosaic_extensions`. No config-derived SQL identifier is permitted.
|
||||
4. `K101-REQ-04`: `mosaicstack/stack` KBN-101-00 SHALL exclusively own `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, `infra/pg-bootstrap/README.md`, and bootstrap tests; KBN-101-05 SHALL exclusively own `tools/db/render-postgres-secrets.ts`, its tests, and current Compose/Portainer/two-gateway deployment declarations, consuming the versioned bootstrap interface without overlap. Environment IaC/Vault is named input and Mosaic deployment control plane/Jason is activation authority. Distinct runtime/migrator/importer URL, importer authenticated provider-version, DB-client CA, Gateway leaf, and PostgreSQL server key/certificate materials are provisioned before a production-like database starts. Importer and migrator have separate immutable URL/version copies at fixed `10002:10002`/`10003:10003` identities; runtime/unrelated containers receive neither importer material, attestation private key, or importer artifact. Runtime, migrator, and importer require their mounted CA plus `sslmode=verify-full`. Exact UID/GID/mode/rendering, service-DNS SANs, Vault/compose/Swarm consumer isolation, two-gateway pair ordering, server activation, pre-enforcement legacy-client drain and `hostssl` zero-plaintext-session proof, fresh/existing transition, CA-overlap rotation, TLS-only rollback, and standalone/federated/Swarm/two-gateway positive/negative TLS evidence are required. No application-generated production certificate or plaintext bootstrap exception is permitted.
|
||||
5. `K101-REQ-05`: KBN immutable relations SHALL permit the real runtime role INSERT/SELECT only and deny UPDATE/DELETE; parent retention remains RESTRICT/no-cascade. Role/password/Vault creation is external platform control, never application migration/source.
|
||||
6. `K101-REQ-06`: N-1 single-URL compatibility, rollout/rollback, Vault ownership/rotation/redaction, CI, installer, compose/Portainer, observability, and deployment handoffs SHALL be separately bounded one-card/one-PR work. Prepared slices remain inactive while current owner-runtime deployments stay N-1; Mosaic control plane/Jason alone authorizes one final atomic activation or rollback, with no force-on-red/bypass. KBN-101 planning itself SHALL not mutate production.
|
||||
7. `K101-REQ-07`: KBN-100 SHALL begin only after the KBN-101 foundation role/schema-boundary certificate; it SHALL rebase on that main head, restore generated Drizzle declaration/snapshot/journal consistency, and bound procedural immutable-table grant/trigger/backfill additions to its schema slice. KBN-101 real deployed-role immutable-operation certification SHALL complete after KBN-100 creates those relations and before KBN-105.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-K101-01`: DTO/command-matrix tests prove required modes, PGlite exception, `mosaic-db-migrator --help|--run|--verify`/stable exits/argv refusal, public-import negative, every finite classified DDL/static-bypass inventory path and both harness pairs reject `DATABASE_URL`-only before connection/DDL, no migration-to-runtime fallback, and `db:push` refusal outside an allowlisted disposable DB. Before inventory, ownership, or status masking, the semantic fixture fails README's exact former commented code-fence generic-wrapper form and the user guide's exact former executable generic-wrapper form; source-consistency proves current `packages/storage/src/cli.ts` directly `execSync`s `pnpm --filter @mosaicstack/db db:migrate` and no `mosaic-db-migrator` bin exists, so runner-delegation documentation fails. The active `docs/guides/migrate-tier.md` route is inventoried to KBN-101-07 and proves runner-produced `--target-url-file /run/secrets/mosaic-migrate-target-url`, fixed paired provider-version file, and `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`; runner-only signing/private-key isolation; Vault KV-v2 same-response version provenance, separate immutable generation mounts, importer CA, JCS/Ed25519 signature/key rotation/revocation, atomic artifact, expiry/replay, safe-fd secret-version/digest, canonical TLS/CA/server/database/role/manifest/schema bindings, dedicated non-DDL importer, consumer isolation/no log-oracle, and exact no-connection versus zero-DML rejection for missing/wrong/stale/replayed/tampered/wrong-key/substituted/generation-mismatched inputs. The full current non-normative docs inventory—including user guide, federation historical task/MILESTONES status, and non-operative SETUP—has an exact safe disposition. Scanner semantic checks reject automatic first-boot/startup extension/schema/migration wording, Compose-up-before-runner, init-script authority, production `.env`/monorepo auto-load/`EnvironmentFile=`/credential-export-or-argv/restart-as-secret-activation routes, and every unqualified operator-document `mosaic-db-migrator --run|--verify` hit regardless of named/normative/status classification. The exact former README/dev/deployment Compose-first sequences, former SETUP wording, exact former MILESTONES wording `pgvector extension installed + verified on startup`, former architecture-plan/PERFORMANCE/backlog runner routes, and any unqualified runner fixture fail before inventory masking. Only one `Held future procedure` Markdown section—bounded through the next equal-or-higher heading—may contain the explicit non-operative/no-current-command-authority form that names KBN-101-00/-03/-05 and preserves external bootstrap → TLS/roles → `mosaic-db-migrator --run` → `mosaic-db-migrator --verify` → Gateway/Compose readiness; every runner hit outside that section fails. The README assertion for the checked-in direct CI `pnpm --filter @mosaicstack/db run db:migrate` with `DATABASE_URL` passes only as active legacy N-1, uncertified, non-authorizing-as-an-operator-route status against an isolated disposable CI database pending KBN-101-06 removal—not as an ordinary operator or approved DDL-authority route. Only local PGlite data-layer work or non-PostgreSQL Compose is current (Gateway/Web local startup is held pending daemon/inherited/project-DSN rejection).
|
||||
2. `AC-K101-02`: Fixed namespace lock contention/crash/readiness/non-interference and exact manifest-v1 reconciliation tests prove no replica race/runtime auto-migration and fail closed on every missing/unknown/duplicate/ambiguous/corrupt/stale ledger state.
|
||||
3. `AC-K101-03`: Actual PostgreSQL 17 + pgvector 0.8.2 control-file, catalog, Drizzle-generation, vector-query/operator, fresh/approved-owner/legacy-shadow/partial/resume/rollback/N-1, and real deployed-role tests prove `trusted` absent/untrusted plus relocatability, external-superuser `SET ROLE` create/update/`RESET ROLE` audit, exact `rolcanlogin=false`/`rolsuper=true`/zero-membership/no-runtime-secret state, platform/schema/extension-owner/migrator/importer/runtime separation, `pg_extension.extowner` plus owner-bearing extension-member/schema/version assertions, and runtime/migrator/schema-owner/importer/all-service-role `SET ROLE`/ALTER/DROP/member-update denial. They also prove `pg_catalog,mosaic` per-session pool safety, `mosaic_extensions` qualification, identifier injection denial, ownership/membership/ledger-read/TEMP/default grants, and unsafe privilege denial.
|
||||
4. `AC-K101-04`: Disposable standalone, federated/Swarm, and two-gateway verified-TLS positives plus for both pairs missing CA/wrong CA/wrong SAN/sslmode downgrade, server/Gateway key mode, UID/GID, secret-consumer isolation, and legacy-drain/`hostssl` negatives prove server bootstrap, ordering, and readiness; PGlite is expressly excluded from this PostgreSQL evidence.
|
||||
5. `AC-K101-05`: Real runtime-role evidence proves INSERT/SELECT succeeds and UPDATE/DELETE fails for every frozen immutable KBN relation.
|
||||
6. `AC-K101-06`: N-1/atomic activation/rollback, Vault/CA-overlap rotation/redaction, health/operator behavior, CI/deployment handoff, independent exact-head security review, and terminal-green CI evidence the foundation before KBN-100; after KBN-100, the real deployed-role immutable-operation certificate and Ultron approval release KBN-105.
|
||||
|
||||
**Normative implementation contract:** [`docs/native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md`](./native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md). `ASSUMPTION:` existing `standalone` and `federated` are all PostgreSQL production-like modes; any new PostgreSQL tier inherits these requirements until an explicit versioned amendment.
|
||||
|
||||
---
|
||||
|
||||
## Tess Interaction Agent Workstream (TESS)
|
||||
|
||||
### Problem and Objective
|
||||
|
||||
Jason needs one durable, operator-facing Mosaic agent outside Hermes that is reachable through a dedicated Discord channel and CLI, can attach to and operate the Mosaic fleet and transitional Hermes agents, and preserves context across restarts and compaction. Mos remains the coding/general fleet orchestrator; Tess is the complementary human interaction, visibility, control, and migration agent.
|
||||
|
||||
The objective is to ship **Tess** (from _tessera_, a piece of a mosaic) as a Pi-native, GPT-5.6 Sol agent with high reasoning. Tess must use Mosaic-owned contracts and plugins so Hermes can be replaced incrementally rather than becoming a permanent architectural dependency.
|
||||
|
||||
### Scope
|
||||
|
||||
#### In Scope
|
||||
|
||||
1. `TESS-ARP-001`: A runtime-neutral `AgentRuntimeProvider` contract supporting `listSessions`, `streamSession`, `sendMessage`, `terminate`, `getSessionTree`, `attach`, health, capability discovery, and normalized events/errors.
|
||||
2. `TESS-PI-001`: A long-running Pi-native Tess agent profile/service pinned to GPT-5.6 Sol with high reasoning, explicit tool policy, lifecycle hooks, durable checkpoints, and restart recovery.
|
||||
3. `TESS-DSC-001`: Dedicated Discord channel binding to Tess through the Mosaic gateway, with allowlists/RBAC, thread/reply policy, streaming, attachments, approvals, and correlation IDs.
|
||||
4. `TESS-CLI-001`: `mosaic tess` CLI commands for chat, status, session listing, attach/detach, send/steer/stop, provider health, and recovery.
|
||||
5. `TESS-FLT-001`: Fleet plugin capabilities for roster/status/heartbeat inspection, message delivery, session hierarchy, safe attach, and controlled restart/recovery.
|
||||
6. `TESS-MOS-001`: Explicit Mos coordination boundary and tools: hand off orchestration requests, observe mission/task state, receive results, and never silently compete for orchestration authority.
|
||||
7. `TESS-HRM-001`: Transitional Hermes adapter for profiles/agents, sessions, streaming/messages, Kanban, skills, memory, tools, cron, and health, using capability negotiation and fail-closed unsupported operations.
|
||||
8. `TESS-MEM-001`: Unified memory/retrieval plugin with scoped search/recent/capture/stats, startup context injection, provenance, redaction, namespace isolation, and flat-file/project truth precedence.
|
||||
9. `TESS-STA-001`: Durable agent state, inbox, handoff, compaction-recovery, and resume reconstruction.
|
||||
10. `TESS-PLG-001`: Plugin/tool catalog covering runtime bootstrap, repository/PR workflow, fleet diagnostics, incident-safe read operations, Discord interaction, and extensible MCP/skill discovery.
|
||||
11. `TESS-TRN-001`: Replaceable transport providers: tmux/fleet now, Matrix/native Mosaic transport later, with no Discord/CLI business logic coupled to transport details.
|
||||
12. `TESS-SEC-001`: RBAC, per-operation authorization, explicit approval for destructive/privileged/customer-visible actions, audit events, secret/PII redaction, tenant isolation, and bounded command execution.
|
||||
13. `TESS-SEC-002`: Command execution SHALL enforce declared scope/role server-side; admin/system and destructive operations SHALL require policy-bound durable approval.
|
||||
14. `TESS-SEC-003`: Every session list/read/attach/send/terminate operation SHALL enforce server-derived owner and tenant scope; guessed or client-supplied IDs SHALL grant no authority.
|
||||
15. `TESS-SEC-004`: MCP tools SHALL derive actor/tenant from authenticated context and SHALL NOT accept caller-controlled identity fields.
|
||||
16. `TESS-SEC-005`: Discord plugin ingress SHALL authenticate service identity, enforce guild/channel/user allowlists, propagate correlation/message IDs, and reject replay.
|
||||
17. `TESS-SEC-006`: Secret/PII classification and redaction SHALL occur before persistence and before channel egress, including tool metadata and authentication flows.
|
||||
18. `TESS-SEC-007`: Approvals SHALL be one-time, expiring, actor/tenant-bound, and cryptographically bound to the exact structured action digest.
|
||||
19. `TESS-SEC-008`: Ingress, provider sends, tool side effects, and responses SHALL use durable inbox/outbox/checkpoints and idempotency records for restart-safe replay.
|
||||
20. `TESS-SEC-009`: Garbage collection and retention SHALL be session/tenant scoped unless executed as a separately authorized and audited system-wide job.
|
||||
21. `TESS-OBS-001`: Structured logs, traces, health/readiness, provider latency/errors, session lifecycle, tool audit, and actionable recovery diagnostics.
|
||||
22. `TESS-MIG-001`: Capability inventory and staged Hermes-to-Mosaic migration matrix with coexistence, cutover, rollback, and deprecation gates.
|
||||
|
||||
#### Out of Scope
|
||||
|
||||
1. Replacing Mos as coding/general fleet orchestrator.
|
||||
2. Making Hermes the Mosaic core or coupling Mosaic domain logic to Hermes schemas.
|
||||
3. Migrating every historical chat verbatim; only policy-compliant indexed summaries and user-selected sessions are migrated.
|
||||
4. Unrestricted shell execution from Discord.
|
||||
5. Full web UI parity in the first Tess operational milestone; gateway contracts must remain web-consumable.
|
||||
6. Replacing tmux before Matrix/native transport reaches operational parity.
|
||||
|
||||
### Stakeholder and User Requirements
|
||||
|
||||
- Jason must be able to converse with the same Tess session from Discord and CLI.
|
||||
- Jason must be able to see what is running, stale, blocked, or unhealthy without attaching manually to every session.
|
||||
- Jason must be able to attach to Tess and authorized fleet sessions through supported CLI controls.
|
||||
- Tess must collaborate with Mos and the fleet while preserving a single clear orchestration authority.
|
||||
- The system must migrate useful Hermes/OpenClaw capabilities intentionally, with evidence, instead of copying implementations wholesale.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
1. **Security:** default-deny provider/tool capabilities, least privilege, no secrets in logs/prompts/commits, Discord user/channel authorization, and auditable approvals.
|
||||
2. **Reliability:** durable inbox/checkpoints; idempotent message handling; reconnect with bounded backoff; no message loss or duplicate execution across gateway restart.
|
||||
3. **Performance:** first acknowledgement within 2 seconds when connected; streamed agent output begins within 5 seconds excluding model/provider delay; status reads return within 2 seconds under nominal local conditions.
|
||||
4. **Observability:** every ingress message and resulting provider/tool operation carries a correlation ID across Discord, gateway, Tess, provider, and audit events.
|
||||
5. **Maintainability:** channel, runtime, transport, memory, and external-agent integrations remain adapter-based with contract tests.
|
||||
6. **Privacy:** only scoped context enters external runtimes; persisted messages/memories follow retention and redaction policy.
|
||||
7. **Portability:** Tess runs through Pi/Mosaic contracts and does not require Hermes to start or serve native Mosaic operations.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
1. `AC-TESS-01`: A dedicated Discord channel and `mosaic tess chat` connect to one durable Tess session and stream responses bidirectionally.
|
||||
2. `AC-TESS-02`: `mosaic tess status|sessions|tree|attach|send|stop` operate against authorized provider capabilities with stable typed outputs and actionable errors.
|
||||
3. `AC-TESS-03`: Tess runs GPT-5.6 Sol at high reasoning and its effective runtime/model/tool policy is visible through status without exposing credentials.
|
||||
4. `AC-TESS-04`: Tess can inspect and message the Mosaic fleet, hand orchestration work to Mos, and demonstrate that Tess does not independently claim Mos-owned orchestration work.
|
||||
5. `AC-TESS-05`: Hermes adapter demonstrates session listing, streaming/message delivery, hierarchy mapping, and at least one approved capability in each of Kanban, skills, memory, tools, and cron—or reports unsupported capabilities fail-closed.
|
||||
6. `AC-TESS-06`: Restart/compaction test preserves session identity, pending inbox, last durable checkpoint, and a resumable handoff without duplicate side effects.
|
||||
7. `AC-TESS-07`: Unauthorized Discord users/channels, cross-tenant access, unsafe tool calls, forged approvals, and sensitive-output cases are denied and audited.
|
||||
8. `AC-TESS-08`: tmux/fleet and Matrix/native transport implementations pass the same provider contract suite; Matrix may remain non-default until readiness gates pass.
|
||||
9. `AC-TESS-09`: Baseline quality gates, unit/integration/contract tests, Discord+CLI E2E, restart/recovery tests, independent code review, and security review are green.
|
||||
10. `AC-TESS-10`: Migration matrix documents every audited Hermes/OpenClaw capability as native, adapted, deferred, or rejected, with cutover and rollback evidence.
|
||||
11. `AC-TESS-11`: User, admin, developer, API/OpenAPI, operations/recovery, and plugin-authoring documentation is current and linked from the sitemap.
|
||||
|
||||
### Constraints, Dependencies, Risks, and Assumptions
|
||||
|
||||
- Dependency: Mosaic gateway remains the single API surface; Pi is the native runtime; Valkey/PostgreSQL provide canonical durable state where required.
|
||||
- Dependency: Discord bot credentials and dedicated channel ID are deployment secrets provisioned outside source control.
|
||||
- Risk: Tess could drift into a second orchestrator. Mitigation: explicit role policy, Mos handoff contract, authority checks, and E2E boundary tests.
|
||||
- Risk: broad Hermes compatibility can freeze legacy semantics into Mosaic. Mitigation: Mosaic-owned normalized contracts and capability negotiation.
|
||||
- Risk: Discord creates a privileged remote-control surface. Mitigation: pairing/allowlists, RBAC, approvals, rate limits, audit, and safe tool classes.
|
||||
- Risk: transcript ingestion can violate privacy or overload memory. Mitigation: scoped opt-in import, redacted summaries, provenance, retention, and deduplication.
|
||||
- Risk: current root filesystem has limited headroom. Mitigation: isolated worktrees, no duplicated dependency installation unless required, and cleanup only after active-lane verification.
|
||||
- `ASSUMPTION:` The public name is **Tess**, because the user requested a name and the tessera/Mosaic relationship is distinctive; config must permit later display-name changes without renaming APIs or storage keys.
|
||||
- `ASSUMPTION:` The dedicated Discord channel ID and final guild policy will be supplied/provisioned during deployment, so implementation uses explicit configuration and fail-fast startup validation.
|
||||
- `ASSUMPTION:` tmux/fleet is the production transport for the first operational milestone; Matrix/native transport is implemented behind the same contract and promoted only after parity/reliability verification.
|
||||
- `ASSUMPTION:` Project/task truth remains in canonical Mosaic/project stores; semantic memory systems are retrieval/mirror layers, not hidden authorities.
|
||||
|
||||
### Testing and Delivery Intent
|
||||
|
||||
Delivery uses five gated milestones: runtime contracts/security; Pi service/state; Discord/CLI; fleet/Hermes/plugin suite; migration/Matrix/recovery/qualification. Every source-code task requires tests, independent review, a PR to `main`, terminal-green CI, and issue/task closure. Production activation additionally requires a clean-host Pi launch, dedicated Discord channel smoke test, CLI attach test, restart/recovery drill, and rollback procedure.
|
||||
|
||||
---
|
||||
|
||||
## Official Channel Plugin Workstream (#756)
|
||||
|
||||
### Problem and Objective
|
||||
|
||||
The Discord plugin currently couples Discord event handling, gateway bridging, and reply routing in one implementation and activates only on mentions. Mosaic needs an official channel adapter that behaves the same no matter whether the bound logical agent currently runs through Claude, Codex, Pi, OpenCode, or a future harness. The Discord connection and conversation address must remain stable while the gateway changes the runtime provider behind that logical session.
|
||||
|
||||
The objective is to make Discord the first implementation of a transport-neutral official channel contract, with explicit authorization and deterministic channel/thread routing that future Matrix, Slack, and other adapters can share.
|
||||
|
||||
### Scope
|
||||
|
||||
#### In Scope
|
||||
|
||||
1. `CHN-001`: Transport-neutral channel adapter, route, message, attachment, authorization-principal, response-target, and health contracts in `@mosaicstack/types`, including trusted per-binding logical-agent configuration selection.
|
||||
2. `CHN-002`: Stable channel conversation addresses based on logical agent plus channel/thread identity; harness, model, and runtime-provider IDs are forbidden from channel session keys.
|
||||
3. `DSC-001`: An authorized untagged message in a configured agent-bound channel routes to the agent and receives its response in that channel.
|
||||
4. `DSC-002`: A bot mention in a configured parent channel creates a Discord thread, or reuses the thread already attached to that same native message; the mentioned turn and subsequent thread turns route and respond in that thread.
|
||||
5. `DSC-003`: A message already inside an authorized thread inherits authorization from its configured parent and never attempts a nested thread.
|
||||
6. `DSC-004`: Guild, parent channel, user, pairing, and role authorization remains default-deny before thread creation or gateway dispatch.
|
||||
7. `DSC-005`: Discord service authentication, HMAC envelope integrity, replay protection, attachments, approvals, response chunking, and correlation behavior remain intact.
|
||||
8. `DSC-006`: The Discord adapter exposes lifecycle and health behavior through the shared channel contract without importing a harness SDK.
|
||||
|
||||
#### Out of Scope
|
||||
|
||||
1. The logical-agent lease, fencing epoch, execution grant, checkpoint, or cross-harness takeover implementation tracked by #754/#755.
|
||||
2. Dynamic Discord authorization administration in the web UI.
|
||||
3. Multi-guild tenant isolation, DMs, slash commands, voice, reactions, or production bot deployment.
|
||||
4. Implementing Matrix or Slack adapters in this slice.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
1. **Security:** no thread or dispatch side effect occurs until guild, parent channel, user, pairing, role, and bounded per-user/channel rate checks pass; attachment metadata is shape- and size-bounded; credentials never enter source, messages, session keys, or logs.
|
||||
2. **Portability:** channel contracts and stable conversation IDs contain no Claude, Codex, Pi, OpenCode, model, process, or provider-specific field; each configuration-owned binding selects its trusted logical agent without changing the channel identity.
|
||||
3. **Reliability:** repeated messages for one channel/thread resolve the same conversation handle; reconnecting the adapter does not require a harness-specific rebinding.
|
||||
4. **Maintainability:** Discord-specific API translation stays in the Discord package; gateway and future adapters depend on transport-neutral contracts.
|
||||
5. **Observability:** thread creation or routing failure is reported without message content or credential material.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
1. `AC-CHN-01`: Contract and behavior tests prove the plugin route contains only logical agent plus channel/thread identity and produces the same stable conversation handle regardless of underlying harness selection.
|
||||
2. `AC-CHN-02`: A mentioned authorized parent-channel message creates a thread (or reuses its already-attached thread), dispatches to the thread conversation, and targets the response to that thread.
|
||||
3. `AC-CHN-03`: An untagged authorized parent-channel message dispatches to the parent conversation and targets the response to the parent channel.
|
||||
4. `AC-CHN-04`: Untagged follow-ups inside an authorized thread dispatch and respond in that same thread without creating a nested thread.
|
||||
5. `AC-CHN-05`: Unauthorized guilds, channels, users, unpaired users, insufficient roles, and rate-limited senders produce no thread and no gateway dispatch.
|
||||
6. `AC-CHN-06`: Shared channel contracts are exported from `@mosaicstack/types`, Discord implements the lifecycle/health seam, and no harness SDK is imported by the plugin.
|
||||
7. `AC-CHN-07`: Focused routing/auth tests, package tests, typecheck, lint, formatting, coverage, independent code/security review, and terminal-green CI pass.
|
||||
|
||||
### Constraints, Risks, and Assumptions
|
||||
|
||||
- Dependency: Mosaic gateway remains the policy, durable-session, audit, and runtime-provider boundary.
|
||||
- Constraint: This work must not modify orchestrator-to-Pi migration or #754/#755 lease/fencing files.
|
||||
- Risk: accepting untagged messages could create noisy or unintended agent input. Mitigation: only explicitly configured channels and paired, role-authorized users are accepted, with bounded per-user/channel message and thread rates.
|
||||
- Risk: Discord thread creation can fail because of channel permissions, archived state, or API rate limits. Mitigation: fail without dispatching a turn whose response destination cannot be honored, and emit sanitized diagnostics.
|
||||
- `ASSUMPTION:` Configured channels are dedicated agent interaction surfaces, so authorized untagged human messages are intentional agent input.
|
||||
- `ASSUMPTION:` Mention in a parent channel selects a public thread; messages already in a thread remain there because Discord has no nested threads.
|
||||
- `ASSUMPTION:` One Discord bot may serve multiple configuration-owned logical-agent bindings.
|
||||
- `ASSUMPTION:` Static allowlists and paired-user roles are the authorization administration surface for this slice.
|
||||
|
||||
### Testing and Delivery Intent
|
||||
|
||||
Use TDD for remote-ingress routing and permission boundaries. Required evidence includes parent-channel mention, untagged parent message, existing-thread follow-up, existing-thread mention, thread reuse, unauthorized side-effect denial, stable harness-neutral conversation identity, adapter health, and regression coverage for signed envelopes and approvals. Deliver through issue #756, a reviewed squash PR to `main`, terminal-green CI, and issue closure.
|
||||
|
||||
---
|
||||
|
||||
## Mos Runtime Portability Workstream (MOS-PORT)
|
||||
|
||||
### Problem and Objective
|
||||
|
||||
Mos is currently identified partly by a harness-native session and communication process. Replacement/rebinding exists, but no gateway-enforced logical identity or fencing prevents a stale harness from continuing to reply or execute effects after takeover.
|
||||
|
||||
The objective is to make Mos a server-derived logical Mosaic identity whose authority can move safely among runtime connectors. The gateway owns identity, lease, policy, and audit; harnesses remain replaceable adapters.
|
||||
|
||||
### M1 Requirements
|
||||
|
||||
1. `MOS-PORT-ID-001`: Define a normalized logical-agent identity independent of Claude Code, Pi, Codex, tmux, Matrix, and provider-native session IDs.
|
||||
2. `MOS-PORT-LEASE-001`: Persist one exclusive connector lease per tenant/logical-agent/binding with CAS acquisition, monotonic fencing epoch, TTL, heartbeat, explicit release, and takeover.
|
||||
3. `MOS-PORT-FENCE-001`: Bind every connector dispatch/execution grant to the current server-derived tenant, logical identity, binding, connector, scopes, expiry, and lease epoch.
|
||||
4. `MOS-PORT-FENCE-002`: Reject and audit stale, expired, forged, cross-tenant, cross-binding, and unauthorized grants before connector, channel, provider, or tool side effects.
|
||||
5. `MOS-PORT-OBS-001`: Emit credential-safe correlation/audit events for lease acquire, renew, takeover, reject, release, and expiry.
|
||||
6. `MOS-PORT-ARCH-001`: Runtime/provider adapters consume normalized lease context without adding harness-native schemas to Mosaic core.
|
||||
|
||||
### M1 Acceptance Criteria
|
||||
|
||||
1. `AC-MOS-PORT-01`: Two contenders for one binding cannot simultaneously hold current authority under concurrency.
|
||||
2. `AC-MOS-PORT-02`: Successful takeover increments the fencing epoch and every operation from the old epoch fails closed before side effects.
|
||||
3. `AC-MOS-PORT-03`: Gateway/database restart preserves lease and epoch state; expired leases can be recovered only through the authorized takeover path.
|
||||
4. `AC-MOS-PORT-04`: Cross-tenant, cross-agent, cross-binding, forged, and expired lease/grant cases are denied and audited.
|
||||
5. `AC-MOS-PORT-05`: Unit, migration, repository close/reopen, concurrency, abuse, gateway integration, independent security review, CI, and documentation gates pass.
|
||||
|
||||
### Deferred to Later #754 Milestones
|
||||
|
||||
Canonical checkpoint/handoff payloads, exactly-once connector receipts, concrete Claude/Pi/Codex adapters, channel cutover, and full cross-harness failover/rollback E2E are explicitly out of M1 scope.
|
||||
|
||||
---
|
||||
|
||||
## Workspace placement guard hardening (#1174)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
The Bash pre-tool guard must prevent Git checkouts and repository state from being placed under
|
||||
`$HOME` without refusing ordinary Git commands merely because a source, option value, branch name,
|
||||
or metadata mentions `$HOME`. A guard that over-blocks routine work is unsafe because operators
|
||||
will route around it.
|
||||
|
||||
### Scope and requirements
|
||||
|
||||
1. `WPG-REQ-01`: `git clone` and `git worktree add` placement SHALL be judged from their placement
|
||||
operands, not from every HOME-shaped word in the command.
|
||||
2. `WPG-REQ-02`: Clone sources, references, templates, environment assignments, and non-placement
|
||||
worktree metadata MAY resolve under HOME when all placement operands resolve elsewhere.
|
||||
3. `WPG-REQ-03`: Both attached and separate-value `--separate-git-dir` forms SHALL remain placement
|
||||
operands and SHALL be refused when they resolve under HOME.
|
||||
4. `WPG-REQ-04`: Option classification SHALL account for Git's rule-generated boolean negations
|
||||
without relying on an enumerable allowlist of flag spellings.
|
||||
5. `WPG-REQ-05`: Quote removal, escapes, shell command boundaries, redirections, and end-of-options
|
||||
handling SHALL preserve existing fail-closed checkout coverage.
|
||||
6. `WPG-REQ-06`: Absolute placement aliases SHALL resolve shell-known HOME spellings, dot segments,
|
||||
repeated separators, and existing symlink parents before the HOME boundary comparison.
|
||||
7. Relative targets whose effective path depends on the shell cwd are out of scope and tracked by
|
||||
#1197.
|
||||
|
||||
### Acceptance and verification
|
||||
|
||||
1. Git's own option parser accepts each tested flag, including generated `--no-*` forms, while the
|
||||
guard allows a HOME-valued source with an explicit safe destination.
|
||||
2. Equivalent clone and worktree fixtures cover rule-generated negations and remain discriminating
|
||||
against the prior head where the defect existed.
|
||||
3. Real HOME destinations and both `--separate-git-dir` forms remain blocked, including placements
|
||||
after shell command boundaries.
|
||||
4. The full hermetic guard suite, syntax/static checks, adversarial probes, independent review, and
|
||||
terminal-green CI pass before merge.
|
||||
5. Any option-classification residual is documented with its deliberate failure direction.
|
||||
|
||||
### Constraints, risks, and assumptions
|
||||
|
||||
- Security and usability are co-equal: neither a placement bypass nor routine over-block is an
|
||||
acceptable repair.
|
||||
- `ASSUMPTION:` The value-taking option surface exposed by the installed Git version is closed and
|
||||
measurable through Git's own parser/help output; rationale: boolean flags are rule-generated,
|
||||
while separate-value options have explicit grammar and must be classified as such.
|
||||
- Risk: a future Git release may add a new value-taking placement option. Mitigation: document the
|
||||
chosen residual direction and pin every currently supported placement option in behavior tests.
|
||||
- Risk: a symlink can be replaced after pre-execution canonicalization. Mitigation: resolve every
|
||||
existing parent physically and document the remaining inherent TOCTOU window; the worktree helper
|
||||
remains the authoritative path-derivation mechanism, with atomic closure tracked by #1199.
|
||||
|
||||
---
|
||||
|
||||
## Release Integrity Workstream (RI, #1275)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
At `next` 476db12b (review of 2026-08-17), publication from `next` is not bound to the full verification pipeline for the same commit: the publish pipeline's publish steps depend on `build` only, while ordinary push CI excludes `next`. Public Forge/MACP paths contain false-success placeholders: a stub executor that reports `completed` with exit zero, planning/remediation gates that execute literal `true`, a review gate that echoes an approving verdict, and a gate runner that treats empty commands and unimplemented CI-provider gates as passing. Shipping UI surfaces can render a failed fetch as an empty, healthy collection.
|
||||
|
||||
Objective: for alpha 0.0.50, the release cannot publish, report, or display work state that the repository has not actually verified. Decisions SDLC-D-033 through SDLC-D-038 (Jason, 2026-08-17) scope this floor; full decision text and required-behavior lists live in jarvis-brain `docs/plans/2026-08-16_mosaic-stack-sdlc-protocol.md` and `data/decisions/mosaic-stack-sdlc-protocol.json`. This section restates only the normative requirements.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
1. **RI-N1 Exact-commit publication verification (SDLC-D-034).** One canonical terminal verification command performs self-contained re-verification in the publish pipeline against the job's checked-out commit before any external publication effect. The command contains or invokes the complete mandatory verification set (semantic parity with the PR merge gate, including sanitization, upgrade-guard, typecheck, lint, format check, tests, and build); CI and publication do not maintain separate semantic checklists. Every publish step depends on the verification step in the executable pipeline DAG. Provider commit identity and `git rev-parse HEAD` must identify the same commit. Missing, skipped, cancelled, stale, or inconclusive checks fail closed. Documentation-only runs may skip publication but cannot bypass verification when a publication effect will occur. A negative control must prove that a broken check blocks every publish step.
|
||||
|
||||
2. **RI-N2 Fail-closed Forge/MACP with explicit simulation (SDLC-D-035).** Simulation requires explicit caller intent (e.g. `--simulate`) and produces a distinct typed `simulated` state that can never satisfy dependencies, acceptance criteria, gates, merge, or release. Normal execution exits nonzero with a typed capability failure when a required executor, reviewer, command, or CI provider is absent — no stub completion, no literal-`true` gates, no synthetic approvals, no empty-command passes. A manual gate with no automation enters a waiting state; it does not pass. Positive tests prove explicit simulation still works; negative controls prove simulation and every missing-provider case cannot advance lifecycle state.
|
||||
|
||||
3. **RI-N3 One transitional PRD authority (SDLC-D-036).** `@mosaicstack/prdy` structured storage under `docs/prdy/`, driven by `mosaic mission --plan`, is the authoritative PRD representation for the alpha. `mosaic prdy` either routes through the same application service or operates only as an explicit, named Markdown import/export adapter; `docs/PRD.md` is not a peer authority. `mission --plan` must persist the mission↔PRD linkage (mission id/version, PRD id/version, selected requirements). Markdown output is a generated view carrying source identity; editing it cannot mutate authority silently. Import is explicit, validated, and conflict-aware (proposed successor, never overwrite). Structural validity is separate from approval.
|
||||
|
||||
4. **RI-N4 One quality-rails evaluator (SDLC-D-037).** The TypeScript quality-rails package is the sole authoritative evaluator. A complete probe inventory maps every current TypeScript and shell check to one canonical check with disposition (preserve/strengthen/retire, each named). Effective shell enforcement probes are absorbed before their independent paths retire; expected-file presence alone is not parity. The evaluator returns typed results (`passed`/`failed`/`blocked`/`error`/`not-applicable`) with check version, subject, and reason; missing implementation, missing input, unknown check, process error, timeout, or malformed output can never become `passed` or an unqualified skip. Check definitions and policy are versioned and digested. Shell commands become thin adapters with no separate verdict logic. The canonical terminal verification command (RI-N1) invokes this evaluator rather than duplicating its logic. Contract, parity, and negative-control tests are required, plus independent review of probe equivalence.
|
||||
|
||||
5. **RI-N5 Consequence-aware stale UI (SDLC-D-038).** Mission Control distinguishes typed freshness states (`current`, `stale`, `partial`, `unknown`, `unavailable`) rather than inferring from empty arrays or null. A failed fetch never renders as an empty healthy collection. Last-known data may display for situational awareness only with source identity, version, and age visibly labeled; any derived completion/assurance/release verdict whose inputs are stale becomes `unknown`; all state-changing actions are disabled until fresh state loads and is revalidated. With no verified snapshot, surfaces show an explicit unavailable state. Cache corruption, cross-workspace data, schema mismatch, and version regression invalidate the snapshot. Tests cover the failure matrix (network, auth, malformed, partial, corruption, stale age, schema mismatch, recovery, stale-action rejection) with negative controls proving no case yields a current green verdict or enabled mutation.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- AC-RI-1: A push to `next` that fails any mandatory verification step publishes nothing (no npm package, no image), demonstrated by a checked-in negative control and by pipeline evidence on a real `next` publish run where the verification step is green and every publish step depends on it.
|
||||
- AC-RI-2: With no executor/reviewer/CI provider wired, Forge and MACP normal runs exit nonzero with typed capability failures; with `--simulate`, runs complete but every result is typed `simulated` and cannot satisfy any gate, dependency, or completion state — proven by unit tests including negative controls.
|
||||
- AC-RI-3: A PRD created or revised through either `mosaic mission --plan` or `mosaic prdy` resolves to one authority under `docs/prdy/` with stable identities and versions; the mission↔PRD linkage survives restart; a Markdown export is labeled as generated and cannot silently become a second writer; divergent legacy content blocks baseline claims until explicitly resolved — proven by contract tests.
|
||||
- AC-RI-4: `quality-rails check` through any entry point (TS CLI, framework shell adapter) returns the same typed verdict for the same subject; the probe inventory names every legacy check's disposition; a deliberately broken probe fails closed — proven by contract/parity/negative-control tests and independent review of probe equivalence.
|
||||
- AC-RI-5: No shipping surface renders a failed fetch as an empty healthy state; stale/partial/unavailable states are typed, labeled, and mutation-disabled — proven by the failure-matrix tests.
|
||||
- AC-RI-6: All cards merged to `next` via squash PR with terminal-green CI; release evidence for 0.0.50 records commit, verification run, and published artifacts.
|
||||
|
||||
### Out of scope
|
||||
|
||||
The canonical dispatcher/control-plane vertical slice (work graph, execution attempts, fenced leases, typed check-in, independent verifier dispatch) is decided post-alpha (SDLC-D-033, option B). Multi-pipeline verification certificates (SDLC-D-034 option B) are post-alpha. Full AF-1..AF-4 objective matrices and Mission Control portfolio surfaces are post-alpha.
|
||||
|
||||
## Official CLI Capability and Tool Migration Workstream (T78)
|
||||
|
||||
Normative contract on integration trunk `next`:
|
||||
[docs/requirements/cli-capability-migration.md](./requirements/cli-capability-migration.md):
|
||||
migrates agent-facing operations from directly invoked scripts into documented, first-class
|
||||
`mosaic` CLI command groups, together with the central-registry resolver, capability catalog,
|
||||
adapter boundary, and phased legacy-tool-tree decommission the migration requires. The contract
|
||||
carries its own implementation hold and delivery stages.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
kind: record
|
||||
status: superseded
|
||||
---
|
||||
|
||||
# PRD rev0 — archive record
|
||||
|
||||
`PRD.md` in this directory is the 2026-08-26 North Star PRD, archived **verbatim** at
|
||||
ratification of rev1 (2026-09-01). It is byte-identical to `origin/next:docs/PRD.md` at
|
||||
commit `9aa4983c` (SHA-256
|
||||
`60cc2f98697471850caa3440d79139d70f67eda585a2ee465fdcd517bc36afdf`). Per GOV.1 the archived
|
||||
bytes are never edited — not even to repair links — so the digest stays verifiable.
|
||||
|
||||
**Its relative links were written for `docs/PRD.md` and do not resolve from this directory.**
|
||||
That is an accepted, intentional consequence of archive-never-edit (owner disposition: the
|
||||
control-plane-surfaces lane, 2026-09-02, review `CPS-PRD-REV1-REVIEW-Q90` F3). Resolve them
|
||||
with this table; every target still exists in the tree.
|
||||
|
||||
| Link text in `PRD.md` (lines) | Resolves to |
|
||||
| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `./archive/PRD-v0.1.md` (17) | [../../archive/PRD-v0.1.md](../../archive/PRD-v0.1.md) |
|
||||
| `./ROADMAP.md` (18, 177, 249) | [../../ROADMAP.md](../../ROADMAP.md) |
|
||||
| `./requirements/native-kanban-sot.md` (98) | [../../requirements/native-kanban-sot.md](../../requirements/native-kanban-sot.md) |
|
||||
| `./fleet/NORTH_STAR.md` (181) | [../../fleet/NORTH_STAR.md](../../fleet/NORTH_STAR.md) |
|
||||
| `./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md` (444) | [../../fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md](../../fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) |
|
||||
| `./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md` (444) | [../../fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md](../../fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md) |
|
||||
| `./TASKS.md` (462) | [../../TASKS.md](../../TASKS.md) |
|
||||
| `./native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md` (623) | [../../native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md](../../native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md) |
|
||||
| `./requirements/cli-capability-migration.md` (906) | [../../requirements/cli-capability-migration.md](../../requirements/cli-capability-migration.md) |
|
||||
|
||||
Rule for future archives (recorded here; GOV.1 carries the general archive contract): every
|
||||
`docs/PRDs/<date>_PRD_revN/` archived from a different original location ships a `README.md`
|
||||
like this one — digest, original path, and a link-resolution table — instead of edited bytes.
|
||||
|
||||
Current revision: see [`docs/PRD.md`](../../PRD.md).
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
id: AUTHN.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# AUTHN.1 — Authentication accounts
|
||||
|
||||
Agent-side provider credentials: the accounts seats use to reach providers.
|
||||
(Human login identity is D10 territory — better-auth as system of record — and
|
||||
is out of this section's scope.)
|
||||
|
||||
## Authentication configuration surface (WebUI page + CLI)
|
||||
|
||||
| Control | Notes |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
|
||||
| in-browser OAuth establishment | the OAuth flow runs in-browser; whether the backing terminal flow is tmux-bridged is open: [[GOV.5-open-questions]] Q-N1 |
|
||||
| configured accounts list | provider, mode (OAuth/API), status, holder |
|
||||
| force renew | |
|
||||
| deactivate | deactivated accounts drop out of every seat/harness selector |
|
||||
| allowed harnesses | which harnesses may use this account |
|
||||
|
||||
## Custody rules
|
||||
|
||||
- Secrets live with the **credential broker** (OpenBao/Vault or flat files per
|
||||
deployment mode — D3), never in the brain tree, never in manifests, never in
|
||||
Postgres records. Enforced role manifests declare
|
||||
`credentials: {store: none, providerTokens: denied}` — the enforced roles
|
||||
hold no credentials at all; accounts are a launcher/broker concern.
|
||||
- Multi-account per provider is a requirement (onboarding D4 already captures
|
||||
multi-account enrollment).
|
||||
- Account shape in the seat record (single account vs per-provider map) is
|
||||
open: [[GOV.5-open-questions]] Q-D2.
|
||||
|
||||
## Credential-broker custody rules (pulled 2026-08-31, generalized from the vault draft)
|
||||
|
||||
- Reads require a token scoped to the needed paths; provisioning and writes go
|
||||
through a declared channel with documented purpose. An ordinary role never
|
||||
mints credentials or creates production paths.
|
||||
- Canonical secret path: `environment / service / component / secret-name`,
|
||||
lowercase kebab-case, nothing sensitive encoded in the path; environments
|
||||
never cross-reference each other's mounts. Standard field names
|
||||
(`username`/`password`, `token`, `host`/`port`/`url`).
|
||||
- Only the needed field is extracted into the consuming process; values are
|
||||
never echoed to logs or transcripts — read success is proven by field
|
||||
presence and digest, never by printing the value.
|
||||
- Least privilege, short-lived tokens, no local copies, immediate rotation on
|
||||
compromise; every access audited by the broker.
|
||||
|
||||
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
|
||||
|
||||
**Canonical ground truth**: `ADMIN-GUIDE/security/sso-providers.md` (D10 ground
|
||||
truth: better-auth + Authentik/WorkOS/Keycloak OIDC).
|
||||
**Pending pulls**: DRAFT S2 `identity-lifecycle.md` (D10 + the #1430 bootstrap
|
||||
fix) and `custody-schema.md`; brain `docs/guides/proposed/operations/vault.md`
|
||||
(credential-broker custody rules this section states without operational detail).
|
||||
|
||||
## S2 contract feed (extraction 2026-08-31)
|
||||
|
||||
Full extraction record: lane `S2-EXTRACTION-2026-08-31.md` (per-contract cores, dependency edges, ruling cross-checks). Pulls binding on this section (identity-lifecycle, contract 4, plus
|
||||
wizard AUTHN clauses):
|
||||
|
||||
- better-auth tables are the **only** account system of record (D10); IdPs are
|
||||
login methods only; account creation grants nothing.
|
||||
- `registration_mode` open/invite/closed, defaults **closed** post-bootstrap,
|
||||
forced closed during the epoch, enforced at a better-auth hook.
|
||||
- Bootstrap/first-admin invariant (#1430): zero-to-one-admin exactly once per
|
||||
epoch, one atomic transaction, durable fail-closed `bootstrap_state`,
|
||||
re-runnable; first-admin-via-SSO runs as a bootstrap-writer transaction,
|
||||
never JIT. **v1 first admin is password-only — a disclosed PRD deviation.**
|
||||
- JIT defaults OFF per-provider always; JIT users get `member`, never
|
||||
elevated; **role/authorization attributes are never mapped from IdP
|
||||
claims**. Linking keyed `(issuer, subject)`; explicit linking = step-up
|
||||
reauth ≤10 min; automatic linking gated by off-by-default
|
||||
`trusted_for_linking` + verified email.
|
||||
- Deactivation (ban) must bound all entry paths — **live defect: the
|
||||
admin-bearer-token path does not check banned status**. Deletion deferred;
|
||||
the existing hard-delete endpoint and `mosaic auth users delete` are
|
||||
mandated for removal.
|
||||
|
||||
## Seat auth shape ruling (Q-D2, Jason 2026-09-01)
|
||||
|
||||
Per-provider map in `profile.json`, values are credential-broker references —
|
||||
never secret material. The broker custody rules above govern resolution;
|
||||
extraction stays field-scoped and digest-proven.
|
||||
@@ -0,0 +1,146 @@
|
||||
---
|
||||
id: AUTHZ.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# AUTHZ.1 — Capability authority, enforcement, and accepted risk
|
||||
|
||||
The agent-side authority model: what binds a seat, where it is enforced, what
|
||||
is closed by construction, and what is accepted as residual risk. Sources: the
|
||||
L2 authorization contracts, `mosaic-core` (measured 2026-08-31), and the lane's
|
||||
`AUTHORIZATION-GAPS.md`.
|
||||
|
||||
## Glossary
|
||||
|
||||
**Privilege escapation** _(Jason, 2026-08-31)_ — the outcome class in which an
|
||||
agent exercises authority it was never granted, regardless of mechanism.
|
||||
Deliberately collapses escalation and misdirection: the outcome is identical;
|
||||
the distinction matters only when choosing a control.
|
||||
|
||||
## Authority composition is pure intersection
|
||||
|
||||
```
|
||||
role capability ceiling
|
||||
∩ assignment scope ∩ lease scope ∩ workflow state
|
||||
∩ target policy ∩ trusted backend availability
|
||||
= effective capability grant
|
||||
```
|
||||
|
||||
No operation adds capability. Only an authenticated principal with
|
||||
role-management authority may create, edit, activate, bind, or roll back roles
|
||||
(L2-D13); agents cannot, ever. Orchestrators cannot deploy seats at all —
|
||||
coordination goes through `mosaic coord` requests to the coordinator service
|
||||
(register OD-08/OD-09). Cycle detection is unnecessary because no grant edge
|
||||
exists.
|
||||
|
||||
## Enforcement point: `mosaic-core`
|
||||
|
||||
A tracked, non-npm Pi extension loaded via role-scoped settings. Per seat, per
|
||||
launch: trusted-path manifest load
|
||||
(`<brainHome>/fleet/roles/<role>/mosaic-core.manifest.json` — committed,
|
||||
non-symlink, role cross-checked), fail-closed state machine
|
||||
(`not-applied | applied | fail-closed`), exact active-tool reconciliation,
|
||||
per-call interception with read containment (canonicalization + realpath +
|
||||
credential-path deny), per-incarnation append-only journal. Capabilities C1–C8
|
||||
are the authority; tools are their bound projections; `tools[]` must equal the
|
||||
bound bindings exactly. The capability map is closed and hardcoded in the
|
||||
canary phase (gap G7 — by design for now; L2-D21 is the end state).
|
||||
|
||||
## The firewall rules
|
||||
|
||||
1. **Agents never reach role-management surfaces** (L2-D13). A seat that can
|
||||
invoke role management can self-grant, voiding the model.
|
||||
2. **The coordinator is a service, never a seat** (gap G4; register OD-08). A
|
||||
coordinator-as-seat turns the intersection chain into a cycle.
|
||||
|
||||
## Closed vs accepted
|
||||
|
||||
**Closed by construction: escalation.** The intersection chain admits no
|
||||
capability-adding edge.
|
||||
|
||||
**Accepted within reason: misdirection.** A rogue or colluding orchestrator can
|
||||
still _route work_ to a seat that already legitimately holds a capability. The
|
||||
authority is genuine, so no structural control can refuse it. Jason's ruling:
|
||||
escalation and misdirection are tiered **by outcome — identically**; locking
|
||||
down far enough to mechanically prevent misdirection cripples the system to
|
||||
inoperability. Mitigation posture: **audit and visibility, not prevention** —
|
||||
see the audit surface in [[UI.1-webui-surfaces]] and gap G3 below.
|
||||
|
||||
## Role separation as the working mitigation
|
||||
|
||||
Design intent (not yet enforced by any manifest — gap C3 in the lane ledger):
|
||||
|
||||
| Role | Must NOT |
|
||||
| -------------- | -------------------------------- |
|
||||
| coding agent | create remote git repositories |
|
||||
| gate agent | code |
|
||||
| review agent | code |
|
||||
| interact agent | code, create remote repositories |
|
||||
|
||||
Breaking the monopoly means no single seat holds an exit path; escape requires
|
||||
inter-seat coordination, which is exactly what the audit exists to catch.
|
||||
Register OD-25–OD-34 (review/gate/arena families, independence rules) carry the
|
||||
role taxonomy this matrix projects onto.
|
||||
|
||||
## Gap register (measured 2026-08-31)
|
||||
|
||||
| ID | Gap | Status |
|
||||
| --- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
||||
| G1 | No least-privilege language anywhere in L1/L2/NORTH-STAR (grep-verified) | **L2-D52 amendment drafted** |
|
||||
| G2 | Assignment issuance criteria unspecified (renewal names criteria; issuance does not) — full-ceiling requests validate cleanly | **L2-D52 amendment drafted** |
|
||||
| G3 | No misdirection audit exists — no tooling, agent, or surface | open → [[UI.1-webui-surfaces]] §Audit |
|
||||
| G4 | Coordinator-as-seat would collapse the model | firewall — never violate |
|
||||
| G5 | Seat config mixes authority classes (role binding beside a model dropdown) | open → [[SEAT.1-seat-profile]] |
|
||||
| G6 | `role-harness-config/DESIGN.md` scope defect (unstated surface) | fix drafted (amendment in `proposed/docs/`) |
|
||||
| G7 | Capability map closed/hardcoded | by design (canary phase) |
|
||||
|
||||
Amendments staged in `proposed/docs/` per the lane convention; ledger items
|
||||
A3/A4 track ratification. The auditor-identity question (an auditor agent is
|
||||
itself a seat, itself subject to misdirection) is on the grill:
|
||||
[[GOV.5-open-questions]] Q-A1.
|
||||
|
||||
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
|
||||
|
||||
**Canonical ground truth**: `DEVELOPER-GUIDE/architecture/mutator-class-gate.md`
|
||||
(the default-deny whole-class gate this section's language must match),
|
||||
`lease-broker-protocol.md` + `lease-broker-security.md` (incl. the named
|
||||
promote-lease-lost-ACK residual), `ADMIN-GUIDE/security/discord-ingress.md`
|
||||
(the one implemented admission/role model).
|
||||
**Pending pulls**: DRAFT S2 `rbac-grant-model.md` (granular RBAC per rev0 §4).
|
||||
|
||||
## S2 contract feed (extraction 2026-08-31)
|
||||
|
||||
Full extraction record: lane `S2-EXTRACTION-2026-08-31.md` (per-contract cores, dependency edges, ruling cross-checks). Pulls binding on this section:
|
||||
|
||||
- **Three-layer authority (contract 2)**: platform role (member/admin,
|
||||
instance administration only, **no implicit tenant access** — two live admin
|
||||
bypass paths named non-conformant and scheduled for retirement:
|
||||
`command-authorization.service.ts` admin short-circuit, `mcp.service.ts`
|
||||
scope derivation); hierarchy grants (viewer/member/owner, deny-by-default,
|
||||
down-chain, effective = max, live fail-closed); workspace membership
|
||||
(its own mechanism, REQ-ID-001). The layers are non-substitutable.
|
||||
- **Agents are not a valid grant subject** — grant subject is exactly-one-of
|
||||
user_id/team_id. Structural enforcement of the agents-never-reach-role-
|
||||
surfaces ruling, stronger than policy.
|
||||
- **Consent ≠ authorization (contract 7 §5.7)**: consent records govern
|
||||
agentic/feature data access, are distinct from hierarchy grants, and confer
|
||||
no platform authorization; default-deny with **no platform-admin bypass**;
|
||||
consent mutation is subject-only (admins refused at write time).
|
||||
- **Bounded revocation propagation**: next authz decision denies; open
|
||||
Socket.IO connections re-evaluated within 30s or next inbound message.
|
||||
- **company-CRUD capability**: platform-scoped, admin-assigned, audited
|
||||
delegation of exactly one visibility-mutation command (`platform_capabilities`
|
||||
table) — the model's template for narrow capability delegation.
|
||||
- **Membership locality + no-existence-oracle (contract 8 §3)**:
|
||||
member-readable workspaces contribute only at their own node, never promoted
|
||||
upward; unreadable vs nonexistent are byte-equivalent.
|
||||
|
||||
## Audit implementation ruling (Q-A1/Q-A2, Jason 2026-09-01)
|
||||
|
||||
The authorization audit is **mechanical tooling**: deterministic checks over
|
||||
the grant/assignment record, witness-style (the S2 writer-coverage pattern),
|
||||
feeding the audit page read-only. Agents may consume audit output but never
|
||||
produce the verdict — prompt adherence is not an enforcement mechanism. Q-A2
|
||||
(who audits the auditor) dissolves: the auditor is code, audited by ordinary
|
||||
review and CI.
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
id: CLI.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# CLI.1 — CLI surface and parity obligation
|
||||
|
||||
## The rule
|
||||
|
||||
The CLI is the **primary execution method** (D8); the WebUI operates the same
|
||||
tooling over the Gateway API and never bypasses it (D12). Register OD-49 fixes
|
||||
`mosaic config` as the stable installation-configuration command family backed
|
||||
by one desired-state engine; register OD-53 makes every interface (CLI, TUI,
|
||||
WebUI, API, automation) a client of that same engine.
|
||||
|
||||
Parity is therefore **structural, not aspirational**: a capability that exists
|
||||
in the CLI without a WebUI surface is an incomplete projection; a WebUI wish
|
||||
with no backing tool is **"blocked on tooling"** and the tool is built first
|
||||
(D8 consequence). Neither side ever grows private logic.
|
||||
|
||||
## Parity matrix obligation
|
||||
|
||||
The ratified bundle must carry (or cite, per D8's baseline inputs) three
|
||||
artifacts, kept current:
|
||||
|
||||
1. **Tool inventory** — what official tooling exists and what is missing.
|
||||
2. **WebUI→tool mapping** — every page control mapped to the tool it calls
|
||||
([[UI.1-webui-surfaces]] page inventory is the row source).
|
||||
3. **Measured `next`-branch state** — what actually works today.
|
||||
|
||||
All three artifacts were measured 2026-08-31 against `origin/next` commit
|
||||
`9aa4983c` and appear below. Grill: [[GOV.5-open-questions]] Q-C1 (matrix
|
||||
freshness ownership after ratification).
|
||||
|
||||
## Artifact 1 — tool inventory (measured, `origin/next` @ `9aa4983c`)
|
||||
|
||||
Registration root: `packages/mosaic/src/cli.ts` (commander); command modules
|
||||
under `packages/mosaic/src/commands/`; `coord`/`prdy`/`doctor`/runtime
|
||||
launchers dispatch to bash tools under `packages/mosaic/framework/tools/`
|
||||
(subcommand tables at `commands/launch.ts:1151–1255`); sibling packages
|
||||
(`brain`, `forge`, `macp`, `quality-rails`, `log`, `memory`, `queue`,
|
||||
`storage`) register their own families.
|
||||
|
||||
Control-plane-relevant families, by rev1 domain:
|
||||
|
||||
| Domain | Families (measured) |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| SEAT | `fleet` (init/install/systemd/lifecycle/roster/add/remove/verify/ps), `fleet` roster-v2 CRUD (`get/plan/create/update/delete`), `fleet apply`/`reconcile`/`doctor`/`regen`, `fleet provision`, `fleet migrate-v1 preview`, `agent` (configs + enroll + nested fleet-agent commands), `promote`, `comms send` |
|
||||
| ROLE | `fleet persona` (`list/show/customize` — baseline ⊕ `roles.local/` overrides), `fleet profile` (`list/show` topology templates) |
|
||||
| HARN | `config` (framework config + hooks), `compose-contract <harness>`, `skill`, `seq`, `init`/`sync`/`bootstrap`, `doctor`, runtime launchers (`claude`/`codex`/`opencode`/`pi`, experimental `claudex`, `yolo`) |
|
||||
| PROV | `gateway config` (raw provider API-key env vars only), `wizard` (setup-time provider config) |
|
||||
| AUTHN | `login`, `auth users {list,create,delete}`, `auth sso {list,test}` (stubbed — see gaps), `auth sessions list` (stubbed), `gateway` token lifecycle (`config rotate-token/recover-token`) |
|
||||
| SESS | `tui`, `sessions {list,resume,destroy}`, `interaction` (durable-session surface: enroll/attach/send/chat/stop/recover), `coord`, `watch`, `mission` |
|
||||
| Governance/other | `prdy {init,update,validate,status}`, `federation {grant,peer}`, `macp tasks gate`, `telemetry`, `upgrade`/`update`/`restore`/`uninstall`, `q`, sibling-package families |
|
||||
|
||||
Notable structural facts: there is **no top-level `mosaic role` verb** — role
|
||||
management lives at `fleet persona`, three levels deep; and `doctor`/`status`
|
||||
exist twice (top-level framework-scoped vs `fleet`-scoped), shadowing by name.
|
||||
|
||||
## Artifact 2 + 3 — WebUI→tool mapping with measured state
|
||||
|
||||
Rows are the [[UI.1-webui-surfaces]] page domains; measured against
|
||||
`apps/web/src/spa/pages/` and the gateway controllers on the same commit.
|
||||
|
||||
| Surface function | WebUI today | CLI today | Parity state |
|
||||
| ---------------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Seat lifecycle & roster (SEAT) | **no Seats page** (routes are only /admin, /settings, /projects, /tasks, /chat) | complete (`fleet`/`agent` families) | CLI-ahead — page is D2's work, tooling exists |
|
||||
| Role/persona config (ROLE) | no page | `fleet persona` | CLI-ahead; naming mismatch: no `mosaic role` verb for the ROLE page to mirror |
|
||||
| User role/ban (AUTHN) | admin UsersTab toggles role/ban via admin endpoints directly | `auth users` lacks `set-role`/`ban`/`unban` | **WebUI-only mutation — violates the D12 rule as implemented** |
|
||||
| SSO admin (AUTHN) | SsoProviderSection reads _public_ `/api/sso/providers` discovery | `auth sso list/test` stubbed: "admin endpoint missing" | **blocked on gateway tooling**; CLI and WebUI don't even hit the same surface |
|
||||
| Auth-session admin (AUTHN) | — | `auth sessions list` stubbed (no server endpoint) | blocked on gateway tooling |
|
||||
| Provider list/test (PROV) | settings ProvidersTab: `GET /api/providers`, `POST /api/providers/test` | none — only `gateway config` raw env-var writes | **WebUI-only read/test — no `mosaic provider` family exists** |
|
||||
| Default harness/provider/model selection (HARN/SESS) | `GET/PUT /api/chat/preferences/selection` per user | none persists the stored preference (`tui --model` is per-session only) | WebUI-only mutation |
|
||||
| Authorization hierarchy & grants (UI-audit) | **no page** | **no command** | **the largest D12 gap**: `hierarchy.controller.ts` exposes full CRUD (companies, estates, platform-projects, grants incl. `grants/:id/change`) with audit repository and grant evaluation behind it — reachable only by raw API |
|
||||
| Federation grants/peers | no page | `federation grant/peer` | CLI-ahead (posture pending Q-T1) |
|
||||
|
||||
**Consequences for the build order** (D8: tool first, then surface):
|
||||
`mosaic provider {list,test}`, `auth users {set-role,ban,unban}`, a stored
|
||||
harness-selection command, the missing gateway admin endpoints for SSO/session
|
||||
listing, and a CLI face for the hierarchy/grant surface all precede their
|
||||
pages. The two **WebUI-only mutations** (role/ban toggle, harness selection)
|
||||
are standing D12 violations to remediate, not precedents to extend. The
|
||||
hierarchy CRUD surface is the natural backing for [[UI.1-webui-surfaces]]'s
|
||||
authorization audit page — but it must get a CLI face and an audit read-path
|
||||
before the page ships.
|
||||
|
||||
## Command families in scope for the control plane
|
||||
|
||||
`mosaic config` (OD-49 desired-state engine), `mosaic coord` (agent coordination
|
||||
boundary — register OD-09), `mosaic prdy` (PRD creation/acceptance — register
|
||||
OD-22), role management (one canonical API, L2-D14), seat lifecycle
|
||||
(launch/relaunch per register OD-59), `mosaic doctor` (drift detection classes,
|
||||
e.g. the #1194 framework-tool drift addendum in [[GOV.4-workstream-contracts]]).
|
||||
|
||||
## `mosaic config` v1 subset (pulled 2026-08-31 from the minimal-subset spec)
|
||||
|
||||
The Q14 ruling (2026-08-29) fixes the current scope: shipped surface
|
||||
`edit/get/set/show/hooks/path` **plus exactly two new read-only verbs** —
|
||||
`mosaic config validate` and `mosaic config plan` (`--file` | `--preset`,
|
||||
mutually exclusive; `--format table|json`). `apply`, `add`, `restructure`,
|
||||
`migrate`, `remove`, `export` are **out of v1 pending a full-engine ruling**
|
||||
([[GOV.5-open-questions]] Q-D5) — the configuration-lifecycle draft's "stable
|
||||
namespace" table describing the full family is aspirational, not current state.
|
||||
|
||||
Contract highlights: **valid** vs **conformant** are distinct verdicts with
|
||||
distinct exit codes (nonconformance is a diagnostic, not a parser failure);
|
||||
`plan` emits `create|update|blocked` operations with risk classes
|
||||
(`none|review-required|full-engine-required`), a SHA-256 `planId`, and
|
||||
`applySupported: false` always in v1; destructive/unsupported drift is
|
||||
`blocked`, never silently normalized; absolute no-mutation during
|
||||
validate/plan (no writes, no network, no credential calls); results ride the
|
||||
T78 `CapabilityResultV1` envelope (capability IDs
|
||||
`config.installation.validate`/`.plan`); inputs capped, YAML
|
||||
aliases/anchors/tags rejected, no secret-shaped fields accepted or echoed.
|
||||
|
||||
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
|
||||
|
||||
**Canonical ground truth**: `requirements/cli-capability-migration.md` (T78,
|
||||
`source_of_truth: true`), `fleet/reference/cli.md` (local fleet CLI vs
|
||||
gateway-backed catalog), `USER-GUIDE/getting-started/quickstart.md`.
|
||||
**Pending pulls**: DRAFT S2 `tool-gateway-mapping.md`; brain
|
||||
`docs/specs/2026-08-29_mosaic-config-minimal-subset.md` (`mosaic config
|
||||
validate/plan`, cites OD-49–OD-55) and
|
||||
`docs/guides/proposed/workflows/configuration-lifecycle.md` (the OD-49 engine
|
||||
family definition).
|
||||
|
||||
## S2 contract feed (extraction 2026-08-31)
|
||||
|
||||
Full extraction record: lane `S2-EXTRACTION-2026-08-31.md` (per-contract cores, dependency edges, ruling cross-checks). Pulls binding on this section:
|
||||
|
||||
- **Contract 5 §4.5 is the parity clause this section's matrix enforces**:
|
||||
CLI remains the primary execution method for every Gateway command; no
|
||||
WebUI-only command exists; a Gateway command without CLI exposure is a
|
||||
conformance gap tracked at the family's implementing issue. The
|
||||
hierarchy/grants CRUD gap measured in this section is exactly such a
|
||||
tracked conformance gap once contract 5 ratifies.
|
||||
- **Command envelope**: typed request/result DTOs (no `any`), closed
|
||||
per-family error taxonomy, audit correlation id, fail-closed — aligns with
|
||||
the T78 `CapabilityResultV1` direction already in this section.
|
||||
- **Contract 9 (api-artifacts)**: `ApiAuthClass` closed six-value enum
|
||||
(`none`/`session`/`api-key`/`admin`/`federation`/`bootstrap`); OPENAPI.yaml
|
||||
generated, CI byte-drift-gated, never hand-edited; hard ordering — nothing
|
||||
under contract 9 lands before contract 5 (PR #1438) is on the trunk.
|
||||
- **Roll-up (contract 8)** ships as a query-only tool with no command
|
||||
counterpart (A5 rank 5) — the taxonomy precedent for read-only surfaces in
|
||||
the parity matrix.
|
||||
- **Mandated removals** the CLI inventory must track: `mosaic auth users
|
||||
delete` (with the hard-delete endpoint) is required to be disabled/removed
|
||||
by contract 4.
|
||||
|
||||
## Parity freshness ruling (Q-C1, Jason 2026-09-01)
|
||||
|
||||
The parity matrix becomes a generated artifact with a CI drift-gate witness in
|
||||
the stack repo (the contract-9 pattern): CI regenerates the tool inventory and
|
||||
WebUI→tool mapping from code and fails on divergence from the committed
|
||||
matrix. No human cadence to forget. Building the witness is E6-return
|
||||
follow-up work.
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
id: DATA.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# DATA.1 — Record-class authority and the configuration data model
|
||||
|
||||
Merges J1 (2026-08-23), operator register OD-13/OD-48/OD-49–OD-52, and the lane's
|
||||
`CONFIG-MODEL.md` findings into one authority table. See
|
||||
[[GOV.3-decision-map]] for registry identities.
|
||||
|
||||
## The rule (J1)
|
||||
|
||||
- **Git owns** reviewed governance and declarative definitions.
|
||||
- **PostgreSQL owns** runtime state and projections.
|
||||
- Flat files on disk are **generated projections**, never authority (L2-D19).
|
||||
|
||||
## Authority table
|
||||
|
||||
| Record | Authority | Rationale |
|
||||
| --------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| Role Definition / Role Revision | **Git** | reviewed governance; revisions immutable and digested |
|
||||
| `mosaic-core.manifest.json` | **Git** | committed, non-symlink, trusted-path — the loader refuses anything else |
|
||||
| PRD revision bundles (`docs/PRDs/`) | **Git** | immutable accepted versions (register OD-17) |
|
||||
| Portable config blueprint | **Git** (`fleet/configuration/installation.yaml`, register OD-50) | declarative desired state |
|
||||
| Host bindings | ignored `config/installation.local.yaml` (OD-50) | host-local, never authority over roles/gates (OD-52) |
|
||||
| Role Binding (seat → revision) | **Postgres** | runtime state; control-plane mutable |
|
||||
| Seat record (harness, model, workdir, auth account) | **Postgres**, projected to flat files | runtime state |
|
||||
| Leases, checkpoints, session/incarnation state | **Postgres** / coordinator (register OD-57–OD-60) | runtime state with fencing |
|
||||
| `settings.json`, `launch.env` | **generated projection** | L2-D19; no writer may treat them as source |
|
||||
|
||||
Transition rule: the WebUI may edit flat files during the transition, but the
|
||||
end state is exactly the table above — every flat file regenerated from Git or
|
||||
Postgres, never authored directly.
|
||||
|
||||
## Seat file consolidation (lane Q1 — proposed, not ratified)
|
||||
|
||||
Current flat-file state (measured 2026-08-31):
|
||||
|
||||
| File | Carries |
|
||||
| ---------------------------------------------- | -------------------------------------------------------------- |
|
||||
| `fleet/agents/<seat>/launch.env` | model, workdir, reasoning level (hand-maintained, git-ignored) |
|
||||
| `fleet/agents/<seat>/profile.json` | role — read by `mosaic-core`'s trusted-path loader |
|
||||
| `fleet/roles/<role>/.pi/agent/settings.json` | provider, model, extensions, skills paths |
|
||||
| `fleet/roles/<role>/mosaic-core.manifest.json` | capability/tool authority (schema v3) |
|
||||
|
||||
Proposal: one `profile.json` rules seat information (role, harness, model,
|
||||
reasoning, workdir, overlay, authentication account); `launch.sh` reads it
|
||||
instead of `launch.env`. Register OD-48 already ratifies `profile.json` as the
|
||||
seat's **structured identity** file, which this consolidation completes.
|
||||
|
||||
**Blocking consideration:** `mosaic-core` reads `profile.json` at every
|
||||
`session_start` to resolve the role. Widening the file widens the read surface
|
||||
of a trusted-path load. The loader must keep ignoring unknown keys (it reads
|
||||
only `.role` and already does); the file must stay non-symlink and committed.
|
||||
Verify `lib/loader.ts seatRole()` before landing. Open on the grill list:
|
||||
[[GOV.5-open-questions]] Q-D1.
|
||||
|
||||
## Multi-provider authentication shape (lane Q2 — open)
|
||||
|
||||
`profile.json` must name the authentication account a seat uses, across
|
||||
providers (Claude, OpenAI, ZAI, N others; OAuth or API key; local providers).
|
||||
Unresolved: one account vs a per-provider map, and the credential-broker
|
||||
relationship. The enforced role manifests hold **no** credentials
|
||||
(`store: none, providerTokens: denied`) — this is a launcher/broker concern,
|
||||
never a manifest concern. Grill: [[GOV.5-open-questions]] Q-D2. See
|
||||
[[AUTHN.1-auth-accounts]] for the account model itself.
|
||||
|
||||
## Reconciliation obligation (lane Q4)
|
||||
|
||||
Every change made through CLI or WebUI automatically configures authentication,
|
||||
`settings.json`, and required symlinks — the user never touches a file. Two
|
||||
directions with different timing (L2-D17): capability **removal** denies
|
||||
centrally and immediately; capability **addition** waits for runtime
|
||||
reconciliation and attestation.
|
||||
|
||||
**Hazard to settle first:** Pi settings ownership is ambiguous today
|
||||
(`launch-seat.sh:259–261` symlinks `.pi/agent/settings.json` under
|
||||
`MOSAIC_SEAT_HOME=1` while the `MOSAIC_SEAT_CONFIG=1` seed fires on `! -s`,
|
||||
which the symlink satisfies). Settle ownership before the WebUI becomes a third
|
||||
writer. Grill: [[GOV.5-open-questions]] Q-D3.
|
||||
|
||||
## Configuration file authority (pulled 2026-08-31 from the mosaic-config v1 spec)
|
||||
|
||||
Four config records with fixed authority (brain spec `2026-08-29_mosaic-config-minimal-subset.md`, register OD-49–OD-55):
|
||||
|
||||
| Record | Path | Authority |
|
||||
| ------------------ | --------------------------------------------------- | --------------------------------------------------- |
|
||||
| Central registry | `~/.config/mosaic/config.json` | resolves brainHome/socket/paths |
|
||||
| Portable blueprint | `<brainHome>/fleet/configuration/installation.yaml` | tracked, secret-free desired state |
|
||||
| Host bindings | `<brainHome>/config/installation.local.yaml` | git-ignored; **runtime and working directory only** |
|
||||
| Packaged presets | immutable, versioned (`bootstrap-minimal@1`) | never `latest` |
|
||||
|
||||
Precedence, high to low: constitution/safety (deny-wins, OD-51) → framework
|
||||
schema/profile/role/roster contracts → blueprint/preset → host bindings →
|
||||
framework binding defaults → observed state (**compared, never authoritative**).
|
||||
Host bindings can never change profile, seat selection, roles, authority,
|
||||
reviews, gates, or safety (OD-52) — the data-model enforcement of the
|
||||
[[AUTHZ.1-capability-authority]] intersection chain. Validation distinguishes
|
||||
**valid** (structurally sound) from **conformant** (observed == desired) with
|
||||
distinct exit codes.
|
||||
|
||||
_Triage note:_ the intended-state reconciler spec was judged **operator
|
||||
host-ops tooling** on full read (systemd/tmux monitoring of the operator
|
||||
estate) — not product scope; its conformance idea is already covered by the
|
||||
validate/plan model above. E2-inputs pull downgraded to SKIP.
|
||||
|
||||
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
|
||||
|
||||
**Canonical ground truth**: `requirements/native-kanban-sot.md` (ratified, the
|
||||
D13 base), `native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md` + `KBN-101-ENVELOPE-A.md`
|
||||
|
||||
- `SHARED-CONTRACT.md` (frozen contracts), `ADMIN-GUIDE/operations/upgrade-safety-and-recovery.md`
|
||||
(PGlite tier support boundary), `fleet/reference/roster-v2-fields.md`.
|
||||
**Pending pulls**: DRAFT S2 contracts `hierarchy-schema.md`, `custody-schema.md`,
|
||||
`rollup-projection.md`, `mode-conversion.md` (**predates D15 — reconcile first**,
|
||||
Q-T4); brain `docs/specs/2026-08-28_intended-state-reconciler.md` (reconciler spec).
|
||||
**Conflicts on the grill**: deployment/federation posture, Q-T1.
|
||||
|
||||
## S2 contract feed (extraction 2026-08-31)
|
||||
|
||||
Full extraction record: lane `S2-EXTRACTION-2026-08-31.md` (per-contract cores, dependency edges, ruling cross-checks). Pulls binding on this section:
|
||||
|
||||
- **Hierarchy schema (contract 1)**: five tables, single-parent FK chains, no
|
||||
parentage edge tables, **no `owner_id` column** — ownership only via grants.
|
||||
All mutations through the sole-writable-SOT audited Gateway command path;
|
||||
three-prong writer-coverage CI witness.
|
||||
- **Custody schema (contract 7)**: sensitive content only in the user's
|
||||
git-tracked brain; Postgres holds pointers/consent/registry only ("not as
|
||||
text, not as excerpts, not as embeddings"); content-first-then-pointer write
|
||||
protocol with brain fence; HMAC content hashes (no oracle); mode-independent
|
||||
schemas with a `custody_config` singleton.
|
||||
- **Roll-up (contract 8)**: the corpus's strongest projection-never-authority
|
||||
statement — non-authoritative, recomputable, never gates work, enforced by
|
||||
read-only DB transactions (mechanical, not conventional). Direct precedent
|
||||
for this section's record-authority chain.
|
||||
- **Route metadata records (contract 9)**: metadata as _registration input_ —
|
||||
auth guard derived from the record makes record-vs-code divergence on those
|
||||
fields structurally impossible; generated OPENAPI.yaml is committed and
|
||||
PR-reviewed yet strictly non-authoritative ("generation documents the code;
|
||||
it does not ratify it") — drafting precedent: "generated" ≠ "uncommitted".
|
||||
- **Authoritative DB settings rows** — ruled (Q-T5, Jason 2026-09-01):
|
||||
generated settings _files_ are projections of the active Role Revision,
|
||||
never authority (L2-D19); DB settings records written through audited
|
||||
Gateway commands (`platform_mode`, `registration_mode`, `custody_config`,
|
||||
`bootstrap.seed-company-name`, and successors) are records of authority
|
||||
like any other SOT row.
|
||||
|
||||
## Seat-record rulings (Jason 2026-09-01)
|
||||
|
||||
- **Q-D1 — one seat record**: `launch.env` folds into `profile.json`; loader
|
||||
tolerance verified (`seatRole()` reads only `role` from a generic record).
|
||||
- **Q-D2 — per-provider auth map**: `profile.json` carries a per-provider map
|
||||
of credential-broker references (`environment/service/component/secret-name`),
|
||||
never secrets; one seat, N providers, zero secrets in the brain tree.
|
||||
- **Q-D3 — one settings writer**: the role-projection engine is the sole
|
||||
writer of seat settings files; the launcher invokes the projector rather
|
||||
than seeding; hand edits are drift flagged by `validate` (L2-D19 + Q-T5).
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
id: GOV.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# GOV.1 — PRD lifecycle: SOT, shim, revisions, archival
|
||||
|
||||
Ratified structurally by Jason, 2026-08-31 (this lane's grill session). Applies
|
||||
to the Mosaic Stack PRD in `mosaicstack/stack` (integration trunk `next`).
|
||||
|
||||
## The PRD is the project SOT
|
||||
|
||||
The PRD is the source of truth for the entire project, independent of any
|
||||
mission. It is not linked to a current mission and is never overwritten by
|
||||
milestone work — the pre-2026-08-26 pattern of repurposing `docs/PRD.md` per
|
||||
milestone is retired. Mission documents are separate: they **reference** the
|
||||
PRD; they never usurp it. This maintains alignment over time.
|
||||
|
||||
## Shim
|
||||
|
||||
`docs/PRD.md` is a permanent shim, not the PRD body:
|
||||
|
||||
- Frontmatter: `kind: shim`, `current_rev:` pointing at the live revision
|
||||
bundle.
|
||||
- Body: one-paragraph summary and a link into `docs/PRDs/`.
|
||||
- Updating the PRD means ratifying a new revision bundle and repointing the
|
||||
shim. The shim's path never changes, so every external reference to
|
||||
`docs/PRD.md` stays valid forever.
|
||||
|
||||
## Revision bundles
|
||||
|
||||
Each ratified revision is a **frozen bundle directory**:
|
||||
|
||||
```
|
||||
docs/PRDs/YYYY-MM-DD_PRD_revN/
|
||||
PRD.md # the assembled PRD for this revision
|
||||
PRD.0-index.md # order authority + domain registry as of this revision
|
||||
<DOMAIN>.<n>-*.md # every section document, frozen with the PRD
|
||||
```
|
||||
|
||||
The PRD and its supporting sections freeze **as a set** — a revision whose
|
||||
sections keep moving underneath it is not a revision. Live editing never
|
||||
happens in `docs/PRDs/`; the next revision is drafted in a lane
|
||||
(class‑2 draft-natives per the lane's `proposed/README.md`) and lands as a new
|
||||
bundle.
|
||||
|
||||
## Archival, never deletion
|
||||
|
||||
A superseded revision is never deleted and never edited. Versioning is
|
||||
maintained: every revision that was ever current remains in `docs/PRDs/`
|
||||
verbatim. Supersession is expressed only from outside the bundle: the shim
|
||||
points elsewhere, and the dated directory names plus the shim's git history are
|
||||
the supersession record. The frozen bundle itself is never touched — not even
|
||||
to add a `superseded_by:` marker.
|
||||
|
||||
## Immutability is convention, not enforcement
|
||||
|
||||
No hook or CI guard protects `docs/PRDs/` today. If teeth are wanted later, a
|
||||
CI check that files under `docs/PRDs/` never change after merge is cheap; that
|
||||
is a separate, future decision.
|
||||
|
||||
## Lineage
|
||||
|
||||
- rev0 = the 2026-08-26 "North Star" PRD currently at `origin/next:docs/PRD.md`
|
||||
(commit `9aa4983c`, sha256 `60cc2f98…36afdf`; lane snapshot
|
||||
[rev0 PRD](../2026-08-26_PRD_rev0/PRD.md)). On ratification of rev1 it archives as
|
||||
`docs/PRDs/2026-08-26_PRD_rev0/PRD.md` — a one-file bundle, so every revision
|
||||
has the same shape.
|
||||
- rev1 = this lane's draft bundle (`proposed/docs/PRDs/2026-08-31_PRD_rev1/`),
|
||||
combining rev0 with the control-plane-surfaces and agent-runtime-ng lane
|
||||
findings and the reconciled docs corpus.
|
||||
|
||||
Related: [[PRD.0-index]] for naming and ordering; the lane `proposed/README.md`
|
||||
for draft-stage conventions.
|
||||
|
||||
## Registry mechanics (pulled 2026-08-31 from the prd-registry draft)
|
||||
|
||||
The operator draft `operations/prd-registry.md` independently specifies the
|
||||
same lifecycle and adds mechanics this doc adopts:
|
||||
|
||||
- **The registry, not the shim, is authoritative.** The shim is generated,
|
||||
regenerated on every acceptance and amendment; a missing or ambiguous shim
|
||||
entry is a generation defect, never an authority question.
|
||||
- Registered versions carry: stable PRD ID, canonical filename/slug, version +
|
||||
status, acceptance timestamp **and actor**, lineage, content digest,
|
||||
requirement IDs. Anonymous or inferred acceptance is invalid.
|
||||
- Missions pin PRD ID + version + digest + in-scope requirement IDs; a digest
|
||||
mismatch between pin and artifact **blocks the readiness transition** (both
|
||||
digests shown as evidence).
|
||||
- Amendment creates a new immutable version; superseded versions remain
|
||||
queryable as lineage; **requirement IDs are never reused**.
|
||||
- Agents read and resolve; they never register versions, rewrite artifacts, or
|
||||
select an implicit latest.
|
||||
|
||||
## Registry prefixes (Q-G2, 2026-09-01)
|
||||
|
||||
Every decision-bearing document declares a unique registry prefix; file-local
|
||||
bare D-numbering is prohibited. Three registries are live: the stack PRD
|
||||
registry (**D1–D15**, and successors **Dn** as new stack decisions ratify),
|
||||
the operator DECISION-REGISTER (**OD-01…OD-65**, renamed from its former
|
||||
zero-padded `D01`–`D65` form per this ruling), and the agent-runtime-ng
|
||||
contract decisions (**L1-Dnn**/**L2-Dnn**) — three distinct namespaces that
|
||||
must never be conflated. Any new decision-bearing document must declare its
|
||||
own unique prefix in its header before citing decisions. In text written
|
||||
before 2026-09-01, a zero-padded bare `Dnn` reads as the operator register's
|
||||
`OD-nn`.
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
id: GOV.2
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# GOV.2 — Documentation inventory & supersession triage
|
||||
|
||||
E2 record and, at ratification, the PRD's answer to mandate item 4 (no central
|
||||
location; drift and naming confusion). Verdict vocabulary, per document:
|
||||
**canonical** | **superseded-by <ref>** | **conflict-with <ref>** |
|
||||
**working-notes** | **dead** | **operator-only** (brain corpora: correct home is
|
||||
the operator estate; nothing migrates).
|
||||
|
||||
Full per-file verdict tables live in the lane evidence record
|
||||
`fleet/lanes/control-plane-surfaces/TRIAGE-2026-08-31_e2-verdicts.md`
|
||||
(point-in-time; this section carries the durable conclusions).
|
||||
|
||||
## Corpora
|
||||
|
||||
| # | Corpus | Files | Scanned at |
|
||||
| --- | ----------------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | `mosaicstack/stack` `origin/next:docs/` | 346 | commit `9aa4983c`, triaged 2026-08-31 (141 live files per-file; archive dirs swept for orphaned decisions) |
|
||||
| 2 | `~/.mosaic/docs/` (excl. guides/proposed) | ~76 | triaged 2026-08-31 |
|
||||
| 3 | `~/.mosaic/docs/guides/proposed/` | 79 | triaged 2026-08-31 |
|
||||
| 4 | `fleet/lanes/agent-runtime-ng/` + `fleet/lanes/control-plane-surfaces/` | — | live lanes, canonical by definition for their scope |
|
||||
|
||||
## Corpus 1 — stack `origin/next:docs/` — conclusions
|
||||
|
||||
195 of 346 files (56%) were pre-triaged by the repo's own archive structure
|
||||
(`archive/` 135, `_old_structure/` 60). The 141 live files triaged per-file:
|
||||
|
||||
**The healthy core.** The five guide trees (DEVELOPER-GUIDE, ADMIN-GUIDE,
|
||||
USER-GUIDE), `fleet/` (concepts/how-to/reference/operations/migration),
|
||||
`native-kanban-sot/`, `webui/`, `API/`, `tess/`, `release-integrity/`, and the
|
||||
root atlas docs (README, ROADMAP, SITEMAP) are overwhelmingly **canonical** and
|
||||
internally consistent. Load-bearing canonical anchors for this PRD:
|
||||
`requirements/native-kanban-sot.md` (ratified, D13), `KBN-101-DB-ROLE-SPLIT.md`
|
||||
(frozen), `requirements/cli-capability-migration.md` (T78),
|
||||
`fleet/NORTH_STAR.md` + `FLEET-DOCTRINE.md`, `mutator-class-gate.md` and the
|
||||
lease-broker pair (AUTHZ ground truth), `compaction-revocation.md` (SESS ground
|
||||
truth), `sso-providers.md` (D10/AUTHN ground truth),
|
||||
`mos-runtime-portability-m1.md` (the only current PROV identity ADR),
|
||||
`web-dashboard.md` (UI route-by-route ground truth).
|
||||
|
||||
**The prime successor material.** The nine DRAFT "webui-audit S2" contracts in
|
||||
`requirements/` (hierarchy-schema, rbac-grant-model, onboarding-wizard,
|
||||
identity-lifecycle, tool-gateway-mapping, mode-conversion, custody-schema,
|
||||
rollup-projection, api-artifacts) are unratified but decision-traceable per
|
||||
clause to rev0 D-numbers — the most direct feed for DATA/AUTHZ/AUTHN/UI/CLI.
|
||||
`mode-conversion.md` predates D15 and needs reconciliation before it ratifies
|
||||
([[GOV.5-open-questions]] Q-T4). Per-contract extraction completed 2026-08-31
|
||||
(lane `S2-EXTRACTION-2026-08-31.md`): normative cores, dependency edges, and
|
||||
ruling cross-checks pulled into the DATA/AUTHZ/AUTHN/UI/CLI sections; two
|
||||
reconciliation questions raised (Q-T4 sharpened — no S2 file references D15;
|
||||
Q-T5 — projection-rule scope vs authoritative DB settings records).
|
||||
|
||||
**Conflicts requiring a ruling** (all carried in [[GOV.5-open-questions]] Q-T1):
|
||||
|
||||
| Document | Conflict |
|
||||
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| root `MISSION-MANIFEST.md` (2026-07-14) | makes federated-tier the "canonical MVP deployment topology", Federation v1 top-priority — vs D3 (deferred) and D15 (compose standalone canonical) |
|
||||
| `federation/MISSION-MANIFEST.md` (2026-04-21) | Federation v1 as active in-progress M3 — vs D3 and ROADMAP P5 "deliberately undesigned" |
|
||||
| `guides/deployment.md` | blocks Compose activation pending KBN-101 gates — vs D15's `docker compose up` v1 bar |
|
||||
| `scratchpads/mvp-20260312.md` | records a _completed_ Federation M2 milestone (peer certs, grants, ScopeService, Step-CA) — vs D3's "deferred" framing |
|
||||
|
||||
Code reality, verified 2026-08-31 at `9aa4983c` (dossier: lane
|
||||
`FEDERATION-DOSSIER-2026-08-31.md`): federation M1–M3 are shipped and wired
|
||||
behind a `tier === 'federated'` gate — M3 landed 2026-06-24/25, beyond what any
|
||||
doc records — M4–M7 absent, dormant since 2026-06-25, absent from the canonical
|
||||
`docker-compose.yml` (so code topology is consistent with D15), and tracked
|
||||
nowhere since the TASKS.md → NORTH_STAR.yaml supersession. The three stale docs'
|
||||
claims date to 2026-04 by true content edits. **Ruled B ("shipped but
|
||||
frozen"), Jason 2026-09-01** — see [[GOV.5-open-questions]] Q-T1 for the
|
||||
amendment consequences the E6 return carries.
|
||||
|
||||
**Superseded set** (all with explicit in-file or index-level signals): root
|
||||
`TASKS.md`, `fleet/TASKS.md`, `federation/TASKS.md` (→ `fleet/NORTH_STAR.yaml`);
|
||||
`fleet/PRD.md` and `fleet/PRD-fleet-suite.md` (→ root `PRD.md`, per
|
||||
`fleet/README.md`); `native-kanban-sot` initial NO-GO review (→ GO re-review).
|
||||
`plans/`, `reports/`, `scratchpads/` are working-notes/evidence, never spec —
|
||||
consistent with their own README disclaimers.
|
||||
|
||||
**Orphaned decisions found in the archive sweep** (ratified once, absent from
|
||||
D1–D15 and every live doc; disposition on the grill, Q-T2):
|
||||
|
||||
1. "No Python" monorepo ruling (`archive/planning/monorepo-consolidation/board-review.md:742`).
|
||||
2. Matrix/MACP "exactly three supported modes" install-topology ruling, Mode A
|
||||
split-domain primary; its DNS/domain prerequisite ruling still open
|
||||
(`archive/planning/matrix-macp/rfc-002:133`, `rfc-001:428`).
|
||||
3. OpenBrain cut from WP1/WP2 consolidation scope (`board-review.md:611`).
|
||||
|
||||
## Corpora 2 + 3 — `~/.mosaic/docs/` — conclusions
|
||||
|
||||
The overwhelming majority is **operator-only**: generic engineering standards,
|
||||
fleet role playbooks, SDLC gates, ops pages, fleet Q&A rulings, incident
|
||||
methods, host-specific plans. Correct home is the brain; nothing migrates.
|
||||
Notable verdicts:
|
||||
|
||||
- `docs/PRD.md` — **trap confirmed** (N1): it is the pi `/goal` extension PRD.
|
||||
Superseded in substance by [[GOV.4-workstream-contracts]] §Pi Persistent Goal
|
||||
Loop (#1150).
|
||||
- `docs/guides/proposed/workflows/prd-lifecycle.md` — superseded by
|
||||
[[GOV.1-prd-lifecycle]] (the draft lifecycle this bundle ratified).
|
||||
- `docs/plans/2026-08-25_unified-roadmap.md` (T72 consolidation charter) —
|
||||
superseded by this rev1 consolidation, its successor.
|
||||
- `docs/MOSAIC-CANON.md` vs `docs/STRUCTURE-CANON.md` — mutual conflict: both
|
||||
claim to be "the canonical definition of a mosaic-brain" (653 vs 127 lines,
|
||||
divergent sections); STRUCTURE-CANON is the copy everything links to.
|
||||
Operator-side ruling needed (Q-T3).
|
||||
- Dead: `plans/2026-08-22_config-json-schema.md` (delivered as stack#1382), two
|
||||
closed questions.
|
||||
|
||||
**Migration candidates** — nine working-notes whose product content should feed
|
||||
rev1 sections during refinement (full list with rationale in the lane evidence
|
||||
record): `specs/2026-08-29_mosaic-config-minimal-subset.md` → CLI;
|
||||
`specs/2026-08-28_intended-state-reconciler.md` → DATA;
|
||||
`operations/seat-identity.md` → SEAT; `operations/vault.md` → AUTHN;
|
||||
`runtime/adapter-contract.md` → HARN; `SPECIALIZATION-MODEL.md` → ROLE;
|
||||
`workflows/session-lifecycle.md` → SESS; `operations/prd-registry.md` → GOV;
|
||||
`workflows/configuration-lifecycle.md` → CLI/GOV.
|
||||
|
||||
`COORDINATION-CONTROL-PLANE.md` and `workflows/coordination-lifecycle.md` are
|
||||
product-adjacent but belong to the `agent-runtime-ng` lane's L1/L2 scope, not
|
||||
this bundle (corpus-4 boundary).
|
||||
|
||||
## Corpus 4 — the two lanes
|
||||
|
||||
Canonical for their scope by definition (they are the drafting record).
|
||||
`agent-runtime-ng` owns L1/L2 contract text; `control-plane-surfaces` owns
|
||||
surface/config findings and this bundle. One-way dependency: surfaces cite
|
||||
contracts, never the reverse.
|
||||
|
||||
## Naming defects register (mandate item 4)
|
||||
|
||||
| # | Defect | Fix proposed |
|
||||
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| N1 | `~/.mosaic/docs/PRD.md` is the pi goal-extension PRD wearing the project-PRD name (confirmed 2026-08-31) | rename to a goal-extension-scoped name during return |
|
||||
| N2 | Stack local `main` is a divergent unpushed fork that shadows `next` | already a lane convention; PRD states trunk identity explicitly |
|
||||
| N3 | Pre-2026-08-26 pattern: `docs/PRD.md` overwritten per milestone | retired by [[GOV.1-prd-lifecycle]] shim model |
|
||||
| N4 | Two decision registries share the D-prefix ID space (stack D1–D15 vs operator D01–D65); "D8" is ambiguous without registry name | OD- prefix applied in rev1 (Q-G2 ruled 2026-09-01); collision rule in [[GOV.3-decision-map]] |
|
||||
| N5 | Triple "PRD" collision in the stack tree: root `PRD.md` vs superseded `fleet/PRD.md` and `fleet/PRD-fleet-suite.md`, with no local supersession signal on the latter two | supersession banner in-file; long-term, the GOV.1 rule that the bare name `PRD.md` is reserved for the shim |
|
||||
| N6 | "Tess" and "Ultron" each name two different things: non-authoritative roster-class display aliases (fleet how-tos) vs the named product agent / validator identity (TESS workstream, native-kanban-sot) | rev1 text always qualifies which sense is meant; flag for upstream rename of the aliases |
|
||||
| N7 | Six stack `TASKS.md` files under three authority regimes (banner-superseded / explicitly-not-superseded / silently active) — the filename signals nothing | uniform status frontmatter on every TASKS.md; superseded ones point at NORTH_STAR.yaml |
|
||||
| N8 | Duplicate basenames across stack dirs: `gateway-security-20260313.md` (qa vs code-review, different content), `2026-08-10-docs-catalog-audit.md` (plan vs report), `1099-pipefail-sweep.md` (report vs scratchpad copy) | disambiguate on next touch; prune the unpromoted scratchpad copy |
|
||||
| N9 | `guides/` is outside the canonical tree per `docs/README.md` yet "protected current authority" per `SITEMAP.md` — contract and sitemap disagree | reconcile the documentation contract; likely fold the four guides into the guide trees |
|
||||
| N10 | Two live front-matter schemas (`type`/`status: current…` per docs/README.md vs the newer `kind`/`status: active…` used by most files) — collision documented in the w4 worklist, unresolved | settle the schema in the documentation contract as part of E6 return |
|
||||
| N11 | Three uncross-referenced descriptions of the `/goal` capability: brain `docs/PRD.md`, `operations/goals.md`, and GOV.4 §#1150 | reconcile under the #1150 identity; brain docs cite it |
|
||||
| N12 | Brain-side: `MOSAIC-CANON.md` vs `STRUCTURE-CANON.md` both claim canon status | operator ruling (Q-T3); retire or fold the unreferenced copy |
|
||||
| N13 | Forward-looking: rev0 Part II (RI-N3) rules `docs/PRD.md` "not a peer authority" once `docs/prdy/` lands — a third contender in "which PRD is real" | GOV.1 disambiguation: prdy is tooling-facing storage; the shim + bundle remain the human-facing SOT chain |
|
||||
| N14 | Commit `a480ee83` (2026-08-21) mass-stamped `status: active` frontmatter across stack docs without content review — status metadata rubber-stamps stale docs as current (root MISSION-MANIFEST's "Last Updated: 2026-07-14" is likewise cosmetic; true content edit 2026-04-19) | status/date frontmatter changes only alongside content review; triage dates by git content edits, never frontmatter |
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
id: GOV.3
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# GOV.3 — Consolidated decision map
|
||||
|
||||
Every ratified decision set that binds this PRD, in one place, with the
|
||||
collisions between their numbering spaces made explicit. This section exists
|
||||
because the estate carried at least four independent decision registries whose
|
||||
IDs overlap — a reader seeing "D8" could not know which law was meant.
|
||||
|
||||
## The registries
|
||||
|
||||
| Registry | IDs | Ratified | Where | Scope |
|
||||
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| Stack PRD registry | **D1–D15** | 2026-08-25/30 | rev0 §12 → [[GOV.3-decision-map]] (this file, below) | product north star |
|
||||
| Operator decision register | **OD-01–OD-65** (renamed from D01–D65 per Q-G2, 2026-09-01; the brain-side source doc renames on its next touch and carries a redirect table) | 2026-08-28 (Q1–Q92 review) | the operator DECISION-REGISTER (estate brain `docs/guides/proposed/DECISION-REGISTER.md`, snapshot 2026-08-28, sha256 `2cc81be1…aabec`; operator-only corpus, not shipped) | roles, coordination, PRD lifecycle, configuration, checkpoints |
|
||||
| L2 authorization decisions | **L2-D01–L2-D51** (+ proposed **L2-D52**) | rolling | `fleet/lanes/agent-runtime-ng/MECHANICAL-AGENT-RUNTIME-L2-AUTHORIZATION.md` | mechanical agent-runtime authorization |
|
||||
| Control-plane rulings | **J1–J5** | 2026-08-23 | `fleet/lanes/docs/mosaic-control-plane/rulings-J1-J5.md` | record-class authority |
|
||||
| PRD structural rulings | (unnumbered, 8 rulings) | 2026-08-31 | [[PRD.0-index]] §Structural rulings | this bundle's lifecycle |
|
||||
|
||||
**Collision rule:** zero-padded `D01`-form IDs = operator register; bare `D1`-form
|
||||
= stack PRD registry; `L2-D` = L2; `J` = control-plane rulings. Writing a bare
|
||||
"D8"-style reference without its registry name is a defect (naming register
|
||||
[[GOV.2-docs-inventory]] N4).
|
||||
|
||||
## Stack PRD registry D1–D15 (carried from rev0 §12)
|
||||
|
||||
| ID | Decision (short form) |
|
||||
| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| D1 | Open-source, AI-first, self-hosted platform for agentic management + life OS |
|
||||
| D2 | Hierarchy company→estate→project→workspace→kanban; bubble-up; granular RBAC |
|
||||
| D3 | Standalone vs Enterprise; one-way conversion; per-user brains + Vault required in Enterprise; federation deferred. **Amended 2026-09-01 (Q-T1 ruling B, "shipped but frozen")**: federation M1–M3 exist in code behind `tier === 'federated'` (M3 landed 2026-06-24/25), are excluded from the v1 bar and frozen; tracked as a dormant workstream in `docs/fleet/NORTH_STAR.yaml`; the frozen cert/auth code carries a security re-audit gate before any resumption; the design itself stays deferred and unforeclosed |
|
||||
| D4 | Re-runnable, extensible, per-mode onboarding wizards |
|
||||
| D5 | North star = docs/PRD.md rewrite; stack docs/ = product SSOT |
|
||||
| D6 | Only product-relevant material migrates from brains; operational records stay and link |
|
||||
| D7 | Spec-inventory sweep (executed; T2 baseline frozen 2026-08-25) |
|
||||
| D8 | webUI sits over official framework tooling; CLI primary |
|
||||
| D9 | Not a hosted business; company = organizational separation for one operator |
|
||||
| D10 | better-auth is the account system of record; external IdPs via OIDC |
|
||||
| D11 | Small v1 slice; ALL phases on the documented roadmap from day one |
|
||||
| D12 | HARD RULE: webUI never bypasses tooling; missing tool ⇒ build the tool first |
|
||||
| D13 | workspace_id stays the hard isolation unit; kanban SOT amended, not rewritten |
|
||||
| D14 | Sensitive profile data in the user's own brain only |
|
||||
| D15 | Tiered containerized deployment: compose standalone + phase-gated k8s |
|
||||
|
||||
Full texts: rev0 §12 and the operator decision log (USC estate brain,
|
||||
webui-audit lane, `GRILL.md`).
|
||||
|
||||
## Operator register decisions this PRD leans on hardest
|
||||
|
||||
Full set: the operator DECISION-REGISTER (estate brain `docs/guides/proposed/DECISION-REGISTER.md`, snapshot 2026-08-28, sha256 `2cc81be1…aabec`; operator-only corpus, not shipped). Load-bearing here:
|
||||
|
||||
| ID | Ruling (short) | Consumed by |
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| OD-02/OD-03 | one role per seat; role change = clean session, ephemeral context discarded | [[SEAT.1-seat-profile]], [[SESS.1-session-continuity]] |
|
||||
| OD-08/OD-09 | coordinator service owns leases/deployment; orchestrators never deploy seats directly | [[AUTHZ.1-capability-authority]] |
|
||||
| OD-16–OD-23 | PRD owns requirements; immutable accepted versions; `docs/PRD.md` = generated pointer under `docs/PRDs/`; missions pin PRD version+digest; `mosaic prdy` owns PRD creation | [[GOV.1-prd-lifecycle]] — **independently re-derived in the 2026-08-31 grill before this register was consulted; the two agree** |
|
||||
| OD-48 | instance contract: `profile.json` structured identity, `overlay.json` generated composition | [[SEAT.1-seat-profile]] |
|
||||
| OD-49–OD-53 | `mosaic config` desired-state engine; blueprint + host-binding split; precedence chain; **all interfaces (CLI/TUI/WebUI/API) share one CLI-backed engine** | [[DATA.1-record-authority]], [[CLI.1-parity]], [[UI.1-webui-surfaces]] |
|
||||
| OD-54 | WebUI drafts are revisioned server-side desired-state; no effect until planned and applied | [[UI.1-webui-surfaces]] |
|
||||
| OD-57–OD-61 | checkpoints tied to incarnation+lease; coordinator-run relaunch (checkpoint→stop→apply→clean incarnation→restore); fencing; full restart recovery | [[SESS.1-session-continuity]] — **this is the ratified mechanism for mid-stream harness/model/provider switching** |
|
||||
| OD-62–OD-65 | watchdog, outage fail-closed, failure isolation/reporting | [[AUTHZ.1-capability-authority]], [[UI.1-webui-surfaces]] (audit/alerts) |
|
||||
|
||||
## Reconciliation notes
|
||||
|
||||
- Register OD-13 (repository-backed mission state canonical first, DB later behind
|
||||
the same interface) and J1 (Git owns governance, PostgreSQL owns runtime
|
||||
state) are compatible: OD-13 governs _mission_ state migration order; J1 governs
|
||||
steady-state record classes. [[DATA.1-record-authority]] carries the merged
|
||||
table.
|
||||
- Register OD-18's "generated pointer" is stricter than the 2026-08-31 grill's
|
||||
hand-maintained shim: **adopted** — the shim should be generated by tooling,
|
||||
not hand-edited ([[GOV.1-prd-lifecycle]] inherits this).
|
||||
- Proposed, not yet ratified: **L2-D52** (least-privilege Assignment issuance),
|
||||
staged at `proposed/docs/MECHANICAL-AGENT-RUNTIME-L2-AUTHORIZATION--least-privilege-issuance.md`.
|
||||
|
||||
## Extraction cross-check notes (2026-08-31)
|
||||
|
||||
- Five highly product-normative operator drafts carry **no decision-register
|
||||
citations at all** (seat-identity, vault, adapter-contract,
|
||||
SPECIALIZATION-MODEL, prd-registry). Their rules were pulled into sections on
|
||||
their merits; before E6 they must be cross-checked against the register
|
||||
rather than assumed pre-vetted.
|
||||
- The intended-state-reconciler spec uses a **file-local D1–D6 numbering** that
|
||||
is neither the stack registry nor the operator register — a live instance of
|
||||
the N4 prefix-collision defect. Do not conflate when compiling
|
||||
cross-references.
|
||||
- The session-lifecycle draft is the densest register consumer (OD-03/OD-04/OD-08,
|
||||
OD-56–OD-65) and is likely the canonical drafting source for OD-56–OD-65; its
|
||||
one-relaunch-path gap is Q-S4.
|
||||
|
||||
## Re-ratified orphaned decisions (Q-T2, Jason 2026-09-01)
|
||||
|
||||
Ratified once in archived planning docs, absent from every live document until
|
||||
this map; re-ratified as live constraints:
|
||||
|
||||
- **No Python in the monorepo** (source:
|
||||
`archive/planning/monorepo-consolidation/board-review.md:742`).
|
||||
- **Matrix/MACP: exactly three supported install modes, Mode A (split-domain)
|
||||
primary** (source: `archive/planning/matrix-macp/rfc-002:133`). Its
|
||||
DNS/domain prerequisite ruling remains open — [[GOV.5-open-questions]] Q-T6
|
||||
blocks Matrix install work, not this map.
|
||||
- **OpenBrain excluded from WP1/WP2 consolidation scope** (source:
|
||||
`board-review.md:611`).
|
||||
|
||||
## Registry prefix ruling (Q-G2, Jason 2026-09-01)
|
||||
|
||||
Distinct prefixes at source: stack keeps **D1–D15**; the operator
|
||||
DECISION-REGISTER renames to **OD-01…OD-65** with a redirect table in the
|
||||
source doc. Applied in this bundle: every stack-side citation of the operator
|
||||
register now reads **OD-nn**; the brain-side source doc itself still carries
|
||||
its old zero-padded `D01`–`D65` numbering and renames (with the redirect
|
||||
table) on its next touch. File-local D-numbering in drafts (the live N4
|
||||
instance: the reconciler spec's D1–D6) is prohibited — every decision doc
|
||||
declares a unique registry prefix. For any text predating 2026-09-01 not yet
|
||||
swept into this bundle, the old reading rule still applies: a zero-padded bare
|
||||
`Dnn` is the operator register (now read as `OD-nn`); a bare `Dn`/`Dnn` in the
|
||||
1–15 range without a zero pad is the stack registry.
|
||||
@@ -0,0 +1,671 @@
|
||||
---
|
||||
id: GOV.4
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# GOV.4 — Active workstream contracts (preserved unchanged)
|
||||
|
||||
Carried verbatim from rev0 ([rev0 PRD](../2026-08-26_PRD_rev0/PRD.md) lines
|
||||
258–910) under rev0's own rule: open issues bind to these contracts; this
|
||||
revision moves no text and changes no requirement in them. They graduate out
|
||||
individually when their workstreams close.
|
||||
|
||||
The sections below are normative, in-flight workstream contracts carried over
|
||||
verbatim from the previous revision of this file. Open issues bind to them.
|
||||
This rewrite moved no text and changed no requirement in them; they are
|
||||
governed by their own issues and review gates, and they graduate out of this
|
||||
file individually when their workstreams close.
|
||||
|
||||
## Current addendum: #1194 — Installed framework-tool drift detection
|
||||
|
||||
- Compare the framework tools shipped with the executing Mosaic package against the deployed `$MOSAIC_HOME/tools` tree by content hash.
|
||||
- Treat every shipped `tools/**` file as framework-owned/required according to `framework-manifest.txt`, while excluding the explicit operator-owned credential carve-out and preserving installed-only operator/unknown files.
|
||||
- Distinguish and count `IN_SYNC`, `STALE`, `NOT_INSTALLED`, and installed-only classifications; fail non-zero when shipped tools are stale or absent and refuse self-comparison that would make drift unobservable.
|
||||
- Surface the observational check through `mosaic doctor`; do not refresh files, restart seats, or mutate live tooling.
|
||||
- Document identity/messaging/gate behavior changes in the current stale set, the reviewed quiet-window keep-mode refresh command, and post-refresh probes against the installed path.
|
||||
- Prove by construction that a stale and missing deployed tool are detected; that regression must fail before this checker exists.
|
||||
|
||||
## Compaction Refresh Trust Lifecycle (M1, #827–#830)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
Context compaction, session replacement, and same-PID runtime reloads can leave a previously VERIFIED runtime lease attached to stale directives. M1 must revoke that authority mechanically for Claude (including Claudex) and Pi without trusting caller-asserted identity or forking the external broker state machine.
|
||||
|
||||
### Requirements
|
||||
|
||||
1. `CR-REQ-01`: Claude `PreCompact` and `SessionStart` with matcher `compact`, plus Pi `session_before_compact` and the first post-`session_compact` `context`, SHALL independently revoke the active broker lease.
|
||||
2. `CR-REQ-02`: Runtime generation increases—including same-PID Pi reload/new/resume/fork and Claude resume/clear—SHALL monotonically replace the prior broker incarnation and inherit no VERIFIED lease.
|
||||
3. `CR-REQ-03`: A fired observer that cannot confirm broker revocation SHALL fail closed through lifecycle cancellation, a private local generation fence, and/or a runtime-local tool latch. The existing all-tools broker gate remains authoritative.
|
||||
4. `CR-REQ-04`: The lease TTL SHALL remain monotonic and capped at 300 seconds. If both observers are missed, within-TTL consequential actions remain allowed and after-TTL actions are denied. This named bounded residual stale window SHALL be documented without claiming a mutator-action bound inside the window.
|
||||
5. `CR-REQ-05`: Hook descendants SHALL use the broker-minted session and owner-only current-generation state inherited from register-before-exec. Caller-minted sessions and parallel lease state machines remain forbidden.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-CR-01`: Real-socket tests prove each Claude observer revokes, Pi lifecycle tests prove both observer paths, and Claudex isolated settings preserve and install the mandatory hooks.
|
||||
2. `AC-CR-02`: A same-PID generation test proves the old generation is stale and the replacement generation is UNVERIFIED across reload/resume/fork-equivalent lifecycle events.
|
||||
3. `AC-CR-03`: RED-first T12b/T30 evidence explicitly reports dual-hook miss within TTL as **ALLOWED** and after TTL as **DENIED**.
|
||||
4. `AC-CR-04`: Attributable executable coverage is at least 85%, the full repository suite is green on deterministic main, and independent code/security review completes before merge.
|
||||
|
||||
---
|
||||
|
||||
## Pi Persistent Goal Loop (#1150)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
A Pi agent can stop after a plausible-looking answer even when the operator's broader objective is
|
||||
not complete, and ordinary compaction can weaken or omit the original objective. Mosaic needs an
|
||||
optional, operator-controlled goal loop that keeps a Pi session oriented, checks progress at native
|
||||
lifecycle boundaries, and resumes work until completion is verified or a bounded safety state is
|
||||
reached.
|
||||
|
||||
The objective is a Mosaic-owned Pi extension deployed from the framework into
|
||||
`~/.config/mosaic/runtime/pi/`. It must not install into or depend on `~/.pi/agent/extensions/`.
|
||||
|
||||
### Scope
|
||||
|
||||
#### In scope
|
||||
|
||||
1. `PGL-REQ-01`: The framework SHALL ship a dedicated Pi goal extension under
|
||||
`packages/mosaic/framework/runtime/pi/`, seed it under `$MOSAIC_HOME/runtime/pi/`, and make
|
||||
`mosaic pi` load it alongside the core Mosaic extension when present.
|
||||
2. `PGL-REQ-02`: `/goal` SHALL support setting a goal plus status, pause, resume, cancel, and help
|
||||
operations without silently replacing an active goal.
|
||||
3. `PGL-REQ-03`: Active branch-specific goal state SHALL be persisted in Pi custom session entries,
|
||||
restored on session start and tree navigation, and never rely on a compaction summary as its
|
||||
source of truth.
|
||||
4. `PGL-REQ-04`: A hidden goal contract SHALL be injected through Pi's `context` event before every
|
||||
model request so it remains effective across tool turns, retries, and post-compaction requests.
|
||||
5. `PGL-REQ-05`: The harness SHALL inspect every `turn_end` and successful `session_compact` event.
|
||||
A structured terminating goal-report tool SHALL capture `continue`, evidence-bearing `achieved`,
|
||||
or `blocked` status without requiring a redundant model turn.
|
||||
6. `PGL-REQ-06`: An achievement claim SHALL remain provisional until a second consecutive
|
||||
evidence-bearing verification report. Any continuation report or successful compaction during
|
||||
verification SHALL reset the verification sequence.
|
||||
7. `PGL-REQ-07`: Continuation SHALL be initiated at safe lifecycle boundaries, primarily
|
||||
`agent_settled`; manual compaction and restored active sessions may schedule a deferred idle
|
||||
continuation without re-entering compaction handlers.
|
||||
8. `PGL-REQ-08`: The loop SHALL have operator cancellation plus bounded turn and repeated-no-progress
|
||||
limits. Exhausted or blocked goals pause rather than continuing indefinitely.
|
||||
9. `PGL-REQ-09`: Framework installation and update SHALL preserve normal manifest ownership: the
|
||||
goal extension is framework-owned under `runtime/**`, while no goal extension or configuration
|
||||
asset is created or modified under the operator's main Pi configuration. Pi remains the owner of
|
||||
its native session files used by `appendEntry()`.
|
||||
|
||||
#### Out of scope
|
||||
|
||||
1. A mathematical guarantee that an arbitrary natural-language goal is semantically complete.
|
||||
2. Automatically executing user-supplied shell predicates or accepting executable validation code in
|
||||
`/goal` arguments.
|
||||
3. Restarting Pi after process, host, or supervisor failure; the existing Mosaic fleet/runtime
|
||||
supervisor owns process durability.
|
||||
4. Gateway, database, web UI, Discord, or cross-harness goal orchestration in this slice.
|
||||
|
||||
### User and stakeholder requirements
|
||||
|
||||
- An operator can start a goal from Pi and see its current phase, evidence, limits, and latest report.
|
||||
- The agent remains oriented after each turn and compaction until verified, paused, blocked,
|
||||
exhausted, or cancelled.
|
||||
- Local testing uses a file under `~/.config/mosaic/runtime/pi/`; the feature never writes an
|
||||
extension asset to `~/.pi/agent/extensions/`.
|
||||
- Framework updates deploy the same reviewed extension source through Mosaic's existing manifest
|
||||
sync path.
|
||||
|
||||
### Non-functional requirements
|
||||
|
||||
1. **Safety:** bounded continuation, explicit cancellation, no arbitrary command execution, and no
|
||||
completion without non-empty reported evidence.
|
||||
2. **Reliability:** serialized continuation scheduling, branch-aware restoration, compaction-safe
|
||||
context injection, and stale-timer cancellation on session shutdown.
|
||||
3. **Performance:** no extra nested judge-model request on every turn; structured reporting uses the
|
||||
active agent's final terminating tool call.
|
||||
4. **Observability:** Pi status/notifications expose phase and bounded counters without recording
|
||||
credentials or hidden model reasoning.
|
||||
5. **Maintainability:** the state machine is deterministic and behavior-tested independently from Pi
|
||||
provider/network access.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-PGL-01`: A framework-sync fixture installs the extension at
|
||||
`$MOSAIC_HOME/runtime/pi/goal-extension.ts`, and launcher tests prove both Mosaic Pi extensions are
|
||||
emitted in deterministic order while absent optional files remain backward-compatible.
|
||||
2. `AC-PGL-02`: Command tests prove set/status/pause/resume/cancel behavior, active-goal replacement
|
||||
refusal, and bounded input handling.
|
||||
3. `AC-PGL-03`: Lifecycle tests prove every turn is recorded, active context is injected on every
|
||||
request, two evidence-bearing achievement reports are required, and `agent_settled` continues an
|
||||
unmet goal without duplicate scheduling.
|
||||
4. `AC-PGL-04`: Compaction and restoration tests prove goal state survives, verification is reset and
|
||||
rechecked after compaction, manual compaction continuation is deferred until idle, and tree/session
|
||||
branch state is reconstructed correctly.
|
||||
5. `AC-PGL-05`: Limit tests prove max-turn and repeated-no-progress exhaustion stop autonomous
|
||||
continuation, while pause/cancel/blocked states do not restart.
|
||||
6. `AC-PGL-06`: Focused tests, package typecheck/lint/test, repository quality gates, a local Pi load
|
||||
smoke test from `~/.config/mosaic/runtime/pi/`, independent review, and terminal-green CI pass before
|
||||
issue #1150 closes.
|
||||
|
||||
### Constraints, risks, and assumptions
|
||||
|
||||
- Dependency: Pi's extension API must continue to provide `registerCommand`, `registerTool`,
|
||||
`context`, `turn_end`, `agent_settled`, `session_compact`, session custom entries, and terminating
|
||||
tool results.
|
||||
- Risk: the working agent can overstate completion. Mitigation: structured evidence, a mandatory
|
||||
second verification pass, explicit semantic limitations, and operator-visible reports.
|
||||
- Risk: an impossible goal can consume unbounded resources. Mitigation: hard turn/no-progress bounds
|
||||
and paused terminal states.
|
||||
- Risk: automatic continuation can race compaction or session replacement. Mitigation: drive from
|
||||
`agent_settled`, defer idle restarts, generation-check timers, and clear timers on shutdown.
|
||||
- `ASSUMPTION:` Two consecutive evidence-bearing reports are the initial local verification policy;
|
||||
rationale: it provides a real recheck without doubling every turn's model cost. Future policy may
|
||||
add independent or deterministic validators.
|
||||
- `ASSUMPTION:` Default limits are 40 turns and 6 repeated no-progress reports, configurable only by
|
||||
bounded Mosaic environment settings; rationale: useful persistence with a finite autonomous budget.
|
||||
- `ASSUMPTION:` Documentation remains canonical in-repo for this slice; no external docs publication
|
||||
is requested.
|
||||
|
||||
### Testing and delivery intent
|
||||
|
||||
Use TDD for the deterministic controller and lifecycle invariants. Test with fake Pi lifecycle
|
||||
objects first, then run a local load/smoke test from the deployed Mosaic path. Deliver source, tests,
|
||||
launcher wiring, framework/runtime documentation, user/developer guides, and sitemap updates in one
|
||||
reviewed squash PR to `main` with terminal-green CI.
|
||||
|
||||
---
|
||||
|
||||
## Fleet Declarative Configuration Management Workstream (FCM, #758)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
The local Mosaic fleet has a roster, generated agent environment files, user-systemd units, tmux
|
||||
sessions, heartbeat files, examples, profiles, and separate gateway-backed agent records. These
|
||||
planes have drifted and are not one safe operator lifecycle. The objective is one **local fleet
|
||||
roster** as the desired-state SSOT, with generated environment, systemd, tmux, and heartbeat
|
||||
artifacts as rebuildable projections; it does not merge the local fleet control plane with the
|
||||
gateway-backed agent catalog.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
| ID | Requirement |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `FCM-REQ-01` | The roster SHALL be the sole writable desired-state source for local fleet membership, launch policy, and persisted lifecycle target. Generated environment files, systemd enablement, tmux sessions, and heartbeat state SHALL be non-authoritative projections. |
|
||||
| `FCM-REQ-02` | The implementation SHALL provide one executable structural contract for YAML/JSON input and one shared semantic validator. Roster load, profile validation, provision, migration, and apply SHALL reuse the existing baseline-plus-`roles.local` profile/persona resolver; a parallel role resolver is forbidden. |
|
||||
| `FCM-REQ-03` | The local fleet CLI SHALL expose documented programmatic validate, show, plan, apply/reconcile, create, inspect, update, delete, start, stop, restart, status, verify, and doctor operations with stable JSON and exit-code behavior. Existing `fleet add/remove` compatibility aliases may remain during the stated deprecation window. |
|
||||
| `FCM-REQ-04` | A fresh create SHALL persist `enabled:true` and `desired_state:stopped` unless an explicit persisted start is requested. The model SHALL distinguish enabled state, persisted desired state, and observed state. Migration, apply, reboot, and rollback SHALL not start an agent that was observed stopped before cutover. |
|
||||
| `FCM-REQ-05` | The launch chain SHALL consume deterministic, digest-stamped generated input only. Optional local overrides SHALL be parsed as strict data, may not shadow authoritative generated keys, and may not contain arbitrary commands, credential values, channels, or unknown `MOSAIC_AGENT_*` keys. Forbidden legacy keys, including `MOSAIC_AGENT_COMMAND`, SHALL be privately quarantined before launch and reported only by key name and content hash. |
|
||||
| `FCM-REQ-06` | Mutations and apply SHALL validate before mutation, use an expected generation/lock, write projections atomically, produce a deterministic plan, and emit recovery information on partial failure. Reconciliation SHALL act only on local, enabled, roster-owned projections and SHALL not kill unmanaged tmux sessions by fuzzy name. |
|
||||
| `FCM-REQ-07` | Canonical required classes are `code`, `review`, `validator`, `orchestrator`, `team-leader`, `enhancer`, and `interaction`. `validator` issues an independent final certificate but has no merge authority; `merge-gate` remains sole approve-to-land/merge authority. Team-leader capacity is bounded by an orchestrator-issued lease, and interaction is request/status only. Tess and Ultron are configurable instance/display names, not required machine identities. |
|
||||
| `FCM-REQ-08` | v1 migration SHALL be field-complete, reversible, and explicit about aliases, unresolved classes, lifecycle inference, generated-file regeneration, local override quarantine, schema-only remote/connector fields, and rollback. Every shipped example, profile, and service preset SHALL be migrated and executable, retained as an explicitly versioned v1 fixture, or retired with a replacement and deprecation note. |
|
||||
| `FCM-REQ-09` | M1–M5 SHALL remain local tmux/systemd control-plane work. Remote/SSH reconciliation, connector mutation, secret references, arbitrary command/channel overrides, gateway/API convergence, and UI configuration storage are excluded and require a separate PRD/threat model. |
|
||||
| `FCM-REQ-10` | Documentation and examples are delivery gates. The M0 checklist at [docs/fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md](../../fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) and the baseline disposition inventory at [docs/fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md](../../fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md) SHALL be maintained as acceptance evidence. |
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-FCM-01`: A valid local v2 roster can be parsed from YAML or JSON, validated structurally and semantically through the shared resolver, and rendered canonically; invalid fields, duplicate names, unresolved classes, unsupported runtime/model combinations, socket ambiguity, and incompatible options fail closed.
|
||||
2. `AC-FCM-02`: `plan` reports deterministic desired-versus-observed differences for roster, generated environment, systemd enablement, tmux/session, heartbeat, installed-asset revision, and provable orphans without mutation; `apply --check` reports drift without mutation.
|
||||
3. `AC-FCM-03`: Local create/update/delete is generation-guarded, atomic, idempotent, and safe by default; it permits supported runtime/model/harness/effort/workdir/role changes without direct editing of generated environment files and does not start a newly created agent unless explicitly persisted.
|
||||
4. `AC-FCM-04`: The generated-env/local-override launch chain rejects generated-key shadowing, arbitrary command override, unknown keys, shell evaluation, and sensitive-value diagnostics before any agent starts; known-safe legacy input is regenerated or strictly relocated, and forbidden input is quarantined.
|
||||
5. `AC-FCM-05`: Local lifecycle reconciliation implements the persisted/transient start-stop rules, exact default/named tmux socket targeting, systemd/tmux status, stale generated state, unmanaged-session reporting, and rollback without surprise restarts or fuzzy destructive targeting.
|
||||
6. `AC-FCM-06`: A v1 roster migration previews field-by-field disposition, preserves observed stopped/running state, inventories rather than reconciles remote/schema-only entries, supports a canary and rollback, and classifies every shipped example, profile, and service preset according to the M0 inventory.
|
||||
7. `AC-FCM-07`: Required role authority is validated: validator certificate is consumed but does not merge, merge-gate is the sole merge authority, team-leader leases do not change roster/credentials/authority, and interaction/Tess cannot claim orchestration or merge powers.
|
||||
8. `AC-FCM-08`: Documentation, examples, migration, troubleshooting, operational recovery, package/update asset drift, schema/example/profile validation, independent code/security review, validator certificate, and terminal-green CI are complete before #758 closes.
|
||||
|
||||
### M0 implementation gate
|
||||
|
||||
No source, schema, role, example, profile, systemd, or live-fleet change is authorized before M0
|
||||
lands. M0 consists only of these normative requirements, the complete task DAG, the scoped
|
||||
documentation IA checklist, and the legacy example/profile disposition inventory. Subsequent cards
|
||||
are defined in [docs/TASKS.md](../../TASKS.md) and must remain one card/one PR.
|
||||
|
||||
### Fleet git identity launch propagation (#1043)
|
||||
|
||||
#### Problem and objective
|
||||
|
||||
A fleet seat can have a registered per-agent Git credential while its launched runtime process lacks
|
||||
`MOSAIC_GIT_IDENTITY`. The credential resolver then cannot select the seat identity reliably, which
|
||||
blocks repository operations on fail-closed estates and can fall through to an unrelated identity on
|
||||
estates where that refusal is not active. The objective is to make Git identity a deterministic,
|
||||
roster-derived part of the generated launch projection and prove it reaches the launched process.
|
||||
|
||||
#### Normative requirements
|
||||
|
||||
1. `FGI-REQ-01`: Every generated fleet agent projection SHALL declare
|
||||
`MOSAIC_GIT_IDENTITY=<MOSAIC_AGENT_NAME>`; a differing or unsafe identity SHALL fail closed before
|
||||
tmux launch.
|
||||
2. `FGI-REQ-02`: The clean `/usr/bin/env -i` pane boundary SHALL pass every variable declared by the
|
||||
generated projection, including `MOSAIC_GIT_IDENTITY`, to the launched runtime process.
|
||||
3. `FGI-REQ-03`: A behavioral integration test SHALL set-compare the complete generated projection
|
||||
against the launched process environment. Source-text/string-presence assertions are insufficient.
|
||||
4. `FGI-REQ-04`: Verification SHALL include RED-first evidence and a delete-the-subject mutation that
|
||||
removes Git-identity pane propagation and makes the behavioral test fail.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
1. `AC-FGI-01`: A launched seat process contains every key/value pair declared by its generated
|
||||
environment projection, including the roster-derived Git identity.
|
||||
2. `AC-FGI-02`: Missing, unsafe, or split Git identity is rejected before a tmux session is created.
|
||||
3. `AC-FGI-03`: Focused launcher and generated-environment tests, repository quality gates,
|
||||
independent review, and the required RED/green/R7 evidence are recorded before push.
|
||||
|
||||
### Framework shell assertion portability (#1098)
|
||||
|
||||
#### Problem and objective
|
||||
|
||||
The blocking framework-shell chain can report that a pane command omitted `/usr/bin/env -i` even when
|
||||
`-i` matched successfully. A short-circuiting `grep -q` under `set -o pipefail` may close its pipe after
|
||||
the match and cause an upstream producer to exit with SIGPIPE, turning a valid semantic result into a
|
||||
nonzero aggregate pipeline. The objective is to inspect the captured NUL-delimited argv directly and
|
||||
make failures carry the observed records needed for diagnosis.
|
||||
|
||||
#### Normative requirements
|
||||
|
||||
1. `FSP-REQ-01`: The pane-boundary test SHALL validate an adjacent `/usr/bin/env`, `-i` argv pair from
|
||||
the authoritative NUL-delimited tmux capture without a short-circuit pipeline whose upstream status
|
||||
can override a successful match.
|
||||
2. `FSP-REQ-02`: Missing, reversed, or non-adjacent boundary tokens SHALL fail, while valid boundaries
|
||||
SHALL remain valid regardless of trailing argv size, pipe capacity, process scheduling, or host/CI
|
||||
utility implementation.
|
||||
3. `FSP-REQ-03`: A failed boundary check SHALL print stable indexed, shell-escaped observed argv records
|
||||
before exiting nonzero; the fixture SHALL continue to contain generated non-secret launch data only.
|
||||
4. `FSP-REQ-04`: Verification SHALL include RED-first large-payload evidence, negative token-order
|
||||
controls, the complete focused launcher suite, canonical Woodpecker CI, and independent review.
|
||||
|
||||
#### Acceptance criteria
|
||||
|
||||
1. `AC-FSP-01`: A large captured argv with adjacent `/usr/bin/env`, `-i` passes even when the former
|
||||
`grep -q` pipeline returns nonzero from an upstream SIGPIPE.
|
||||
2. `AC-FSP-02`: Missing executable, missing flag, and detached/reversed flag fixtures return nonzero and
|
||||
emit the indexed observed argv.
|
||||
3. `AC-FSP-03`: The focused suite passes on the development host and CI image, and the merged-main
|
||||
Woodpecker pipeline is terminal green before #1098 closes.
|
||||
|
||||
---
|
||||
|
||||
## Exact Cross-Harness Fleet Communications Contract (#766)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
Fleet runtime contracts currently combine exact peer rows with generic operational metavariables and
|
||||
independently parsed roster data. Non-Claude harnesses can mistake those metavariables for values to
|
||||
infer, producing incorrect host, session, socket, or helper targets. The objective is one
|
||||
roster-resolved communications contract that every supported harness receives unchanged.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
1. `FCOM-REQ-01`: Fleet commands and runtime composition SHALL use one shared v1 roster structural
|
||||
resolver. A second lenient communications parser is forbidden.
|
||||
2. `FCOM-REQ-02`: The composed contract SHALL render the local roster member's authoritative host,
|
||||
exact agent/session name, resolved tmux socket, exact helper path, and deterministic communications
|
||||
generation.
|
||||
3. `FCOM-REQ-03`: Every known peer SHALL have one exact executable command. Same-host commands SHALL
|
||||
omit `-H`; cross-host commands SHALL use only that peer's explicit roster `ssh` target; the one
|
||||
supported fleet-wide named socket SHALL use `-L` with its exact value. A per-agent socket declaration
|
||||
must equal that fleet-wide value; unsupported independent sockets and missing cross-host SSH data SHALL
|
||||
fail closed.
|
||||
4. `FCOM-REQ-04`: Operational fleet examples SHALL not contain unresolved host, session, socket, or
|
||||
helper-path metavariables. Agents SHALL select an exact rendered peer row and SHALL NOT infer,
|
||||
substitute, or fuzzy-match targeting values.
|
||||
5. `FCOM-REQ-05`: An unknown local member or requested peer SHALL fail closed with exact-name discovery
|
||||
guidance. Runtime composition SHALL not silently omit a requested fleet member's communications
|
||||
contract.
|
||||
6. `FCOM-REQ-06`: Claude Code, Codex, OpenCode, and Pi SHALL receive equivalent authoritative
|
||||
communications data through the common runtime composer.
|
||||
7. `FCOM-REQ-07`: Tests SHALL prove the contract from framework-source `TOOLS.md`, through a fresh
|
||||
installed `TOOLS.md`, to final runtime composition and helper executability. User-owned installed
|
||||
`TOOLS.md` content SHALL remain preserved.
|
||||
8. `FCOM-REQ-08`: Stale installed or active composed context SHALL be reported with deterministic
|
||||
generation/repair/relaunch guidance. Currency requires the expected source and installed contract
|
||||
marker/version plus bounded byte equality. The supported current-version repair SHALL run independently
|
||||
of package updates, preserve divergent `TOOLS.md` bytes in a digest-qualified no-clobber backup, restore
|
||||
a regular executable helper without following symlinks, and be idempotent. Detection and reporting SHALL
|
||||
NOT rewrite active context, restart a session, or mutate a live fleet.
|
||||
9. `FCOM-REQ-09`: The shared resolver SHALL preserve and strictly validate every schema-supported v1
|
||||
connector kind (`tmux`, `discord`, and `matrix`) from YAML and JSON. Every accepted snake/camel alias
|
||||
pair SHALL reject differing dual declarations and accept identical declarations. JSON roster fallback
|
||||
SHALL occur only when `roster.yaml` is absent; all other YAML access failures SHALL fail closed.
|
||||
10. `FCOM-REQ-10`: The communications generation SHALL cover the complete canonical rendered semantic
|
||||
contract, including identity, role/class, resolved host/socket/helper, peer metadata, and exact commands.
|
||||
Installed helpers SHALL be validated with no-follow filesystem inspection as regular executable files.
|
||||
Keep-mode reseed and relaunch discovery SHALL preserve and support both YAML and JSON rosters.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-FCOM-01`: Contract fixtures contain no unresolved operational targeting metavariables; local
|
||||
identity contains exact host/session/socket/helper values.
|
||||
2. `AC-FCOM-02`: Same-host, cross-host, named-socket, literal-default-socket, and missing-SSH tests prove
|
||||
exact targeting and fail-closed behavior.
|
||||
3. `AC-FCOM-03`: Unknown identities and peers report known exact names plus an exact self-scoped
|
||||
discovery command; no fuzzy session selection is emitted.
|
||||
4. `AC-FCOM-04`: Four-harness tests prove byte-equal authoritative communications sections.
|
||||
5. `AC-FCOM-05`: Source, fresh-install, preserved-custom-install, stale-installed, composed-generation,
|
||||
helper executable, agent-send socket isolation, and exact-target tests pass.
|
||||
6. `AC-FCOM-06`: Documentation defines non-mutating stale-context detection and operator-authorized,
|
||||
exact-agent relaunch; no implementation path performs automatic session mutation.
|
||||
7. `AC-FCOM-07`: YAML and JSON fixtures cover every connector kind; all snake/camel aliases cover
|
||||
identical acceptance and conflicting rejection; non-`ENOENT` YAML failures do not fall back.
|
||||
8. `AC-FCOM-08`: Missing, directory, symlink, and non-executable installed helpers fail closed. Explicit
|
||||
current-version repair proves partial-deletion recovery, digest-qualified backup collision safety,
|
||||
symlink-target safety, and repeated-run idempotence.
|
||||
9. `AC-FCOM-09`: Markerless-equal and wrong-version source/installed contracts are stale, and a rendered
|
||||
role/class change produces a different communications generation.
|
||||
|
||||
---
|
||||
|
||||
## KBN-101 Database Runtime/Migration Role Split (#771)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
PostgreSQL Gateway/storage currently uses one `DATABASE_URL` for runtime queries and migrations. That makes the deployed application identity an owner and prevents certification that KBN immutable event, artifact, checkpoint, and evidence relations reject runtime `UPDATE`/`DELETE`. KBN-101 freezes a least-privilege runtime/migration split before KBN-100 schema work.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
1. `K101-REQ-01`: `DATABASE_URL` SHALL be the non-owner PostgreSQL runtime connection and `DATABASE_MIGRATION_URL` SHALL be the migration-only owner/migrator connection. They are required respectively for runtime and the dedicated `mosaic-db-migrator --run|--verify` phase in `standalone`/`federated`; local PGlite is the explicit exception. The published `@mosaicstack/db` bin maps exactly `mosaic-db-migrator` to `./dist/cli.js`, its image entrypoint is exactly `mosaic-db-migrator`, accepts no URL/SQL/schema/role argv, and returns stable sanitized exits. Every current/future PostgreSQL DDL entrypoint SHALL route to that runner or be denied, and SHALL reject `DATABASE_URL`-only execution before connection/DDL. Data migration may connect only after the runner prepares and verifies the PostgreSQL target, through dedicated non-DDL `mosaic_data_importer` and exactly `--target-url-file /run/secrets/mosaic-migrate-target-url`, its fixed paired authenticated provider-version file `/run/secrets/mosaic-migrate-target-version`, plus `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`. KBN-101-05 obtains URL key `url` and version only from the same successful Vault KV-v2 response at `secret-{env}/mosaic-stack/database/importer` (`data.metadata.version`), renders them as one immutable generation into separate consumer copies, and never infers a provider version from DSN bytes. The trusted runner verifies TLS/identity/manifest, reads its fixed importer URL/version copies only for binding through safe no-follow fd checks, and signs a credential-free JCS/Ed25519 attestation using its runner-only fixed root-owned private-key file; no signing key reaches importer/runtime. The artifact binds secret version and SHA-256 of exact high-entropy credential-file bytes, canonical TLS host/port/database, CA/SPKI, PostgreSQL system identifier/database OID, importer role, manifest/schema fingerprints, producer invocation/build/image digest, issued/expires/nonce, and correlation. Before target connection the importer validates URL/version/attestation/public-key files, signature/key/expiry/replay/authenticated provider version/digest/generation/bindings and the importer-only CA at exact `DATABASE_TLS_CA_CERT_PATH`; after verified TLS and before DML it validates server/database/role/CA/schema identity, with same-fd/in-memory-byte TOCTOU protection, rotation/revocation, a privileged producer-only-to-importer-only artifact handoff controller that verifies/copies/fsyncs/atomically renames/seals before importer start, consumer isolation/no logging-oracle, and sanitized errors. Raw `--target-url`, `DATABASE_URL` fallback, runtime-owner use, missing/unsafe/substituted files, stale/replayed/tampered/wrong-key attestation, wrong binding, and DDL attempt fail before target connection/DDL; post-connect mismatch closes with zero DML/DDL. A reviewed finite classifier inventories executable current source/scripts/package bins, operator docs, deploy manifests, and exact normative contracts by path; active secure records pin both options/files, producer/key/bindings/tests, while normative contracts cannot mask instructions. Unknown active commands, duplicate-owner, ownerless, missing-path, and historical/status-only masking hits fail. `db:push` is forbidden outside an explicitly disposable local developer database and cannot accept a production-like URL.
|
||||
2. `K101-REQ-02`: Gateway runtime/replicas SHALL not execute migrations or DDL. The runner SHALL hold one `max:1` session and fixed two-int advisory namespace `1297044289` (`MOSA`), `1262636593` (`KBN1`) across preflight, reconciliation, migration, verification, and release. It SHALL compare the versioned canonical manifest v1 tuple (journal logical index/tag plus exact SQL-byte SHA-256) to the complete observed ledger mapping; count/set-only, timestamps, and physical insertion order are non-normative and insufficient.
|
||||
3. `K101-REQ-03`: PostgreSQL SHALL separate non-login platform database owner, non-login schema owner, dedicated `NOLOGIN SUPERUSER` `mosaic_extension_owner`, login migrator, dedicated login non-DDL data importer, non-login runtime capability, and login runtime roles. For PostgreSQL 17 + pgvector 0.8.2, `vector` is untrusted (`trusted` is absent and `relocatable=true`): only an externally controlled audited platform-bootstrap superuser session may `SET ROLE mosaic_extension_owner` for CREATE/UPDATE/SET SCHEMA, then `RESET ROLE`; the role has `rolcanlogin=false`, `rolsuper=true`, zero members, no runtime credential/Vault secret, and is never provided to app containers. It owns `mosaic_extensions`, fresh `vector`, and owner-bearing extension members, while `mosaic_schema_owner` receives only `USAGE` for type resolution and never ownership/`CREATE`/`ALTER`/`DROP`/member-change/default-privilege authority there. Superuser cannot be constrained by `GRANT`/`REVOKE`; this is identity/non-login/no-membership/external-control/audit isolation, not a false least-privilege claim. Extension operations require control-plane change, independent review, backup/rollback, maintenance window, and audit evidence. Managed targets that cannot establish this exact role are ineligible until an independently approved versioned provider-owned extension-owner profile exists; app/migrator ownership is never silently retained. Existing approved-owner extension relocation validates exact `pg_namespace.nspowner`, `pg_extension.extowner`, member ownership/schema/version, while legacy runtime-owned extension fails closed to a controlled shadow-database migration—never unsupported ownership alteration, catalog mutation, ownership adoption, or `DROP CASCADE`. Runtime, migrator, schema owner, importer, and all service roles must fail `SET ROLE`, catalog/direct `ALTER`/`UPDATE`/`DROP`/membership-change denial, role ownership, superuser/role-creation/schema-creation/TEMPORARY, unsafe membership, untrusted search path, missing grants, unauthenticated TLS, and immutable privilege drift checks. Application schema is fixed `mosaic` with exact `pg_catalog,mosaic` session path; historical public migrations remain byte-immutable legacy bootstrap only, every future Drizzle application declaration targets `mosaic`, and `vector` is explicitly qualified from non-writable `mosaic_extensions`. No config-derived SQL identifier is permitted.
|
||||
4. `K101-REQ-04`: `mosaicstack/stack` KBN-101-00 SHALL exclusively own `infra/pg-bootstrap/roles.sql`, `infra/pg-bootstrap/extensions.sql`, `infra/pg-bootstrap/README.md`, and bootstrap tests; KBN-101-05 SHALL exclusively own `tools/db/render-postgres-secrets.ts`, its tests, and current Compose/Portainer/two-gateway deployment declarations, consuming the versioned bootstrap interface without overlap. Environment IaC/Vault is named input and Mosaic deployment control plane/Jason is activation authority. Distinct runtime/migrator/importer URL, importer authenticated provider-version, DB-client CA, Gateway leaf, and PostgreSQL server key/certificate materials are provisioned before a production-like database starts. Importer and migrator have separate immutable URL/version copies at fixed `10002:10002`/`10003:10003` identities; runtime/unrelated containers receive neither importer material, attestation private key, or importer artifact. Runtime, migrator, and importer require their mounted CA plus `sslmode=verify-full`. Exact UID/GID/mode/rendering, service-DNS SANs, Vault/compose/Swarm consumer isolation, two-gateway pair ordering, server activation, pre-enforcement legacy-client drain and `hostssl` zero-plaintext-session proof, fresh/existing transition, CA-overlap rotation, TLS-only rollback, and standalone/federated/Swarm/two-gateway positive/negative TLS evidence are required. No application-generated production certificate or plaintext bootstrap exception is permitted.
|
||||
5. `K101-REQ-05`: KBN immutable relations SHALL permit the real runtime role INSERT/SELECT only and deny UPDATE/DELETE; parent retention remains RESTRICT/no-cascade. Role/password/Vault creation is external platform control, never application migration/source.
|
||||
6. `K101-REQ-06`: N-1 single-URL compatibility, rollout/rollback, Vault ownership/rotation/redaction, CI, installer, compose/Portainer, observability, and deployment handoffs SHALL be separately bounded one-card/one-PR work. Prepared slices remain inactive while current owner-runtime deployments stay N-1; Mosaic control plane/Jason alone authorizes one final atomic activation or rollback, with no force-on-red/bypass. KBN-101 planning itself SHALL not mutate production.
|
||||
7. `K101-REQ-07`: KBN-100 SHALL begin only after the KBN-101 foundation role/schema-boundary certificate; it SHALL rebase on that main head, restore generated Drizzle declaration/snapshot/journal consistency, and bound procedural immutable-table grant/trigger/backfill additions to its schema slice. KBN-101 real deployed-role immutable-operation certification SHALL complete after KBN-100 creates those relations and before KBN-105.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
1. `AC-K101-01`: DTO/command-matrix tests prove required modes, PGlite exception, `mosaic-db-migrator --help|--run|--verify`/stable exits/argv refusal, public-import negative, every finite classified DDL/static-bypass inventory path and both harness pairs reject `DATABASE_URL`-only before connection/DDL, no migration-to-runtime fallback, and `db:push` refusal outside an allowlisted disposable DB. Before inventory, ownership, or status masking, the semantic fixture fails README's exact former commented code-fence generic-wrapper form and the user guide's exact former executable generic-wrapper form; source-consistency proves current `packages/storage/src/cli.ts` directly `execSync`s `pnpm --filter @mosaicstack/db db:migrate` and no `mosaic-db-migrator` bin exists, so runner-delegation documentation fails. The active `docs/guides/migrate-tier.md` route is inventoried to KBN-101-07 and proves runner-produced `--target-url-file /run/secrets/mosaic-migrate-target-url`, fixed paired provider-version file, and `--target-attestation-file /run/mosaic-attestations/migrate-target.v1.json`; runner-only signing/private-key isolation; Vault KV-v2 same-response version provenance, separate immutable generation mounts, importer CA, JCS/Ed25519 signature/key rotation/revocation, atomic artifact, expiry/replay, safe-fd secret-version/digest, canonical TLS/CA/server/database/role/manifest/schema bindings, dedicated non-DDL importer, consumer isolation/no log-oracle, and exact no-connection versus zero-DML rejection for missing/wrong/stale/replayed/tampered/wrong-key/substituted/generation-mismatched inputs. The full current non-normative docs inventory—including user guide, federation historical task/MILESTONES status, and non-operative SETUP—has an exact safe disposition. Scanner semantic checks reject automatic first-boot/startup extension/schema/migration wording, Compose-up-before-runner, init-script authority, production `.env`/monorepo auto-load/`EnvironmentFile=`/credential-export-or-argv/restart-as-secret-activation routes, and every unqualified operator-document `mosaic-db-migrator --run|--verify` hit regardless of named/normative/status classification. The exact former README/dev/deployment Compose-first sequences, former SETUP wording, exact former MILESTONES wording `pgvector extension installed + verified on startup`, former architecture-plan/PERFORMANCE/backlog runner routes, and any unqualified runner fixture fail before inventory masking. Only one `Held future procedure` Markdown section—bounded through the next equal-or-higher heading—may contain the explicit non-operative/no-current-command-authority form that names KBN-101-00/-03/-05 and preserves external bootstrap → TLS/roles → `mosaic-db-migrator --run` → `mosaic-db-migrator --verify` → Gateway/Compose readiness; every runner hit outside that section fails. The README assertion for the checked-in direct CI `pnpm --filter @mosaicstack/db run db:migrate` with `DATABASE_URL` passes only as active legacy N-1, uncertified, non-authorizing-as-an-operator-route status against an isolated disposable CI database pending KBN-101-06 removal—not as an ordinary operator or approved DDL-authority route. Only local PGlite data-layer work or non-PostgreSQL Compose is current (Gateway/Web local startup is held pending daemon/inherited/project-DSN rejection).
|
||||
2. `AC-K101-02`: Fixed namespace lock contention/crash/readiness/non-interference and exact manifest-v1 reconciliation tests prove no replica race/runtime auto-migration and fail closed on every missing/unknown/duplicate/ambiguous/corrupt/stale ledger state.
|
||||
3. `AC-K101-03`: Actual PostgreSQL 17 + pgvector 0.8.2 control-file, catalog, Drizzle-generation, vector-query/operator, fresh/approved-owner/legacy-shadow/partial/resume/rollback/N-1, and real deployed-role tests prove `trusted` absent/untrusted plus relocatability, external-superuser `SET ROLE` create/update/`RESET ROLE` audit, exact `rolcanlogin=false`/`rolsuper=true`/zero-membership/no-runtime-secret state, platform/schema/extension-owner/migrator/importer/runtime separation, `pg_extension.extowner` plus owner-bearing extension-member/schema/version assertions, and runtime/migrator/schema-owner/importer/all-service-role `SET ROLE`/ALTER/DROP/member-update denial. They also prove `pg_catalog,mosaic` per-session pool safety, `mosaic_extensions` qualification, identifier injection denial, ownership/membership/ledger-read/TEMP/default grants, and unsafe privilege denial.
|
||||
4. `AC-K101-04`: Disposable standalone, federated/Swarm, and two-gateway verified-TLS positives plus for both pairs missing CA/wrong CA/wrong SAN/sslmode downgrade, server/Gateway key mode, UID/GID, secret-consumer isolation, and legacy-drain/`hostssl` negatives prove server bootstrap, ordering, and readiness; PGlite is expressly excluded from this PostgreSQL evidence.
|
||||
5. `AC-K101-05`: Real runtime-role evidence proves INSERT/SELECT succeeds and UPDATE/DELETE fails for every frozen immutable KBN relation.
|
||||
6. `AC-K101-06`: N-1/atomic activation/rollback, Vault/CA-overlap rotation/redaction, health/operator behavior, CI/deployment handoff, independent exact-head security review, and terminal-green CI evidence the foundation before KBN-100; after KBN-100, the real deployed-role immutable-operation certificate and Ultron approval release KBN-105.
|
||||
|
||||
**Normative implementation contract:** [`docs/native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md`](../../native-kanban-sot/KBN-101-DB-ROLE-SPLIT.md). `ASSUMPTION:` existing `standalone` and `federated` are all PostgreSQL production-like modes; any new PostgreSQL tier inherits these requirements until an explicit versioned amendment.
|
||||
|
||||
---
|
||||
|
||||
## Tess Interaction Agent Workstream (TESS)
|
||||
|
||||
### Problem and Objective
|
||||
|
||||
Jason needs one durable, operator-facing Mosaic agent outside Hermes that is reachable through a dedicated Discord channel and CLI, can attach to and operate the Mosaic fleet and transitional Hermes agents, and preserves context across restarts and compaction. Mos remains the coding/general fleet orchestrator; Tess is the complementary human interaction, visibility, control, and migration agent.
|
||||
|
||||
The objective is to ship **Tess** (from _tessera_, a piece of a mosaic) as a Pi-native, GPT-5.6 Sol agent with high reasoning. Tess must use Mosaic-owned contracts and plugins so Hermes can be replaced incrementally rather than becoming a permanent architectural dependency.
|
||||
|
||||
### Scope
|
||||
|
||||
#### In Scope
|
||||
|
||||
1. `TESS-ARP-001`: A runtime-neutral `AgentRuntimeProvider` contract supporting `listSessions`, `streamSession`, `sendMessage`, `terminate`, `getSessionTree`, `attach`, health, capability discovery, and normalized events/errors.
|
||||
2. `TESS-PI-001`: A long-running Pi-native Tess agent profile/service pinned to GPT-5.6 Sol with high reasoning, explicit tool policy, lifecycle hooks, durable checkpoints, and restart recovery.
|
||||
3. `TESS-DSC-001`: Dedicated Discord channel binding to Tess through the Mosaic gateway, with allowlists/RBAC, thread/reply policy, streaming, attachments, approvals, and correlation IDs.
|
||||
4. `TESS-CLI-001`: `mosaic tess` CLI commands for chat, status, session listing, attach/detach, send/steer/stop, provider health, and recovery.
|
||||
5. `TESS-FLT-001`: Fleet plugin capabilities for roster/status/heartbeat inspection, message delivery, session hierarchy, safe attach, and controlled restart/recovery.
|
||||
6. `TESS-MOS-001`: Explicit Mos coordination boundary and tools: hand off orchestration requests, observe mission/task state, receive results, and never silently compete for orchestration authority.
|
||||
7. `TESS-HRM-001`: Transitional Hermes adapter for profiles/agents, sessions, streaming/messages, Kanban, skills, memory, tools, cron, and health, using capability negotiation and fail-closed unsupported operations.
|
||||
8. `TESS-MEM-001`: Unified memory/retrieval plugin with scoped search/recent/capture/stats, startup context injection, provenance, redaction, namespace isolation, and flat-file/project truth precedence.
|
||||
9. `TESS-STA-001`: Durable agent state, inbox, handoff, compaction-recovery, and resume reconstruction.
|
||||
10. `TESS-PLG-001`: Plugin/tool catalog covering runtime bootstrap, repository/PR workflow, fleet diagnostics, incident-safe read operations, Discord interaction, and extensible MCP/skill discovery.
|
||||
11. `TESS-TRN-001`: Replaceable transport providers: tmux/fleet now, Matrix/native Mosaic transport later, with no Discord/CLI business logic coupled to transport details.
|
||||
12. `TESS-SEC-001`: RBAC, per-operation authorization, explicit approval for destructive/privileged/customer-visible actions, audit events, secret/PII redaction, tenant isolation, and bounded command execution.
|
||||
13. `TESS-SEC-002`: Command execution SHALL enforce declared scope/role server-side; admin/system and destructive operations SHALL require policy-bound durable approval.
|
||||
14. `TESS-SEC-003`: Every session list/read/attach/send/terminate operation SHALL enforce server-derived owner and tenant scope; guessed or client-supplied IDs SHALL grant no authority.
|
||||
15. `TESS-SEC-004`: MCP tools SHALL derive actor/tenant from authenticated context and SHALL NOT accept caller-controlled identity fields.
|
||||
16. `TESS-SEC-005`: Discord plugin ingress SHALL authenticate service identity, enforce guild/channel/user allowlists, propagate correlation/message IDs, and reject replay.
|
||||
17. `TESS-SEC-006`: Secret/PII classification and redaction SHALL occur before persistence and before channel egress, including tool metadata and authentication flows.
|
||||
18. `TESS-SEC-007`: Approvals SHALL be one-time, expiring, actor/tenant-bound, and cryptographically bound to the exact structured action digest.
|
||||
19. `TESS-SEC-008`: Ingress, provider sends, tool side effects, and responses SHALL use durable inbox/outbox/checkpoints and idempotency records for restart-safe replay.
|
||||
20. `TESS-SEC-009`: Garbage collection and retention SHALL be session/tenant scoped unless executed as a separately authorized and audited system-wide job.
|
||||
21. `TESS-OBS-001`: Structured logs, traces, health/readiness, provider latency/errors, session lifecycle, tool audit, and actionable recovery diagnostics.
|
||||
22. `TESS-MIG-001`: Capability inventory and staged Hermes-to-Mosaic migration matrix with coexistence, cutover, rollback, and deprecation gates.
|
||||
|
||||
#### Out of Scope
|
||||
|
||||
1. Replacing Mos as coding/general fleet orchestrator.
|
||||
2. Making Hermes the Mosaic core or coupling Mosaic domain logic to Hermes schemas.
|
||||
3. Migrating every historical chat verbatim; only policy-compliant indexed summaries and user-selected sessions are migrated.
|
||||
4. Unrestricted shell execution from Discord.
|
||||
5. Full web UI parity in the first Tess operational milestone; gateway contracts must remain web-consumable.
|
||||
6. Replacing tmux before Matrix/native transport reaches operational parity.
|
||||
|
||||
### Stakeholder and User Requirements
|
||||
|
||||
- Jason must be able to converse with the same Tess session from Discord and CLI.
|
||||
- Jason must be able to see what is running, stale, blocked, or unhealthy without attaching manually to every session.
|
||||
- Jason must be able to attach to Tess and authorized fleet sessions through supported CLI controls.
|
||||
- Tess must collaborate with Mos and the fleet while preserving a single clear orchestration authority.
|
||||
- The system must migrate useful Hermes/OpenClaw capabilities intentionally, with evidence, instead of copying implementations wholesale.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
1. **Security:** default-deny provider/tool capabilities, least privilege, no secrets in logs/prompts/commits, Discord user/channel authorization, and auditable approvals.
|
||||
2. **Reliability:** durable inbox/checkpoints; idempotent message handling; reconnect with bounded backoff; no message loss or duplicate execution across gateway restart.
|
||||
3. **Performance:** first acknowledgement within 2 seconds when connected; streamed agent output begins within 5 seconds excluding model/provider delay; status reads return within 2 seconds under nominal local conditions.
|
||||
4. **Observability:** every ingress message and resulting provider/tool operation carries a correlation ID across Discord, gateway, Tess, provider, and audit events.
|
||||
5. **Maintainability:** channel, runtime, transport, memory, and external-agent integrations remain adapter-based with contract tests.
|
||||
6. **Privacy:** only scoped context enters external runtimes; persisted messages/memories follow retention and redaction policy.
|
||||
7. **Portability:** Tess runs through Pi/Mosaic contracts and does not require Hermes to start or serve native Mosaic operations.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
1. `AC-TESS-01`: A dedicated Discord channel and `mosaic tess chat` connect to one durable Tess session and stream responses bidirectionally.
|
||||
2. `AC-TESS-02`: `mosaic tess status|sessions|tree|attach|send|stop` operate against authorized provider capabilities with stable typed outputs and actionable errors.
|
||||
3. `AC-TESS-03`: Tess runs GPT-5.6 Sol at high reasoning and its effective runtime/model/tool policy is visible through status without exposing credentials.
|
||||
4. `AC-TESS-04`: Tess can inspect and message the Mosaic fleet, hand orchestration work to Mos, and demonstrate that Tess does not independently claim Mos-owned orchestration work.
|
||||
5. `AC-TESS-05`: Hermes adapter demonstrates session listing, streaming/message delivery, hierarchy mapping, and at least one approved capability in each of Kanban, skills, memory, tools, and cron—or reports unsupported capabilities fail-closed.
|
||||
6. `AC-TESS-06`: Restart/compaction test preserves session identity, pending inbox, last durable checkpoint, and a resumable handoff without duplicate side effects.
|
||||
7. `AC-TESS-07`: Unauthorized Discord users/channels, cross-tenant access, unsafe tool calls, forged approvals, and sensitive-output cases are denied and audited.
|
||||
8. `AC-TESS-08`: tmux/fleet and Matrix/native transport implementations pass the same provider contract suite; Matrix may remain non-default until readiness gates pass.
|
||||
9. `AC-TESS-09`: Baseline quality gates, unit/integration/contract tests, Discord+CLI E2E, restart/recovery tests, independent code review, and security review are green.
|
||||
10. `AC-TESS-10`: Migration matrix documents every audited Hermes/OpenClaw capability as native, adapted, deferred, or rejected, with cutover and rollback evidence.
|
||||
11. `AC-TESS-11`: User, admin, developer, API/OpenAPI, operations/recovery, and plugin-authoring documentation is current and linked from the sitemap.
|
||||
|
||||
### Constraints, Dependencies, Risks, and Assumptions
|
||||
|
||||
- Dependency: Mosaic gateway remains the single API surface; Pi is the native runtime; Valkey/PostgreSQL provide canonical durable state where required.
|
||||
- Dependency: Discord bot credentials and dedicated channel ID are deployment secrets provisioned outside source control.
|
||||
- Risk: Tess could drift into a second orchestrator. Mitigation: explicit role policy, Mos handoff contract, authority checks, and E2E boundary tests.
|
||||
- Risk: broad Hermes compatibility can freeze legacy semantics into Mosaic. Mitigation: Mosaic-owned normalized contracts and capability negotiation.
|
||||
- Risk: Discord creates a privileged remote-control surface. Mitigation: pairing/allowlists, RBAC, approvals, rate limits, audit, and safe tool classes.
|
||||
- Risk: transcript ingestion can violate privacy or overload memory. Mitigation: scoped opt-in import, redacted summaries, provenance, retention, and deduplication.
|
||||
- Risk: current root filesystem has limited headroom. Mitigation: isolated worktrees, no duplicated dependency installation unless required, and cleanup only after active-lane verification.
|
||||
- `ASSUMPTION:` The public name is **Tess**, because the user requested a name and the tessera/Mosaic relationship is distinctive; config must permit later display-name changes without renaming APIs or storage keys.
|
||||
- `ASSUMPTION:` The dedicated Discord channel ID and final guild policy will be supplied/provisioned during deployment, so implementation uses explicit configuration and fail-fast startup validation.
|
||||
- `ASSUMPTION:` tmux/fleet is the production transport for the first operational milestone; Matrix/native transport is implemented behind the same contract and promoted only after parity/reliability verification.
|
||||
- `ASSUMPTION:` Project/task truth remains in canonical Mosaic/project stores; semantic memory systems are retrieval/mirror layers, not hidden authorities.
|
||||
|
||||
### Testing and Delivery Intent
|
||||
|
||||
Delivery uses five gated milestones: runtime contracts/security; Pi service/state; Discord/CLI; fleet/Hermes/plugin suite; migration/Matrix/recovery/qualification. Every source-code task requires tests, independent review, a PR to `main`, terminal-green CI, and issue/task closure. Production activation additionally requires a clean-host Pi launch, dedicated Discord channel smoke test, CLI attach test, restart/recovery drill, and rollback procedure.
|
||||
|
||||
---
|
||||
|
||||
## Official Channel Plugin Workstream (#756)
|
||||
|
||||
### Problem and Objective
|
||||
|
||||
The Discord plugin currently couples Discord event handling, gateway bridging, and reply routing in one implementation and activates only on mentions. Mosaic needs an official channel adapter that behaves the same no matter whether the bound logical agent currently runs through Claude, Codex, Pi, OpenCode, or a future harness. The Discord connection and conversation address must remain stable while the gateway changes the runtime provider behind that logical session.
|
||||
|
||||
The objective is to make Discord the first implementation of a transport-neutral official channel contract, with explicit authorization and deterministic channel/thread routing that future Matrix, Slack, and other adapters can share.
|
||||
|
||||
### Scope
|
||||
|
||||
#### In Scope
|
||||
|
||||
1. `CHN-001`: Transport-neutral channel adapter, route, message, attachment, authorization-principal, response-target, and health contracts in `@mosaicstack/types`, including trusted per-binding logical-agent configuration selection.
|
||||
2. `CHN-002`: Stable channel conversation addresses based on logical agent plus channel/thread identity; harness, model, and runtime-provider IDs are forbidden from channel session keys.
|
||||
3. `DSC-001`: An authorized untagged message in a configured agent-bound channel routes to the agent and receives its response in that channel.
|
||||
4. `DSC-002`: A bot mention in a configured parent channel creates a Discord thread, or reuses the thread already attached to that same native message; the mentioned turn and subsequent thread turns route and respond in that thread.
|
||||
5. `DSC-003`: A message already inside an authorized thread inherits authorization from its configured parent and never attempts a nested thread.
|
||||
6. `DSC-004`: Guild, parent channel, user, pairing, and role authorization remains default-deny before thread creation or gateway dispatch.
|
||||
7. `DSC-005`: Discord service authentication, HMAC envelope integrity, replay protection, attachments, approvals, response chunking, and correlation behavior remain intact.
|
||||
8. `DSC-006`: The Discord adapter exposes lifecycle and health behavior through the shared channel contract without importing a harness SDK.
|
||||
|
||||
#### Out of Scope
|
||||
|
||||
1. The logical-agent lease, fencing epoch, execution grant, checkpoint, or cross-harness takeover implementation tracked by #754/#755.
|
||||
2. Dynamic Discord authorization administration in the web UI.
|
||||
3. Multi-guild tenant isolation, DMs, slash commands, voice, reactions, or production bot deployment.
|
||||
4. Implementing Matrix or Slack adapters in this slice.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
1. **Security:** no thread or dispatch side effect occurs until guild, parent channel, user, pairing, role, and bounded per-user/channel rate checks pass; attachment metadata is shape- and size-bounded; credentials never enter source, messages, session keys, or logs.
|
||||
2. **Portability:** channel contracts and stable conversation IDs contain no Claude, Codex, Pi, OpenCode, model, process, or provider-specific field; each configuration-owned binding selects its trusted logical agent without changing the channel identity.
|
||||
3. **Reliability:** repeated messages for one channel/thread resolve the same conversation handle; reconnecting the adapter does not require a harness-specific rebinding.
|
||||
4. **Maintainability:** Discord-specific API translation stays in the Discord package; gateway and future adapters depend on transport-neutral contracts.
|
||||
5. **Observability:** thread creation or routing failure is reported without message content or credential material.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
1. `AC-CHN-01`: Contract and behavior tests prove the plugin route contains only logical agent plus channel/thread identity and produces the same stable conversation handle regardless of underlying harness selection.
|
||||
2. `AC-CHN-02`: A mentioned authorized parent-channel message creates a thread (or reuses its already-attached thread), dispatches to the thread conversation, and targets the response to that thread.
|
||||
3. `AC-CHN-03`: An untagged authorized parent-channel message dispatches to the parent conversation and targets the response to the parent channel.
|
||||
4. `AC-CHN-04`: Untagged follow-ups inside an authorized thread dispatch and respond in that same thread without creating a nested thread.
|
||||
5. `AC-CHN-05`: Unauthorized guilds, channels, users, unpaired users, insufficient roles, and rate-limited senders produce no thread and no gateway dispatch.
|
||||
6. `AC-CHN-06`: Shared channel contracts are exported from `@mosaicstack/types`, Discord implements the lifecycle/health seam, and no harness SDK is imported by the plugin.
|
||||
7. `AC-CHN-07`: Focused routing/auth tests, package tests, typecheck, lint, formatting, coverage, independent code/security review, and terminal-green CI pass.
|
||||
|
||||
### Constraints, Risks, and Assumptions
|
||||
|
||||
- Dependency: Mosaic gateway remains the policy, durable-session, audit, and runtime-provider boundary.
|
||||
- Constraint: This work must not modify orchestrator-to-Pi migration or #754/#755 lease/fencing files.
|
||||
- Risk: accepting untagged messages could create noisy or unintended agent input. Mitigation: only explicitly configured channels and paired, role-authorized users are accepted, with bounded per-user/channel message and thread rates.
|
||||
- Risk: Discord thread creation can fail because of channel permissions, archived state, or API rate limits. Mitigation: fail without dispatching a turn whose response destination cannot be honored, and emit sanitized diagnostics.
|
||||
- `ASSUMPTION:` Configured channels are dedicated agent interaction surfaces, so authorized untagged human messages are intentional agent input.
|
||||
- `ASSUMPTION:` Mention in a parent channel selects a public thread; messages already in a thread remain there because Discord has no nested threads.
|
||||
- `ASSUMPTION:` One Discord bot may serve multiple configuration-owned logical-agent bindings.
|
||||
- `ASSUMPTION:` Static allowlists and paired-user roles are the authorization administration surface for this slice.
|
||||
|
||||
### Testing and Delivery Intent
|
||||
|
||||
Use TDD for remote-ingress routing and permission boundaries. Required evidence includes parent-channel mention, untagged parent message, existing-thread follow-up, existing-thread mention, thread reuse, unauthorized side-effect denial, stable harness-neutral conversation identity, adapter health, and regression coverage for signed envelopes and approvals. Deliver through issue #756, a reviewed squash PR to `main`, terminal-green CI, and issue closure.
|
||||
|
||||
---
|
||||
|
||||
## Mos Runtime Portability Workstream (MOS-PORT)
|
||||
|
||||
### Problem and Objective
|
||||
|
||||
Mos is currently identified partly by a harness-native session and communication process. Replacement/rebinding exists, but no gateway-enforced logical identity or fencing prevents a stale harness from continuing to reply or execute effects after takeover.
|
||||
|
||||
The objective is to make Mos a server-derived logical Mosaic identity whose authority can move safely among runtime connectors. The gateway owns identity, lease, policy, and audit; harnesses remain replaceable adapters.
|
||||
|
||||
### M1 Requirements
|
||||
|
||||
1. `MOS-PORT-ID-001`: Define a normalized logical-agent identity independent of Claude Code, Pi, Codex, tmux, Matrix, and provider-native session IDs.
|
||||
2. `MOS-PORT-LEASE-001`: Persist one exclusive connector lease per tenant/logical-agent/binding with CAS acquisition, monotonic fencing epoch, TTL, heartbeat, explicit release, and takeover.
|
||||
3. `MOS-PORT-FENCE-001`: Bind every connector dispatch/execution grant to the current server-derived tenant, logical identity, binding, connector, scopes, expiry, and lease epoch.
|
||||
4. `MOS-PORT-FENCE-002`: Reject and audit stale, expired, forged, cross-tenant, cross-binding, and unauthorized grants before connector, channel, provider, or tool side effects.
|
||||
5. `MOS-PORT-OBS-001`: Emit credential-safe correlation/audit events for lease acquire, renew, takeover, reject, release, and expiry.
|
||||
6. `MOS-PORT-ARCH-001`: Runtime/provider adapters consume normalized lease context without adding harness-native schemas to Mosaic core.
|
||||
|
||||
### M1 Acceptance Criteria
|
||||
|
||||
1. `AC-MOS-PORT-01`: Two contenders for one binding cannot simultaneously hold current authority under concurrency.
|
||||
2. `AC-MOS-PORT-02`: Successful takeover increments the fencing epoch and every operation from the old epoch fails closed before side effects.
|
||||
3. `AC-MOS-PORT-03`: Gateway/database restart preserves lease and epoch state; expired leases can be recovered only through the authorized takeover path.
|
||||
4. `AC-MOS-PORT-04`: Cross-tenant, cross-agent, cross-binding, forged, and expired lease/grant cases are denied and audited.
|
||||
5. `AC-MOS-PORT-05`: Unit, migration, repository close/reopen, concurrency, abuse, gateway integration, independent security review, CI, and documentation gates pass.
|
||||
|
||||
### Deferred to Later #754 Milestones
|
||||
|
||||
Canonical checkpoint/handoff payloads, exactly-once connector receipts, concrete Claude/Pi/Codex adapters, channel cutover, and full cross-harness failover/rollback E2E are explicitly out of M1 scope.
|
||||
|
||||
---
|
||||
|
||||
## Workspace placement guard hardening (#1174)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
The Bash pre-tool guard must prevent Git checkouts and repository state from being placed under
|
||||
`$HOME` without refusing ordinary Git commands merely because a source, option value, branch name,
|
||||
or metadata mentions `$HOME`. A guard that over-blocks routine work is unsafe because operators
|
||||
will route around it.
|
||||
|
||||
### Scope and requirements
|
||||
|
||||
1. `WPG-REQ-01`: `git clone` and `git worktree add` placement SHALL be judged from their placement
|
||||
operands, not from every HOME-shaped word in the command.
|
||||
2. `WPG-REQ-02`: Clone sources, references, templates, environment assignments, and non-placement
|
||||
worktree metadata MAY resolve under HOME when all placement operands resolve elsewhere.
|
||||
3. `WPG-REQ-03`: Both attached and separate-value `--separate-git-dir` forms SHALL remain placement
|
||||
operands and SHALL be refused when they resolve under HOME.
|
||||
4. `WPG-REQ-04`: Option classification SHALL account for Git's rule-generated boolean negations
|
||||
without relying on an enumerable allowlist of flag spellings.
|
||||
5. `WPG-REQ-05`: Quote removal, escapes, shell command boundaries, redirections, and end-of-options
|
||||
handling SHALL preserve existing fail-closed checkout coverage.
|
||||
6. `WPG-REQ-06`: Absolute placement aliases SHALL resolve shell-known HOME spellings, dot segments,
|
||||
repeated separators, and existing symlink parents before the HOME boundary comparison.
|
||||
7. Relative targets whose effective path depends on the shell cwd are out of scope and tracked by
|
||||
#1197.
|
||||
|
||||
### Acceptance and verification
|
||||
|
||||
1. Git's own option parser accepts each tested flag, including generated `--no-*` forms, while the
|
||||
guard allows a HOME-valued source with an explicit safe destination.
|
||||
2. Equivalent clone and worktree fixtures cover rule-generated negations and remain discriminating
|
||||
against the prior head where the defect existed.
|
||||
3. Real HOME destinations and both `--separate-git-dir` forms remain blocked, including placements
|
||||
after shell command boundaries.
|
||||
4. The full hermetic guard suite, syntax/static checks, adversarial probes, independent review, and
|
||||
terminal-green CI pass before merge.
|
||||
5. Any option-classification residual is documented with its deliberate failure direction.
|
||||
|
||||
### Constraints, risks, and assumptions
|
||||
|
||||
- Security and usability are co-equal: neither a placement bypass nor routine over-block is an
|
||||
acceptable repair.
|
||||
- `ASSUMPTION:` The value-taking option surface exposed by the installed Git version is closed and
|
||||
measurable through Git's own parser/help output; rationale: boolean flags are rule-generated,
|
||||
while separate-value options have explicit grammar and must be classified as such.
|
||||
- Risk: a future Git release may add a new value-taking placement option. Mitigation: document the
|
||||
chosen residual direction and pin every currently supported placement option in behavior tests.
|
||||
- Risk: a symlink can be replaced after pre-execution canonicalization. Mitigation: resolve every
|
||||
existing parent physically and document the remaining inherent TOCTOU window; the worktree helper
|
||||
remains the authoritative path-derivation mechanism, with atomic closure tracked by #1199.
|
||||
|
||||
---
|
||||
|
||||
## Release Integrity Workstream (RI, #1275)
|
||||
|
||||
### Problem and objective
|
||||
|
||||
At `next` 476db12b (review of 2026-08-17), publication from `next` is not bound to the full verification pipeline for the same commit: the publish pipeline's publish steps depend on `build` only, while ordinary push CI excludes `next`. Public Forge/MACP paths contain false-success placeholders: a stub executor that reports `completed` with exit zero, planning/remediation gates that execute literal `true`, a review gate that echoes an approving verdict, and a gate runner that treats empty commands and unimplemented CI-provider gates as passing. Shipping UI surfaces can render a failed fetch as an empty, healthy collection.
|
||||
|
||||
Objective: for alpha 0.0.50, the release cannot publish, report, or display work state that the repository has not actually verified. Decisions SDLC-D-033 through SDLC-D-038 (Jason, 2026-08-17) scope this floor; full decision text and required-behavior lists live in jarvis-brain `docs/plans/2026-08-16_mosaic-stack-sdlc-protocol.md` and `data/decisions/mosaic-stack-sdlc-protocol.json`. This section restates only the normative requirements.
|
||||
|
||||
### Normative requirements
|
||||
|
||||
1. **RI-N1 Exact-commit publication verification (SDLC-D-034).** One canonical terminal verification command performs self-contained re-verification in the publish pipeline against the job's checked-out commit before any external publication effect. The command contains or invokes the complete mandatory verification set (semantic parity with the PR merge gate, including sanitization, upgrade-guard, typecheck, lint, format check, tests, and build); CI and publication do not maintain separate semantic checklists. Every publish step depends on the verification step in the executable pipeline DAG. Provider commit identity and `git rev-parse HEAD` must identify the same commit. Missing, skipped, cancelled, stale, or inconclusive checks fail closed. Documentation-only runs may skip publication but cannot bypass verification when a publication effect will occur. A negative control must prove that a broken check blocks every publish step.
|
||||
|
||||
2. **RI-N2 Fail-closed Forge/MACP with explicit simulation (SDLC-D-035).** Simulation requires explicit caller intent (e.g. `--simulate`) and produces a distinct typed `simulated` state that can never satisfy dependencies, acceptance criteria, gates, merge, or release. Normal execution exits nonzero with a typed capability failure when a required executor, reviewer, command, or CI provider is absent — no stub completion, no literal-`true` gates, no synthetic approvals, no empty-command passes. A manual gate with no automation enters a waiting state; it does not pass. Positive tests prove explicit simulation still works; negative controls prove simulation and every missing-provider case cannot advance lifecycle state.
|
||||
|
||||
3. **RI-N3 One transitional PRD authority (SDLC-D-036).** `@mosaicstack/prdy` structured storage under `docs/prdy/`, driven by `mosaic mission --plan`, is the authoritative PRD representation for the alpha. `mosaic prdy` either routes through the same application service or operates only as an explicit, named Markdown import/export adapter; `docs/PRD.md` is not a peer authority. `mission --plan` must persist the mission↔PRD linkage (mission id/version, PRD id/version, selected requirements). Markdown output is a generated view carrying source identity; editing it cannot mutate authority silently. Import is explicit, validated, and conflict-aware (proposed successor, never overwrite). Structural validity is separate from approval.
|
||||
|
||||
4. **RI-N4 One quality-rails evaluator (SDLC-D-037).** The TypeScript quality-rails package is the sole authoritative evaluator. A complete probe inventory maps every current TypeScript and shell check to one canonical check with disposition (preserve/strengthen/retire, each named). Effective shell enforcement probes are absorbed before their independent paths retire; expected-file presence alone is not parity. The evaluator returns typed results (`passed`/`failed`/`blocked`/`error`/`not-applicable`) with check version, subject, and reason; missing implementation, missing input, unknown check, process error, timeout, or malformed output can never become `passed` or an unqualified skip. Check definitions and policy are versioned and digested. Shell commands become thin adapters with no separate verdict logic. The canonical terminal verification command (RI-N1) invokes this evaluator rather than duplicating its logic. Contract, parity, and negative-control tests are required, plus independent review of probe equivalence.
|
||||
|
||||
5. **RI-N5 Consequence-aware stale UI (SDLC-D-038).** Mission Control distinguishes typed freshness states (`current`, `stale`, `partial`, `unknown`, `unavailable`) rather than inferring from empty arrays or null. A failed fetch never renders as an empty healthy collection. Last-known data may display for situational awareness only with source identity, version, and age visibly labeled; any derived completion/assurance/release verdict whose inputs are stale becomes `unknown`; all state-changing actions are disabled until fresh state loads and is revalidated. With no verified snapshot, surfaces show an explicit unavailable state. Cache corruption, cross-workspace data, schema mismatch, and version regression invalidate the snapshot. Tests cover the failure matrix (network, auth, malformed, partial, corruption, stale age, schema mismatch, recovery, stale-action rejection) with negative controls proving no case yields a current green verdict or enabled mutation.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- AC-RI-1: A push to `next` that fails any mandatory verification step publishes nothing (no npm package, no image), demonstrated by a checked-in negative control and by pipeline evidence on a real `next` publish run where the verification step is green and every publish step depends on it.
|
||||
- AC-RI-2: With no executor/reviewer/CI provider wired, Forge and MACP normal runs exit nonzero with typed capability failures; with `--simulate`, runs complete but every result is typed `simulated` and cannot satisfy any gate, dependency, or completion state — proven by unit tests including negative controls.
|
||||
- AC-RI-3: A PRD created or revised through either `mosaic mission --plan` or `mosaic prdy` resolves to one authority under `docs/prdy/` with stable identities and versions; the mission↔PRD linkage survives restart; a Markdown export is labeled as generated and cannot silently become a second writer; divergent legacy content blocks baseline claims until explicitly resolved — proven by contract tests.
|
||||
- AC-RI-4: `quality-rails check` through any entry point (TS CLI, framework shell adapter) returns the same typed verdict for the same subject; the probe inventory names every legacy check's disposition; a deliberately broken probe fails closed — proven by contract/parity/negative-control tests and independent review of probe equivalence.
|
||||
- AC-RI-5: No shipping surface renders a failed fetch as an empty healthy state; stale/partial/unavailable states are typed, labeled, and mutation-disabled — proven by the failure-matrix tests.
|
||||
- AC-RI-6: All cards merged to `next` via squash PR with terminal-green CI; release evidence for 0.0.50 records commit, verification run, and published artifacts.
|
||||
|
||||
### Out of scope
|
||||
|
||||
The canonical dispatcher/control-plane vertical slice (work graph, execution attempts, fenced leases, typed check-in, independent verifier dispatch) is decided post-alpha (SDLC-D-033, option B). Multi-pipeline verification certificates (SDLC-D-034 option B) are post-alpha. Full AF-1..AF-4 objective matrices and Mission Control portfolio surfaces are post-alpha.
|
||||
|
||||
## Official CLI Capability and Tool Migration Workstream (T78)
|
||||
|
||||
Normative contract on integration trunk `next`:
|
||||
[docs/requirements/cli-capability-migration.md](../../requirements/cli-capability-migration.md):
|
||||
migrates agent-facing operations from directly invoked scripts into documented, first-class
|
||||
`mosaic` CLI command groups, together with the central-registry resolver, capability catalog,
|
||||
adapter boundary, and phased legacy-tool-tree decommission the migration requires. The contract
|
||||
carries its own implementation hold and delivery stages.
|
||||
|
||||
## Graduation ruling (Q-G3, Jason 2026-09-01)
|
||||
|
||||
Graduation of a Part II workstream contract is a ratification act: Jason marks
|
||||
it (grill or direct ruling). The graduated contract text archives inside the
|
||||
then-current PRD revision bundle — this section gains a graduated-set record
|
||||
per revision — keeping the frozen-bundle model intact.
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
id: GOV.5
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# GOV.5 — Open questions (the grill list)
|
||||
|
||||
Every question the corpus could not settle. This is the E5 ms-grill-me input;
|
||||
ratification is blocked until this list is empty or every remaining row is
|
||||
explicitly deferred with an owner. IDs are stable; answered questions get their
|
||||
ruling recorded here and flow into the owning section.
|
||||
|
||||
**Frontier status (2026-09-01, grill rounds 3–8 complete): EMPTY.** Every row
|
||||
is ruled, dissolved, or deferred-with-owner. The operator-side Q-T3 canon
|
||||
coalescence closed in round 8 and was executed the same day (brain commit
|
||||
`59d43270`). E6 ratification executed 2026-09-01; this list is frozen with the
|
||||
bundle.
|
||||
|
||||
## Data model
|
||||
|
||||
- **Q-D1** — **RULED, Jason 2026-09-01: consolidate.** `launch.env` folds
|
||||
into `profile.json` (one seat record). Precondition verified same day:
|
||||
`mosaic-core/lib/loader.ts seatRole()` parses profile.json as a generic
|
||||
record and reads only `role` — widened files tolerated by construction.
|
||||
Migration staged as lane work (ledger B1).
|
||||
- **Q-D2** — **RULED, Jason 2026-09-01: per-provider map + broker refs.**
|
||||
`profile.json` carries a per-provider map whose values are credential-broker
|
||||
references (`environment/service/component/secret-name`), never secrets —
|
||||
one seat, N providers, zero secrets in the brain tree. Flows to
|
||||
[[DATA.1-record-authority]] and [[AUTHN.1-auth-accounts]].
|
||||
- **Q-D3** — **RULED, Jason 2026-09-01: the projection engine owns.** One
|
||||
writer: the role-projection engine (`role apply` path). The launcher seeds
|
||||
nothing itself — it invokes the projector; Pi and every harness get
|
||||
regenerated files on each role/seat change; hand edits are drift, flagged by
|
||||
`validate`. Consistent with L2-D19 + Q-T5. Unblocks the reconciliation
|
||||
features (lane ledger B3 → D8).
|
||||
- **Q-D4** — **RULED, Jason 2026-09-01: role ceiling + seat choice.** The
|
||||
Role Revision defines the allowed model set (policy ceiling); the seat
|
||||
records preferences within it; effective models = intersection, consistent
|
||||
with the L2-D39 intersection chain. Flows to [[HARN.1-harness-config]] and
|
||||
role-harness-config DESIGN Q2 (same ruling, both doors).
|
||||
- **Q-D5** — **DEFERRED with owner (Jason 2026-09-01)**: the mutating
|
||||
config-engine half gets its own ruling after v1 read-only `validate`/`plan`
|
||||
ships; owner = the mosaic-config workstream. ([[CLI.1-parity]] §v1 subset)
|
||||
|
||||
## Sessions
|
||||
|
||||
- **Q-S1** — **DEFERRED with owner (Jason 2026-09-01)**: the
|
||||
session-id ↔ incarnation-id contract is settled inside the session-lifecycle
|
||||
draft before it lands (which now carries the Q-S4 two-path requirement);
|
||||
owner = that draft's ratification. ([[SESS.1-session-continuity]])
|
||||
- **Q-S2** — **RULED, Jason 2026-09-01: step-up re-auth required.** Role
|
||||
rebinding is an authority-changing act: fresh principal re-authentication
|
||||
≤10 minutes before the confirmation lands, matching the S2
|
||||
identity-lifecycle linking precedent. Flows to [[SEAT.1-seat-profile]]
|
||||
§role-binding and the UI.1 seat-page spec.
|
||||
- **Q-S3** — **DEFERRED with owner (Jason 2026-09-01)**: the measurable
|
||||
continuity-degradation bar is settled inside the session-lifecycle draft
|
||||
before it lands; owner = that draft's ratification.
|
||||
- **Q-S4** — **RULED, Jason 2026-09-01: two-path requirement ratified.** The
|
||||
session-lifecycle draft may not land with one relaunch path. Role change →
|
||||
clean-session path (new incarnation + fencing token, ephemeral context
|
||||
discarded, OD-02/OD-03); harness/model/provider change → continuity path (same
|
||||
Stack session id, checkpointed context restored, OD-57–OD-61, no noticeable
|
||||
degradation). Binding requirement on the draft, recorded in
|
||||
[[SESS.1-session-continuity]] §state machine.
|
||||
|
||||
## Audit
|
||||
|
||||
- **Q-A1** — **RULED, Jason 2026-09-01: mechanical tooling.** The audit is
|
||||
deterministic tooling over the grant/assignment record (witness-style, per
|
||||
the S2 writer-coverage pattern); its output feeds the audit page read-only.
|
||||
Agents may consume audit output but never produce the verdict ("prompt
|
||||
adherence is not an enforcement mechanism"). Flows to
|
||||
[[AUTHZ.1-capability-authority]] and [[UI.1-webui-surfaces]] §audit.
|
||||
- **Q-A2** — **DISSOLVED by the Q-A1 ruling**: the auditor is code, audited by
|
||||
ordinary review and CI witnesses, not a seat subject to misdirection.
|
||||
- **Q-A3** — **DEFERRED with owner (Jason 2026-09-01)**: the computable
|
||||
misdirection metric is designed inside the mechanical audit tooling ruled by
|
||||
Q-A1; owner = the audit-tooling workstream (lane ledger C2).
|
||||
|
||||
## Surfaces
|
||||
|
||||
- **Q-N1** — **DEFERRED with owner (Jason 2026-09-01)**: technical
|
||||
investigation of the in-browser OAuth flow (tmux-bridged terminal vs
|
||||
server-side) runs before the auth page builds; owner = the auth-page
|
||||
workstream (lane ledger D6). ([[AUTHN.1-auth-accounts]])
|
||||
- **Q-C1** — **RULED, Jason 2026-09-01: CI witness in the stack repo.** The
|
||||
parity matrix becomes a generated artifact with a drift-gate witness
|
||||
(contract-9 pattern): CI regenerates the inventory from code and fails on
|
||||
divergence from the committed matrix. Flows to [[CLI.1-parity]]; the
|
||||
witness itself is E6-return follow-up work.
|
||||
|
||||
## Governance
|
||||
|
||||
- **Q-G1** — **RULED, Jason 2026-09-01: both ratified.** L2-D52
|
||||
(least-privilege Assignment issuance, closes G1+G2) and the G6 WebUI
|
||||
surface-scope fix applied per their return procedures after digest
|
||||
re-verification; amendment files flipped to ratified; lane ledger A3/A4
|
||||
closed.
|
||||
- **Q-G2** — **RULED, Jason 2026-09-01: distinct prefixes at source.**
|
||||
Stack keeps D1–D15; the operator DECISION-REGISTER renames to **OD-01…OD-65**
|
||||
with a redirect table at the source doc; file-local D-numbering in drafts is
|
||||
prohibited going forward (each decision doc declares a unique registry
|
||||
prefix, rule lands in [[GOV.1-prd-lifecycle]]). Applied at E6 return for
|
||||
stack references; brain-side rename on next DECISION-REGISTER touch.
|
||||
- **Q-G3** — **RULED, Jason 2026-09-01: Jason marks; archive in bundle.**
|
||||
Graduation is a ratification act — Jason marks it (grill or direct ruling);
|
||||
the graduated contract text archives inside the then-current PRD revision
|
||||
bundle ([[GOV.4-workstream-contracts]] gains a graduated-set section),
|
||||
keeping the frozen-bundle model intact.
|
||||
|
||||
## Triage-raised (E2 sweep, 2026-08-31)
|
||||
|
||||
- **Q-T1** — **RULED B, Jason 2026-09-01: "shipped but frozen."** Amend D3 to
|
||||
acknowledge federation M1–M3 exist (Step-CA, enrollment, grants, mTLS auth
|
||||
guard, ScopeService, list/get/capabilities verbs; M3 landed 2026-06-24/25),
|
||||
are excluded from the v1 bar, and are frozen; re-home tracking in
|
||||
NORTH_STAR.yaml as a dormant workstream; frozen cert/auth code carries a
|
||||
**security re-audit gate** before any resumption. Consequences at E6 return:
|
||||
supersession/status banners on the three stale docs (root MISSION-MANIFEST,
|
||||
federation/MISSION-MANIFEST, scratchpads/mvp-20260312), reconcile
|
||||
guides/deployment.md with D15, NORTH_STAR.yaml dormant entry. The P5
|
||||
scope ambiguity (governance-federation vs shipped mTLS-query federation)
|
||||
stays open inside the future federation PRD, not rev1. Evidence: lane
|
||||
`FEDERATION-DOSSIER-2026-08-31.md`.
|
||||
- **Q-T2** — **RULED, Jason 2026-09-01: all three re-ratified** into the
|
||||
rev1 decision map ([[GOV.3-decision-map]] §re-ratified orphans): "No Python"
|
||||
in the monorepo; Matrix/MACP exactly-three-install-modes with Mode A
|
||||
(split-domain) primary; OpenBrain excluded from consolidation scope. The
|
||||
Matrix ruling's open DNS/domain prerequisite gets its own row (Q-T6).
|
||||
- **Q-T3** — **RULED (partial), Jason 2026-09-01: coalesce under the
|
||||
STRUCTURE-CANON name.** MOSAIC-CANON's more comprehensive content is
|
||||
authoritative; `STRUCTURE-CANON.md` is the logical surviving document name;
|
||||
the two coalesce into one. Conflict report delivered and all six
|
||||
decision points ruled (grill round 8, 2026-09-01): per-seat credential
|
||||
slots win; doc paths corrected to `fleet/auth/`/`fleet/memory/`;
|
||||
ENTITY.md/README.md stay required with a 42-seat backfill task; merge
|
||||
executed with the full reference sweep, MOSAIC-CANON reduced to a pointer
|
||||
shim. Record: lane `CANON-COALESCENCE-2026-09-01.md`. Operator-side; not a
|
||||
rev1 blocker.
|
||||
- **Q-T4** — **RULED, Jason 2026-09-01, two parts.**
|
||||
**(a) Two independent axes**: "Standalone/Enterprise" in the S2 corpus is a
|
||||
multi-tenancy/isolation _mode_ (`platform_mode`, D3/D11); D15's "compose
|
||||
standalone tier" is deployment _packaging_. Orthogonal. rev1 text always
|
||||
says "standalone mode" vs "compose tier"; mode-conversion.md needs a
|
||||
terminology note only, not a rewrite.
|
||||
**(b) Own track, rev1 cites**: rev1 ratifies citing the nine contracts as
|
||||
DRAFT successor material with status noted; each contract ratifies on its
|
||||
own PR when its family lands. Extraction record: lane
|
||||
`S2-EXTRACTION-2026-08-31.md`.
|
||||
- **Q-T5** — **RULED, Jason 2026-09-01: scope to files.** Adopted wording:
|
||||
"Generated settings _files_ are projections of the active Role Revision,
|
||||
never authority (L2-D19). DB settings records written through audited
|
||||
Gateway commands (`platform_mode`, `registration_mode`, `custody_config`,
|
||||
`bootstrap.seed-company-name`, and their successors) are records of
|
||||
authority like any other SOT row." No corpus conflict remains. Case law:
|
||||
contract 1 §5.4, contract 8 §2.4, contract 9 §3.2.
|
||||
- **Q-T6** — **DEFERRED with owner (Jason 2026-09-01)**: the Matrix/MACP
|
||||
Mode A DNS/domain prerequisite (`archive/planning/matrix-macp/rfc-001:428`)
|
||||
rules before any Matrix install work resumes; owner = whoever reopens
|
||||
Matrix. Until then Mode A is primary-on-paper only.
|
||||
|
||||
## Deferred-by-scope (recorded, not blocking rev1)
|
||||
|
||||
- Federation design (D3 — roadmap placeholder; nothing in v1 may foreclose it).
|
||||
_Q-T1 ruled B (2026-09-01): D3 to be amended — M1–M3 acknowledged, frozen,
|
||||
security re-audit gate before resumption; design itself stays deferred._
|
||||
- OS/kernel-level seat sandboxing (explicit lane non-goal; role-lane
|
||||
discipline, not process containment).
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
id: HARN.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# HARN.1 — Harness configuration
|
||||
|
||||
A harness is an installed agent runtime (claude, pi, codex, opencode, …).
|
||||
Shared contracts speak capability language; harness commands, model IDs,
|
||||
hooks, and settings live in runtime adapters (register OD-38).
|
||||
|
||||
## Harness configuration surface (WebUI page + CLI)
|
||||
|
||||
| Control | Notes |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| install harness | single button push; installer runs server-side through official tooling |
|
||||
| enable / disable | disabled harnesses are not selectable on any seat page |
|
||||
| available models | an **allowlist** a seat may select from — not a selection. Whether `enabledModels` is role policy or harness/seat preference is open: [[GOV.5-open-questions]] Q-H1 |
|
||||
| reasoning level defaults | |
|
||||
| provider | which provider(s) back this harness ([[PROV.1-providers]]) |
|
||||
| linked auth accounts | which accounts may drive this harness ([[AUTHN.1-auth-accounts]]) |
|
||||
|
||||
Enable/disable and install are runtime state (Postgres-owned) projected into
|
||||
whatever flat state the launcher needs ([[DATA.1-record-authority]]).
|
||||
|
||||
## Runtime adapter contract (pulled 2026-08-31 from adapter-contract draft)
|
||||
|
||||
Every harness adapter binds a required capability set or **fails closed**:
|
||||
repository ops via wrapper capability, scoped file/command execution,
|
||||
structured reasoning, shared-memory capture/search/recall, inter-seat
|
||||
messaging/wake, checkpoint persistence + mechanical telemetry, a `mosaic coord`
|
||||
client that cannot mutate Kanban state or deploy seats directly, and credential
|
||||
resolution through the seat's own slot. Rules:
|
||||
|
||||
- An unavailable capability is a **named blocker**, never silent degradation.
|
||||
**"Prompt adherence is not an enforcement mechanism"** — a harness that
|
||||
cannot persist checkpoints, emit telemetry, or honor fencing does not run
|
||||
workflows that need them.
|
||||
- Each adapter publishes a capability→binding table (capability, binding
|
||||
surface, config source, verification check) and proves its bindings at
|
||||
session start; verification failure is a named blocker.
|
||||
- Adapters bind capabilities but **never redefine role authority, delivery
|
||||
policy, gate outcomes, or review independence** — a harness whose native
|
||||
workflow conflicts with shared policy keeps the shared policy and records the
|
||||
conflict as an adapter limitation.
|
||||
|
||||
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
|
||||
|
||||
**Canonical ground truth**: the `fleet/` book — `concepts/desired-vs-observed-state.md`
|
||||
(roster-v2 sole writable authority), `concepts/generated-env-launch-chain.md` +
|
||||
`reference/generated-env-boundary.md`, `reference/roster-v2-fields.md`,
|
||||
`operations/reconcile-and-recover.md` (lock/generation semantics),
|
||||
`NORTH_STAR.md`/`FLEET-DOCTRINE.md` (delivery-fleet north star, subordinate to
|
||||
this PRD per rev0 §10).
|
||||
**Pending pulls**: brain `docs/guides/proposed/runtime/adapter-contract.md`
|
||||
(the register-OD-38 runtime-adapter capability contract this section cites).
|
||||
|
||||
## enabledModels ruling (Q-D4, Jason 2026-09-01)
|
||||
|
||||
The Role Revision defines the allowed model set — a policy ceiling. The seat
|
||||
records model preferences within that set. Effective models = the
|
||||
intersection, consistent with the L2-D39 authority-intersection chain. A seat
|
||||
preference outside the role ceiling is refused, not silently clamped.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
id: PRD.0
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# PRD.0 — Index: naming standard, domain registry, reading order
|
||||
|
||||
This file is the order authority for the PRD section documents. Lexical sort of
|
||||
the directory is **not** authoritative; this index is.
|
||||
|
||||
## Naming standard (ratified 2026-08-31)
|
||||
|
||||
`<DOMAIN>.<n[.n[.n]]>-<kebab-slug>.md` — e.g. `AUTHN.1.1-oidc.md`
|
||||
|
||||
- **Domain code**: short uppercase code from the registry below. Codes are
|
||||
append-only; a code is never reused or renamed.
|
||||
- **Number**: hierarchical, dotted, **append-only at every level**. A new
|
||||
subtopic under `AUTHN.1` takes the next free number (`AUTHN.1.3`). Nothing
|
||||
ever renumbers; depth absorbs insertions. Added topics augment, never
|
||||
reshuffle.
|
||||
- **Slug**: kebab-case; names only what the number does not. The domain word is
|
||||
never repeated in the slug (`AUTHN.1.1-oidc.md`, not
|
||||
`AUTHN.1.1-authentication-oidc.md`).
|
||||
- **Separators**: dots between number levels only; one hyphen between number
|
||||
and slug; hyphens inside the slug. No underscores, no spaces.
|
||||
- **ID in three places** that must agree: filename, frontmatter `id:`, H1.
|
||||
Wikilinks use the basename, e.g. `[[AUTHN.1.1-oidc]]` (illustrative — no such section exists yet).
|
||||
- **Flat directory**: hierarchy lives in the number, not nested folders.
|
||||
|
||||
## Domain registry (append-only)
|
||||
|
||||
| Code | Domain |
|
||||
| ----- | ----------------------------------------------------------------------------------------------- |
|
||||
| PRD | The PRD assembly itself: index, preamble, revision log |
|
||||
| GOV | Governance: document lifecycle, decision registers, amendment process, ratification |
|
||||
| VIS | Vision / north star: what the Stack is, premises, non-goals |
|
||||
| AUTHZ | Authorization & enforcement: capabilities, role policy, mosaic-core, L2 contracts, gap register |
|
||||
| AUTHN | Authentication: provider accounts, OAuth/API keys, renewal, deactivation, allowed harnesses |
|
||||
| ROLE | Roles: manifests, role config surfaces, role/seat separation |
|
||||
| SEAT | Seats: profiles, launch config, seat config surfaces, profile.json consolidation |
|
||||
| HARN | Harnesses: install/enable, model availability, reasoning, linked auth |
|
||||
| PROV | Providers: supported providers, local providers (Ollama, LM Studio), provider config |
|
||||
| SESS | Sessions: Stack session identity, continuity, mid-stream harness/model/provider switching |
|
||||
| UI | WebUI: pages, page-scope rules, interaction patterns, audit surfaces |
|
||||
| CLI | mosaic CLI: command surface, CLI↔WebUI parity |
|
||||
| DATA | Record classes & storage: J1 git/DB authority split, flat-file vs DB, reconciliation |
|
||||
|
||||
New domains append below this line with a dated note.
|
||||
|
||||
## Reading order
|
||||
|
||||
Order is by lifecycle of understanding, not by code:
|
||||
|
||||
1. [[PRD.0-index]] (this file)
|
||||
2. [[GOV.1-prd-lifecycle]]
|
||||
3. [[GOV.2-docs-inventory]]
|
||||
4. [[GOV.3-decision-map]]
|
||||
5. [[VIS.1-north-star]]
|
||||
6. [[DATA.1-record-authority]]
|
||||
7. [[AUTHZ.1-capability-authority]]
|
||||
8. [[ROLE.1-role-governance]] → [[SEAT.1-seat-profile]] (separation is load-bearing; role before seat)
|
||||
9. [[HARN.1-harness-config]] → [[PROV.1-providers]] → [[AUTHN.1-auth-accounts]]
|
||||
10. [[SESS.1-session-continuity]]
|
||||
11. [[UI.1-webui-surfaces]] → [[CLI.1-parity]] (surfaces last; they project everything above)
|
||||
12. [[GOV.4-workstream-contracts]] (preserved contracts; bind after the model is understood)
|
||||
13. [[GOV.5-open-questions]] (the grill list; ratification gate)
|
||||
|
||||
Pulled sources (inputs, never ratified): [rev0 PRD](../2026-08-26_PRD_rev0/PRD.md),
|
||||
the operator DECISION-REGISTER (estate brain `docs/guides/proposed/DECISION-REGISTER.md`, snapshot 2026-08-28, sha256 `2cc81be1…aabec`; operator-only corpus, not shipped).
|
||||
|
||||
Sections are added to this list as they are authored; an unlisted file is a
|
||||
defect.
|
||||
|
||||
## Structural rulings (2026-08-31, Jason)
|
||||
|
||||
- The finished PRD is the **project SOT**. `docs/PRD.md` in the stack repo
|
||||
becomes a shim to the current dated revision. Missions reference the PRD,
|
||||
never usurp it. See [[GOV.1-prd-lifecycle]].
|
||||
- The entire PRD lives in the **stack repo** (mosaicstack/stack, trunk `next`).
|
||||
Brain documents are operator-instance documents that cite it.
|
||||
- This work is the **successor** to the 2026-08-26 "North Star" PRD on
|
||||
`origin/next` (snapshot: [rev0 PRD](../2026-08-26_PRD_rev0/PRD.md)).
|
||||
- Revisions **archive, never delete**. Each ratified revision is a frozen
|
||||
bundle directory — `docs/PRDs/YYYY-MM-DD_PRD_revN/` holding the PRD, this
|
||||
index, and every section doc as a set (layout: [[GOV.1-prd-lifecycle]]
|
||||
§Revision bundles).
|
||||
- Shim and revision immutability are **convention, not enforcement** (no hook
|
||||
or CI guard yet).
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
kind: spec
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
succeeds: origin/next:docs/PRD.md (2026-08-26 North Star, commit 9aa4983c, sha256 60cc2f98697471850caa3440d79139d70f67eda585a2ee465fdcd517bc36afdf)
|
||||
---
|
||||
|
||||
# PRD: Mosaic Stack — rev1
|
||||
|
||||
The project source of truth, successor to the 2026-08-26 North Star PRD
|
||||
(rev0). Ratified 2026-09-01; this bundle is frozen — the next revision is
|
||||
drafted in a lane and lands as a new bundle ([[GOV.1-prd-lifecycle]]). This file assembles the section documents in this bundle; the
|
||||
sections own the detail. Order authority and naming: [[PRD.0-index]].
|
||||
Lifecycle (shim, frozen revision bundles, archival): [[GOV.1-prd-lifecycle]].
|
||||
|
||||
The PRD is **mission-independent**: missions pin an accepted PRD version and
|
||||
reference it (register OD-16/OD-19); they never usurp it. rev0 ([rev0 PRD](../2026-08-26_PRD_rev0/PRD.md)) is archived verbatim beside this bundle;
|
||||
`docs/PRD.md` is the permanent shim pointing here.
|
||||
|
||||
## Metadata
|
||||
|
||||
- **Owner / decision authority:** Jason Woltje
|
||||
- **Status:** ratified 2026-09-01 (Jason Woltje). Drafted as a class-2 draft-native in lane `fleet/lanes/control-plane-surfaces` (estate brain); grill record in [[GOV.5-open-questions]]
|
||||
- **Base text:** rev0, pinned at `origin/next` commit `9aa4983c`
|
||||
- **Pulled sources (inputs, never ratified; not shipped in this bundle):** [rev0 PRD](../2026-08-26_PRD_rev0/PRD.md) and the operator DECISION-REGISTER (estate brain `docs/guides/proposed/DECISION-REGISTER.md`, snapshot 2026-08-28, sha256 `2cc81be1…aabec`; operator-only corpus, not shipped)
|
||||
|
||||
## Revision log
|
||||
|
||||
| Rev | Date | State | Notes |
|
||||
| ---- | ---------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| rev0 | 2026-08-26 | superseded 2026-09-01; archived verbatim as `docs/PRDs/2026-08-26_PRD_rev0/PRD.md` | North Star PRD (Part I product north star from D1–D14 + D15; Part II workstream contracts) |
|
||||
| rev1 | 2026-08-31 | **ratified 2026-09-01** (Jason; grill rounds 1–8 closed the GOV.5 frontier) | rev0 + control-plane surfaces (seats, roles, harnesses, providers, authentication, sessions, WebUI/CLI), consolidated decision map, authorization gap register, docs-estate consolidation |
|
||||
|
||||
## Mandate (2026-08-31 drafting directive)
|
||||
|
||||
1. Combine the official PRD (rev0) with the `control-plane-surfaces` and
|
||||
`agent-runtime-ng` lane findings.
|
||||
2. Ingest and reconcile the pertinent document corpus — stack `docs/` on
|
||||
`origin/next`, `~/.mosaic/docs`, `~/.mosaic/docs/guides/proposed`
|
||||
([[GOV.2-docs-inventory]] is the audit trail).
|
||||
3. Specify all functions of the site and the north star in one place, with
|
||||
full `mosaic` CLI ↔ WebUI parity.
|
||||
4. Remove the no-central-location ambiguity; clear up drift and naming issues;
|
||||
clarify ambiguous structural language.
|
||||
5. Walk open questions via ms-grill-me before ratification
|
||||
([[GOV.5-open-questions]]).
|
||||
|
||||
---
|
||||
|
||||
## Part I — Product north star
|
||||
|
||||
**[[VIS.1-north-star]]** — what Mosaic Stack is, who it is for, deployment
|
||||
modes, hierarchy and tenancy, identity, onboarding, data custody, the
|
||||
webUI-over-tooling architecture gate, the v1 slice, the fleet-north-star
|
||||
subordination, non-goals, and tiered containerized deployment. rev0 Part I
|
||||
preserved as base text with marked rev1 annotations.
|
||||
|
||||
## Part II — Platform model (control plane)
|
||||
|
||||
| Section | Owns |
|
||||
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [[DATA.1-record-authority]] | record-class authority (J1), the configuration data model, seat-file consolidation, reconciliation obligation |
|
||||
| [[AUTHZ.1-capability-authority]] | intersection authority model, `mosaic-core` enforcement, firewalls, privilege escapation, accepted risk, gap register G1–G7 |
|
||||
| [[ROLE.1-role-governance]] | role definitions/revisions, manifest invariants, role surface, seat/role separation rule |
|
||||
| [[SEAT.1-seat-profile]] | instance contract, seat surface, the separated role-binding control (G5), OD-02/OD-03 semantics |
|
||||
| [[HARN.1-harness-config]] | harness install/enable, model allowlists, adapter boundary (register OD-38) |
|
||||
| [[PROV.1-providers]] | hosted and local providers, named instances, activation |
|
||||
| [[AUTHN.1-auth-accounts]] | agent-side provider accounts, OAuth/API, custody rules, broker boundary |
|
||||
| [[SESS.1-session-continuity]] | Stack session id, incarnation layering, mid-stream switching via register OD-57–OD-61, the two-operations rule |
|
||||
|
||||
## Part III — Surfaces
|
||||
|
||||
| Section | Owns |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| [[UI.1-webui-surfaces]] | governing rules and the complete page/function inventory, including the authorization-audit page |
|
||||
| [[CLI.1-parity]] | CLI primacy, the one-engine rule (OD-53), the parity-matrix obligation, command families |
|
||||
|
||||
## Part IV — Governance
|
||||
|
||||
| Section | Owns |
|
||||
| ------------------------ | --------------------------------------------------------------------- |
|
||||
| [[GOV.1-prd-lifecycle]] | SOT rule, shim, frozen revision bundles, archival |
|
||||
| [[GOV.2-docs-inventory]] | corpus inventory, supersession verdicts, naming-defects register |
|
||||
| [[GOV.3-decision-map]] | every binding decision registry, collision rule, reconciliation notes |
|
||||
| [[GOV.5-open-questions]] | the grill list; ratification gate |
|
||||
|
||||
## Part V — Active workstream contracts (preserved unchanged)
|
||||
|
||||
**[[GOV.4-workstream-contracts]]** — rev0 Part II carried verbatim: #1194
|
||||
drift detection, Compaction Refresh Trust Lifecycle, Pi Persistent Goal Loop,
|
||||
FCM, cross-harness comms, KBN-101, TESS, channel plugins, MOS-PORT, workspace
|
||||
placement guard, Release Integrity, T78 CLI migration. Open issues bind to
|
||||
them; they graduate out individually as workstreams close.
|
||||
|
||||
---
|
||||
|
||||
## Ratification
|
||||
|
||||
Per [[GOV.1-prd-lifecycle]]: this bundle freezes into
|
||||
`mosaicstack/stack docs/PRDs/` (branch off `origin/next`), rev0 archives as a
|
||||
one-file bundle, `docs/PRD.md` becomes the generated pointer (register OD-18).
|
||||
Gate: [[GOV.5-open-questions]] empty or explicitly deferred; then reviewed PR
|
||||
per stack delivery gates.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: PROV.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# PROV.1 — Provider configuration
|
||||
|
||||
A provider is a model-inference source: hosted (Claude, OpenAI, ZAI, N others)
|
||||
or local (Ollama, LM Studio, other).
|
||||
|
||||
## Provider configuration surface (WebUI page + CLI)
|
||||
|
||||
| Control | Notes |
|
||||
| --------------------- | -------------------------------------------------------------------------------------- |
|
||||
| provider selection | dropdown of supported providers |
|
||||
| name | user-chosen instance name (multiple named instances of one provider type are expected) |
|
||||
| auth mode | OAuth or API key — the account itself lives in [[AUTHN.1-auth-accounts]] |
|
||||
| local provider setup | endpoint/port for Ollama, LM Studio, other local providers |
|
||||
| activate / deactivate | inactive providers are not selectable downstream |
|
||||
|
||||
Provider records are runtime state (Postgres-owned, projected). Credentials
|
||||
never enter provider records; they live with the credential broker
|
||||
([[AUTHN.1-auth-accounts]]).
|
||||
|
||||
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
|
||||
|
||||
**Canonical ground truth**: `DEVELOPER-GUIDE/architecture/decisions/mos-runtime-portability-m1.md`
|
||||
(the only current ADR for the logical-agent/connector-lease identity model);
|
||||
`ADMIN-GUIDE/operations/mos-connector-lease-operations.md` — connector
|
||||
activation is a **deliberate deny-all hold**; nothing in this section may imply
|
||||
it is live.
|
||||
**Drafts noted**: `rfcs/optional-ai-egress-gateways.md` (non-operative;
|
||||
separates `IProviderAdapter` from egress-gateway concerns).
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
id: ROLE.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# ROLE.1 — Role governance and configuration
|
||||
|
||||
## What a role is
|
||||
|
||||
A role is the reviewed, Git-owned capability ceiling for a class of seats:
|
||||
Role Definition → immutable, digested Role Revisions → the active revision
|
||||
projects `mosaic-core.manifest.json` and role-scoped settings. Authority table:
|
||||
[[DATA.1-record-authority]]. Enforcement: [[AUTHZ.1-capability-authority]].
|
||||
|
||||
## The separation rule (Jason, 2026-08-31 — closes gap G6)
|
||||
|
||||
- The **seat** configuration surface NEVER directly modifies role config.
|
||||
- The **role** configuration surface NEVER directly modifies seat config.
|
||||
- A seat page writes at most a per-seat **overlay**, never the role file.
|
||||
|
||||
The original `role-harness-config/DESIGN.md` sentence constrained a surface it
|
||||
never named — both readings were faithful, and reviewer context decided the
|
||||
meaning. The staged amendment names the surface explicitly.
|
||||
|
||||
## Role configuration surface (WebUI page + CLI)
|
||||
|
||||
| Control | Notes |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| manifest editing | capability grants against the C1–C8 (later open) registry; schema-validated before commit |
|
||||
| revision management | create revision, diff against active, activate, roll back — every revision immutable and digested |
|
||||
| role links | which seats bind this role (read-only here; binding happens on the seat surface — see [[SEAT.1-seat-profile]]) |
|
||||
| projection status | whether each bound seat's on-disk projection matches the active revision (`role check` class) |
|
||||
|
||||
All writes go through the one canonical role-management API (L2-D14) shared
|
||||
with the CLI — the WebUI holds no separate role logic. Role management is
|
||||
**principal-only** (L2-D13): no agent identity may ever invoke these
|
||||
operations, and the API enforces that, not the page.
|
||||
|
||||
## Manifest invariants (must survive any surface)
|
||||
|
||||
- Committed, non-symlink, trusted-path — `mosaic-core`'s loader refuses
|
||||
violations; no surface may "fix" that by writing a symlink.
|
||||
- Nothing env-overridable, nothing cwd-relative.
|
||||
- `tools[]` equals exactly the bound bindings of granted capabilities.
|
||||
- Role cross-checked against path at load.
|
||||
|
||||
## Specialization model (pulled 2026-08-31 from SPECIALIZATION-MODEL draft)
|
||||
|
||||
Four layers: **Role** (decision ownership and prohibited actions — few,
|
||||
stable) → **Seat** (durable identity performing the role) → **Specialization**
|
||||
(recurring domain/tools/behavior — open-ended, composable, never changes
|
||||
authority) → **Task** (current activity). Rules:
|
||||
|
||||
- A seat has **exactly one role at a time**; never activate a second role
|
||||
inside a session. If authority changes, `mosaic config` reconfigures the seat
|
||||
and the coordinator starts a **clean session** — seat identity, history, and
|
||||
authorship survive; the old lease is revoked and a new incarnation starts.
|
||||
(Independent confirmation of register OD-02/OD-03 and the
|
||||
[[SESS.1-session-continuity]] two-operations rule.)
|
||||
- Promotion to a new role only when decision ownership or prohibited actions
|
||||
materially differ; otherwise a formal specialization profile. Promotion
|
||||
triggers: different authority/external side effects, distinct
|
||||
credential/identity/data boundaries, added compliance controls, stable
|
||||
machine-readable I/O contract, required independence, deterministic gate
|
||||
behavior, repeated cross-seat use.
|
||||
- Ad-hoc task-scoped specialization is valid only inside existing authority,
|
||||
with no new credential/safety/independence boundary; it dies with the task
|
||||
unless intentionally promoted.
|
||||
- Anti-patterns: per-topic role explosion; role-subtype hierarchies no workflow
|
||||
consumes; model IDs or harness syntax inside specialization definitions;
|
||||
using specialization to bypass role authority or gates.
|
||||
|
||||
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
|
||||
|
||||
**Canonical ground truth**: `fleet/reference/role-classes.md`,
|
||||
`fleet/concepts/role-authority-and-leases.md`, `fleet/how-to/customize-roles.md`
|
||||
(baseline + `roles.local` resolver), `fleet/migration/legacy-class-aliases.md`,
|
||||
`ADMIN-GUIDE/security/discord-ingress.md` (viewer/operator/admin precedent).
|
||||
**Pending pulls**: brain `docs/guides/proposed/SPECIALIZATION-MODEL.md`
|
||||
(Role/Seat/Specialization/Task layering — the conceptual basis of this
|
||||
section's separation rule); `plans/2026-08-29-agent-enrollment-command-design.md`
|
||||
(enrollment authority composed across three contracts — fragility to fix or document).
|
||||
**Naming hazard**: "Tess"/"Ultron" are roster-class display aliases in fleet
|
||||
how-tos and named product identities elsewhere (defect N6) — qualify every use.
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
id: SEAT.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# SEAT.1 — Seat identity, profile, and configuration
|
||||
|
||||
## Instance contract (register OD-48)
|
||||
|
||||
| File | Carries |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `profile.json` | structured identity — and, post-consolidation, the full seat record ([[DATA.1-record-authority]] §consolidation) |
|
||||
| `overlay.json` | generated composition |
|
||||
| seat-local `AGENTS.md` | narrative specialization |
|
||||
| `SOUL.md` | persona |
|
||||
|
||||
## Seat configuration surface (WebUI page + CLI)
|
||||
|
||||
| Control | Notes |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------- |
|
||||
| harness | from **enabled** harnesses only ([[HARN.1-harness-config]]) |
|
||||
| model | constrained by the harness's available-models allowlist |
|
||||
| reasoning level | |
|
||||
| work dir | |
|
||||
| authentication account | from configured, active accounts allowed for that harness ([[AUTHN.1-auth-accounts]]) |
|
||||
| overlay | per-seat overlay only — never the role file (ROLE separation rule) |
|
||||
| role binding | **separated section — see below** |
|
||||
|
||||
## The role-binding control (gap G5)
|
||||
|
||||
Role Binding is the single highest-authority action in the system,
|
||||
principal-only under L2-D13. `model` is a preference. They must not share one
|
||||
undifferentiated form — a privilege grant must not inherit the ceremony of a
|
||||
dropdown. Requirements:
|
||||
|
||||
- Visually and structurally separate section on the page.
|
||||
- Distinct confirmation step; re-authentication of the principal is under
|
||||
consideration ([[GOV.5-open-questions]] Q-S2).
|
||||
- Register OD-02/OD-03 bind the semantics: a seat has exactly one role; an
|
||||
**active session never switches roles**. A role change reconfigures the
|
||||
existing seat, preserves identity and history, **discards ephemeral context,
|
||||
and starts a clean session**. The surface must say so before confirming.
|
||||
- Role-transition history is recorded: old role, new role, reason, authorizer,
|
||||
checkpoint, activation time (register OD-04).
|
||||
|
||||
Role changes are therefore a _different operation_ from harness/model/provider
|
||||
changes ([[SESS.1-session-continuity]]) and must not share a code path.
|
||||
|
||||
## Seat identity and credential rules (pulled 2026-08-31 from seat-identity draft)
|
||||
|
||||
- **One seat = one identity = one token slot.** A second copy of a token
|
||||
anywhere is drift and is removed without reading it.
|
||||
- Agents never mint their own tokens; provisioning, rotation, and scope changes
|
||||
are operator authority. Credential refusal is _correct behavior_ — the fix is
|
||||
the seat's identity, never another seat's or a shared credential.
|
||||
- Fail-closed everywhere: an empty/unreadable slot is a designed state reported
|
||||
at launch; the credential helper refuses, records, notifies — never falls
|
||||
back to a shared or owner credential.
|
||||
- Git identity resolution order: explicit environment identity → configured
|
||||
identity → git's own answer. Identity is named on every invocation and never
|
||||
persisted inside a shared clone/worktree config (silent attribution rewrite).
|
||||
Commit author must identify the seat that did the work.
|
||||
- Tokens are compared by digest, never by value; scopes are verified from the
|
||||
authority's own report, never transcription.
|
||||
|
||||
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
|
||||
|
||||
**Canonical ground truth**: `fleet/reference/agent-mutations.md`,
|
||||
`fleet/reference/lifecycle-transitions.md` (`enabled`/`desired_state` authority),
|
||||
`fleet/how-to/create-update-delete-agent.md`, `guides/fleet-local-canary.md`.
|
||||
**Pending pulls**: brain `docs/guides/proposed/operations/seat-identity.md`
|
||||
(credential-resolution mechanics under the OD-48 instance contract).
|
||||
|
||||
## Role-binding step-up ruling (Q-S2, Jason 2026-09-01)
|
||||
|
||||
Confirming a role-binding change requires fresh principal re-authentication no
|
||||
older than 10 minutes — the same step-up bar the S2 identity-lifecycle
|
||||
contract sets for account linking. An active session alone is insufficient;
|
||||
this closes the stolen-session → privilege-misdirection path through the seat
|
||||
surface.
|
||||
|
||||
## Seat record consolidation ruling (Q-D1, Jason 2026-09-01)
|
||||
|
||||
`launch.env` consolidates into `profile.json`: one seat record. Verified:
|
||||
`mosaic-core/lib/loader.ts seatRole()` reads only the `role` key from a
|
||||
generically-parsed record, so widened files are tolerated by construction.
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
id: SESS.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# SESS.1 — Session identity and mid-stream switching
|
||||
|
||||
## Requirement (Jason, 2026-08-31)
|
||||
|
||||
An agent session stays active on the system, tied to a **Stack session id**.
|
||||
Changing harness, model, or provider mid-stream preserves the session id and
|
||||
fully switches context from one provider/harness to another, with no user
|
||||
intervention and no noticeable performance degradation.
|
||||
|
||||
## Two operations, two code paths — never merged
|
||||
|
||||
| | Harness / model / provider switch | Role switch |
|
||||
| ----------- | --------------------------------- | ------------------------------------------------------- |
|
||||
| Session id | preserved | seat identity preserved; session is **clean** |
|
||||
| Context | fully transferred | **ephemeral context discarded** (register OD-03) |
|
||||
| Governed by | this section | [[SEAT.1-seat-profile]] §role-binding |
|
||||
| Why | continuity requirement | an active session never switches roles (register OD-02) |
|
||||
|
||||
## The ratified mechanism already exists: register OD-57–OD-61
|
||||
|
||||
The 2026-08-28 register confirms the machinery this requirement needs:
|
||||
|
||||
- **OD-57 checkpoints** — atomic, schema-valid, revisioned seat checkpoints tied
|
||||
to incarnation and lease; freshness enforced mechanically.
|
||||
- **OD-59 relaunch** — the coordinator requests and validates a checkpoint, stops
|
||||
the session, applies configuration, starts a **clean incarnation**, restores
|
||||
the assignment, verifies readiness.
|
||||
- **OD-60 fencing** — leases, epochs, incarnation IDs, fencing tokens prevent a
|
||||
stale session from mutating state after the switch.
|
||||
- **OD-61 restart recovery** — the relaunched seat restores role, mission, task,
|
||||
PRD pin, constraints, evidence, blockers, leases, dependencies, and next
|
||||
action **without prior conversation**.
|
||||
|
||||
A mid-stream harness switch is therefore an OD-59 relaunch keyed to a persistent
|
||||
Stack session id: checkpoint → stop → reconfigure → new incarnation →
|
||||
restore → resume. What OD-59 does not yet promise is the _experience_ bar — no
|
||||
user intervention, no noticeable degradation — which is this PRD's addition.
|
||||
|
||||
## Identity layering
|
||||
|
||||
`mosaic-core` mints a per-launch **incarnation id** and keys its journal on it,
|
||||
deliberately not on any session id. A harness switch is a new process → new
|
||||
incarnation → new journal, **while the Stack session id persists**. So:
|
||||
|
||||
```
|
||||
Stack session id (durable; user-facing continuity)
|
||||
└─ incarnation id (per launch; enforcement journal, fencing per OD-60)
|
||||
```
|
||||
|
||||
The precise contract between the two ids — minting, custody, what the
|
||||
coordinator records at each relaunch — must be specified before build:
|
||||
[[GOV.5-open-questions]] Q-S1.
|
||||
|
||||
## Open hard problem
|
||||
|
||||
Context-transfer fidelity between harnesses with different context formats,
|
||||
tool-call encodings, and system-prompt injection points. The checkpoint (OD-57)
|
||||
is the transfer vehicle; whether a checkpoint alone meets "no noticeable
|
||||
degradation" across harness families is unproven: [[GOV.5-open-questions]] Q-S3.
|
||||
|
||||
## Session lifecycle state machine (pulled 2026-08-31 from the session-lifecycle draft — with one required extension)
|
||||
|
||||
The operator draft (`workflows/session-lifecycle.md`, the densest
|
||||
decision-register consumer: OD-03/OD-04/OD-08, OD-56–OD-65) supplies the checkpoint/
|
||||
lease/fencing machinery this section's continuity requirement runs on:
|
||||
|
||||
- **States**: Active → Relaunch-requested (triggers per OD-59: context
|
||||
utilization, session age, milestone, drift, degraded health, role
|
||||
reconfiguration, authorized request) → Checkpointing (atomic, revisioned,
|
||||
bound to identity + incarnation + epoch + lease, OD-57) → Relaunching
|
||||
(validated checkpoint, old lease revoked → **new incarnation, new fencing
|
||||
token**, OD-59) → Restoring (readiness proof: role, task, PRD pin, blockers,
|
||||
next action, OD-61) → Active/Degraded. Role change routes through
|
||||
Reconfiguring first (old-role record, transition history, revoked lease,
|
||||
OD-03/OD-04).
|
||||
- **Fencing**: a stale session cannot mutate after its replacement holds the
|
||||
new token (OD-60); mutation authority is lease-gated and not renewed while the
|
||||
checkpoint is stale. Coordinator outage fails closed for new
|
||||
assignments/relaunches/renewals; existing leases run to expiry; read-only
|
||||
work continues (OD-63).
|
||||
- **Checkpoint contents** (required fields): role, config version, mission,
|
||||
outcome node, task, PRD pin, constraints, completed work with evidence refs,
|
||||
blockers and failed attempts, active leases/external ops, next action with
|
||||
required inputs. The checkpoint is an operational projection — mission truth
|
||||
stays in the ledger (OD-58). Telemetry is append-only and never the resumable
|
||||
checkpoint (OD-56).
|
||||
|
||||
**Structural gap found at extraction (must be fixed before this machine
|
||||
ratifies):** the draft models exactly **one** relaunch mechanism — every
|
||||
trigger, without exception, mints a new incarnation and fencing token. There is
|
||||
no continuity-preserving path at all, and harness/model/provider switching does
|
||||
not appear among the triggers. This PRD's two-operations rule (above) requires
|
||||
**two code paths**: the state machine must gain a switch path that preserves
|
||||
the Stack session id and full context per OD-57–OD-61 while still rotating the
|
||||
fencing token safely. Adopting the draft's table verbatim would silently
|
||||
collapse the two operations back into one — the exact defect register OD-02/OD-03
|
||||
vs the continuity requirement exists to prevent.
|
||||
|
||||
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
|
||||
|
||||
**Canonical ground truth**: `DEVELOPER-GUIDE/architecture/compaction-revocation.md`
|
||||
(the only current continuity/revocation lifecycle — observer/generation-fencing,
|
||||
test-consumed), `channel-protocol.md`.
|
||||
**Pending pulls**: brain `docs/guides/proposed/workflows/session-lifecycle.md`
|
||||
(checkpoint/relaunch/recovery/role-reconfig — complements this section's
|
||||
switching focus; its role-reconfig path must respect the OD-02/OD-03 clean-session rule).
|
||||
|
||||
## Two-path requirement ratified (Q-S4, Jason 2026-09-01)
|
||||
|
||||
The state-machine gap flagged above is now a binding requirement: the
|
||||
session-lifecycle draft may not land with a single relaunch path. Role change
|
||||
→ clean-session path (new incarnation + fencing token, context discarded,
|
||||
OD-02/OD-03). Harness/model/provider change → continuity path (same Stack session
|
||||
id, OD-57 checkpoint restored under OD-61, no noticeable degradation). The two
|
||||
paths must not share a code path.
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
id: UI.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# UI.1 — WebUI control-plane surfaces (all functions of the site)
|
||||
|
||||
The complete function inventory of the WebUI control plane. Every page obeys
|
||||
the governing rules; every control ultimately calls the same engine as the CLI.
|
||||
|
||||
## Governing rules
|
||||
|
||||
1. **Full CLI parity** — every aspect of the `mosaic` CLI surfaces in the WebUI
|
||||
([[CLI.1-parity]] carries the matrix obligation).
|
||||
2. **One canonical API** (L2-D14; register OD-53) — CLI, TUI, WebUI, API, and
|
||||
automation share one CLI-backed schema, resolver, planner, authorization,
|
||||
transaction, validation, and audit engine. The WebUI holds no separate
|
||||
logic.
|
||||
3. **The webUI sits OVER official tooling** (D8/D12 hard rule) — no page ever
|
||||
reaches the database or filesystem around the tooling; a missing tool means
|
||||
the gap is "blocked on tooling" and the tool is built first.
|
||||
4. **Strict surface separation** — seat pages never modify role config; role
|
||||
pages never modify seat config ([[ROLE.1-role-governance]]).
|
||||
5. **No direct settings-file authorship** — settings are generated projections
|
||||
(L2-D19; [[DATA.1-record-authority]]).
|
||||
6. **Agents can never reach these surfaces** (L2-D13; the API refuses agent
|
||||
identity — the enforcement is not the page's absence).
|
||||
7. **WebUI drafts** (register OD-54) — draft configuration is revisioned
|
||||
server-side desired-state; drafts have no effect until planned and applied.
|
||||
|
||||
## Interaction conventions
|
||||
|
||||
Logically separated pages; dropdowns, activate/deactivate buttons, drag-drop
|
||||
actions performed on-page.
|
||||
|
||||
## Page inventory
|
||||
|
||||
| Page | Section doc | Functions |
|
||||
| ---------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| Seat configuration | [[SEAT.1-seat-profile]] | harness, model, reasoning, workdir, auth account, overlay; separated role-binding section |
|
||||
| Role configuration | [[ROLE.1-role-governance]] | manifest editing, revision create/diff/activate/rollback, role links, projection status |
|
||||
| Harness configuration | [[HARN.1-harness-config]] | install (button), enable/disable, available-models allowlist, reasoning defaults, provider link, linked accounts |
|
||||
| Provider configuration | [[PROV.1-providers]] | provider dropdown, named instances, OAuth/API mode, local providers, activate/deactivate |
|
||||
| Authentication | [[AUTHN.1-auth-accounts]] | in-browser OAuth establishment, account list, force renew, deactivate, allowed harnesses |
|
||||
| Authorization audit | below | effective grants, escapation potential, drift |
|
||||
|
||||
## Page: Authorization audit (closes gap G3)
|
||||
|
||||
Surfaces, per seat, to the user:
|
||||
|
||||
- **Effective capability grant** — the live intersection
|
||||
(role ∩ assignment ∩ lease ∩ workflow ∩ target policy ∩ backend).
|
||||
- **Misdirection potential** — which seats hold capabilities that would let
|
||||
another seat's work be routed around its own role lane.
|
||||
- **Escalation potential** — any path that would add capability. Should be
|
||||
provably empty; the audit's job is proving it _stays_ empty.
|
||||
- **Drift** — seats whose on-disk projection diverges from their active role
|
||||
revision (`role check` class).
|
||||
- **Failure/blocked surfacing** (register OD-64) — the canonical alert stream's
|
||||
WebUI adapter.
|
||||
|
||||
Implementation choice (dedicated auditor agent vs mechanical tooling) and the
|
||||
auditor-identity problem are on the grill: [[GOV.5-open-questions]] Q-A1/Q-A2.
|
||||
|
||||
## Cross-cutting requirement
|
||||
|
||||
Every change made through these pages — or the CLI — automatically reconciles
|
||||
authentication, `settings.json`, and required symlinks
|
||||
([[DATA.1-record-authority]] §reconciliation; removal-fast / addition-attested
|
||||
per L2-D17). The user never touches a file.
|
||||
**Measured 2026-08-31** ([[CLI.1-parity]] Artifacts 2–3): the shipped WebUI
|
||||
already contains two D12 violations — the admin role/ban toggles and the stored
|
||||
harness/provider/model selection mutate state with no backing CLI command.
|
||||
Remediation, not precedent. The server-side hierarchy/grants CRUD surface
|
||||
(`hierarchy.controller.ts`) is the natural backing for the authorization audit
|
||||
page below, but needs a CLI face and an audit read-path first.
|
||||
|
||||
## E2 inputs (triage 2026-08-31; see [[GOV.2-docs-inventory]])
|
||||
|
||||
**Canonical ground truth**: `USER-GUIDE/product/web-dashboard.md` (route-by-route
|
||||
current state, incl. explicit gaps — no New Project/Task UI),
|
||||
`webui/PHASE-P-STRUCTURE.md` (Next→Vite SPA migration).
|
||||
**Pending pulls**: DRAFT S2 `onboarding-wizard.md` (D4/D11/D8),
|
||||
`tool-gateway-mapping.md` (the D8/D12 gate made concrete), `api-artifacts.md`.
|
||||
|
||||
## S2 contract feed (extraction 2026-08-31)
|
||||
|
||||
Full extraction record: lane `S2-EXTRACTION-2026-08-31.md` (per-contract cores, dependency edges, ruling cross-checks). Pulls binding on this section:
|
||||
|
||||
- **Contract 5 verbatim-affirms the parity rule**: "The webUI is a Gateway
|
||||
client only"; "No webUI-only command exists; a Gateway command without CLI
|
||||
exposure is a conformance gap." "Blocked on tooling" closure is mandatory;
|
||||
UI workarounds (direct DB/filesystem, legacy endpoints, domain logic in the
|
||||
web app) are non-conformant. This is the ratifiable D8/D12 text this
|
||||
section's violation findings measure against.
|
||||
- **Legacy non-substitutes** barred from backing any P1 surface, frozen for
|
||||
new consumers: `/api/projects`, `/api/tasks` CRUD, `POST /api/workspaces`,
|
||||
`/api/teams` reads, `POST /api/bootstrap/setup`, MCP `brain_*` mutations.
|
||||
- **Onboarding wizard (contract 3)** is the reference pattern for every config
|
||||
page this section specifies: pure client-side composition of Gateway
|
||||
commands, exactly one disclosed server-side composed transaction (bootstrap
|
||||
finalize), wizard state always derived from canonical state — never a
|
||||
persisted answer file that can drift.
|
||||
- **Company visibility** (`private` default vs `directory`) is a UI-facing
|
||||
disclosure control with a bounded existence-only carve-out.
|
||||
- P1 build rank order (T10): hierarchy → hierarchy RBAC → typed kanban →
|
||||
agent enrollment → authorized roll-up → onboarding orchestration.
|
||||
@@ -0,0 +1,214 @@
|
||||
---
|
||||
id: VIS.1
|
||||
status: ratified
|
||||
ratified: 2026-09-01 (Jason Woltje; PRD rev1 ratification PR)
|
||||
---
|
||||
|
||||
# VIS.1 — Product north star
|
||||
|
||||
Successor text to rev0 Part I ([rev0 PRD](../2026-08-26_PRD_rev0/PRD.md) lines
|
||||
33–215, preserved there verbatim). Base text unchanged except marked **rev1**
|
||||
annotations; the decision registry moves to [[GOV.3-decision-map]].
|
||||
|
||||
### 1. What Mosaic Stack is (D1)
|
||||
|
||||
Mosaic Stack is an **open-source, AI-first platform for people who want a
|
||||
self-hosted environment for agentic management and a life operating system.**
|
||||
It serves personal, business, and employee needs from one deployment, and the
|
||||
work is offered freely.
|
||||
|
||||
"AI-first" means agents are first-class operators of the system, not a bolted-on
|
||||
chat box: the platform exists to let humans direct fleets of agents over their
|
||||
projects, tasks, communications, and infrastructure, with the same tools and
|
||||
the same guarantees whether a human or an agent is acting.
|
||||
|
||||
### 2. Who it is for (D1, D9)
|
||||
|
||||
The operator of a deployment is its user. Mosaic Stack is **not a hosted
|
||||
business**: running the system as a service for external customers is outside
|
||||
the north star. Multi-tenancy exists WITHIN a deployment so that one operator
|
||||
can separate their world — for example, several LLCs plus a personal domain —
|
||||
while every deployment is self-hosted by its own operator.
|
||||
|
||||
"Company" in the hierarchy is organizational separation for one operator's
|
||||
world, not a customer account.
|
||||
|
||||
### 3. Deployment modes (D3)
|
||||
|
||||
Two modes, chosen at install time:
|
||||
|
||||
| | Standalone / personal | Enterprise |
|
||||
| ------------------- | -------------------------------------- | ----------------------------------------------------- |
|
||||
| Brains | one mosaic-brain (system + user files) | system brain for config + one brain per user |
|
||||
| User-data isolation | single user | no user-data leakage between users; sharing is opt-in |
|
||||
| Secrets | OpenBao/Vault or flat files | OpenBao/Vault REQUIRED |
|
||||
| Conversion | Standalone → Enterprise, **one-way** | terminal state |
|
||||
|
||||
Brains are configurable as external git repositories (recommended, not
|
||||
required); git tracking is always on locally.
|
||||
|
||||
**Federation** (connecting deployments: system-level config, assigned users,
|
||||
rights and data-access control, trusts with boundaries, exfiltration
|
||||
monitoring) is intentionally not fully designed. It is deferred, appears on the
|
||||
roadmap as a placeholder phase per D11, and nothing in v1 may foreclose it.
|
||||
_D3 as amended 2026-09-01 (Q-T1 ruling B):_ federation milestones M1–M3
|
||||
(Step-CA, enrollment, grants, mTLS auth guard, ScopeService, list/get/
|
||||
capabilities verbs) are **shipped but frozen** — present in code behind the
|
||||
`tier === 'federated'` gate, dormant since 2026-06-25, absent from the canonical
|
||||
compose topology (D15), excluded from the v1 bar, tracked as a dormant
|
||||
workstream in `docs/fleet/NORTH_STAR.yaml`, and gated on a security re-audit
|
||||
before any resumption. See [[GOV.5-open-questions]] Q-T1.
|
||||
|
||||
### 4. Structure and tenancy (D2, D9, D13)
|
||||
|
||||
The hierarchy:
|
||||
|
||||
```
|
||||
company/organization (N per deployment)
|
||||
└─ estate (each in exactly one company)
|
||||
└─ project (each in exactly one estate)
|
||||
└─ workspace (project-specific; carries the Kanban)
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Users can create N companies, N estates, N projects.
|
||||
- Tasks bubble UP the hierarchy so whole-system status is visible at every
|
||||
level. Bubble-up is **read-only aggregation**, never a cross-workspace write.
|
||||
- Granular RBAC: admins restrict access per company, estate, and project;
|
||||
grants are evaluated down the chain. Assets are transferable subject to the
|
||||
structure.
|
||||
- **`workspace_id` remains the hard mechanical isolation unit** exactly as
|
||||
ratified in
|
||||
[docs/requirements/native-kanban-sot.md](../../requirements/native-kanban-sot.md)
|
||||
(#751): PostgreSQL sole writable SOT, cross-workspace relationships rejected,
|
||||
fail-closed mutations. The hierarchy is parent structure ABOVE workspaces,
|
||||
used for RBAC evaluation and read-only roll-ups. The kanban SOT carries this
|
||||
as Amendment A1, added by reviewed PR — an amendment, not a rewrite (D13).
|
||||
|
||||
### 5. Identity (D10)
|
||||
|
||||
Built-in auth (better-auth) is the **account system of record**. Authentik and
|
||||
other external IdPs federate in via OIDC as login methods; they never become
|
||||
the system of record. Perimeter shims (forward-auth in front of a web host) are
|
||||
deployment workarounds, not the design.
|
||||
|
||||
### 6. Onboarding (D4)
|
||||
|
||||
Onboarding is a **wizard that differs by mode, is re-runnable (no lock-in), and
|
||||
is extensible** — new wizards attach as tabs.
|
||||
|
||||
Standalone flow captures: system and company name; component choices (Mosaic
|
||||
Comms/Matrix vs external; Mosaic SSO/Authentik vs external; Mosaic
|
||||
DB/PostgreSQL vs external; vector DB); the initial user
|
||||
(email/password/name/SSO); comms setup (Matrix/Discord/Slack); agent enrollment
|
||||
(harness choice and install, OAuth or API-key login, multi-account, model
|
||||
choice with recommendation, agent name and persona, account assignment,
|
||||
optional comms auto-enroll); a user onboarding profile (disabilities including
|
||||
ADHD/autism/PDA/vision, professional background, education, desired agent
|
||||
communication style, optional voice-matching interview, family/pets/friends/
|
||||
hobbies/likes-dislikes); email and drive connectors (Gmail/IMAP, Google
|
||||
Drive/OneDrive/Dropbox) with granular agentic-access consent; SSO/OIDC
|
||||
configuration; an initial estate, an initial project, and seeded example data.
|
||||
|
||||
Enterprise uses the same skeleton with personal data optional; the focus moves
|
||||
to business structure, org chart, RBAC, M365 and external systems, immediate
|
||||
OIDC, SSO prominent.
|
||||
|
||||
Profile answers feed `USER.md` and/or the user's data store subject to the
|
||||
custody rule in §7.
|
||||
|
||||
### 7. Data custody (D6, D14)
|
||||
|
||||
- **Sensitive profile categories** (disabilities, family, communication style,
|
||||
and similar) live in the **user's own brain ONLY**. PostgreSQL holds
|
||||
structural data, consent records, and pointers — never the content. "User
|
||||
data does not leak" is enforced by architecture, not policy (D14).
|
||||
- Standalone (one user, one brain) **may** keep the same split — D14 makes it
|
||||
optional in Standalone, not required. Keeping it is the recommended default
|
||||
because it preserves forward-compatibility with the one-way Enterprise
|
||||
conversion (D3).
|
||||
- Estate brains hold operational records. Only product-relevant material
|
||||
migrates into this repository's docs; operational records stay in their
|
||||
brains and are linked (D6).
|
||||
|
||||
### 8. Architecture gate — the webUI sits OVER official tooling (D8, D12)
|
||||
|
||||
**HARD RULE:** every webUI operation goes through the Gateway API backed by the
|
||||
same official framework tooling the CLI uses. The CLI remains the primary
|
||||
execution method; the webUI uses the tools to operate and configure the
|
||||
system. The webUI never bypasses tooling to reach the database or filesystem
|
||||
directly.
|
||||
|
||||
Consequence for planning: when a desired webUI operation has no backing tool,
|
||||
the gap is scored **"blocked on tooling"** and the tool is built first. The
|
||||
product baseline therefore always includes all three D8 inputs: the tool
|
||||
inventory (what exists and what is missing), the webUI→tool mapping, and the
|
||||
measured current state of the `next` branch.
|
||||
|
||||
### 9. v1 slice (D11)
|
||||
|
||||
v1 is deliberately small:
|
||||
|
||||
1. **Standalone onboarding wizard** — system/company name, component choices,
|
||||
initial user, initial estate + project, seeded examples, re-runnable.
|
||||
2. **Hierarchy core** — company → estate → project → workspace → kanban, with
|
||||
read-only task bubble-up.
|
||||
3. **Basic RBAC** on the hierarchy.
|
||||
4. **Minimal agent enrollment** — one harness, API key, name/persona.
|
||||
|
||||
Deferred beyond v1: connectors, comms integrations, voice-matching, M365,
|
||||
Enterprise conversion, federation. Every deferred item appears in
|
||||
[docs/ROADMAP.md](../../ROADMAP.md) per the D11 rule: nothing exists only in heads.
|
||||
|
||||
### 10. Relationship to the fleet north star
|
||||
|
||||
[docs/fleet/NORTH_STAR.md](../../fleet/NORTH_STAR.md) (generated from
|
||||
`docs/fleet/NORTH_STAR.yaml`) is the **delivery-fleet** north star: how the
|
||||
agent fleet that builds and operates the system should run (NS-1..NS-10,
|
||||
workstreams A–L). This PRD is the **product** north star. They are not
|
||||
competitors: the fleet north star is subordinate product-wise — its workstream
|
||||
J ("Web control plane") is one consumer of this PRD's D8/D12 gate — and this
|
||||
PRD does not redefine fleet invariants. The subordination rule is ratified in
|
||||
the frozen audit-input baseline (T2 operator freeze, 2026-08-25: "the PRD must
|
||||
cite and subordinate it, never fork it"). A change that would put the two in
|
||||
conflict must amend one of them explicitly, never fork a third document
|
||||
(drafting addition — see §12.1).
|
||||
|
||||
### 11. Explicit non-goals
|
||||
|
||||
- Hosted/SaaS operation for external customers (D9).
|
||||
- A webUI that writes to the database or filesystem around the tooling (D12).
|
||||
- A second writable task store beside PostgreSQL (native-kanban-sot invariants).
|
||||
- Fully-designed federation in v1 (D3 — roadmap placeholder only; the shipped M1–M3 code is frozen, not a v1 feature).
|
||||
|
||||
### D15 — Tiered containerized deployment (2026-08-30, containerization lane)
|
||||
|
||||
The stack ships a tiered deployment target, additive to the architecture
|
||||
gate (D8): (1) Standalone tier — docker compose is the canonical
|
||||
single-host deployment: postgres, valkey, openbao, gateway, appservice
|
||||
and the served webUI in one composition, with migrations, health checks,
|
||||
and a documented install/upgrade path; the registry (CI-published
|
||||
images) is the only deployment source. (2) Enterprise tier — Kubernetes
|
||||
manifests for the same service set, phase-gated on the standalone tier
|
||||
holding its acceptance bar. The v1 acceptance bar for the standalone
|
||||
tier: compose-up healthy; webUI hosts agent chat; an in-stack agent can
|
||||
open a PR to this repo; CI validates it; the running deployment adopts
|
||||
the merged change (pull + restart). Federation (D3 clause) remains
|
||||
deferred and unforeclosed. Implementation plan:
|
||||
docs/plans/2026-08-30_containerization.md.
|
||||
|
||||
---
|
||||
|
||||
## rev1 annotations (2026-08-31)
|
||||
|
||||
- §8's architecture gate (webUI over official tooling, CLI primary) is
|
||||
elaborated for the control plane by [[UI.1-webui-surfaces]] and
|
||||
[[CLI.1-parity]]; register decision OD-53 confirms all interfaces share one
|
||||
CLI-backed engine.
|
||||
- §4's RBAC and §5's identity are joined by the **agent-side** authority model
|
||||
in [[AUTHZ.1-capability-authority]]: role capability ceilings enforced at the
|
||||
harness by `mosaic-core`, composed by pure intersection.
|
||||
- The fleet north star subordination (§10) gains a control-plane consequence:
|
||||
the WebUI workstream consumes this PRD's surface specifications
|
||||
([[UI.1-webui-surfaces]]) rather than defining its own.
|
||||
+1
-1
@@ -59,7 +59,7 @@ Active workstream is **W1 — Federation v1**. Workers should:
|
||||
|
||||
## Fleet configuration management (#758) — M0–M5 implementation DAG
|
||||
|
||||
> **PRD:** [Fleet declarative configuration management](./PRD.md#fleet-declarative-configuration-management-workstream-fcm-758) · **M0 acceptance:** [docs IA checklist](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) · **baseline dispositions:** [legacy example/profile inventory](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md)
|
||||
> **PRD:** [Fleet declarative configuration management](./PRDs/2026-08-31_PRD_rev1/GOV.4-workstream-contracts.md#fleet-declarative-configuration-management-workstream-fcm-758) · **M0 acceptance:** [docs IA checklist](./fleet/FLEET-CONFIG-DOCS-IA-CHECKLIST.md) · **baseline dispositions:** [legacy example/profile inventory](./fleet/LEGACY-EXAMPLE-PROFILE-DISPOSITION-INVENTORY.md)
|
||||
>
|
||||
> Every row below is one independently reviewable card and **one PR**. `depends_on` is a
|
||||
> hard DAG edge; no card may silently absorb another card's scope. All source cards require
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
kind: tracking
|
||||
status: active
|
||||
status: superseded
|
||||
---
|
||||
|
||||
> **Superseded (2026-09-01, PRD rev1 ratification).** This document's framing of Federation v1 as an active, in-progress mission (M3) is historical. Federation M1–M3 are shipped but **frozen** (dormant since 2026-06-25, excluded from the v1 bar, security re-audit gate before any resumption); the canonical v1 deployment topology is the compose standalone tier (PRD rev1, D15). Authority: `docs/PRD.md` → `docs/PRDs/2026-08-31_PRD_rev1/` (decision D3 as amended, GOV.5 Q-T1). Tracking: `docs/fleet/NORTH_STAR.yaml` (dormant federation workstream). Content below is preserved verbatim as a record — do not edit it.
|
||||
|
||||
# Mission Manifest — Federation v1
|
||||
|
||||
> Persistent document tracking full mission scope, status, and session history.
|
||||
|
||||
+15
-14
@@ -39,20 +39,21 @@ The Mosaic Backlog is the backlog of record + dispatch engine, built on Mosaic's
|
||||
|
||||
## Workstreams
|
||||
|
||||
| id | title |
|
||||
| --- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| A | Substrate — Mosaic Backlog on native Postgres storage service |
|
||||
| B | Supervisor — movement guarantee, two-agent floor, dispatch/claim |
|
||||
| C | Planner — goal decomposition into independently-shippable cards |
|
||||
| D | Merge-gate — single approver, pr-merge.sh after CI wait |
|
||||
| E | Meta-loop — session-review + enhancer improvement PRs |
|
||||
| F | Safety-rails — TTL claims, advisory spend, PAUSE kill-switch |
|
||||
| G | Kill-switch — operator PAUSE honored before dispatch and merge |
|
||||
| H | Personas & system profiles — cross-domain library, system-type provisioning, update-surviving customization |
|
||||
| I | Operator surface — launcher, fleet visibility, reliable steering (tier 0) |
|
||||
| J | Web control plane — browser surface over the gateway (tier 1) |
|
||||
| K | Clients — desktop and mobile over the same backend (tier 2) |
|
||||
| L | Auth profiles — per-provider accounts, per-session selection (tier 2) |
|
||||
| id | title |
|
||||
| --- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| A | Substrate — Mosaic Backlog on native Postgres storage service |
|
||||
| B | Supervisor — movement guarantee, two-agent floor, dispatch/claim |
|
||||
| C | Planner — goal decomposition into independently-shippable cards |
|
||||
| D | Merge-gate — single approver, pr-merge.sh after CI wait |
|
||||
| E | Meta-loop — session-review + enhancer improvement PRs |
|
||||
| F | Safety-rails — TTL claims, advisory spend, PAUSE kill-switch |
|
||||
| G | Kill-switch — operator PAUSE honored before dispatch and merge |
|
||||
| H | Personas & system profiles — cross-domain library, system-type provisioning, update-surviving customization |
|
||||
| I | Operator surface — launcher, fleet visibility, reliable steering (tier 0) |
|
||||
| J | Web control plane — browser surface over the gateway (tier 1) |
|
||||
| K | Clients — desktop and mobile over the same backend (tier 2) |
|
||||
| L | Auth profiles — per-provider accounts, per-session selection (tier 2) |
|
||||
| M | Federation — DORMANT; M1–M3 shipped and frozen (PRD rev1 D3 as amended; security re-audit gate before resumption) |
|
||||
|
||||
## Goals (backlog projection)
|
||||
|
||||
|
||||
@@ -145,8 +145,15 @@ workstreams:
|
||||
title: Clients — desktop and mobile over the same backend (tier 2)
|
||||
- id: L
|
||||
title: Auth profiles — per-provider accounts, per-session selection (tier 2)
|
||||
# M is DORMANT by ruling (PRD rev1, D3 as amended 2026-09-01, GOV.5 Q-T1
|
||||
# ruling B). Federation M1–M3 exist in code behind `tier === 'federated'`
|
||||
# (M3 landed 2026-06-24/25), are excluded from the v1 bar and frozen. It
|
||||
# projects no goals on purpose: none may be added before a security
|
||||
# re-audit of the frozen cert/auth code and a federation PRD revision.
|
||||
- id: M
|
||||
title: Federation — DORMANT; M1–M3 shipped and frozen (PRD rev1 D3 as amended; security re-audit gate before resumption)
|
||||
|
||||
# NOTE: workstreams C, D, E and F are declared but currently project no goals.
|
||||
# NOTE: workstreams C, D, E, F and M are declared but currently project no goals.
|
||||
# That is planning debt, not an editing error: their goals have not been written
|
||||
# yet. The A5 validator below reports it rather than letting it stay invisible.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Fleet Configuration Management
|
||||
|
||||
This book documents the local roster-v2 desired-state control plane delivered under issue #758. The normative requirements are the [FCM section of the repository PRD](../PRD.md#fleet-declarative-configuration-management-workstream-fcm-758), not the older fleet-suite or observability planning pages.
|
||||
This book documents the local roster-v2 desired-state control plane delivered under issue #758. The normative requirements are the [FCM section of the repository PRD](../PRDs/2026-08-31_PRD_rev1/GOV.4-workstream-contracts.md#fleet-declarative-configuration-management-workstream-fcm-758), not the older fleet-suite or observability planning pages.
|
||||
|
||||
## Authority boundary
|
||||
|
||||
|
||||
@@ -5,12 +5,24 @@ status: active
|
||||
|
||||
# Deployment Guide
|
||||
|
||||
> **Status: non-operative for PostgreSQL, federated, and bare-metal production.** The checked-in
|
||||
> **Status: non-operative for PostgreSQL, federated (federation is frozen — PRD rev1 D3 as
|
||||
> amended; not a v1 route), and bare-metal production.** The checked-in
|
||||
> Compose PostgreSQL service mounts legacy initialization SQL and the KBN-101 bootstrap, runner,
|
||||
> secret-renderer, and process-exec interfaces do not exist yet. This page does not authorize a
|
||||
> production deployment, database initialization, manual DDL, secret provisioning, or service
|
||||
> activation.
|
||||
|
||||
## Relationship to the PRD (D15)
|
||||
|
||||
Per PRD rev1 Decision D15 (`docs/PRD.md`), the compose standalone tier — `docker compose up` — is
|
||||
the canonical v1 deployment topology; this guide describes the interim path to that bar, not a
|
||||
competing one. The KBN-101 holds documented below (bootstrap, runner, secret-renderer, process-exec)
|
||||
are operational gates on the road to the standalone-tier bar, not an alternative or federated
|
||||
topology. They remain fully binding: nothing in this guide authorizes PostgreSQL, federated, or
|
||||
bare-metal production activation until the named KBN-101-00/03/05 artifacts land, pass review, and
|
||||
satisfy the order specified below. Federation M1–M3 references elsewhere in this guide are
|
||||
historical/frozen (PRD rev1 D3 as amended) and do not describe a live or v1-bound route.
|
||||
|
||||
## Current safe local route
|
||||
|
||||
Use PGlite only for current in-process data-layer work; it requires no PostgreSQL. A Gateway/Web
|
||||
@@ -22,12 +34,14 @@ docker compose up -d valkey
|
||||
```
|
||||
|
||||
This command intentionally does not start PostgreSQL. Do not run a broad Compose start, use its
|
||||
PostgreSQL initialization mount, infer that current Compose is a production/federated route, or
|
||||
PostgreSQL initialization mount, infer that current Compose is a production/federated (federation
|
||||
is frozen — PRD rev1 D3 as amended; not a v1 route) route, or
|
||||
start Gateway/Web until KBN-101-02 supplies fail-closed local-tier/DSN isolation.
|
||||
|
||||
## Held future procedure
|
||||
|
||||
PostgreSQL local, federated, Compose, and bare-metal production activation are held until these
|
||||
PostgreSQL local, federated (federation is frozen — PRD rev1 D3 as amended; not a v1 route),
|
||||
Compose, and bare-metal production activation are held until these
|
||||
artifacts land and pass their independent gates:
|
||||
|
||||
1. **KBN-101-00** external privileged bootstrap artifact;
|
||||
@@ -69,4 +83,5 @@ For local PGlite development, diagnose application behavior without introducing
|
||||
connection.
|
||||
|
||||
Non-database local services may be inspected with their ordinary local health/log tools. Those
|
||||
checks do not certify PostgreSQL, federated deployment, or production readiness.
|
||||
checks do not certify PostgreSQL, federated (federation is frozen — PRD rev1 D3 as amended; not a
|
||||
v1 route) deployment, or production readiness.
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Plan — Stack Containerization (tiered deployment)
|
||||
|
||||
Status: DRAFT for review. Charter: fleet/lanes/stack-containerization
|
||||
(brain) NORTH-STAR.md; PRD amendment in the same PR adds D15.
|
||||
Supersedes nothing; sequences the absorbed M4 remainder per its lane.
|
||||
|
||||
## Measured baseline (origin/next @ 143ba0f5, 2026-08-30)
|
||||
|
||||
- `docker-compose.yml`: dev infrastructure only — postgres (pgvector),
|
||||
valkey, otel-collector, jaeger. No application services.
|
||||
- `docker-compose.federated.yml`: standalone overlay for the FEDERATED
|
||||
storage tier (own postgres/valkey; port-conflicts the base stack by
|
||||
design). Not an app deployment.
|
||||
- `docker/gateway.Dockerfile`, `docker/appservice.Dockerfile`:
|
||||
multi-stage production builds (node:22-alpine) EXIST; the gateway image
|
||||
includes the web SPA bundle (#1444).
|
||||
- CI (`publish.yml`) builds and publishes these images (next-channel
|
||||
prereleases + main stable), and runs `verify:release` fail-closed.
|
||||
- Gap: no stack-level composition wires gateway+appservice+data plane
|
||||
into one deployable unit; no blessed install/upgrade path; no
|
||||
in-container agent-runtime story for the dogfood loop.
|
||||
|
||||
## Target (PRD D15 amendment)
|
||||
|
||||
Tiered deployment, additive to the existing architecture:
|
||||
|
||||
1. **Standalone tier (v1 bar)**: `docker compose up` on one host brings
|
||||
postgres, valkey, openbao, gateway, appservice (and the webUI the
|
||||
gateway serves) to healthy; migrations apply; the webUI hosts agent
|
||||
chat; an in-stack agent can read this repo and open a PR; CI
|
||||
validates; the deployment adopts merged images (pull + restart).
|
||||
2. **Enterprise tier (post-v1)**: Kubernetes manifests (or Helm) for the
|
||||
same service set, phase-gated on the standalone bar holding.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase A — blessed standalone compose
|
||||
|
||||
- A1 Compose service definitions for gateway + appservice joining the
|
||||
existing infra compose (profiles: `dev` keeps today's behavior;
|
||||
`stack` adds the app tier), with health checks and dependency order.
|
||||
- A2 Migrations on boot (or an explicit migrate step) with idempotency
|
||||
and version pinning; init-db.sql folded into pg-init.
|
||||
- A3 Openbao in the compose set (secret plumbing for the app tier).
|
||||
- A4 `.env.example` + `mosaic.config.json` defaults documented for the
|
||||
standalone mode; mode recorded per the mode-conversion contract.
|
||||
- A5 Smoke: `docker compose --profile stack up` green on a scratch host;
|
||||
webUI served; agent chat reachable; failures catalogued and fixed.
|
||||
- Acceptance: the five-point NORTH-STAR bar measured live.
|
||||
|
||||
### Phase B — component completion
|
||||
|
||||
- Interface assumption (velma verdict A1, P5-RM-005/006): in-stack
|
||||
dogfood agents inherit SEAT-GRADE identity — credential-slot
|
||||
isolation, wrapper-first enforcement, no privileged coordination
|
||||
identity, evidence by references that resolve outside the container
|
||||
lifetime.
|
||||
- Decompose JIT from A5's catalogue. Known candidates: agent runtime
|
||||
bits (brain/tool access paths in-container), repo credentials for the
|
||||
dogfood agent, watch/comms surfaces inside the deployment.
|
||||
|
||||
### Phase C — CI/CD parity
|
||||
|
||||
- Publish pipeline is the only image source (already true); add the
|
||||
deployment-side pull/upgrade path (compose pull + migrate + restart =
|
||||
next iteration); document the promotion flow next -> registry ->
|
||||
deployment.
|
||||
|
||||
### Phase D — coordinator integration (GATED)
|
||||
|
||||
- Gate (velma verdict C2): blocked until the checkpoint-and-lease child
|
||||
of the guides-proposed control-plane refactor — core + WU-P1-CHECKPOINT
|
||||
(schema, freshness, incarnation, clean-replacement resume; D57-D60
|
||||
lineage) — carries an independent target-bound PASS. Wiring restarts
|
||||
against the core alone re-creates the stale-incarnation failure class
|
||||
D57-D60 closed. Transitive: inherits the T108 gates (P0 exit + Jason
|
||||
P1 authorization).
|
||||
- Scope (velma verdict C1): lifecycle actions (start/stop/restart/
|
||||
health/recovery) executed by the SHIPPED coord client over the one
|
||||
typed coordination contract (request id, actor identity, epoch,
|
||||
revision, lease, correlation; typed stale rejection; worker role
|
||||
boundary). No second coordination interface gets designed here —
|
||||
containerization consumes the coordination contract, never defines it.
|
||||
|
||||
### Phase E — enterprise tier
|
||||
|
||||
- k8s manifests/Helm for the same set; phase-gated on Phase A holding.
|
||||
|
||||
### Absorbed M4 remainder
|
||||
|
||||
- M4-3 pivot: KBN-101 foundation first (per ruling R6), then expand DDL.
|
||||
- M4-5: lands inside Phase B/C where natural.
|
||||
- M4-6 (composes M4-1+M4-4): last, as designed.
|
||||
|
||||
## Non-goals (v1)
|
||||
|
||||
- No Kubernetes in v1; no multi-host federation; no replacement of the
|
||||
fleet's brain-based seats (the stack is an additional operator
|
||||
surface); no on-host image builds for deployment (registry only).
|
||||
@@ -8,7 +8,7 @@ status: active
|
||||
> Single-writer: the RI-050 orchestrator (jarvis, dragon-lin) only. Workers read but never modify.
|
||||
>
|
||||
> **Mission:** alpha 0.0.50 release-integrity floor (decisions SDLC-D-033..038).
|
||||
> **PRD:** [docs/PRD.md § Release Integrity Workstream](../PRD.md#release-integrity-workstream-ri-1275)
|
||||
> **PRD:** [PRD rev1 GOV.4 § Release Integrity Workstream](../PRDs/2026-08-31_PRD_rev1/GOV.4-workstream-contracts.md#release-integrity-workstream-ri-1275)
|
||||
> **Issue:** #1275 (remains open until RI-V-001 closes)
|
||||
> **Base branch:** `next` (all cards branch from `origin/next`, squash-merge via PR)
|
||||
>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
kind: record
|
||||
status: active
|
||||
status: superseded
|
||||
---
|
||||
|
||||
> **Superseded (2026-09-01, PRD rev1 ratification).** This document's record of a completed Federation M2 milestone is historical. Federation M1–M3 are shipped but **frozen** (dormant since 2026-06-25, excluded from the v1 bar, security re-audit gate before any resumption); the canonical v1 deployment topology is the compose standalone tier (PRD rev1, D15). Authority: `docs/PRD.md` → `docs/PRDs/2026-08-31_PRD_rev1/` (decision D3 as amended, GOV.5 Q-T1). Tracking: `docs/fleet/NORTH_STAR.yaml` (dormant federation workstream). Content below is preserved verbatim as a record — do not edit it.
|
||||
|
||||
# Mission Scratchpad — MVP
|
||||
|
||||
> Append-only log. NEVER delete entries. NEVER overwrite sections.
|
||||
|
||||
Executable
+150
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
# mosaic — fleet launcher (shipped-first, split-home safe).
|
||||
#
|
||||
# T110 / P5-RM-009 stack side. Carries the T106 brain launcher contract
|
||||
# (shipped-first, worktree dev opt-in, OFF pass-through, typed failure) with
|
||||
# one split-home correction: the SHIPPED npm mosaic is resolved from the real
|
||||
# user's home (passwd database), never from $HOME. Under split-home seat
|
||||
# layouts HOME is a seat home: it carries no npm prefix, and a
|
||||
# $HOME/.npm-global there would be a plantable descriptor, so the $HOME
|
||||
# candidate is consulted only when the passwd lookup itself fails, and then
|
||||
# only with a full symlink-component refusal (secure descriptor traversal).
|
||||
#
|
||||
# Contract:
|
||||
# 1. SHIPPED npm mosaic is the default. Candidate order:
|
||||
# a. <real-home>/.npm-global/bin/mosaic — real home from the passwd
|
||||
# database. The final component may be npm's own bin symlink into
|
||||
# lib/node_modules; that indirection is npm's layout, not a plant.
|
||||
# b. $HOME/.npm-global/bin/mosaic — ONLY when the passwd lookup
|
||||
# fails, and then only when the candidate is a trusted-shape
|
||||
# absolute path: relative HOME and parent-escape (..) components
|
||||
# are refused outright, and every remaining component must be a
|
||||
# non-symlink (secure descriptor traversal). Refused candidates
|
||||
# are never executed.
|
||||
# 2. Worktree build is DEV OPT-IN: used only when MOSAIC_CLI_WORKTREE is
|
||||
# explicitly set. Health-checked via --version; ANY doubt (absent,
|
||||
# unreadable, or failing) falls back to the shipped npm mosaic with a
|
||||
# warning on stderr. With no environment set, worktree candidates are
|
||||
# never consulted — stale worktree builds cannot regain precedence.
|
||||
# 3. MOSAIC_FLEET_CLI_OFF keeps its pass-through semantics: set (any
|
||||
# value) forces pure pass-through. The dev path is not consulted even
|
||||
# when MOSAIC_CLI_WORKTREE is also set.
|
||||
# 4. Typed failure: with no runnable candidate the launcher prints one
|
||||
# stderr line naming what was checked and exits 127.
|
||||
# 5. NEVER writes to the mosaic home or the npm prefix. Deployment to the
|
||||
# fleet goes through the real channel (PR to next -> mosaic update).
|
||||
#
|
||||
# Env:
|
||||
# MOSAIC_CLI_WORKTREE dev opt-in: path to a stack worktree whose
|
||||
# packages/mosaic/dist/cli.js is used (health-checked,
|
||||
# shipped fallback on doubt)
|
||||
# MOSAIC_FLEET_CLI_OFF set (any value) to force pure pass-through
|
||||
#
|
||||
# Component walk note: the descriptor guard splits on "/" without quoting so
|
||||
# multi-byte HOME paths with spaces are not supported for the FALLBACK
|
||||
# candidate; the passwd candidate needs no walk (trusted derivation).
|
||||
|
||||
set -u
|
||||
|
||||
fail() {
|
||||
echo "mosaic: $*" >&2
|
||||
exit 127
|
||||
}
|
||||
|
||||
# Real user home from the passwd database (HOME-independent).
|
||||
real_home() {
|
||||
getent passwd "$(id -u)" 2>/dev/null | cut -d: -f6
|
||||
}
|
||||
|
||||
# True when any component of an ABSOLUTE candidate path is a symlink. Only
|
||||
# ever called after fallback_candidate_usable's absolute-shape check.
|
||||
path_has_symlink_component() {
|
||||
local path="$1" dir base acc="" part
|
||||
dir="$(dirname -- "$path")"
|
||||
base="$(basename -- "$path")"
|
||||
local IFS='/'
|
||||
for part in $dir; do
|
||||
acc="$acc/$part"
|
||||
[ -L "$acc" ] && return 0
|
||||
done
|
||||
[ -L "$dir/$base" ] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# Reject the untrusted $HOME fallback candidate unless it is a trusted-shape
|
||||
# absolute path: absolute, no parent-escape (..) components, and no symlink
|
||||
# components anywhere on the path. Every rejection is named on stderr so the
|
||||
# typed failure explains itself. This is the launcher's descriptor guard; the
|
||||
# suite's mutation control (guard bypassed) must plant-exec, proving the guard
|
||||
# is what stands between a hostile HOME and code execution.
|
||||
fallback_candidate_usable() {
|
||||
local candidate="$1"
|
||||
case "$candidate" in
|
||||
/*) ;;
|
||||
*)
|
||||
echo "mosaic: refusing \$HOME candidate $candidate: relative path is untrusted without a passwd home" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if printf '%s' "$candidate" | grep -qE '(^|/)\.\.(/|$)'; then
|
||||
echo "mosaic: refusing \$HOME candidate $candidate: parent-escape component" >&2
|
||||
return 1
|
||||
fi
|
||||
if path_has_symlink_component "$candidate"; then
|
||||
echo "mosaic: refusing \$HOME candidate $candidate: symlink component (untrusted without a passwd home)" >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Print shipped candidates in contract order. Refusals are reported on stderr
|
||||
# so the typed failure names the cause.
|
||||
shipped_candidates() {
|
||||
local rh home_candidate
|
||||
rh="$(real_home)"
|
||||
if [ -n "$rh" ]; then
|
||||
printf '%s\n' "$rh/.npm-global/bin/mosaic"
|
||||
return 0
|
||||
fi
|
||||
# passwd lookup failed: the only fallback is $HOME, descriptor-guarded.
|
||||
if [ -n "${HOME:-}" ]; then
|
||||
home_candidate="$HOME/.npm-global/bin/mosaic"
|
||||
if fallback_candidate_usable "$home_candidate"; then
|
||||
printf '%s\n' "$home_candidate"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
resolve_shipped() {
|
||||
local candidate
|
||||
while IFS= read -r candidate; do
|
||||
[ -n "$candidate" ] || continue
|
||||
if [ -x "$candidate" ]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done < <(shipped_candidates)
|
||||
return 1
|
||||
}
|
||||
|
||||
# Dev opt-in only: an explicit MOSAIC_CLI_WORKTREE reaches the worktree build,
|
||||
# and pure pass-through (MOSAIC_FLEET_CLI_OFF) outranks it.
|
||||
if [ -n "${MOSAIC_CLI_WORKTREE:-}" ] && [ -z "${MOSAIC_FLEET_CLI_OFF:-}" ]; then
|
||||
CLI="$MOSAIC_CLI_WORKTREE/packages/mosaic/dist/cli.js"
|
||||
if [ -r "$CLI" ]; then
|
||||
if v="$(node "$CLI" --version 2>/dev/null)" && [ -n "$v" ]; then
|
||||
exec node "$CLI" "$@"
|
||||
fi
|
||||
echo "mosaic: worktree build at $CLI failed its health check; using shipped npm mosaic" >&2
|
||||
else
|
||||
echo "mosaic: worktree build at $CLI absent or unreadable; using shipped npm mosaic" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
SHIPPED="$(resolve_shipped)" || true
|
||||
if [ -n "${SHIPPED:-}" ]; then
|
||||
exec "$SHIPPED" "$@"
|
||||
fi
|
||||
|
||||
fail "no runnable CLI (shipped npm mosaic absent from the passwd-home npm prefix and \$HOME; worktree build requires MOSAIC_CLI_WORKTREE)"
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hermetic suite for the fleet/bin/mosaic launcher (T110 / P5-RM-009).
|
||||
#
|
||||
# Arms cover the plan acceptance: split-home shipped-first positive, typed
|
||||
# failure on missing candidates, stale-worktree non-precedence, OFF
|
||||
# pass-through, and secure-descriptor refusal on the untrusted $HOME
|
||||
# fallback. No network, no real npm install, no node package build: the
|
||||
# "shipped mosaic" is a stub script and getent is PATH-stubbed (set
|
||||
# GETENT_STUB=fail to make the passwd lookup fail, exercising the guarded
|
||||
# $HOME fallback).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
|
||||
LAUNCHER="$SCRIPT_DIR/mosaic"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -f "$LAUNCHER" ] || fail "missing launcher"
|
||||
[ -x "$LAUNCHER" ] || fail "launcher is not executable"
|
||||
bash -n "$LAUNCHER" || fail "launcher fails bash -n"
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
cleanup() { rm -rf "$WORK"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
REAL_HOME="$WORK/real-home"
|
||||
SEAT_HOME="$WORK/seat-home"
|
||||
STUB_BIN="$WORK/stub-bin"
|
||||
mkdir -p "$REAL_HOME/.npm-global/bin" "$SEAT_HOME" "$STUB_BIN"
|
||||
|
||||
cat >"$REAL_HOME/.npm-global/bin/mosaic" <<'SH'
|
||||
#!/bin/sh
|
||||
echo "0.0.0-shipped-stub"
|
||||
SH
|
||||
chmod +x "$REAL_HOME/.npm-global/bin/mosaic"
|
||||
|
||||
# PATH-stubbed getent: reports the real home for the current uid, unless
|
||||
# GETENT_STUB=fail is in the launcher environment (exercises the guarded
|
||||
# $HOME fallback path).
|
||||
cat >"$STUB_BIN/getent" <<SH
|
||||
#!/bin/sh
|
||||
if [ "\${GETENT_STUB:-}" = "fail" ]; then exit 2; fi
|
||||
if [ "\$1" = "passwd" ]; then
|
||||
echo "stub:x:$(id -u):$(id -g):stub:$REAL_HOME:/bin/sh"
|
||||
exit 0
|
||||
fi
|
||||
exit 2
|
||||
SH
|
||||
chmod +x "$STUB_BIN/getent"
|
||||
|
||||
run_launcher() { # run_launcher <home> [VAR=value ...] -- [args...]
|
||||
local home="$1"; shift
|
||||
[ "${1:-}" = "--" ] && shift
|
||||
env -i PATH="$STUB_BIN:/usr/bin:/bin" HOME="$home" TERM="${TERM:-dumb}" "$LAUNCHER" "$@"
|
||||
}
|
||||
|
||||
# A1 — acceptance 1: split-home positive. HOME is an empty seat home; the
|
||||
# shipped mosaic resolves through the passwd real home.
|
||||
out="$(printf '' | run_launcher "$SEAT_HOME" -- --version)"
|
||||
[ "$out" = "0.0.0-shipped-stub" ] || fail "A1 split-home positive: got '$out', want shipped stub version"
|
||||
|
||||
# A3 — acceptance 3: a stale worktree build is NEVER consulted without the
|
||||
# explicit opt-in, even when a worktree exists on disk.
|
||||
WT="$WORK/stale-wt"
|
||||
mkdir -p "$WT/packages/mosaic/dist"
|
||||
printf 'console.log("0.0.0-stale-worktree")\n' >"$WT/packages/mosaic/dist/cli.js"
|
||||
out="$(printf '' | run_launcher "$SEAT_HOME" -- --version)"
|
||||
[ "$out" = "0.0.0-shipped-stub" ] || fail "A3 stale worktree regained precedence without opt-in: got '$out'"
|
||||
|
||||
# A2 (opt-in healthy) — explicit MOSAIC_CLI_WORKTREE reaches the worktree.
|
||||
out="$(printf '' | env MOSAIC_CLI_WORKTREE="$WT" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version)"
|
||||
[ "$out" = "0.0.0-stale-worktree" ] || fail "A2 opt-in worktree not used: got '$out'"
|
||||
|
||||
# A2b (opt-in unhealthy) — absent dist falls back to shipped with a warning.
|
||||
out2="$(printf '' | env MOSAIC_CLI_WORKTREE="$WORK/empty-wt" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>/dev/null)"
|
||||
[ "$out2" = "0.0.0-shipped-stub" ] || fail "A2b unhealthy worktree fallback output: '$out2'"
|
||||
err2="$(printf '' | env MOSAIC_CLI_WORKTREE="$WORK/empty-wt" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&1 >/dev/null)"
|
||||
case "$err2" in *"absent or unreadable"*|*"health check"*) ;; *) fail "A2b unhealthy worktree fallback warning missing: '$err2'" ;; esac
|
||||
|
||||
# A4 — OFF pass-through: worktree opt-in is ignored when OFF is set.
|
||||
out="$(printf '' | env MOSAIC_FLEET_CLI_OFF=1 MOSAIC_CLI_WORKTREE="$WT" HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version)"
|
||||
[ "$out" = "0.0.0-shipped-stub" ] || fail "A4 OFF did not force pass-through: got '$out'"
|
||||
|
||||
# A5 — acceptance 4: typed failure when no candidate exists (passwd lookup
|
||||
# fails, seat home carries no npm prefix). Expect 127 + documented message.
|
||||
set +e
|
||||
err="$(printf '' | env GETENT_STUB=fail HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&1 >/dev/null)"
|
||||
rc=$?
|
||||
set -e
|
||||
[ "$rc" = "127" ] || fail "A5 typed failure rc: got $rc, want 127"
|
||||
case "$err" in *"no runnable CLI"*) ;; *) fail "A5 typed failure message missing: '$err'" ;; esac
|
||||
|
||||
# A6 — secure descriptor traversal, ABSOLUTE symlink plant (corrected per
|
||||
# rev-code-02 B3: the symlink points at $PLANT/.npm-global so the candidate
|
||||
# resolves EXACTLY to the planted executable). passwd lookup fails and a
|
||||
# symlink-planted $HOME/.npm-global is refused without execution.
|
||||
PLANT="$WORK/planted-target"
|
||||
mkdir -p "$PLANT/.npm-global/bin"
|
||||
cat >"$PLANT/.npm-global/bin/mosaic" <<SH
|
||||
#!/bin/sh
|
||||
touch "$WORK/planted-sentinel"
|
||||
echo "0.0.0-planted"
|
||||
SH
|
||||
chmod +x "$PLANT/.npm-global/bin/mosaic"
|
||||
ln -s "$PLANT/.npm-global" "$SEAT_HOME/.npm-global"
|
||||
set +e
|
||||
err="$(printf '' | env GETENT_STUB=fail HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&1)"
|
||||
rc=$?
|
||||
set -e
|
||||
[ "$rc" = "127" ] || fail "A6 planted descriptor was followed (rc $rc, out '$err')"
|
||||
case "$err" in *"symlink component"*) ;; *) fail "A6 refusal diagnostic missing: '$err'" ;; esac
|
||||
[ ! -e "$WORK/planted-sentinel" ] || fail "A6 planted mosaic EXECUTED"
|
||||
|
||||
# A6b — mutation control (rev-code-02 B3): a copy of the launcher with the
|
||||
# descriptor guard bypassed MUST execute the plant under the identical hostile
|
||||
# arm. If the mutant stays clean, the plant path is wrong and A6 proves
|
||||
# nothing.
|
||||
MUTANT="$WORK/mutant-mosaic"
|
||||
sed 's/if fallback_candidate_usable "\$home_candidate"; then/if true; then/' "$LAUNCHER" >"$MUTANT"
|
||||
chmod +x "$MUTANT"
|
||||
[ "$(grep -c 'if true; then' "$MUTANT")" -eq 1 ] || fail "A6b mutant not created (guard call not replaced)"
|
||||
set +e
|
||||
mout="$(printf '' | env GETENT_STUB=fail HOME="$SEAT_HOME" PATH="$STUB_BIN:/usr/bin:/bin" "$MUTANT" --version 2>&1)"
|
||||
mrc=$?
|
||||
set -e
|
||||
[ "$mrc" = "0" ] || fail "A6b mutant did not execute the plant (rc $mrc, out '$mout') - A6 proves nothing"
|
||||
[ -e "$WORK/planted-sentinel" ] || fail "A6b mutant ran but sentinel absent - plant path wrong, A6 proves nothing"
|
||||
|
||||
# A7 — relative-HOME hostile arm (rev-code-02 B2): a relative HOME whose name
|
||||
# is a symlink in the launcher CWD must be refused outright, never resolved
|
||||
# against the working directory.
|
||||
CWD_SANDBOX="$WORK/cwd-sandbox"
|
||||
REL_PLANT="$WORK/relative-plant"
|
||||
mkdir -p "$CWD_SANDBOX" "$REL_PLANT/.npm-global/bin"
|
||||
cat >"$REL_PLANT/.npm-global/bin/mosaic" <<SH
|
||||
#!/bin/sh
|
||||
touch "$WORK/relative-sentinel"
|
||||
echo "0.0.0-relative-planted"
|
||||
SH
|
||||
chmod +x "$REL_PLANT/.npm-global/bin/mosaic"
|
||||
ln -s "$REL_PLANT" "$CWD_SANDBOX/relative-home"
|
||||
set +e
|
||||
rout="$(cd "$CWD_SANDBOX" && printf '' | env GETENT_STUB=fail HOME="relative-home" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&1)"
|
||||
rrc=$?
|
||||
set -e
|
||||
[ "$rrc" = "127" ] || fail "A7 relative HOME was followed (rc $rrc, out '$rout')"
|
||||
case "$rout" in *"relative path"*) ;; *) fail "A7 relative-refusal diagnostic missing: '$rout'" ;; esac
|
||||
[ ! -e "$WORK/relative-sentinel" ] || fail "A7 relative plant EXECUTED"
|
||||
|
||||
# A7b — mutation control for the absolute-shape check: the same mutant (guard
|
||||
# bypassed) MUST execute the relative plant under the identical arm.
|
||||
set +e
|
||||
rmout="$(cd "$CWD_SANDBOX" && printf '' | env GETENT_STUB=fail HOME="relative-home" PATH="$STUB_BIN:/usr/bin:/bin" "$MUTANT" --version 2>&1)"
|
||||
rmrc=$?
|
||||
set -e
|
||||
[ "$rmrc" = "0" ] || fail "A7b mutant did not execute the relative plant (rc $rmrc, out '$rmout') - A7 proves nothing"
|
||||
[ -e "$WORK/relative-sentinel" ] || fail "A7b mutant ran but relative sentinel absent - arm wrong, A7 proves nothing"
|
||||
|
||||
# A8 — parent-escape hostile arm (rev-code-02 delta, B2 remains): an absolute
|
||||
# HOME containing a literal '..' component must be refused by the
|
||||
# parent-escape check — the traversal would otherwise land on a planted tree
|
||||
# OUTSIDE the seat home with no symlink involved.
|
||||
ESC_BASE="$WORK/escape-base"
|
||||
ESC_TARGET="$WORK/escape-target"
|
||||
mkdir -p "$ESC_BASE" "$ESC_TARGET/.npm-global/bin"
|
||||
cat >"$ESC_TARGET/.npm-global/bin/mosaic" <<SH
|
||||
#!/bin/sh
|
||||
touch "$WORK/escape-sentinel"
|
||||
echo "0.0.0-escape-planted"
|
||||
SH
|
||||
chmod +x "$ESC_TARGET/.npm-global/bin/mosaic"
|
||||
set +e
|
||||
eout="$(printf '' | env GETENT_STUB=fail HOME="$ESC_BASE/../escape-target" PATH="$STUB_BIN:/usr/bin:/bin" "$LAUNCHER" --version 2>&1)"
|
||||
erc=$?
|
||||
set -e
|
||||
[ "$erc" = "127" ] || fail "A8 parent-escape HOME was followed (rc $erc, out '$eout')"
|
||||
case "$eout" in *"parent-escape component"*) ;; *) fail "A8 parent-escape diagnostic missing: '$eout'" ;; esac
|
||||
[ ! -e "$WORK/escape-sentinel" ] || fail "A8 escape plant EXECUTED"
|
||||
|
||||
# A8b — mutation control: the guard-bypassed copy MUST execute the parent-
|
||||
# escape plant under the identical arm (sentinel present, rc 0), proving the
|
||||
# parent-escape check is what stands.
|
||||
set +e
|
||||
emout="$(printf '' | env GETENT_STUB=fail HOME="$ESC_BASE/../escape-target" PATH="$STUB_BIN:/usr/bin:/bin" "$MUTANT" --version 2>&1)"
|
||||
emrc=$?
|
||||
set -e
|
||||
[ "$emrc" = "0" ] || fail "A8b mutant did not execute the escape plant (rc $emrc, out '$emout') - A8 proves nothing"
|
||||
[ -e "$WORK/escape-sentinel" ] || fail "A8b mutant ran but escape sentinel absent - arm wrong, A8 proves nothing"
|
||||
|
||||
echo "mosaic launcher suite: all arms passed"
|
||||
@@ -46,7 +46,12 @@ systemd/**
|
||||
templates/**
|
||||
tools/**
|
||||
# Fleet: only the framework-seeded fleet subtrees are framework-owned.
|
||||
# fleet/bin is exact-entry on purpose (T110 B1): the estate's fleet/bin carries
|
||||
# operator-owned executables this package does not ship; a subtree glob here
|
||||
# would make keep-mode update prune them.
|
||||
fleet/README.md
|
||||
fleet/bin/mosaic
|
||||
fleet/bin/test-mosaic-launcher.sh
|
||||
fleet/examples/**
|
||||
fleet/profiles/**
|
||||
fleet/roles/**
|
||||
|
||||
@@ -1,220 +1,64 @@
|
||||
#!/bin/bash
|
||||
# git-credential-mosaic — git credential helper. Resolves a Gitea token from the
|
||||
# Mosaic credential store at runtime so remote URLs never embed secrets.
|
||||
#!/usr/bin/python3
|
||||
# git-credential-mosaic — production entrypoint (P0-SEC R4, rev-code-02 B1).
|
||||
#
|
||||
# Install (one-time, per clone or globally):
|
||||
# git config credential.helper "$HOME/.config/mosaic/tools/git/git-credential-mosaic"
|
||||
# WHY THIS IS NOT BASH: three review rounds falsified every in-bash startup
|
||||
# guard. A non-interactive bash sources $BASH_ENV and imports exported
|
||||
# functions BEFORE the first script line, so read(), unset(), exit(),
|
||||
# declare(), printf() — every callable — can be shadows that fake the
|
||||
# ancestry, defeat the scrub, or forge diagnostics (rev-code-02 probes 1 and
|
||||
# 2, artifacts fc49e9d9 lineage). No in-language dispatch survives that.
|
||||
#
|
||||
# Per-agent identity (Gate-16 author != reviewer separation):
|
||||
# git config mosaic.gitIdentity <agent-id> # per-worktree, persists on disk
|
||||
# # or: export MOSAIC_GIT_IDENTITY=<agent-id>
|
||||
# This entrypoint is unshapable at the bash level: python does not read
|
||||
# BASH_ENV and imports no bash functions, and the interpreter is pinned by
|
||||
# absolute shebang (no PATH resolution). It builds the child environment BY
|
||||
# ALLOWLIST and execve's the bash implementation directly — the child bash
|
||||
# starts with no BASH_ENV, no BASH_FUNC_*, no SHELLOPTS/BASHOPTS, and exactly
|
||||
# the variables the credential protocol needs. stdin/stdout/stderr and argv
|
||||
# pass through untouched.
|
||||
#
|
||||
# ── WHY THIS FAILS CLOSED ──────────────────────────────────────────────────────
|
||||
# This helper used to end by emitting the shared account's token for any request
|
||||
# it could not resolve to an identity. A seat with no identity, or with an
|
||||
# identity whose token was never provisioned, therefore received the most
|
||||
# privileged credential configured on the host — silently, and indistinguishably
|
||||
# from correct operation. Every record it then created (commit, push, PR, review)
|
||||
# was attributed to that shared account, so author != reviewer separation was
|
||||
# unenforceable and the true actor was unrecoverable after the fact.
|
||||
#
|
||||
# Under-provisioning must fail loudly, not impersonate. A refused git operation
|
||||
# is recoverable in one command; a merged pull request attributed to the wrong
|
||||
# principal is not.
|
||||
#
|
||||
# ── CONTRACT ───────────────────────────────────────────────────────────────────
|
||||
# identity : MOSAIC_GIT_IDENTITY > git config mosaic.gitIdentity > the
|
||||
# username git supplies on stdin
|
||||
# store : chosen by what the identity IS, with no precedence and no
|
||||
# cross-store fallback (see "Credential store selection" below)
|
||||
# hit : emit username + password, exit 0
|
||||
# miss : emit NOTHING, spool a durable escalation record, explain on
|
||||
# stderr, exit 1 — git surfaces the failure and nothing is attributed
|
||||
# unknown host : exit 0 with no output, no record (passthrough for non-Mosaic
|
||||
# remotes handled by another helper)
|
||||
#
|
||||
# Backward compatibility is preserved for exactly one case: a host with no fleet
|
||||
# and no identity requested still gets the shared account, because on such a host
|
||||
# the shared account is the operator's own and there is no attribution to lose.
|
||||
# A host that HAS a fleet has agents whose records must be distinguishable, so
|
||||
# the shared fallback is refused there.
|
||||
#
|
||||
# A token is never written to stderr, to the escalation record, or to any log.
|
||||
# The implementation file (git-credential-mosaic.impl) refuses to run without
|
||||
# the clean-mode marker, so it cannot be invoked directly as a shaped-entry
|
||||
# bypass of this wrapper.
|
||||
|
||||
[ "$1" = "get" ] || exit 0
|
||||
import os
|
||||
import sys
|
||||
|
||||
host=""; username_in=""
|
||||
while IFS= read -r line; do
|
||||
[ -z "$line" ] && break
|
||||
case "$line" in
|
||||
host=*) host=${line#host=};;
|
||||
username=*) username_in=${line#username=};;
|
||||
esac
|
||||
done
|
||||
IMPL = os.path.join(os.path.dirname(os.path.realpath(__file__)), "git-credential-mosaic.impl")
|
||||
# Absolute-path candidates ONLY — never PATH resolution (an attacker-shaped
|
||||
# PATH must not choose the interpreter). /usr/bin/bash is the fleet-host
|
||||
# layout; /bin/bash is alpine and other FHS variants (found by the T125
|
||||
# gateway-image verification: the hardcoded /usr/bin/bash made every call
|
||||
# exit 127 inside node:22-alpine).
|
||||
BASH_CANDIDATES = ("/usr/bin/bash", "/bin/bash")
|
||||
BASH = next((p for p in BASH_CANDIDATES if os.access(p, os.X_OK)), None)
|
||||
|
||||
# Recognized Gitea hosts carry the per-identity token scheme. Anything else is
|
||||
# declined quietly — another helper owns it, and refusing would break it.
|
||||
case "$host" in
|
||||
git.uscllc.com) idpfx=gitea-usc;;
|
||||
git.mosaicstack.dev) idpfx=gitea-mosaicstack;;
|
||||
*) exit 0;;
|
||||
esac
|
||||
# Allowlist: everything else in the environment dies at this boundary. Adding
|
||||
# a variable here is a security decision — it crosses into a shell that no
|
||||
# longer has any startup shaping, but it also becomes the only context the
|
||||
# implementation can see.
|
||||
KEEP = (
|
||||
"HOME",
|
||||
"PATH",
|
||||
"LANG",
|
||||
"MOSAIC_GIT_IDENTITY",
|
||||
"MOSAIC_AGENT_NAME",
|
||||
"MOSAIC_BRAIN_HOME",
|
||||
"MOSAIC_CREDENTIAL_SPOOL",
|
||||
"MOSAIC_CREDENTIAL_LINEAGE_FENCE",
|
||||
)
|
||||
|
||||
ident="$MOSAIC_GIT_IDENTITY"; ident_src="MOSAIC_GIT_IDENTITY"
|
||||
if [ -z "$ident" ]; then
|
||||
ident=$(git config --get mosaic.gitIdentity 2>/dev/null)
|
||||
ident_src="git config mosaic.gitIdentity"
|
||||
fi
|
||||
if [ -z "$ident" ]; then
|
||||
ident="$username_in"
|
||||
ident_src="the username git supplied"
|
||||
fi
|
||||
env = {"_MOSAIC_HELPER_CLEAN": "1"}
|
||||
for name in KEEP:
|
||||
value = os.environ.get(name)
|
||||
if value is not None:
|
||||
env[name] = value
|
||||
|
||||
# ── Credential store selection ────────────────────────────────────────────────
|
||||
# An identity is a SEAT or it is a SERVICE, and which one it is determines where
|
||||
# its credential lives. There is no precedence rule between the two stores and no
|
||||
# fallback from one to the other: a seat whose slot is empty fails closed rather
|
||||
# than reading a service credential that happens to share its name.
|
||||
#
|
||||
# seat — <brain>/fleet/agents/<ident>/ exists
|
||||
# credential at <brain>/fleet/agents/<ident>/secrets/<idpfx>-<ident>.token
|
||||
# service — it does not
|
||||
# credential at ~/.config/mosaic/secrets/gitea-tokens/<idpfx>-<ident>.token
|
||||
#
|
||||
# One credential, one location. Two copies of one credential diverge, and the
|
||||
# stale copy fails in a way that reads as a revoked token rather than as drift.
|
||||
#
|
||||
# Brain-home resolution mirrors packages/mosaic/src/fleet/brain-home.ts and
|
||||
# tools/fleet/start-agent-session.sh: MOSAIC_BRAIN_HOME wins, else ~/.mosaic.
|
||||
brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
|
||||
svc_store="$HOME/.config/mosaic/secrets/gitea-tokens"
|
||||
|
||||
idtok=""; ident_kind=""
|
||||
if [ -n "$ident" ]; then
|
||||
if [ -d "$brain_home/fleet/agents/$ident" ]; then
|
||||
ident_kind="seat"
|
||||
idtok="$brain_home/fleet/agents/$ident/secrets/${idpfx}-${ident}.token"
|
||||
else
|
||||
ident_kind="service identity"
|
||||
idtok="$svc_store/${idpfx}-${ident}.token"
|
||||
fi
|
||||
if [ -r "$idtok" ]; then
|
||||
echo "username=${ident}"
|
||||
echo "password=$(cat "$idtok")"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Shared-account fallback: ONLY on a host with no fleet and no identity ──────
|
||||
# `fleet/agents` existing is the same signal brain-home.ts uses to decide a brain
|
||||
# is active. Where there are seats, records must be attributable, so an
|
||||
# unresolvable request is refused instead of borrowing the shared account.
|
||||
fleet_present=0
|
||||
[ -d "$brain_home/fleet/agents" ] && fleet_present=1
|
||||
|
||||
if [ -z "$ident" ] && [ "$fleet_present" -eq 0 ]; then
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=../_lib/credentials.sh
|
||||
source "$script_dir/../_lib/credentials.sh"
|
||||
load_credentials "$idpfx" >/dev/null 2>&1 || exit 0
|
||||
# GITEA_USER is not populated by load_credentials (it exports GITEA_URL and
|
||||
# GITEA_TOKEN only). Gitea's git-over-HTTP auth authenticates from the token in
|
||||
# the password field, not from the username string, so any non-empty
|
||||
# placeholder works — deliberately NOT a real account name, since framework
|
||||
# files stay operator-agnostic (tools/quality/scripts/verify-sanitized.sh).
|
||||
echo "username=${GITEA_USER:-git}"
|
||||
echo "password=$GITEA_TOKEN"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── FAIL CLOSED ───────────────────────────────────────────────────────────────
|
||||
if [ -z "$ident" ]; then
|
||||
reason="no-identity"
|
||||
else
|
||||
reason="no-token-for-identity"
|
||||
fi
|
||||
|
||||
seat="${MOSAIC_AGENT_NAME:-unknown}"
|
||||
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# The escalation RECORD is durable and unconditional; any notification built on
|
||||
# top of it is best-effort. Record and alert are deduplicated separately — a cap
|
||||
# on the alert alone lets the spool grow without bound exactly while the operator
|
||||
# is being told nothing, so the louder the failure the quieter it gets.
|
||||
#
|
||||
# A record field is arbitrary operator-supplied text: an identity comes from git
|
||||
# config or the environment, and cwd is whatever directory git ran in. Either can
|
||||
# contain a quote or a backslash, which would make the line unparseable JSON --
|
||||
# and a spool that silently stops parsing is worse than no spool, because the
|
||||
# operator only discovers it while reading the record that explains an outage.
|
||||
json_escape() {
|
||||
local s=$1
|
||||
s=${s//\\/\\\\}
|
||||
s=${s//\"/\\\"}
|
||||
s=${s//$'\t'/\\t}
|
||||
s=${s//$'\r'/\\r}
|
||||
s=${s//$'\n'/\\n}
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
spool="${MOSAIC_CREDENTIAL_SPOOL:-$HOME/.local/state/mosaic-credential-escalations}"
|
||||
spool_record=""
|
||||
if mkdir -p "$spool" 2>/dev/null; then
|
||||
chmod 700 "$spool" 2>/dev/null
|
||||
spoolfile="$spool/$(date -u +%Y%m%d).jsonl"
|
||||
dedupe="$spool/.spooled-${seat}-${ident:-none}-${reason}-$(date -u +%Y%m%d%H%M)"
|
||||
if [ ! -e "$dedupe" ]; then
|
||||
: > "$dedupe" 2>/dev/null
|
||||
printf '{"ts":"%s","reason":"%s","identity":"%s","identity_source":"%s","kind":"%s","seat":"%s","host":"%s","cwd":"%s"}\n' \
|
||||
"$(json_escape "$ts")" "$(json_escape "$reason")" \
|
||||
"$(json_escape "${ident:-<unset>}")" "$(json_escape "$ident_src")" \
|
||||
"$(json_escape "${ident_kind:-none}")" "$(json_escape "$seat")" \
|
||||
"$(json_escape "$host")" "$(json_escape "$PWD")" \
|
||||
>> "$spoolfile" 2>/dev/null
|
||||
chmod 600 "$spoolfile" 2>/dev/null
|
||||
fi
|
||||
# Name the record only if one is actually on disk. Printing the path
|
||||
# unconditionally sends the operator to a file that does not exist on exactly
|
||||
# the hosts where the spool could not be created.
|
||||
[ -s "$spoolfile" ] && spool_record="$spoolfile"
|
||||
find "$spool" -maxdepth 1 -name '.spooled-*' -mmin +120 -delete 2>/dev/null
|
||||
fi
|
||||
|
||||
cat >&2 <<EOF
|
||||
git-credential-mosaic: REFUSED (fail-closed).
|
||||
host : ${host}
|
||||
identity : ${ident:-<unset>}${ident:+ (from ${ident_src}; resolved as a ${ident_kind})}
|
||||
reason : ${reason}
|
||||
EOF
|
||||
|
||||
if [ -n "$ident" ]; then
|
||||
cat >&2 <<EOF
|
||||
expected : ${idtok}
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat >&2 <<EOF
|
||||
|
||||
No per-identity credential resolved. This helper does NOT fall back to the shared
|
||||
account: that fallback makes every record it creates attributable to one
|
||||
principal, which is unrecoverable once a pull request has merged under it.
|
||||
|
||||
Fix (pick one):
|
||||
export MOSAIC_GIT_IDENTITY=<agent-id> # process-scoped
|
||||
git config mosaic.gitIdentity <agent-id> # per-repo/worktree, persists
|
||||
Then provision that identity's credential at the path named above. An identity
|
||||
with a directory under \${MOSAIC_BRAIN_HOME:-\$HOME/.mosaic}/fleet/agents/ is a
|
||||
seat and is read ONLY from its own secrets/ slot; any other identity is read from
|
||||
~/.config/mosaic/secrets/gitea-tokens/. There is no fallback between the two.
|
||||
|
||||
If this identity legitimately needs git access and has none, ask the orchestrator
|
||||
to provision one.
|
||||
|
||||
EOF
|
||||
|
||||
if [ -n "$spool_record" ]; then
|
||||
echo " record: ${spool_record}" >&2
|
||||
else
|
||||
echo " record: NOT WRITTEN — spool unavailable at ${spool}" >&2
|
||||
fi
|
||||
exit 1
|
||||
argv = [BASH, IMPL] + sys.argv[1:]
|
||||
if BASH is None:
|
||||
sys.stderr.write("git-credential-mosaic: no executable bash at " + " or ".join(BASH_CANDIDATES) + "\n")
|
||||
sys.exit(127)
|
||||
try:
|
||||
os.execve(BASH, argv, env)
|
||||
except OSError as exc:
|
||||
sys.stderr.write(f"git-credential-mosaic: entrypoint exec failed: {exc}\n")
|
||||
sys.exit(127)
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
#!/bin/bash
|
||||
# git-credential-mosaic — git credential helper. Resolves a Gitea token from the
|
||||
# Mosaic credential store at runtime so remote URLs never embed secrets.
|
||||
#
|
||||
# Install (one-time, per clone or globally):
|
||||
# git config credential.helper "$HOME/.config/mosaic/tools/git/git-credential-mosaic"
|
||||
#
|
||||
# Per-agent identity (Gate-16 author != reviewer separation):
|
||||
# git config mosaic.gitIdentity <agent-id> # per-worktree, persists on disk
|
||||
# # or: export MOSAIC_GIT_IDENTITY=<agent-id>
|
||||
#
|
||||
# ── WHY THIS FAILS CLOSED ──────────────────────────────────────────────────────
|
||||
# This helper used to end by emitting the shared account's token for any request
|
||||
# it could not resolve to an identity. A seat with no identity, or with an
|
||||
# identity whose token was never provisioned, therefore received the most
|
||||
# privileged credential configured on the host — silently, and indistinguishably
|
||||
# from correct operation. Every record it then created (commit, push, PR, review)
|
||||
# was attributed to that shared account, so author != reviewer separation was
|
||||
# unenforceable and the true actor was unrecoverable after the fact.
|
||||
#
|
||||
# Under-provisioning must fail loudly, not impersonate. A refused git operation
|
||||
# is recoverable in one command; a merged pull request attributed to the wrong
|
||||
# principal is not.
|
||||
#
|
||||
# ── CONTRACT ───────────────────────────────────────────────────────────────────
|
||||
# identity : MOSAIC_GIT_IDENTITY > git config mosaic.gitIdentity > the
|
||||
# username git supplies on stdin
|
||||
# ownership: a FLEET SEAT caller may resolve ONLY its own identity, where
|
||||
# the CALLER is established by process ANCESTRY, not by the
|
||||
# current environment: every ancestor's /proc/<pid>/environ is
|
||||
# frozen at exec, so a child can rewrite its own MOSAIC_AGENT_NAME
|
||||
# but can never make an ancestor disagree with what the launcher
|
||||
# gave it (P5-RM-006; the dual-variable override was measured by
|
||||
# rev-code-02 F1). An anonymous caller (no lineage, no consensus)
|
||||
# may resolve NOTHING on a fleet host — seat or service
|
||||
# (rev-code-02 F2). Non-fleet hosts keep the documented legacy
|
||||
# paths below.
|
||||
# perms : a slot whose mode lets group or other read it (anything but
|
||||
# ?00) is refused — a loose slot is provisioning drift, and
|
||||
# serving from it silently widens every seat's exposure on a
|
||||
# single-account host.
|
||||
# store : chosen by what the identity IS, with no precedence and no
|
||||
# cross-store fallback (see "Credential store selection" below)
|
||||
# hit : emit username + password, exit 0
|
||||
# miss : emit NOTHING, spool a durable escalation record, explain on
|
||||
# stderr, exit 1 — git surfaces the failure and nothing is attributed
|
||||
# unknown host : exit 0 with no output, no record (passthrough for non-Mosaic
|
||||
# remotes handled by another helper)
|
||||
#
|
||||
# Backward compatibility is preserved for exactly one case: a host with no fleet
|
||||
# and no identity requested still gets the shared account, because on such a host
|
||||
# the shared account is the operator's own and there is no attribution to lose.
|
||||
# A host that HAS a fleet has agents whose records must be distinguishable, so
|
||||
# the shared fallback is refused there.
|
||||
#
|
||||
# A token is never written to stderr, to the escalation record, or to any log.
|
||||
|
||||
[ "$1" = "get" ] || exit 0
|
||||
|
||||
|
||||
# ── The shared refusal path ──────────────────────────────────────────────────
|
||||
# Every fail-closed exit funnels through refuse(): a durable escalation record
|
||||
# (deduped, JSON-escaped), a stderr diagnostic naming host/identity/reason,
|
||||
# caller-supplied guidance when the refusing site has specific advice, exit 1.
|
||||
# Defined here because the ownership gate below must be able to reach it.
|
||||
refuse() {
|
||||
local guidance="${1:-}"
|
||||
local seat ts spool spool_record spoolfile dedupe
|
||||
seat="${MOSAIC_AGENT_NAME:-unknown}"
|
||||
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# A record field is arbitrary operator-supplied text: an identity comes from git
|
||||
# config or the environment, and cwd is whatever directory git ran in. Either can
|
||||
# contain a quote or a backslash, which would make the line unparseable JSON --
|
||||
# and a spool that silently stops parsing is worse than no spool, because the
|
||||
# operator only discovers it while reading the record that explains an outage.
|
||||
json_escape() {
|
||||
local s=$1
|
||||
s=${s//\\/\\\\}
|
||||
s=${s//\"/\\\"}
|
||||
s=${s//$'\t'/\\t}
|
||||
s=${s//$'\r'/\\r}
|
||||
s=${s//$'\n'/\\n}
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
spool="${MOSAIC_CREDENTIAL_SPOOL:-$HOME/.local/state/mosaic-credential-escalations}"
|
||||
spool_record=""
|
||||
if mkdir -p "$spool" 2>/dev/null; then
|
||||
chmod 700 "$spool" 2>/dev/null
|
||||
spoolfile="$spool/$(date -u +%Y%m%d).jsonl"
|
||||
dedupe="$spool/.spooled-${seat}-${ident:-none}-${reason}-$(date -u +%Y%m%d%H%M)"
|
||||
if [ ! -e "$dedupe" ]; then
|
||||
: > "$dedupe" 2>/dev/null
|
||||
printf '{"ts":"%s","reason":"%s","identity":"%s","identity_source":"%s","kind":"%s","seat":"%s","host":"%s","cwd":"%s"}\n' \
|
||||
"$(json_escape "$ts")" "$(json_escape "$reason")" \
|
||||
"$(json_escape "${ident:-<unset>}")" "$(json_escape "$ident_src")" \
|
||||
"$(json_escape "${ident_kind:-none}")" "$(json_escape "$seat")" \
|
||||
"$(json_escape "$host")" "$(json_escape "$PWD")" \
|
||||
>> "$spoolfile" 2>/dev/null
|
||||
chmod 600 "$spoolfile" 2>/dev/null
|
||||
fi
|
||||
# Name the record only if one is actually on disk. Printing the path
|
||||
# unconditionally sends the operator to a file that does not exist on exactly
|
||||
# the hosts where the spool could not be created.
|
||||
[ -s "$spoolfile" ] && spool_record="$spoolfile"
|
||||
find "$spool" -maxdepth 1 -name '.spooled-*' -mmin +120 -delete 2>/dev/null
|
||||
fi
|
||||
|
||||
while IFS= builtin read -r _diag_line; do builtin printf '%s\n' "$_diag_line" >&2; done <<EOF
|
||||
git-credential-mosaic: REFUSED (fail-closed).
|
||||
host : ${host}
|
||||
identity : ${ident:-<unset>}${ident:+ (from ${ident_src}; resolved as a ${ident_kind})}
|
||||
reason : ${reason}
|
||||
EOF
|
||||
|
||||
if [ -n "$ident" ]; then
|
||||
while IFS= builtin read -r _diag_line; do builtin printf '%s\n' "$_diag_line" >&2; done <<EOF
|
||||
expected : ${idtok}
|
||||
EOF
|
||||
fi
|
||||
|
||||
while IFS= builtin read -r _diag_line; do builtin printf '%s\n' "$_diag_line" >&2; done <<EOF
|
||||
|
||||
${guidance}
|
||||
EOF
|
||||
|
||||
if [ -n "$spool_record" ]; then
|
||||
echo " record: ${spool_record}" >&2
|
||||
else
|
||||
echo " record: NOT WRITTEN — spool unavailable at ${spool}" >&2
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── Bash environment injection guard (rev-code-02 R3, B1) ───────────────────
|
||||
# Non-interactive bash sources $BASH_ENV at startup and imports exported
|
||||
# functions from BASH_FUNC_* environment entries; either can define a read()
|
||||
# or printf() that shadows the builtin the ancestry walker and diagnostics
|
||||
# rely on — measured live by the reviewer's fixture (BASH_ENV read() rewrote
|
||||
# every ancestry entry). A legitimate fleet seat environment carries neither
|
||||
# (verified: zero BASH_FUNC_* in seat envs), so their presence in a helper
|
||||
# request is an injection attempt: scrub the shadows first (so even the
|
||||
# refusal machinery cannot be subverted), then refuse fail-closed.
|
||||
# Imported functions are detected by ENUMERATION, not env-var names: bash
|
||||
# consumes BASH_FUNC_* variables while importing the functions, so the
|
||||
# environment no longer shows them (measured). At this point the script has
|
||||
# defined exactly one function of its own (refuse); anything else in the
|
||||
# function table arrived from the caller's environment. BASH_ENV is checked
|
||||
# directly (it remains visible after sourcing).
|
||||
_injected=0
|
||||
_inj_names=""
|
||||
while IFS=' ' builtin read -r _decl _kind _fn; do
|
||||
[ -n "$_fn" ] || continue
|
||||
case "$_fn" in
|
||||
refuse) ;;
|
||||
*) _injected=1; _inj_names="$_inj_names $_fn";;
|
||||
esac
|
||||
done < <(declare -F)
|
||||
_inj_vars="${!BASH_FUNC_@}"
|
||||
if [ -n "$_inj_vars" ]; then
|
||||
_injected=1
|
||||
for _iv in $_inj_vars; do
|
||||
case "$_iv" in
|
||||
BASH_FUNC_*%%) _ifn="${_iv#BASH_FUNC_}"; _ifn="${_ifn%%%}";;
|
||||
BASH_FUNC_*) _ifn="${_iv#BASH_FUNC_}";;
|
||||
*) _ifn="";;
|
||||
esac
|
||||
[ -n "$_ifn" ] && { unset -f "$_ifn" 2>/dev/null; _inj_names="$_inj_names $_ifn"; }
|
||||
done
|
||||
fi
|
||||
if [ "$_injected" = 1 ] || [ -n "${BASH_ENV:-}" ]; then
|
||||
while IFS=' ' builtin read -r _decl _kind _fn; do
|
||||
[ "$_fn" = refuse ] || unset -f "$_fn" 2>/dev/null
|
||||
done < <(declare -F)
|
||||
unset BASH_ENV 2>/dev/null
|
||||
reason="bash-environment-injection-refused"
|
||||
refuse "The helper's bash startup state was externally shaped: BASH_ENV is
|
||||
set and/or exported BASH_FUNC_* functions are present in the request
|
||||
environment. Non-interactive bash sources BASH_ENV and imports those
|
||||
functions BEFORE any script line runs, so builtins this helper's security
|
||||
decisions rely on could be shadowed. Nothing resolves from a shaped request
|
||||
environment. If this surprised a legitimate workflow, the caller environment
|
||||
must be cleaned (no BASH_ENV, no exported functions) before invoking git."
|
||||
fi
|
||||
|
||||
|
||||
host=""; username_in=""
|
||||
while IFS= builtin read -r line; do
|
||||
[ -z "$line" ] && break
|
||||
case "$line" in
|
||||
host=*) host=${line#host=};;
|
||||
username=*) username_in=${line#username=};;
|
||||
esac
|
||||
done
|
||||
|
||||
# Recognized Gitea hosts carry the per-identity token scheme. Anything else is
|
||||
# declined quietly — another helper owns it, and refusing would break it.
|
||||
case "$host" in
|
||||
git.uscllc.com) idpfx=gitea-usc;;
|
||||
git.mosaicstack.dev) idpfx=gitea-mosaicstack;;
|
||||
*) exit 0;;
|
||||
esac
|
||||
|
||||
# ── Clean-entrypoint assert (P0-SEC R4) ─────────────────────────────────────
|
||||
# This implementation only runs behind the python entrypoint
|
||||
# (git-credential-mosaic), which execve's it with an allowlist environment:
|
||||
# no BASH_ENV, no imported functions, nothing shapable at bash startup. A
|
||||
# direct invocation without the marker is a bypass attempt on that boundary
|
||||
# and refuses. Placed after refuse() and the host parse so the refusal path
|
||||
# exists when it fires (an earlier placement died on 'refuse: command not
|
||||
# found' — the failure mode is real, keep this after every definition it
|
||||
# calls).
|
||||
if [ "${_MOSAIC_HELPER_CLEAN:-}" != "1" ]; then
|
||||
reason="direct-entrypoint-refused"
|
||||
refuse "This implementation refuses to run outside the production
|
||||
entrypoint. git-credential-mosaic (the python wrapper in this directory)
|
||||
execve's it with a hand-built, unshapable environment; invoking the .impl
|
||||
directly bypasses that boundary. Credential requests go through git, which
|
||||
invokes the wrapper named in gitconfig."
|
||||
fi
|
||||
|
||||
ident="$MOSAIC_GIT_IDENTITY"; ident_src="MOSAIC_GIT_IDENTITY"
|
||||
if [ -z "$ident" ]; then
|
||||
ident=$(git config --get mosaic.gitIdentity 2>/dev/null)
|
||||
ident_src="git config mosaic.gitIdentity"
|
||||
fi
|
||||
if [ -z "$ident" ]; then
|
||||
ident="$username_in"
|
||||
ident_src="the username git supplied"
|
||||
fi
|
||||
|
||||
# ── Credential store selection ────────────────────────────────────────────────
|
||||
# An identity is a SEAT or it is a SERVICE, and which one it is determines where
|
||||
# its credential lives. There is no precedence rule between the two stores and no
|
||||
# fallback from one to the other: a seat whose slot is empty fails closed rather
|
||||
# than reading a service credential that happens to share its name.
|
||||
#
|
||||
# seat — <brain>/fleet/agents/<ident>/ exists
|
||||
# credential at <brain>/fleet/agents/<ident>/secrets/<idpfx>-<ident>.token
|
||||
# service — it does not
|
||||
# credential at ~/.config/mosaic/secrets/gitea-tokens/<idpfx>-<ident>.token
|
||||
#
|
||||
# One credential, one location. Two copies of one credential diverge, and the
|
||||
# stale copy fails in a way that reads as a revoked token rather than as drift.
|
||||
#
|
||||
# Brain-home resolution mirrors packages/mosaic/src/fleet/brain-home.ts and
|
||||
# tools/fleet/start-agent-session.sh: MOSAIC_BRAIN_HOME wins, else ~/.mosaic.
|
||||
brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
|
||||
svc_store="$HOME/.config/mosaic/secrets/gitea-tokens"
|
||||
|
||||
# ── Caller-identity ownership (P5-RM-006) ──────────────────────────────────────
|
||||
# A credential request is honourable only when the CALLER owns the identity it
|
||||
# asks for. On a fleet host every seat shares one unix account, so the
|
||||
# launcher-established MOSAIC_AGENT_NAME is the only attribution signal the
|
||||
# helper has. Two measured paths made the old contract unsafe:
|
||||
#
|
||||
# - a seat exporting MOSAIC_GIT_IDENTITY=<another-seat> resolved that seat's
|
||||
# token through the normal precedence chain (T97 G2, jarvis V2 probe), and
|
||||
# - an anonymous caller (no seat name) inherited the host gitconfig's
|
||||
# username=jarvis line and resolved jarvis's slot (T94: five watcher units
|
||||
# flapping on exactly this class).
|
||||
#
|
||||
# Ownership rules, fail-closed on fleet hosts only; a host with no fleet keeps
|
||||
# the legacy contract unchanged:
|
||||
# 1. a SEAT caller may resolve only its own identity;
|
||||
# 2. an anonymous caller may not resolve any SEAT identity (service
|
||||
# identities remain available to non-seat automation such as CI).
|
||||
# [P5-RM-006r1 ancestry binding begin]
|
||||
# ── Caller identity from exec-frozen ancestry (rev-code-02 F1/F2) ───────────
|
||||
# Walk /proc self->root collecting MOSAIC_AGENT_NAME from each ancestor's
|
||||
# frozen environ. Rules:
|
||||
# - any DISAGREEMENT (an ancestor value != the current value, or ancestors
|
||||
# disagreeing among themselves) is a rewrite -> spoof-refused, nothing
|
||||
# resolves. A child can inject variables downward but cannot alter an
|
||||
# ancestor's exec-frozen environ, so the launcher-established value always
|
||||
# participates in the comparison.
|
||||
# - consensus (all ancestors that carry the var agree with the current env,
|
||||
# or with each other when the current env is empty) -> caller = that value.
|
||||
# - no ancestor carries it -> the current claim is unlineaged: caller is
|
||||
# anonymous regardless of what the environment says. A name with no
|
||||
# lineage is a claim, not an identity.
|
||||
# The walk stops at PID 1, at a missing /proc entry, or INCLUSIVE at an
|
||||
# ancestor that carries MOSAIC_CREDENTIAL_LINEAGE_FENCE with an EMPTY agent
|
||||
# name — the test-suite lineage root. A fence beside a non-empty name is
|
||||
# IGNORED and the walk continues, so an attacker cannot fence off the true
|
||||
# ancestry by planting the marker next to a victim name.
|
||||
trusted_caller() {
|
||||
# PATH-HARDENED (rev-code-02 R1 F1): every /proc read below uses ONLY bash
|
||||
# builtins (read/case/parameter expansion). The first implementation piped
|
||||
# through PATH-resolved tr/sed/head/grep, and a caller that prepends hostile
|
||||
# utilities to PATH in the same invocation that overrides the identity
|
||||
# variables could forge the ancestry itself. Builtins cannot be shadowed.
|
||||
local pid ppid v entry line fence
|
||||
local -a vals=()
|
||||
pid=$$
|
||||
while :; do
|
||||
v=""
|
||||
fence=0
|
||||
if [ -r "/proc/$pid/environ" ]; then
|
||||
# Read inside a captured subshell whose stderr is closed: opening
|
||||
# /proc/<pid>/environ can fail with EACCES on ancestors that are
|
||||
# readable-by-mode but not openable (session managers), and that open
|
||||
# failure prints from the SHELL, immune to loop-level 2>/dev/null
|
||||
# (measured). The subshell makes the skip silent; NUL separators are
|
||||
# converted to newlines for the parent's builtin parse.
|
||||
_env_text=$( { while IFS= builtin read -r -d '' _e; do builtin printf '%s\n' "$_e"; done < "/proc/$pid/environ"; } 2>/dev/null )
|
||||
while IFS= builtin read -r entry; do
|
||||
[ -n "$entry" ] || continue
|
||||
case "$entry" in
|
||||
MOSAIC_AGENT_NAME=*) v="${entry#MOSAIC_AGENT_NAME=}";;
|
||||
MOSAIC_CREDENTIAL_LINEAGE_FENCE=*) fence=1;;
|
||||
esac
|
||||
done <<EOF_ENV
|
||||
$_env_text
|
||||
EOF_ENV
|
||||
fi
|
||||
if [ "$pid" != "$$" ]; then
|
||||
[ -n "$v" ] && vals+=("$v")
|
||||
if [ "$fence" = 1 ] && [ -z "$v" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
ppid=""
|
||||
if [ -r "/proc/$pid/status" ]; then
|
||||
while IFS= builtin read -r line; do
|
||||
case "$line" in
|
||||
PPid:*) ppid="${line#PPid:}"; ppid="${ppid//[[:space:]]/}";;
|
||||
esac
|
||||
done < "/proc/$pid/status"
|
||||
fi
|
||||
case "$ppid" in ''|0|1) break;; esac
|
||||
pid=$ppid
|
||||
done
|
||||
local self="${MOSAIC_AGENT_NAME:-}" i consensus=""
|
||||
if [ "${#vals[@]}" -gt 0 ]; then
|
||||
consensus="${vals[0]}"
|
||||
for i in "${vals[@]}"; do
|
||||
if [ "$i" != "$consensus" ]; then
|
||||
printf 'SPOOF'
|
||||
return
|
||||
fi
|
||||
done
|
||||
if [ -n "$self" ] && [ "$self" != "$consensus" ]; then
|
||||
printf 'SPOOF'
|
||||
return
|
||||
fi
|
||||
fi
|
||||
printf '%s' "$consensus"
|
||||
}
|
||||
|
||||
if [ -d "$brain_home/fleet/agents" ]; then
|
||||
caller="$(trusted_caller)"
|
||||
if [ "$caller" = "SPOOF" ]; then
|
||||
reason="caller-identity-spoof-refused"
|
||||
refuse "The MOSAIC_AGENT_NAME lineage disagrees within this process tree:
|
||||
an ancestor established by exec carries a different value than the request.
|
||||
A child process can rewrite its own environment but never an ancestor's
|
||||
frozen environ, so disagreement is a rewrite, not a race. Nothing resolves
|
||||
under a rewritten caller identity. If this surprised a legitimate workflow,
|
||||
run git from the seat's own session, not from a rewritten environment."
|
||||
fi
|
||||
if [ -n "$caller" ] && [ -d "$brain_home/fleet/agents/$caller" ]; then
|
||||
if [ -n "$ident" ] && [ "$ident" != "$caller" ]; then
|
||||
reason="cross-seat-identity-refused"
|
||||
refuse "A seat may resolve only its own credential slot. Caller seat is
|
||||
'$caller' (ancestry-established); the request names '$ident'. Overriding
|
||||
MOSAIC_GIT_IDENTITY (or a git config / URL username) to another seat's name is
|
||||
exactly the path this refusal exists to close. If '$ident' auth is genuinely
|
||||
required, that seat runs the operation itself or the orchestrator provisions
|
||||
an explicit grant."
|
||||
fi
|
||||
else
|
||||
# Anonymous caller on a fleet host (no lineage, or the lineage root is not
|
||||
# a seat): NOTHING resolves — seat slots (T94 jarvis@ class) or legacy
|
||||
# service credentials (rev-code-02 F2: credentialed services are seats;
|
||||
# the legacy store is vestigial and not anonymously reachable).
|
||||
if [ -n "$ident" ]; then
|
||||
ident_kind="${ident_kind:-}"
|
||||
[ -d "$brain_home/fleet/agents/$ident" ] && ident_kind="seat" || ident_kind="service identity"
|
||||
reason="anonymous-credential-refused"
|
||||
refuse "This caller has no seat lineage on a fleet host and asked for
|
||||
'$ident' (a ${ident_kind}). Anonymous callers resolve nothing on fleet hosts:
|
||||
seat credentials must never serve an unattributable caller, and credentialed
|
||||
services are seats with their own sessions (the legacy service store is
|
||||
vestigial). Run from the owning seat's session."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# [P5-RM-006r1 ancestry binding end]
|
||||
|
||||
idtok=""; ident_kind=""
|
||||
if [ -n "$ident" ]; then
|
||||
if [ -d "$brain_home/fleet/agents/$ident" ]; then
|
||||
ident_kind="seat"
|
||||
idtok="$brain_home/fleet/agents/$ident/secrets/${idpfx}-${ident}.token"
|
||||
else
|
||||
ident_kind="service identity"
|
||||
idtok="$svc_store/${idpfx}-${ident}.token"
|
||||
fi
|
||||
if [ -r "$idtok" ]; then
|
||||
# P5-RM-006 seat permissions: a SEAT slot readable by group or other is
|
||||
# provisioning drift, and on a single-account fleet host it widens every
|
||||
# seat's exposure at once. Refuse rather than serve from a loose slot; the
|
||||
# record names the path so the provisioning fix is one chmod away.
|
||||
# Scoped to seat slots: the framework service store is operator-managed
|
||||
# and outside this work unit's permission surface.
|
||||
if [ "${ident_kind:-}" = "seat" ]; then
|
||||
# command -p resolves stat from the POSIX default PATH (system
|
||||
# directories), never the caller's PATH (rev-code-02 R3 B2: a shadowed
|
||||
# stat reported a 0644 slot as 600 and the helper served it). Output is
|
||||
# shape-validated: anything that is not 3-4 octal digits refuses.
|
||||
slot_mode="$(command -p stat -c '%a' "$idtok" 2>/dev/null || true)"
|
||||
case "$slot_mode" in
|
||||
[0-7][0-7][0-7]|[0-7][0-7][0-7][0-7]) ;;
|
||||
*) slot_mode="unverifiable";;
|
||||
esac
|
||||
if [ "${slot_mode:1:2}" != "00" ]; then
|
||||
reason="slot-permission-violation"
|
||||
refuse "Slot $idtok has mode ${slot_mode:-unknown}; expected owner-only
|
||||
(0600 or stricter). Tighten it: chmod 600 '$idtok'. This refusal is the seat
|
||||
permissions half of P5-RM-006: a loose slot on a shared-account host is every
|
||||
seat's exposure, so the helper declines to serve from it. Mode inspection uses
|
||||
command -p (trusted PATH) and fails closed on unverifiable output."
|
||||
fi
|
||||
fi
|
||||
echo "username=${ident}"
|
||||
echo "password=$(<"$idtok")"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Shared-account fallback: ONLY on a host with no fleet and no identity ──────
|
||||
# `fleet/agents` existing is the same signal brain-home.ts uses to decide a brain
|
||||
# is active. Where there are seats, records must be attributable, so an
|
||||
# unresolvable request is refused instead of borrowing the shared account.
|
||||
fleet_present=0
|
||||
[ -d "$brain_home/fleet/agents" ] && fleet_present=1
|
||||
|
||||
if [ -z "$ident" ] && [ "$fleet_present" -eq 0 ]; then
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=../_lib/credentials.sh
|
||||
source "$script_dir/../_lib/credentials.sh"
|
||||
load_credentials "$idpfx" >/dev/null 2>&1 || exit 0
|
||||
# GITEA_USER is not populated by load_credentials (it exports GITEA_URL and
|
||||
# GITEA_TOKEN only). Gitea's git-over-HTTP auth authenticates from the token in
|
||||
# the password field, not from the username string, so any non-empty
|
||||
# placeholder works — deliberately NOT a real account name, since framework
|
||||
# files stay operator-agnostic (tools/quality/scripts/verify-sanitized.sh).
|
||||
echo "username=${GITEA_USER:-git}"
|
||||
echo "password=$GITEA_TOKEN"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── FAIL CLOSED ───────────────────────────────────────────────────────────────
|
||||
# The escalation RECORD is durable and unconditional; any notification built on
|
||||
# top of it is best-effort (see refuse()). Record and alert are deduplicated
|
||||
# separately — a cap on the alert alone lets the spool grow without bound
|
||||
# exactly while the operator is being told nothing, so the louder the failure
|
||||
# the quieter it gets.
|
||||
if [ -z "$ident" ]; then
|
||||
reason="no-identity"
|
||||
else
|
||||
reason="no-token-for-identity"
|
||||
fi
|
||||
refuse "No per-identity credential resolved. This helper does NOT fall back to the shared
|
||||
account: that fallback makes every record it creates attributable to one
|
||||
principal, which is unrecoverable once a pull request has merged under it.
|
||||
|
||||
Fix (pick one):
|
||||
export MOSAIC_GIT_IDENTITY=<agent-id> # process-scoped
|
||||
git config mosaic.gitIdentity <agent-id> # per-repo/worktree, persists
|
||||
Then provision that identity's credential at the path named above. An identity
|
||||
with a directory under \${MOSAIC_BRAIN_HOME:-\$HOME/.mosaic}/fleet/agents/ is a
|
||||
seat and is read ONLY from its own secrets/ slot; any other identity is read from
|
||||
~/.config/mosaic/secrets/gitea-tokens/. There is no fallback between the two.
|
||||
|
||||
If this identity legitimately needs git access and has none, ask the orchestrator
|
||||
to provision one."
|
||||
|
||||
@@ -34,20 +34,23 @@ REPO_DIR="$WORK_DIR/repo"
|
||||
BRAIN_DIR="$WORK_DIR/brain"
|
||||
SPOOL_DIR="$WORK_DIR/spool"
|
||||
SVC_STORE="$FAKE_HOME/.config/mosaic/secrets/gitea-tokens"
|
||||
# Mirror the real deployed layout (~/.config/mosaic/tools/{git,_lib}/) under the
|
||||
# Mirror the real deployed layout (~/.mosaic/tools/{git,_lib}/) under the
|
||||
# fake HOME: git-credential-mosaic resolves its credentials.sh sibling via a
|
||||
# script-relative path (BASH_SOURCE), so the copy must live next to a stubbed
|
||||
# _lib/credentials.sh, not the real one, to keep this test hermetic.
|
||||
HELPER="$FAKE_HOME/.config/mosaic/tools/git/git-credential-mosaic"
|
||||
HELPER="$FAKE_HOME/.mosaic/tools/git/git-credential-mosaic"
|
||||
IMPL="$FAKE_HOME/.mosaic/tools/git/git-credential-mosaic.impl"
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$SVC_STORE" \
|
||||
"$FAKE_HOME/.config/mosaic/tools/git" \
|
||||
"$FAKE_HOME/.config/mosaic/tools/_lib" \
|
||||
"$FAKE_HOME/.mosaic/tools/git" \
|
||||
"$FAKE_HOME/.mosaic/tools/_lib" \
|
||||
"$REPO_DIR" "$BRAIN_DIR"
|
||||
|
||||
cp "$SCRIPT_DIR/git-credential-mosaic" "$HELPER"
|
||||
chmod +x "$HELPER"
|
||||
cp "$SCRIPT_DIR/git-credential-mosaic.impl" "$IMPL"
|
||||
chmod +x "$IMPL"
|
||||
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" config user.email "[email protected]"
|
||||
@@ -55,7 +58,7 @@ git -C "$REPO_DIR" config user.name "Test"
|
||||
|
||||
# Fake shared-account credential loader — stands in for
|
||||
# tools/_lib/credentials.sh's load_credentials(), scoped to this test only.
|
||||
cat > "$FAKE_HOME/.config/mosaic/tools/_lib/credentials.sh" <<'SH'
|
||||
cat > "$FAKE_HOME/.mosaic/tools/_lib/credentials.sh" <<'SH'
|
||||
load_credentials() {
|
||||
case "$1" in
|
||||
gitea-mosaicstack) GITEA_URL="https://git.mosaicstack.dev"; GITEA_TOKEN="shared-mosaicstack-token"; export GITEA_URL GITEA_TOKEN; return 0 ;;
|
||||
@@ -82,7 +85,7 @@ run_helper() {
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$SPOOL_DIR" "$@" \
|
||||
bash "$HELPER" get <<EOF
|
||||
"$HELPER" get <<EOF
|
||||
host=$host
|
||||
username=$username_in
|
||||
|
||||
@@ -90,6 +93,80 @@ EOF
|
||||
)
|
||||
}
|
||||
|
||||
# ── Lineage harness (rev-code-02 F1/F2 rework) ─────────────────────────────
|
||||
# Establishes a seat CALLER the way production does — frozen into an
|
||||
# ancestor's exec environment — instead of injecting MOSAIC_AGENT_NAME into
|
||||
# the helper's own env. Two scripts are generated into WORK_DIR:
|
||||
#
|
||||
# lineage-root.sh (pid A): invoked with a fence marker and NO agent name.
|
||||
# With a non-empty caller arg it forks the carrier (pid C); with an
|
||||
# empty caller it forks the helper directly (deterministic anonymous
|
||||
# lineage even when the suite itself runs inside a seat).
|
||||
# lineage-carrier.sh (pid C): MOSAIC_AGENT_NAME=<caller> frozen at exec;
|
||||
# forks the helper (pid D) with a fully controlled env.
|
||||
#
|
||||
# The helper's walk then sees exactly: self -> C(caller) or D-direct ->
|
||||
# A(fence, empty name -> stop). EXTRA assignments ride pid D's environment
|
||||
# (that is where a rewrite would live — which is the point of the F1 arms).
|
||||
cat > "$WORK_DIR/lineage-root.sh" <<'LINROOT'
|
||||
#!/usr/bin/env bash
|
||||
# pid A — lineage root. Args: <caller> <carrier-script> <helper> <spool>
|
||||
# <brain> <repo> [extra KEY=VALUE...]
|
||||
set -u
|
||||
caller="$1"; carrier="$2"; helper="$3"; spool="$4"; brain="$5"; repo="$6"; shift 6
|
||||
if [ -n "$caller" ]; then
|
||||
env MOSAIC_AGENT_NAME="$caller" PATH="$PATH" HOME="$HOME" \
|
||||
bash "$carrier" "$helper" "$spool" "$brain" "$repo" "$@"
|
||||
else
|
||||
env -i HOME="$HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$spool" \
|
||||
MOSAIC_BRAIN_HOME="$brain" "$@" "$helper" get
|
||||
fi
|
||||
LINROOT
|
||||
cat > "$WORK_DIR/lineage-carrier.sh" <<'LINCARR'
|
||||
#!/usr/bin/env bash
|
||||
# pid C — the caller's frozen environment. Forks the helper (pid D).
|
||||
set -u
|
||||
helper="$1"; spool="$2"; brain="$3"; repo="$4"; shift 4
|
||||
cd "$repo"
|
||||
env -i HOME="$HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$spool" \
|
||||
MOSAIC_BRAIN_HOME="$brain" "$@" "$helper" get
|
||||
LINCARR
|
||||
chmod +x "$WORK_DIR/lineage-root.sh" "$WORK_DIR/lineage-carrier.sh"
|
||||
|
||||
run_lineage() {
|
||||
# run_lineage <caller|empty-for-anonymous> [helper-env KEY=VALUE...]
|
||||
local caller="$1"; shift
|
||||
printf 'host=git.mosaicstack.dev\nusername=probe\n\n' | \
|
||||
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_LINEAGE_FENCE=1 \
|
||||
bash "$WORK_DIR/lineage-root.sh" "$caller" "$WORK_DIR/lineage-carrier.sh" \
|
||||
"$HELPER" "$SPOOL_DIR" "$BRAIN_DIR" "$REPO_DIR" "$@"
|
||||
}
|
||||
|
||||
assert_refused_lineage() {
|
||||
# assert_refused_lineage <desc> <caller> <want-reason> [helper-env...]
|
||||
local desc="$1" caller="$2" want="$3"; shift 3
|
||||
local stderr_file="$WORK_DIR/stderr-lin.tmp" rc stdout
|
||||
: > "$stderr_file"
|
||||
set +e
|
||||
stdout=$(run_lineage "$caller" "$@" 2>"$stderr_file")
|
||||
rc=$?
|
||||
set -e
|
||||
local stderr; stderr=$(cat "$stderr_file")
|
||||
if [[ "$rc" -eq 0 ]]; then
|
||||
echo "FAIL: $desc — expected nonzero exit, got 0 (stdout='$stdout')" >&2; fail=1
|
||||
fi
|
||||
if [[ -n "$stdout" ]]; then
|
||||
echo "FAIL: $desc — expected empty stdout, got '$stdout'" >&2; fail=1
|
||||
fi
|
||||
if [[ -n "$want" && "$stderr" != *"$want"* ]]; then
|
||||
echo "FAIL: $desc — stderr lacks '$want':" >&2; echo "$stderr" >&2; fail=1
|
||||
fi
|
||||
if [[ "$stdout$stderr" == *"seatG-slot-token"* || "$stdout$stderr" == *"seatE-slot-token"* \
|
||||
|| "$stdout$stderr" == *"shared-mosaicstack-token"* || "$stdout$stderr" == *"shared-usc-token"* ]]; then
|
||||
echo "FAIL: $desc — a slot or shared token VALUE appeared in output" >&2; fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
# A refusal must be observable in four independent ways: nonzero exit, EMPTY
|
||||
# stdout, a stderr diagnostic naming the identity and host, and — the assertion
|
||||
# that actually catches a regression to the old behavior — NO shared token value
|
||||
@@ -222,7 +299,10 @@ fi
|
||||
# ---------------------------------------------------------------------------
|
||||
mkdir -p "$BRAIN_DIR/fleet/agents/seatE/secrets"
|
||||
echo -n "seatE-slot-token" > "$BRAIN_DIR/fleet/agents/seatE/secrets/gitea-mosaicstack-seatE.token"
|
||||
out=$(run_helper "git.mosaicstack.dev" "seatE" MOSAIC_BRAIN_HOME="$BRAIN_DIR")
|
||||
chmod 600 "$BRAIN_DIR/fleet/agents/seatE/secrets/gitea-mosaicstack-seatE.token"
|
||||
# Seat arms run through the lineage harness below (rev-code-02 F1/F2 rework):
|
||||
# a seat caller must be established by ancestry, not by the helper's own env.
|
||||
out=$(run_lineage seatE MOSAIC_AGENT_NAME=seatE MOSAIC_GIT_IDENTITY=seatE)
|
||||
assert_eq "seat reads its own slot: username" "username=seatE" "$(echo "$out" | grep '^username=')"
|
||||
assert_eq "seat reads its own slot: password" "password=seatE-slot-token" "$(echo "$out" | grep '^password=')"
|
||||
|
||||
@@ -236,8 +316,8 @@ assert_eq "seat reads its own slot: password" "password=seatE-slot-token" "$(ech
|
||||
# ---------------------------------------------------------------------------
|
||||
mkdir -p "$BRAIN_DIR/fleet/agents/seatF/secrets"
|
||||
echo -n "seatF-SERVICE-STORE-token" > "$SVC_STORE/gitea-mosaicstack-seatF.token"
|
||||
assert_fail_closed "seat with empty slot does NOT fall back to the framework store" \
|
||||
"git.mosaicstack.dev" "seatF" "fleet/agents/seatF/secrets" MOSAIC_BRAIN_HOME="$BRAIN_DIR"
|
||||
assert_refused_lineage "seat with empty slot does NOT fall back to the framework store" \
|
||||
seatF no-token-for-identity MOSAIC_AGENT_NAME=seatF MOSAIC_GIT_IDENTITY=seatF
|
||||
: > "$WORK_DIR/stderr.tmp"
|
||||
set +e
|
||||
xstore_out=$(run_helper "git.mosaicstack.dev" "seatF" MOSAIC_BRAIN_HOME="$BRAIN_DIR" 2>"$WORK_DIR/stderr.tmp")
|
||||
@@ -288,7 +368,7 @@ assert_eq "unknown host on a fleet host: still passthrough, not a refusal" "" "$
|
||||
# 13. Non-"get" verb (store/erase) -> exit 0, no output (git-credential
|
||||
# protocol: this helper only implements get).
|
||||
# ---------------------------------------------------------------------------
|
||||
store_out=$(cd "$REPO_DIR" && env -i HOME="$FAKE_HOME" PATH="$PATH" bash "$HELPER" store <<EOF
|
||||
store_out=$(cd "$REPO_DIR" && env -i HOME="$FAKE_HOME" PATH="$PATH" "$HELPER" store <<EOF
|
||||
host=git.mosaicstack.dev
|
||||
username=no-such-agent
|
||||
password=whatever
|
||||
@@ -310,7 +390,7 @@ hostile_spool="$WORK_DIR/spool-hostile"
|
||||
cd "$hostile_dir"
|
||||
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$hostile_spool" \
|
||||
MOSAIC_GIT_IDENTITY=no-such-agent \
|
||||
bash "$HELPER" get <<EOF >/dev/null 2>&1
|
||||
"$HELPER" get <<EOF >/dev/null 2>&1
|
||||
host=git.mosaicstack.dev
|
||||
username=no-such-agent
|
||||
|
||||
@@ -349,7 +429,7 @@ nospool_err=$(
|
||||
cd "$REPO_DIR"
|
||||
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$unwritable_spool" \
|
||||
MOSAIC_GIT_IDENTITY=no-such-agent \
|
||||
bash "$HELPER" get <<EOF 2>&1 >/dev/null
|
||||
"$HELPER" get <<EOF 2>&1 >/dev/null
|
||||
host=git.mosaicstack.dev
|
||||
username=no-such-agent
|
||||
|
||||
@@ -365,6 +445,193 @@ if [[ "$nospool_err" != *"NOT WRITTEN"* ]]; then
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 11. P5-RM-006 (+r1 rework) — caller identity from exec-frozen ancestry.
|
||||
# A seat caller is established by lineage, not by the helper's own env;
|
||||
# disagreement anywhere in the lineage is a rewrite and refuses; an
|
||||
# anonymous caller resolves NOTHING on a fleet host (seat or service);
|
||||
# a loose seat-slot mode refuses. Enforcement-removal red control at 11h.
|
||||
# ---------------------------------------------------------------------------
|
||||
mkdir -p "$BRAIN_DIR/fleet/agents/seatG/secrets"
|
||||
echo -n "seatG-slot-token" > "$BRAIN_DIR/fleet/agents/seatG/secrets/gitea-mosaicstack-seatG.token"
|
||||
chmod 600 "$BRAIN_DIR/fleet/agents/seatG/secrets/gitea-mosaicstack-seatG.token"
|
||||
|
||||
# 11a. Cross-seat negative (lineage seatE, env ident=seatG): still refused.
|
||||
assert_refused_lineage "seat cannot override identity to another seat's slot" \
|
||||
seatE cross-seat-identity-refused MOSAIC_GIT_IDENTITY=seatG
|
||||
|
||||
# 11b. Seat asking for a SERVICE identity: cross-seat territory.
|
||||
assert_refused_lineage "seat cannot resolve a service identity either" \
|
||||
seatE cross-seat-identity-refused MOSAIC_GIT_IDENTITY=agentA
|
||||
|
||||
# 11c. Anonymous caller asking for a seat slot is refused.
|
||||
assert_refused_lineage "anonymous caller cannot resolve a seat slot on a fleet host" \
|
||||
"" anonymous-credential-refused MOSAIC_GIT_IDENTITY=seatG
|
||||
|
||||
# 11d. Anonymous caller asking for a SERVICE identity: ALSO refused
|
||||
# (rev-code-02 F2 — credentialed services are seats; the legacy store is
|
||||
# not anonymously reachable on fleet hosts).
|
||||
assert_refused_lineage "anonymous caller cannot resolve a legacy service credential either" \
|
||||
"" anonymous-credential-refused MOSAIC_GIT_IDENTITY=agentA
|
||||
|
||||
# 11e. Slot permissions: a group-readable slot is refused; mode restored -> serves.
|
||||
chmod 644 "$BRAIN_DIR/fleet/agents/seatG/secrets/gitea-mosaicstack-seatG.token"
|
||||
assert_refused_lineage "loose slot mode is refused" \
|
||||
seatG slot-permission-violation MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG
|
||||
chmod 600 "$BRAIN_DIR/fleet/agents/seatG/secrets/gitea-mosaicstack-seatG.token"
|
||||
out=$(run_lineage seatG MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG)
|
||||
assert_eq "mode restored to 600: seatG serves again" "password=seatG-slot-token" "$(echo "$out" | grep '^password=')"
|
||||
|
||||
# 11f. rev-code-02 F1 repro: dual-variable override (caller lineage seatE,
|
||||
# helper env carrying MOSAIC_AGENT_NAME=seatG AND MOSAIC_GIT_IDENTITY=seatG).
|
||||
assert_refused_lineage "F1: dual MOSAIC_AGENT_NAME+MOSAIC_GIT_IDENTITY override refused" \
|
||||
seatE caller-identity-spoof-refused MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG
|
||||
|
||||
# 11g. Stripped lineage still serves the rightful seat: caller frozen at the
|
||||
# ancestor, helper env clean (self empty), own ident.
|
||||
out=$(run_lineage seatE MOSAIC_GIT_IDENTITY=seatE)
|
||||
assert_eq "lineage consensus with stripped self still serves the owning seat" \
|
||||
"password=seatE-slot-token" "$(echo "$out" | grep '^password=')"
|
||||
|
||||
# 11h. RED CONTROL: delete the ancestry binding between markers from a copy
|
||||
# of the IMPLEMENTATION (run directly with the clean marker — a red control
|
||||
# measures the binding itself, deliberately not through the wrapper);
|
||||
# the F1 dual-override request must then RESOLVE seatG's token — the
|
||||
# exact measured failure — proving the binding is the enforcement.
|
||||
RED_HELPER="$WORK_DIR/red/git-credential-mosaic.impl"
|
||||
mkdir -p "$WORK_DIR/red"
|
||||
sed '/P5-RM-006r1 ancestry binding begin/,/P5-RM-006r1 ancestry binding end/d' "$IMPL" > "$RED_HELPER"
|
||||
chmod +x "$RED_HELPER"
|
||||
if cmp -s "$IMPL" "$RED_HELPER"; then
|
||||
echo "FAIL: red control is vacuous — marker deletion removed nothing" >&2
|
||||
fail=1
|
||||
fi
|
||||
set +e
|
||||
red_out=$(printf 'host=git.mosaicstack.dev\nusername=probe\n\n' | \
|
||||
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_LINEAGE_FENCE=1 \
|
||||
bash "$WORK_DIR/lineage-root.sh" seatE "$WORK_DIR/lineage-carrier.sh" \
|
||||
"$RED_HELPER" "$SPOOL_DIR" "$BRAIN_DIR" "$REPO_DIR" \
|
||||
_MOSAIC_HELPER_CLEAN=1 MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG 2>/dev/null)
|
||||
set -e
|
||||
if [[ "$(echo "$red_out" | grep '^password=')" != "password=seatG-slot-token" ]]; then
|
||||
echo "FAIL: red control — with the binding removed, the F1 dual-override should have resolved seatG's token, got: $red_out" >&2
|
||||
fail=1
|
||||
else
|
||||
echo "ok: red control — binding removed -> F1 dual-override resolves the victim token (the binding is the enforcement)"
|
||||
fi
|
||||
|
||||
# 11h2. rev-code-02 R1 F1: PATH-shadowed tr/sed/head/grep must not forge the
|
||||
# ancestry. The dual-override request runs with a hostile PATH whose
|
||||
# utilities claim the victim name for every /proc read; the walker uses
|
||||
# only bash builtins, so the shadows never execute and the refusal holds.
|
||||
HOSTILE_BIN="$WORK_DIR/hostile-bin"
|
||||
mkdir -p "$HOSTILE_BIN"
|
||||
for tool in tr sed head grep cat stat; do
|
||||
printf '#!/usr/bin/env bash\ncat >/dev/null\necho "MOSAIC_AGENT_NAME=seatG"\nexit 0\n' > "$HOSTILE_BIN/$tool"
|
||||
chmod +x "$HOSTILE_BIN/$tool"
|
||||
done
|
||||
assert_refused_lineage "F1-R1: hostile PATH utilities cannot forge ancestry (dual override still refused)" \
|
||||
seatE caller-identity-spoof-refused \
|
||||
MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG PATH="$HOSTILE_BIN:$PATH"
|
||||
|
||||
# 11i. Service automation integration arm (rev-code-02 R2 bar): a SERVICE
|
||||
# seat bound by lineage resolves its own slot — the brain-git-sync shape.
|
||||
mkdir -p "$BRAIN_DIR/fleet/agents/svc-fixture/secrets"
|
||||
echo -n "svc-fixture-slot-token" > "$BRAIN_DIR/fleet/agents/svc-fixture/secrets/gitea-mosaicstack-svc-fixture.token"
|
||||
chmod 600 "$BRAIN_DIR/fleet/agents/svc-fixture/secrets/gitea-mosaicstack-svc-fixture.token"
|
||||
out=$(run_lineage svc-fixture MOSAIC_AGENT_NAME=svc-fixture MOSAIC_GIT_IDENTITY=svc-fixture)
|
||||
assert_eq "service automation with bound seat lineage resolves its own slot" \
|
||||
"password=svc-fixture-slot-token" "$(echo "$out" | grep '^password=')"
|
||||
|
||||
# 11j. rev-code-02 R3 B1: BASH_ENV shaping. A read() shadow defined through
|
||||
# BASH_ENV must be refused before any resolution — the guard scrubs and
|
||||
# refuses with bash-environment-injection-refused.
|
||||
INJ_SH="$WORK_DIR/inj-read.sh"
|
||||
printf 'read() { builtin read -r _x || return 0; printf "MOSAIC_AGENT_NAME=seatG\\n"; return 0; }\n' > "$INJ_SH"
|
||||
assert_refused_lineage "F1-R3: BASH_ENV read() shadow is dropped at the wrapper boundary (identity gate governs)" \
|
||||
seatE caller-identity-spoof-refused \
|
||||
MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG BASH_ENV="$INJ_SH"
|
||||
|
||||
# 11k. rev-code-02 R3 B1: exported functions (BASH_FUNC_* import) refused too.
|
||||
assert_refused_lineage "F1-R3: exported BASH_FUNC_* import never crosses the wrapper boundary (identity gate governs)" \
|
||||
seatE caller-identity-spoof-refused \
|
||||
MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG \
|
||||
'BASH_FUNC_read%%=() { builtin read -r _x || return 0; printf "MOSAIC_AGENT_NAME=seatG\\n"; return 0; }'
|
||||
|
||||
# 11l. rev-code-02 R3 B2: hostile stat cannot launder a loose slot. Own-slot
|
||||
# lineage (legit caller), 0644 slot, PATH-shadowed stat reporting 600 —
|
||||
# mode inspection must come from the trusted PATH and still refuse.
|
||||
chmod 644 "$BRAIN_DIR/fleet/agents/svc-fixture/secrets/gitea-mosaicstack-svc-fixture.token"
|
||||
printf '#!/usr/bin/env bash\necho 600\n' > "$HOSTILE_BIN/stat"
|
||||
assert_refused_lineage "F2-R3: hostile stat cannot make a 0644 slot pass as 600 (own-slot path)" \
|
||||
svc-fixture slot-permission-violation \
|
||||
MOSAIC_AGENT_NAME=svc-fixture MOSAIC_GIT_IDENTITY=svc-fixture PATH="$HOSTILE_BIN:$PATH"
|
||||
chmod 600 "$BRAIN_DIR/fleet/agents/svc-fixture/secrets/gitea-mosaicstack-svc-fixture.token"
|
||||
|
||||
# 11m. rev-code-02 R3 probe 1 through the PRODUCTION ENTRYPOINT: BASH_ENV
|
||||
# defines unset()/exit() no-ops (defeating in-bash scrub/termination).
|
||||
# The python wrapper never passes BASH_ENV across the boundary, so the
|
||||
# implementation cannot be shaped and the dual override still refuses.
|
||||
INJ_P1="$WORK_DIR/inj-probe1.sh"
|
||||
cat > "$INJ_P1" <<'P1'
|
||||
unset() { return 0; }
|
||||
exit() { return 0; }
|
||||
read() { builtin read -r _x || return 0; printf 'MOSAIC_AGENT_NAME=seatG\n'; return 0; }
|
||||
P1
|
||||
assert_refused_lineage "F1-R4 probe1: BASH_ENV unset/exit no-ops cannot shape the helper (wrapper boundary)" \
|
||||
seatE caller-identity-spoof-refused \
|
||||
MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG BASH_ENV="$INJ_P1"
|
||||
|
||||
# 11n. rev-code-02 R3 probe 2 through the PRODUCTION ENTRYPOINT: declare()
|
||||
# hides imported functions, unsets the marker, printf() forges the
|
||||
# ancestry. Dropped at the wrapper boundary; refusal holds.
|
||||
INJ_P2="$WORK_DIR/inj-probe2.sh"
|
||||
cat > "$INJ_P2" <<'P2'
|
||||
declare() { return 0; }
|
||||
printf() { builtin printf '%s' "MOSAIC_AGENT_NAME=seatG"; return 0; }
|
||||
read() { builtin read -r _x || return 0; printf 'MOSAIC_AGENT_NAME=seatG\n'; return 0; }
|
||||
P2
|
||||
assert_refused_lineage "F1-R4 probe2: declare-hide + printf-forge cannot shape the helper (wrapper boundary)" \
|
||||
seatE caller-identity-spoof-refused \
|
||||
MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG BASH_ENV="$INJ_P2"
|
||||
|
||||
# 11o. RED CONTROL for the wrapper boundary (enforcement-removal): invoke the
|
||||
# IMPLEMENTATION directly, bypassing the wrapper, with the PROBE-1 shape
|
||||
# (unset/exit no-ops) and a forged clean marker — exactly the falsified
|
||||
# in-bash world the reviewer measured: the refusal prints, exit is
|
||||
# no-oped, execution continues, and the forged ancestry SERVES seatG.
|
||||
# The wrapper boundary is the enforcement; this arm proves it bites.
|
||||
set +e
|
||||
bypass_out=$(cd "$REPO_DIR" && printf 'host=git.mosaicstack.dev\nusername=probe\n\n' | \
|
||||
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$SPOOL_DIR" \
|
||||
MOSAIC_BRAIN_HOME="$BRAIN_DIR" _MOSAIC_HELPER_CLEAN=1 BASH_ENV="$INJ_P1" \
|
||||
MOSAIC_AGENT_NAME=seatG MOSAIC_GIT_IDENTITY=seatG \
|
||||
bash "$IMPL" get 2>/dev/null)
|
||||
set -e
|
||||
if [[ "$(echo "$bypass_out" | grep -c '^password=')" -lt 1 ]]; then
|
||||
echo "FAIL: wrapper red control — direct shaped .impl should have served (wrapper is the enforcement), got: $bypass_out" >&2
|
||||
fail=1
|
||||
else
|
||||
echo "ok: red control — wrapper bypassed + probe1 shape serves (the wrapper boundary is the enforcement)"
|
||||
fi
|
||||
|
||||
# 11p. Direct .impl invocation WITHOUT the clean marker: refused by the
|
||||
# implementation's own entrypoint assert.
|
||||
set +e
|
||||
direct_out=$(cd "$REPO_DIR" && printf 'host=git.mosaicstack.dev\nusername=probe\n\n' | \
|
||||
env -i HOME="$FAKE_HOME" PATH="$PATH" MOSAIC_CREDENTIAL_SPOOL="$SPOOL_DIR" \
|
||||
MOSAIC_BRAIN_HOME="$BRAIN_DIR" MOSAIC_GIT_IDENTITY=seatE \
|
||||
bash "$IMPL" get 2>"$WORK_DIR/stderr-direct.tmp")
|
||||
direct_rc=$?
|
||||
set -e
|
||||
if [[ "$direct_rc" -eq 0 || -n "$direct_out" ]]; then
|
||||
echo "FAIL: direct .impl without marker must refuse (got rc=$direct_rc out='$direct_out')" >&2
|
||||
fail=1
|
||||
elif ! grep -q 'direct-entrypoint-refused' "$WORK_DIR/stderr-direct.tmp"; then
|
||||
echo "FAIL: direct .impl refusal lacks direct-entrypoint-refused" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
if [[ "$fail" -eq 0 ]]; then
|
||||
echo "git-credential-mosaic identity resolution regression passed"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { Command } from 'commander';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { registerAgentCommand, runEnroll, runGetEnrollment } from './agent.js';
|
||||
import { enrollAgent, fetchEnrollment } from '../tui/gateway-api.js';
|
||||
|
||||
/**
|
||||
* CLI-parity witness for the agent enrollment family (design
|
||||
* docs/plans/2026-08-29-agent-enrollment-command-design.md §5 item 10;
|
||||
* contract 5 §4.5): `mosaic agent enroll` / `mosaic agent enrollment <id>`
|
||||
* invoke the same gateway commands with the same request/result/error
|
||||
* contracts the web client uses. The gateway side of the same routes is
|
||||
* witnessed in apps/gateway/src/enrollment/enrollment-commands.integration.test.ts.
|
||||
*/
|
||||
|
||||
const gateway = 'https://gateway.example.test';
|
||||
const auth = { gateway, cookie: 'session=test' };
|
||||
|
||||
const agentBody = {
|
||||
id: '3fca4f6a-1111-4222-8333-444455556666',
|
||||
name: 'Nova',
|
||||
provider: 'anthropic',
|
||||
model: 'claude-test',
|
||||
status: 'idle',
|
||||
harness: 'fake-harness',
|
||||
persona: null,
|
||||
ownerId: 'user-1',
|
||||
enrolledAt: '2026-08-29T00:00:00.000Z',
|
||||
createdAt: '2026-08-29T00:00:00.000Z',
|
||||
};
|
||||
|
||||
const okResponse = (correlationId: string) =>
|
||||
new Response(JSON.stringify({ ok: true, correlationId, agent: agentBody }), {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
process.exitCode = undefined;
|
||||
});
|
||||
|
||||
describe('agent enrollment CLI registration', (): void => {
|
||||
it('registers enroll and enrollment subcommands under `mosaic agent`', () => {
|
||||
const program = new Command();
|
||||
const cmd = registerAgentCommand(program);
|
||||
const names = cmd.commands.map((c) => c.name());
|
||||
expect(names).toContain('enroll');
|
||||
expect(names).toContain('enrollment');
|
||||
});
|
||||
|
||||
it('the enroll subcommand exposes no argv flag that carries a credential value', () => {
|
||||
const program = new Command();
|
||||
const cmd = registerAgentCommand(program);
|
||||
const enroll = cmd.commands.find((c) => c.name() === 'enroll');
|
||||
expect(enroll).toBeDefined();
|
||||
const flags = (enroll as Command).options.map((o) => o.flags);
|
||||
// --credential selects the MODE only; the intake value arrives via stdin.
|
||||
expect(flags).toContain('--credential <mode>');
|
||||
for (const flag of flags) {
|
||||
expect(flag).not.toMatch(/value|key <secret>|api-key/i);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('agent.enroll CLI parity', (): void => {
|
||||
it('posts the §3.1 request shape for reference mode and surfaces the typed result', async () => {
|
||||
const fetchMock = vi.fn(async () => okResponse('corr-ref'));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
await runEnroll(auth, {
|
||||
gateway,
|
||||
harness: 'fake-harness',
|
||||
name: 'Nova',
|
||||
model: 'claude-test',
|
||||
provider: 'anthropic',
|
||||
credential: 'reference',
|
||||
idempotencyKey: '9c1a26be-0000-4000-8000-000000000001',
|
||||
correlationId: 'corr-ref',
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
||||
expect(url).toBe(`${gateway}/api/enrollment/agents`);
|
||||
expect(init.method).toBe('POST');
|
||||
expect(JSON.parse(init.body as string)).toEqual({
|
||||
harness: 'fake-harness',
|
||||
name: 'Nova',
|
||||
model: 'claude-test',
|
||||
provider: 'anthropic',
|
||||
credential: { mode: 'reference' },
|
||||
idempotencyKey: '9c1a26be-0000-4000-8000-000000000001',
|
||||
correlationId: 'corr-ref',
|
||||
});
|
||||
expect(log.mock.calls.flat().join('\n')).toContain(agentBody.id);
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('intake mode reads the credential from the injected stdin reader, never argv', async () => {
|
||||
const fetchMock = vi.fn(async () => okResponse('corr-intake'));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
await runEnroll(
|
||||
auth,
|
||||
{
|
||||
gateway,
|
||||
harness: 'fake-harness',
|
||||
name: 'Nova',
|
||||
model: 'claude-test',
|
||||
provider: 'anthropic',
|
||||
credential: 'intake',
|
||||
idempotencyKey: '9c1a26be-0000-4000-8000-000000000002',
|
||||
},
|
||||
async () => 'stdin-provided-key',
|
||||
);
|
||||
|
||||
const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
|
||||
const body = JSON.parse(init.body as string) as { credential: Record<string, string> };
|
||||
expect(body.credential).toEqual({
|
||||
mode: 'intake',
|
||||
type: 'api_key',
|
||||
value: 'stdin-provided-key',
|
||||
});
|
||||
});
|
||||
|
||||
it('an empty intake credential refuses locally with no gateway call', async () => {
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
await runEnroll(
|
||||
auth,
|
||||
{
|
||||
gateway,
|
||||
harness: 'fake-harness',
|
||||
name: 'Nova',
|
||||
model: 'claude-test',
|
||||
provider: 'anthropic',
|
||||
credential: 'intake',
|
||||
},
|
||||
async () => '',
|
||||
);
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it('preserves the gateway refusal contract (closed error enum + correlation) in the CLI error', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
statusCode: 409,
|
||||
error: 'conflict',
|
||||
message: 'idempotency conflict',
|
||||
correlationId: 'corr-409',
|
||||
}),
|
||||
{ status: 409, headers: { 'Content-Type': 'application/json' } },
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
enrollAgent(gateway, auth.cookie, {
|
||||
harness: 'fake-harness',
|
||||
name: 'Nova',
|
||||
model: 'claude-test',
|
||||
provider: 'anthropic',
|
||||
credential: { mode: 'reference' },
|
||||
idempotencyKey: '9c1a26be-0000-4000-8000-000000000003',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'Failed to enroll agent (409): {"statusCode":409,"error":"conflict",' +
|
||||
'"message":"idempotency conflict","correlationId":"corr-409"}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('agent.enrollment.get CLI parity', (): void => {
|
||||
it('reads one enrollment with the correlation envelope on the query string', async () => {
|
||||
const fetchMock = vi.fn(async () => okResponse('corr-get'));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
|
||||
await runGetEnrollment(auth, agentBody.id, 'corr-get');
|
||||
|
||||
const [url] = fetchMock.mock.calls[0] as unknown as [string];
|
||||
expect(url).toBe(`${gateway}/api/enrollment/agents/${agentBody.id}?correlationId=corr-get`);
|
||||
expect(log.mock.calls.flat().join('\n')).toContain('corr-get');
|
||||
});
|
||||
|
||||
it('preserves the folded not_found contract in the CLI error', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
statusCode: 404,
|
||||
error: 'not_found',
|
||||
message: 'agent not found',
|
||||
correlationId: 'corr-404',
|
||||
}),
|
||||
{ status: 404, headers: { 'Content-Type': 'application/json' } },
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await expect(fetchEnrollment(gateway, auth.cookie, agentBody.id)).rejects.toThrow(
|
||||
'Failed to get enrollment (404): {"statusCode":404,"error":"not_found",' +
|
||||
'"message":"agent not found","correlationId":"corr-404"}',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Command } from 'commander';
|
||||
import { registerFleetAgentCommands, type FleetCommandDeps } from './fleet.js';
|
||||
import { withAuth } from './with-auth.js';
|
||||
@@ -9,8 +10,10 @@ import {
|
||||
deleteAgentConfig,
|
||||
fetchProjects,
|
||||
fetchProviders,
|
||||
enrollAgent,
|
||||
fetchEnrollment,
|
||||
} from '../tui/gateway-api.js';
|
||||
import type { AgentConfigInfo } from '../tui/gateway-api.js';
|
||||
import type { AgentConfigInfo, EnrolledAgentInfo } from '../tui/gateway-api.js';
|
||||
|
||||
function formatAgent(a: AgentConfigInfo): string {
|
||||
const sys = a.isSystem ? ' [system]' : '';
|
||||
@@ -75,11 +78,140 @@ export function registerAgentCommand(program: Command, fleetDeps: FleetCommandDe
|
||||
},
|
||||
);
|
||||
|
||||
registerEnrollmentCommands(cmd);
|
||||
registerFleetAgentCommands(cmd, fleetDeps);
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
// ── Agent enrollment (design docs/plans/2026-08-29-agent-enrollment-command-design.md §3;
|
||||
// CLI parity bound by contract 5 §4.5) ──
|
||||
|
||||
export interface EnrollCommandOptions {
|
||||
gateway: string;
|
||||
harness: string;
|
||||
name: string;
|
||||
model: string;
|
||||
provider: string;
|
||||
persona?: string;
|
||||
credential: string;
|
||||
idempotencyKey?: string;
|
||||
correlationId?: string;
|
||||
replayMode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an intake credential value from stdin. Never accepted via argv — a
|
||||
* process argument is world-readable in `ps` for the process lifetime.
|
||||
*/
|
||||
export async function readCredentialFromStdin(): Promise<string> {
|
||||
if (process.stdin.isTTY) {
|
||||
console.error('Enter API key, then press Enter and Ctrl-D:');
|
||||
}
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of process.stdin) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
}
|
||||
return Buffer.concat(chunks)
|
||||
.toString('utf8')
|
||||
.replace(/\r?\n$/, '');
|
||||
}
|
||||
|
||||
function showEnrollment(correlationId: string, agent: EnrolledAgentInfo): void {
|
||||
console.log(` ID: ${agent.id}`);
|
||||
console.log(` Name: ${agent.name}`);
|
||||
console.log(` Harness: ${agent.harness ?? '—'}`);
|
||||
console.log(` Provider: ${agent.provider}`);
|
||||
console.log(` Model: ${agent.model}`);
|
||||
console.log(` Status: ${agent.status}`);
|
||||
console.log(` Owner: ${agent.ownerId ?? '—'}`);
|
||||
console.log(` Enrolled: ${agent.enrolledAt ?? '—'}`);
|
||||
console.log(` Correlation: ${correlationId}`);
|
||||
}
|
||||
|
||||
export async function runEnroll(
|
||||
auth: { gateway: string; cookie: string },
|
||||
opts: EnrollCommandOptions,
|
||||
readSecret: () => Promise<string> = readCredentialFromStdin,
|
||||
): Promise<void> {
|
||||
if (opts.credential !== 'reference' && opts.credential !== 'intake') {
|
||||
console.error(`Unknown credential mode "${opts.credential}" (use reference or intake).`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let credential: { mode: 'reference' } | { mode: 'intake'; type: 'api_key'; value: string };
|
||||
if (opts.credential === 'intake') {
|
||||
const value = await readSecret();
|
||||
if (!value) {
|
||||
console.error('Intake credential requires a non-empty API key on stdin.');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
credential = { mode: 'intake', type: 'api_key', value };
|
||||
} else {
|
||||
credential = { mode: 'reference' };
|
||||
}
|
||||
|
||||
const result = await enrollAgent(auth.gateway, auth.cookie, {
|
||||
harness: opts.harness,
|
||||
name: opts.name,
|
||||
model: opts.model,
|
||||
provider: opts.provider,
|
||||
...(opts.persona !== undefined ? { persona: opts.persona } : {}),
|
||||
credential,
|
||||
idempotencyKey: opts.idempotencyKey ?? randomUUID(),
|
||||
...(opts.correlationId !== undefined ? { correlationId: opts.correlationId } : {}),
|
||||
...(opts.replayMode !== undefined ? { replayMode: opts.replayMode } : {}),
|
||||
});
|
||||
|
||||
console.log(`Agent "${result.agent.name}" enrolled.\n`);
|
||||
showEnrollment(result.correlationId, result.agent);
|
||||
}
|
||||
|
||||
export async function runGetEnrollment(
|
||||
auth: { gateway: string; cookie: string },
|
||||
agentId: string,
|
||||
correlationId?: string,
|
||||
): Promise<void> {
|
||||
const result = await fetchEnrollment(auth.gateway, auth.cookie, agentId, correlationId);
|
||||
showEnrollment(result.correlationId, result.agent);
|
||||
}
|
||||
|
||||
export function registerEnrollmentCommands(cmd: Command): void {
|
||||
cmd
|
||||
.command('enroll')
|
||||
.description('Enroll an agent through the gateway enrollment command (agent.enroll)')
|
||||
.requiredOption('--harness <id>', 'Harness the agent runs on (must be registered)')
|
||||
.requiredOption('--name <name>', 'Agent display name')
|
||||
.requiredOption('--model <model>', 'Model identifier')
|
||||
.requiredOption('--provider <provider>', 'Provider the credential belongs to')
|
||||
.option('--persona <text>', 'Agent persona / system prompt')
|
||||
.option(
|
||||
'--credential <mode>',
|
||||
'Credential mode: "reference" (already stored) or "intake" (API key read from stdin, never argv)',
|
||||
'reference',
|
||||
)
|
||||
.option('--idempotency-key <uuid>', 'Idempotency key (generated when omitted)')
|
||||
.option('--correlation-id <uuid>', 'Correlation id to carry through the audit trail')
|
||||
.option('--replay-mode <mode>', 'Idempotency replay mode (actor-bound)')
|
||||
.action(async (opts: Omit<EnrollCommandOptions, 'gateway'>) => {
|
||||
const parent = cmd.opts<{ gateway: string }>();
|
||||
const auth = await withAuth(parent.gateway);
|
||||
await runEnroll(auth, { ...opts, gateway: parent.gateway });
|
||||
});
|
||||
|
||||
cmd
|
||||
.command('enrollment <agentId>')
|
||||
.description('Read one enrolled agent (agent.enrollment.get; owner or admin)')
|
||||
.option('--correlation-id <uuid>', 'Correlation id to carry through the read')
|
||||
.action(async (agentId: string, opts: { correlationId?: string }) => {
|
||||
const parent = cmd.opts<{ gateway: string }>();
|
||||
const auth = await withAuth(parent.gateway);
|
||||
await runGetEnrollment(auth, agentId, opts.correlationId);
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveAgent(
|
||||
gateway: string,
|
||||
cookie: string,
|
||||
|
||||
@@ -173,6 +173,8 @@ describe('registerFleetCommand', () => {
|
||||
expect(agent!.options.map((option) => option.long)).toContain('--list');
|
||||
expect(agent!.commands.map((command) => command.name()).sort()).toEqual([
|
||||
'comms-block',
|
||||
'enroll',
|
||||
'enrollment',
|
||||
'reset',
|
||||
'roster',
|
||||
'send',
|
||||
|
||||
@@ -148,6 +148,26 @@ describe.skipIf(!hasBash)('bash ↔ TS manifest parity (§6.1)', () => {
|
||||
const manifest = loadManifest(FRAMEWORK_ROOT);
|
||||
expect(bashSubtreeRoots().sort()).toEqual(frameworkSubtreeRoots(manifest).sort());
|
||||
});
|
||||
|
||||
it('fleet/bin ownership is exact and does not prune existing executables (T110 B1)', () => {
|
||||
// fleet/bin carries estate executables this package does not ship. A
|
||||
// subtree glob here would classify them framework-owned and keep-mode
|
||||
// update would prune them. The manifest must own EXACTLY the two shipped
|
||||
// launcher files and nothing else in fleet/bin, on BOTH resolvers.
|
||||
const manifest = loadManifest(FRAMEWORK_ROOT);
|
||||
const expected: Array<[string, string]> = [
|
||||
['fleet/bin/mosaic', 'framework'],
|
||||
['fleet/bin/test-mosaic-launcher.sh', 'framework'],
|
||||
['fleet/bin/seat-up.sh', 'operator'], // shipped-by-estate, unshipped here
|
||||
['fleet/bin/launch-seat.sh', 'operator'],
|
||||
['fleet/bin', 'operator'], // the directory itself is unlisted
|
||||
];
|
||||
for (const [path, want] of expected) {
|
||||
const ts = resolveOwnership(manifest, path);
|
||||
expect(ts).toBe(want);
|
||||
expect(bashResolve(path)).toBe(want);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -562,6 +562,74 @@ export async function fetchInteractionHealth(gatewayUrl: string): Promise<unknow
|
||||
return handleResponse<unknown>(res, 'Failed to get interaction readiness');
|
||||
}
|
||||
|
||||
// ── Agent Enrollment types (design docs/plans/2026-08-29-agent-enrollment-command-design.md §3) ──
|
||||
|
||||
export interface EnrolledAgentInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
status: string;
|
||||
harness: string | null;
|
||||
persona: string | null;
|
||||
ownerId: string | null;
|
||||
enrolledAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface EnrollAgentRequest {
|
||||
harness: string;
|
||||
name: string;
|
||||
persona?: string;
|
||||
model: string;
|
||||
provider: string;
|
||||
credential: { mode: 'reference' } | { mode: 'intake'; type: 'api_key'; value: string };
|
||||
idempotencyKey: string;
|
||||
correlationId?: string;
|
||||
replayMode?: string;
|
||||
}
|
||||
|
||||
export interface EnrollmentOutcome {
|
||||
ok: true;
|
||||
correlationId: string;
|
||||
agent: EnrolledAgentInfo;
|
||||
}
|
||||
|
||||
// ── Agent Enrollment endpoints ──
|
||||
|
||||
/**
|
||||
* agent.enroll. The gateway's typed refusal body (closed error enum +
|
||||
* correlationId) is preserved verbatim in the thrown error, so the CLI
|
||||
* surfaces the same contract the web client receives.
|
||||
*/
|
||||
export async function enrollAgent(
|
||||
gatewayUrl: string,
|
||||
sessionCookie: string,
|
||||
data: EnrollAgentRequest,
|
||||
): Promise<EnrollmentOutcome> {
|
||||
const res = await fetch(`${gatewayUrl}/api/enrollment/agents`, {
|
||||
method: 'POST',
|
||||
headers: jsonHeaders(sessionCookie, gatewayUrl),
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return handleResponse<EnrollmentOutcome>(res, 'Failed to enroll agent');
|
||||
}
|
||||
|
||||
/** agent.enrollment.get: owner-or-admin read of one enrolled agent. */
|
||||
export async function fetchEnrollment(
|
||||
gatewayUrl: string,
|
||||
sessionCookie: string,
|
||||
agentId: string,
|
||||
correlationId?: string,
|
||||
): Promise<EnrollmentOutcome> {
|
||||
const params = correlationId ? `?${new URLSearchParams({ correlationId }).toString()}` : '';
|
||||
const res = await fetch(
|
||||
`${gatewayUrl}/api/enrollment/agents/${encodeURIComponent(agentId)}${params}`,
|
||||
{ headers: headers(sessionCookie, gatewayUrl) },
|
||||
);
|
||||
return handleResponse<EnrollmentOutcome>(res, 'Failed to get enrollment');
|
||||
}
|
||||
|
||||
// ── Conversation Message types ──
|
||||
|
||||
export interface ConversationMessage {
|
||||
|
||||
Executable
+176
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hermetic structural check for the explicit dogfood Compose overlay.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
mkdir -p "$tmp/worktree" "$tmp/common.git" "$tmp/seat/secrets"
|
||||
|
||||
base_config_json=$(
|
||||
cd "$repo_root"
|
||||
BETTER_AUTH_SECRET=test-only-not-a-credential \
|
||||
docker compose --profile stack config --format json
|
||||
)
|
||||
|
||||
BASE_CONFIG_JSON="$base_config_json" python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
config = json.loads(os.environ["BASE_CONFIG_JSON"])
|
||||
gateway = config["services"]["gateway"]
|
||||
env = gateway["environment"]
|
||||
for key in (
|
||||
"MOSAIC_AGENT_NAME",
|
||||
"MOSAIC_GIT_IDENTITY",
|
||||
"MOSAIC_BRAIN_HOME",
|
||||
"AGENT_FILE_SANDBOX_DIR",
|
||||
"AGENT_USER_TOOLS",
|
||||
"AGENT_SHELL_ENABLED",
|
||||
"AGENT_DELIVERY_ENABLED",
|
||||
"MOSAIC_GIT_TOOLS_DIR",
|
||||
"MOSAIC_INTEGRATION_TRUNK",
|
||||
):
|
||||
assert key not in env, f"base compose unexpectedly sets dogfood variable {key}"
|
||||
|
||||
targets = {mount["target"] for mount in gateway["volumes"]}
|
||||
assert "/workspace/stack" not in targets
|
||||
assert not any(target.startswith("/opt/mosaic/brain/") for target in targets)
|
||||
PY
|
||||
|
||||
config_json=$(
|
||||
cd "$repo_root"
|
||||
BETTER_AUTH_SECRET=test-only-not-a-credential \
|
||||
MOSAIC_DOGFOOD_WORKTREE="$tmp/worktree" \
|
||||
MOSAIC_DOGFOOD_COMMON_GIT_DIR="$tmp/common.git" \
|
||||
MOSAIC_DOGFOOD_SEAT_HOME="$tmp/seat" \
|
||||
docker compose \
|
||||
-f docker-compose.yml \
|
||||
-f docker-compose.dogfood.yml \
|
||||
--profile stack \
|
||||
config --format json
|
||||
)
|
||||
|
||||
CONFIG_JSON="$config_json" EXPECT_WORKTREE="$tmp/worktree" EXPECT_COMMON_GIT="$tmp/common.git" EXPECT_SEAT="$tmp/seat" python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
config = json.loads(os.environ["CONFIG_JSON"])
|
||||
gateway = config["services"]["gateway"]
|
||||
assert gateway.get("init") is True, "gateway must run below an init process for R4 lineage"
|
||||
env = gateway["environment"]
|
||||
|
||||
expected_env = {
|
||||
"MOSAIC_AGENT_NAME": "code-dogfood-01",
|
||||
"MOSAIC_GIT_IDENTITY": "code-dogfood-01",
|
||||
"MOSAIC_BRAIN_HOME": "/opt/mosaic/brain",
|
||||
"AGENT_FILE_SANDBOX_DIR": "/workspace/stack",
|
||||
"AGENT_SHELL_ENABLED": "false",
|
||||
"AGENT_DELIVERY_ENABLED": "true",
|
||||
"MOSAIC_GIT_TOOLS_DIR": "/opt/mosaic/tools/git",
|
||||
"MOSAIC_INTEGRATION_TRUNK": "next",
|
||||
}
|
||||
for key, value in expected_env.items():
|
||||
assert env.get(key) == value, f"{key}: expected {value!r}, got {env.get(key)!r}"
|
||||
|
||||
allowed = set(env["AGENT_USER_TOOLS"].split(","))
|
||||
assert allowed == {
|
||||
"fs_read_file",
|
||||
"fs_write_file",
|
||||
"fs_list_directory",
|
||||
"fs_edit_file",
|
||||
"git_status",
|
||||
"git_log",
|
||||
"git_diff",
|
||||
"git_publish_branch",
|
||||
"git_open_pull_request",
|
||||
}, f"unexpected dogfood tool set: {sorted(allowed)}"
|
||||
assert "shell_exec" not in allowed
|
||||
|
||||
mounts = {mount["target"]: mount for mount in gateway["volumes"]}
|
||||
worktree = mounts["/workspace/stack"]
|
||||
assert worktree["type"] == "bind"
|
||||
assert worktree["source"] == os.environ["EXPECT_WORKTREE"]
|
||||
assert not worktree.get("read_only", False), "dogfood worktree must be writable"
|
||||
|
||||
common_git = mounts[os.environ["EXPECT_COMMON_GIT"]]
|
||||
assert common_git["type"] == "bind"
|
||||
assert common_git["source"] == os.environ["EXPECT_COMMON_GIT"]
|
||||
assert not common_git.get("read_only", False), "common Git directory must accept branch updates"
|
||||
|
||||
seat = mounts["/opt/mosaic/brain/fleet/agents/code-dogfood-01"]
|
||||
assert seat["type"] == "bind"
|
||||
assert seat["source"] == os.environ["EXPECT_SEAT"]
|
||||
assert seat.get("read_only") is True, "seat credential slot must be read-only"
|
||||
|
||||
other_seat_mounts = [
|
||||
target
|
||||
for target in mounts
|
||||
if target.startswith("/opt/mosaic/brain/fleet/agents/")
|
||||
and target != "/opt/mosaic/brain/fleet/agents/code-dogfood-01"
|
||||
]
|
||||
assert other_seat_mounts == [], f"other seat mounts leaked: {other_seat_mounts}"
|
||||
PY
|
||||
|
||||
# Each required path must fail closed rather than falling back to the current checkout.
|
||||
expect_missing_path() {
|
||||
local missing=$1 output rc
|
||||
set +e
|
||||
case "$missing" in
|
||||
MOSAIC_DOGFOOD_WORKTREE)
|
||||
output=$(
|
||||
cd "$repo_root"
|
||||
env -u MOSAIC_DOGFOOD_WORKTREE \
|
||||
BETTER_AUTH_SECRET=test-only-not-a-credential \
|
||||
MOSAIC_DOGFOOD_COMMON_GIT_DIR="$tmp/common.git" \
|
||||
MOSAIC_DOGFOOD_SEAT_HOME="$tmp/seat" \
|
||||
docker compose -f docker-compose.yml -f docker-compose.dogfood.yml \
|
||||
--profile stack config 2>&1
|
||||
)
|
||||
rc=$?
|
||||
;;
|
||||
MOSAIC_DOGFOOD_COMMON_GIT_DIR)
|
||||
output=$(
|
||||
cd "$repo_root"
|
||||
env -u MOSAIC_DOGFOOD_COMMON_GIT_DIR \
|
||||
BETTER_AUTH_SECRET=test-only-not-a-credential \
|
||||
MOSAIC_DOGFOOD_WORKTREE="$tmp/worktree" \
|
||||
MOSAIC_DOGFOOD_SEAT_HOME="$tmp/seat" \
|
||||
docker compose -f docker-compose.yml -f docker-compose.dogfood.yml \
|
||||
--profile stack config 2>&1
|
||||
)
|
||||
rc=$?
|
||||
;;
|
||||
MOSAIC_DOGFOOD_SEAT_HOME)
|
||||
output=$(
|
||||
cd "$repo_root"
|
||||
env -u MOSAIC_DOGFOOD_SEAT_HOME \
|
||||
BETTER_AUTH_SECRET=test-only-not-a-credential \
|
||||
MOSAIC_DOGFOOD_WORKTREE="$tmp/worktree" \
|
||||
MOSAIC_DOGFOOD_COMMON_GIT_DIR="$tmp/common.git" \
|
||||
docker compose -f docker-compose.yml -f docker-compose.dogfood.yml \
|
||||
--profile stack config 2>&1
|
||||
)
|
||||
rc=$?
|
||||
;;
|
||||
*)
|
||||
echo "FAIL: test requested unknown path variable $missing" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
set -e
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
echo "FAIL: dogfood compose accepted missing $missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$output" != *"$missing"* ]]; then
|
||||
echo "FAIL: missing-path failure did not name $missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
expect_missing_path MOSAIC_DOGFOOD_WORKTREE
|
||||
expect_missing_path MOSAIC_DOGFOOD_COMMON_GIT_DIR
|
||||
expect_missing_path MOSAIC_DOGFOOD_SEAT_HOME
|
||||
|
||||
printf 'dogfood compose verification passed\n'
|
||||
Reference in New Issue
Block a user