Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068d0f9b1c | ||
|
|
24bbd40dc7 |
@@ -48,7 +48,7 @@ mosaic wizard # Full guided setup (gateway install → verify)
|
||||
|
||||
### Requirements
|
||||
|
||||
- Node.js ≥ 22
|
||||
- Node.js ≥ 20
|
||||
- npm (for global @mosaicstack/mosaic install)
|
||||
- One or more runtimes: [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai), or [Pi](https://github.com/mariozechner/pi-coding-agent)
|
||||
|
||||
@@ -200,7 +200,7 @@ Consent state is persisted in config. Remote upload is a no-op until you run `mo
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js ≥ 22
|
||||
- Node.js ≥ 20
|
||||
- pnpm 10.6+
|
||||
- Docker & Docker Compose
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mosaic</title>
|
||||
<meta name="description" content="Mosaic Stack Dashboard" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap"
|
||||
/>
|
||||
<script>
|
||||
// set data-theme before first paint so the stored theme never flashes
|
||||
(function () {
|
||||
try {
|
||||
var theme = window.localStorage.getItem('mosaic-theme') || 'dark';
|
||||
document.documentElement.setAttribute('data-theme', theme === 'light' ? 'light' : 'dark');
|
||||
} catch (error) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,7 +4,9 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node ../../scripts/build-web.mjs",
|
||||
"build:vite": "vite build",
|
||||
"dev": "next dev",
|
||||
"dev:vite": "vite",
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
@@ -19,6 +21,7 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.18.2",
|
||||
"socket.io-client": "^4.8.0",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
@@ -28,9 +31,11 @@
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"jsdom": "^29.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^2.0.0"
|
||||
"vite": "^8.2.1",
|
||||
"vitest": "^3.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { ThemeProvider } from '@/providers/theme-provider';
|
||||
import { createAppRouter } from '@/routes';
|
||||
import '@/app/globals.css';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
if (!container) {
|
||||
throw new Error('missing #root element');
|
||||
}
|
||||
|
||||
createRoot(container).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider>
|
||||
<RouterProvider router={createAppRouter()} />
|
||||
</ThemeProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createBrowserRouter, Navigate, type RouteObject } from 'react-router-dom';
|
||||
import { AuthGuard, GuestGuard } from '@/spa/guards';
|
||||
import { Placeholder } from '@/spa/placeholder';
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
{
|
||||
element: <GuestGuard />,
|
||||
children: [
|
||||
{ path: '/login', element: <Placeholder title="Login" /> },
|
||||
{ path: '/register', element: <Placeholder title="Register" /> },
|
||||
{ path: '/auth/provider/:provider', element: <Placeholder title="Signing in" /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <AuthGuard />,
|
||||
children: [
|
||||
{ path: '/', element: <Navigate to="/chat" replace /> },
|
||||
{ path: '/chat', element: <Placeholder title="Chat" /> },
|
||||
{ path: '/projects', element: <Placeholder title="Projects" /> },
|
||||
{ path: '/projects/:id', element: <Placeholder title="Project" /> },
|
||||
{ path: '/tasks', element: <Placeholder title="Tasks" /> },
|
||||
{ path: '/settings', element: <Placeholder title="Settings" /> },
|
||||
{ path: '/admin', element: <Placeholder title="Admin" /> },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function createAppRouter(): ReturnType<typeof createBrowserRouter> {
|
||||
return createBrowserRouter(routes);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
// P1 shells: session-aware redirects arrive with the reworked auth client in P2.
|
||||
|
||||
export function GuestGuard(): ReactElement {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function AuthGuard(): ReactElement {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
export function Placeholder({ title }: { title: string }): ReactElement {
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center">
|
||||
<h1 className="text-xl font-medium">{title}</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import { routes } from '@/routes';
|
||||
|
||||
function collectPaths(routeObjects: RouteObject[]): string[] {
|
||||
return routeObjects.flatMap((route) => [
|
||||
...(route.path ? [route.path] : []),
|
||||
...(route.children ? collectPaths(route.children) : []),
|
||||
]);
|
||||
}
|
||||
|
||||
describe('SPA route table', () => {
|
||||
it('covers every v1 parity route from the Phase P RFC', () => {
|
||||
expect(collectPaths(routes).sort()).toEqual(
|
||||
[
|
||||
'/',
|
||||
'/admin',
|
||||
'/auth/provider/:provider',
|
||||
'/chat',
|
||||
'/login',
|
||||
'/projects',
|
||||
'/projects/:id',
|
||||
'/register',
|
||||
'/settings',
|
||||
'/tasks',
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('separates guest and authenticated route groups', () => {
|
||||
const guestPaths = collectPaths(routes.at(0)?.children ?? []);
|
||||
const authPaths = collectPaths(routes.at(1)?.children ?? []);
|
||||
expect(guestPaths).toContain('/login');
|
||||
expect(guestPaths).not.toContain('/chat');
|
||||
expect(authPaths).toContain('/chat');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// The proxy exists only in dev; in production the SPA is same-origin with the gateway
|
||||
// (served by it under Candidate A, or behind one FQDN under Candidate B) and every
|
||||
// request uses a relative path, so no origin may ever be configured here or in src/.
|
||||
const gatewayTarget = 'http://localhost:14242';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3100,
|
||||
proxy: {
|
||||
'/api': gatewayTarget,
|
||||
'/socket.io': { target: gatewayTarget, ws: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,16 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
// tsconfig uses "jsx": "preserve" for Next; tests need esbuild to compile it
|
||||
esbuild: {
|
||||
jsx: 'automatic',
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
||||
# WebUI Fleet Bridge Planning Scratchpad
|
||||
|
||||
**Mode:** Task 0 docs-only decision PR authorized; Task 1 and all executable/runtime work remain blocked.
|
||||
**Owner:** Velma
|
||||
**Opened:** 2026-08-09
|
||||
**Scope:** One enrolled Agent Host launching one native `mosaic claude` OAuth session and streaming it into `apps/web` through `apps/gateway`.
|
||||
|
||||
## Objective
|
||||
|
||||
Turn the approved direction in `jarvis-brain/docs/scratchpads/MOSAIC-WEBUI-FLEET-BRIDGE.md` into a test-first implementation plan while preserving Fred's harness-home/launcher contract and Scooby's greenfield safety findings.
|
||||
|
||||
## Source reconciliation
|
||||
|
||||
- Current `origin/main`: `b0f7d26dd9c14d91eaaefc35d6c9fd6618a0bd92`.
|
||||
- Current `origin/next`: `4df478cdd150fdf8d52ea109f02ade5d85017acd`.
|
||||
- Branches currently diverge (`main` has 11 unique commits; `next` has 13). `next` contains local-tier Redis fix #689; `main` contains later fleet/shell fixes.
|
||||
- Fred's three-root harness-home design and promotion stack are not yet fully present on either baseline.
|
||||
- Therefore neither current SHA is an acceptable implementation pin. Code may begin only from a Fred-certified reconciled SHA containing the required launcher/home contract and safe Gateway startup prerequisites.
|
||||
- The deployed `mosaic.woltje.com` v0.0.20 UI remains reference-only.
|
||||
|
||||
## Verified seams
|
||||
|
||||
- Current browser chat uses an in-process Pi SDK session.
|
||||
- `AgentRuntimeProvider` supports list/tree/stream/send/attach/detach/terminate, but not create/start.
|
||||
- `InteractionController` enrolls an already-existing runtime session; it cannot launch one.
|
||||
- Hermes is the only runtime provider registered in Gateway.
|
||||
- Tmux streaming is explicitly unsupported and remains out of scope.
|
||||
- `mosaic claude` is the authoritative launcher and accepts Claude's machine-facing stream-json flags.
|
||||
- Installed discovery version: Claude Code 2.1.226. Target Distrobox version must be independently pinned and certified.
|
||||
|
||||
## Non-negotiable dependencies
|
||||
|
||||
1. Fred approves the machine-facing launcher/seat-home contract before code.
|
||||
2. No WebUI/Gateway direct read of lease broker state, daemon socket, or state files (F-V3).
|
||||
3. No provider OAuth token leaves the Agent Host.
|
||||
4. No local Gateway/Web startup around the KBN/database hold.
|
||||
5. Greenfield work runs in a Debian Distrobox with an isolated home.
|
||||
6. The initial plan PR targets `next`; Fred binds D1–D15 on its exact head before issue/PRD/tracker completion or implementation work.
|
||||
|
||||
## Reproducible evidence
|
||||
|
||||
Run from a clean Stack clone:
|
||||
|
||||
```bash
|
||||
git fetch origin main next
|
||||
git rev-parse origin/main origin/next
|
||||
git rev-list --left-right --count origin/main...origin/next
|
||||
rg -n "interface AgentRuntimeProvider|createSession|streamEvents|terminate" \
|
||||
packages/types/src/agent packages/agent/src apps/gateway/src/agent
|
||||
rg -n "AgentService\.prompt|interaction_sessions|createRuntimeTerminationApproval" \
|
||||
apps/gateway/src packages/db/src/schema.ts
|
||||
```
|
||||
|
||||
Primary inspected source seams:
|
||||
|
||||
- `packages/types/src/agent/agent-runtime-provider.ts`
|
||||
- `packages/agent/src/{runtime-provider-registry,hermes-runtime-provider,matrix-native-runtime-provider,tmux-fleet-runtime-provider}.ts`
|
||||
- `apps/gateway/src/agent/{runtime-provider-registry.service,interaction.controller,durable-session.repository,durable-session.service}.ts`
|
||||
- `apps/gateway/src/chat/chat.gateway.ts`
|
||||
- `packages/mosaic/src/commands/{launch,interaction}.ts`
|
||||
- `packages/mosaic/src/fleet/generated-env-boundary.ts`
|
||||
- `packages/db/src/schema.ts`
|
||||
- `apps/web/src/app/(dashboard)/chat/page.tsx`
|
||||
|
||||
Planning-only investigation transcripts are local and intentionally uncommitted:
|
||||
|
||||
- `/tmp/velma-plan-stack-surface.txt`
|
||||
- `/tmp/velma-plan-structure.txt`
|
||||
- `/tmp/velma-plan-scooby.txt`
|
||||
- `/tmp/velma-plan-runtime-contract.txt`
|
||||
|
||||
## Source findings that constrain the design
|
||||
|
||||
- `interaction_sessions.id` is the stable primary key; there are no create/policy/enrollment/state columns.
|
||||
- `interaction_outbox` has a unique `(session_id, idempotency_key)` index and only `pending | processing | delivered`.
|
||||
- Baseline `DurableSessionRepository.create()` can replace provider/runtime identity for the same owner; M1 must remove that implicit mutation.
|
||||
- Baseline termination approval is Redis-backed and currently consumes separately from PostgreSQL; M1 therefore needs durable authorization acceptance before destructive token deletion/dispatch.
|
||||
- Existing interaction HTTP base is `/api/interaction/:agentName`; the plan extends it rather than inventing a second route family.
|
||||
- Baseline `launch.ts`/lease launcher still use ambient lookup/literal interpreters. Section 4.1 is non-binding consumer input to Fred's W-F design; W-F's final resolved-launch contract must exist in the certified base before Velma can certify it.
|
||||
- Root `pnpm test` is not KBN-safe: it includes PGlite migration and framework-shell/lease-broker suites.
|
||||
|
||||
## Independent draft review
|
||||
|
||||
Seven adversarial review rounds found and drove explicit fixes for:
|
||||
|
||||
- a candidate resolved-seat consumer descriptor and threat model, now explicitly non-binding input to Fred's W-F-owned launcher design;
|
||||
- one active launch per stable conversation, exact pending/failed encodings, durable pre-dispatch reservation, CAS activation, and crash lookup without a migration;
|
||||
- generation-bound enrollment, command/event revalidation, `SIGHUP` config epochs, and stale-epoch rejection;
|
||||
- Redis/PostgreSQL exact-stop crash safety via non-destructive verify, durable authorization acceptance, atomic claim/`GETDEL`, and same-operation status reconciliation;
|
||||
- sequence gaps/reorder limits, deterministic UUIDv5 completion, and Gateway restart fail-closed behavior;
|
||||
- shared streaming redaction before host ring/transport and again before Gateway persistence/browser;
|
||||
- D2/D12-selected path-free provenance and a separate safe browser DTO—never raw or hashed path strings;
|
||||
- exact F-V3 boundary: bridge has no broker API, while Fred's sealed launcher may enforce broker policy internally;
|
||||
- migration-free focused tests in Tasks 1–8, with live OAuth, repository transaction, Gateway/Web, and Playwright restricted to the Fred/Scooby-certified Task 9 path;
|
||||
- candidate private/public commitment and artifact-binding mechanisms that W-F may accept, simplify, replace, or defer;
|
||||
- JCS event-digest recomputation and equal/different duplicate handling in both accepted and future-buffer states;
|
||||
- a safe browser presentation DTO for host/workspace/seat/persona labels, readiness, and connection state;
|
||||
- explicit `OnApplicationBootstrap` create/stop recovery enumeration with no auto-launch;
|
||||
- all POSIX/Windows/UNC/file/tilde path classes in the streaming redactor and definitive failed-stop response semantics;
|
||||
- the full 14-column canonical task schema, fake-only Task 5 repository tests, migration-free certified Task 9 DB test, server-owned operation correlations/routes, and exact merged-SHA smoke;
|
||||
- per-commit independent review, queue guards, exact-head PR review, squash merge, exact merged-next SHA/CI wait, worktree-bound smoke, issue-state readback, and reviewed tracker-closure PR with its own merged-next CI;
|
||||
- a private DB URL loader captured/exported per session without recording or echoing the credential;
|
||||
- an attached Task 9 implementation branch, provider-filtered exact issue-state readback, and machine-verified smoke JSON binding source/worktree/deployed SHA;
|
||||
- a capped, duplicate-key-rejecting, exact-key/type smoke schema so report extensions or JSON boolean/float coercion cannot smuggle data or fake child-count evidence.
|
||||
|
||||
The plan remains deliberately **decision-PR-ready, not implementation-ready**. Fred authorized only Task 0's initial two-document PR on `next`. He must still return every Section 2 value, replace all `[FRED-GATE]` entries, certify storage/startup, and provide the W-F-dependent `IMPLEMENTATION_BASE_SHA` before Task 0 closes or Task 1 starts.
|
||||
|
||||
## Current status
|
||||
|
||||
- Gitea principal verified as `velma`; helper and API wrapper resolution are fail-closed and correct.
|
||||
- Fred authorized the initial Task 0 docs-only PR in `comms/20260809T094952Z__from-fred__ec0e85.md` and confirmed its `next` lane/W-F descriptor corrections in `comms/20260809T095437Z__from-fred__4ee79d.md`.
|
||||
- Provisional decisions: D1/D8/D9/D10/D13/D14 approved; D4 tool labels exactly `{Read, Grep, Glob}`; D5/D12 provisional; D11 remains a single-operator seam; D2/D3/D6/D7 and `IMPLEMENTATION_BASE_SHA` are `BLOCKED-ON-W-F`.
|
||||
- Awaiting exact plan-PR-head review and Fred's complete D1–D15 binding contract.
|
||||
- No source code, database, deployment, or live runtime changes made.
|
||||
Generated
+774
-71
File diff suppressed because it is too large
Load Diff
@@ -12,23 +12,6 @@ MOSAIC_HOME="$TMP/mosaic"
|
||||
STATE="$TMP/state"
|
||||
LOG="$TMP/npm.log"
|
||||
mkdir -p "$FAKE_BIN" "$HOME_DIR" "$STATE"
|
||||
REAL_NODE="$(command -v node)"
|
||||
export MOSAIC_TEST_REAL_NODE="$REAL_NODE"
|
||||
|
||||
cat > "$FAKE_BIN/node" <<'FAKE_NODE'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "$*" == *'process.versions.node.split'* ]]; then
|
||||
printf '%s' "${MOSAIC_TEST_NODE_MAJOR:-22}"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${1:-}" == "--version" ]]; then
|
||||
printf 'v%s.0.0\n' "${MOSAIC_TEST_NODE_MAJOR:-22}"
|
||||
exit 0
|
||||
fi
|
||||
exec "${MOSAIC_TEST_REAL_NODE:?}" "$@"
|
||||
FAKE_NODE
|
||||
chmod +x "$FAKE_BIN/node"
|
||||
|
||||
cat > "$FAKE_BIN/npm" <<'FAKE_NPM'
|
||||
#!/usr/bin/env bash
|
||||
@@ -152,20 +135,6 @@ reset_state() {
|
||||
rm -f "$STATE"/*
|
||||
}
|
||||
|
||||
reset_state
|
||||
echo "[test] --next rejects Node 20 before any install action"
|
||||
if OUTPUT="$(
|
||||
HOME="$HOME_DIR" MOSAIC_HOME="$MOSAIC_HOME" MOSAIC_PREFIX="$PREFIX" MOSAIC_NO_COLOR=1 \
|
||||
MOSAIC_TEST_NPM_LOG="$LOG" MOSAIC_TEST_STATE="$STATE" MOSAIC_TEST_REAL_NODE="$REAL_NODE" \
|
||||
MOSAIC_TEST_NODE_MAJOR=20 PATH="$FAKE_BIN:$PATH" \
|
||||
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch 2>&1
|
||||
)"; then
|
||||
echo "expected Node 20 next-lane install to fail" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -qF 'Node.js >= 22 required for the --next lane' <<<"$OUTPUT"
|
||||
[[ ! -s "$LOG" ]] || { echo "Node 20 gate ran npm actions" >&2; exit 1; }
|
||||
|
||||
reset_state
|
||||
echo "[test] --next fast path pins resolved package versions"
|
||||
OUTPUT="$(
|
||||
|
||||
@@ -526,10 +526,6 @@ if [[ "$NODE_MAJOR" -lt 20 ]]; then
|
||||
fail "Node.js >= 20 required (found v$(node --version))"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$FLAG_NEXT" == "true" && "$NODE_MAJOR" -lt 22 ]]; then
|
||||
fail "Node.js >= 22 required for the --next lane (found v$(node --version))"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "${BOLD}Mosaic Stack Installer${RESET}"
|
||||
|
||||
Reference in New Issue
Block a user