From 6fbe8b5bc6db33dbf55a4ebefefd41aa8b460d06 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Sun, 26 Jul 2026 22:57:03 +1000 Subject: [PATCH 001/129] fix: verify clean workspace installs --- .github/workflows/ci.yml | 5 +- package.json | 1 + packages/database/package.json | 1 + packages/database/src/bootstrap.ts | 206 +++++++++++++----- packages/database/src/seed-data.ts | 4 +- packages/database/src/seed.ts | 4 + packages/database/src/verify-clean-install.ts | 52 +++++ playwright.clean.config.ts | 25 +++ scripts/generate-screenshots.sh | 3 +- tests/clean-install.spec.ts | 45 ++++ 10 files changed, 289 insertions(+), 57 deletions(-) create mode 100644 packages/database/src/verify-clean-install.ts create mode 100644 playwright.clean.config.ts create mode 100644 tests/clean-install.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbc9565..c54a063 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,8 @@ jobs: - name: Build database dependencies run: pnpm exec turbo build --filter=@muster/database - run: pnpm db:migrate - - run: pnpm db:seed + - run: pnpm db:bootstrap + - run: pnpm db:verify-clean - run: pnpm lint - run: pnpm typecheck - run: pnpm test:unit @@ -60,6 +61,8 @@ jobs: git diff --exit-code -- packages/database/migrations - run: pnpm build - run: pnpm exec playwright install --with-deps chromium + - run: pnpm exec playwright test --config=playwright.clean.config.ts + - run: MUSTER_DEMO_MODE=true pnpm db:seed - run: pnpm exec playwright test tests/muster.spec.ts --project=chromium - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() diff --git a/package.json b/package.json index 64063e3..0f08eee 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "db:generate": "pnpm --filter @muster/database generate", "db:migrate": "pnpm --filter @muster/database migrate", "db:bootstrap": "pnpm --filter @muster/database bootstrap", + "db:verify-clean": "pnpm --filter @muster/database verify-clean", "db:seed": "pnpm --filter @muster/database seed", "screenshots": "playwright test tests/screenshots.spec.ts", "format": "prettier --write .", diff --git a/packages/database/package.json b/packages/database/package.json index 4425fba..cda7c19 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -28,6 +28,7 @@ "generate": "drizzle-kit generate", "migrate": "tsx src/migrate.ts", "bootstrap": "tsx src/bootstrap.ts", + "verify-clean": "tsx src/verify-clean-install.ts", "seed": "tsx src/seed.ts" }, "dependencies": { diff --git a/packages/database/src/bootstrap.ts b/packages/database/src/bootstrap.ts index 5216559..b20f808 100644 --- a/packages/database/src/bootstrap.ts +++ b/packages/database/src/bootstrap.ts @@ -1,6 +1,6 @@ import { sql } from "drizzle-orm"; import { closeDatabase, database, schema } from "./index.ts"; -import { demoIds } from "./seed-data.ts"; +import { starterIds } from "./seed-data.ts"; const db = database(); const organisationName = @@ -61,7 +61,7 @@ const administratorCapabilities = [ await db .insert(schema.organisations) .values({ - id: demoIds.organisation, + id: starterIds.organisation, name: organisationName, slug: organisationSlug, dataRegion: process.env.MUSTER_DATA_REGION?.trim() || "local", @@ -83,16 +83,16 @@ await db .insert(schema.actors) .values([ { - id: demoIds.actors.jordan, - organisationId: demoIds.organisation, + id: starterIds.actors.jordan, + organisationId: starterIds.organisation, actorType: "human", displayName: "Muster Administrator", identityReference: administratorEmail, capabilityAssignments: administratorCapabilities, }, { - id: demoIds.actors.triage, - organisationId: demoIds.organisation, + id: starterIds.actors.triage, + organisationId: starterIds.organisation, actorType: "agent", displayName: "Alfie", identityReference: "agent:alfie-threat-research", @@ -104,8 +104,8 @@ await db ], }, { - id: demoIds.actors.tawnyHunt, - organisationId: demoIds.organisation, + id: starterIds.actors.tawnyHunt, + organisationId: starterIds.organisation, actorType: "agent", displayName: "Jessie", identityReference: "agent:jessie-hunt", @@ -121,8 +121,8 @@ await db ], }, { - id: demoIds.actors.threatIntel, - organisationId: demoIds.organisation, + id: starterIds.actors.threatIntel, + organisationId: starterIds.organisation, actorType: "agent", displayName: "Parker", identityReference: "agent:parker-executive", @@ -134,8 +134,8 @@ await db ], }, { - id: demoIds.actors.system, - organisationId: demoIds.organisation, + id: starterIds.actors.system, + organisationId: starterIds.organisation, actorType: "system", displayName: "Muster", identityReference: "system:muster", @@ -151,23 +151,121 @@ await db }, }); +await db + .insert(schema.agentDefinitions) + .values([ + { + id: starterIds.actors.triage, + organisationId: starterIds.organisation, + name: "Alfie", + description: + "Researches approved public and vendor sources and produces evidence-backed security briefs.", + runtime: "codex-subscription", + model: process.env.MUSTER_CODEX_MODEL?.trim() || "configured", + ownerActorId: starterIds.actors.jordan, + systemPromptVersion: "alfie-v1", + allowedTools: ["alerts.read", "kelpie.cases.read", "sentinel.rules.read"], + allowedRooms: [starterIds.rooms.soc, starterIds.rooms.triageDirect], + capabilityRequirements: [ + "alerts.read", + "kelpie.cases.read", + "sentinel.rules.read", + ], + maximumRuntimeSeconds: 900, + maximumTokenBudget: 30_000, + maximumCostCents: 500, + approvalRequirements: { externalWrites: "human" }, + }, + { + id: starterIds.actors.tawnyHunt, + organisationId: starterIds.organisation, + name: "Jessie", + description: + "Runs bounded threat hunts, maps observables to ATT&CK, and prepares governed case enrichment.", + runtime: "codex-subscription", + model: process.env.MUSTER_CODEX_MODEL?.trim() || "configured", + ownerActorId: starterIds.actors.jordan, + systemPromptVersion: "jessie-v1", + allowedTools: [ + "tawny.telemetry.read", + "tawny.hunts.execute", + "sentinel.query.execute", + "kelpie.cases.read", + ], + allowedRooms: [starterIds.rooms.soc, starterIds.rooms.tawnyDirect], + capabilityRequirements: [ + "tawny.telemetry.read", + "tawny.hunts.execute", + "sentinel.query.execute", + "kelpie.cases.read", + ], + maximumRuntimeSeconds: 1_800, + maximumTokenBudget: 40_000, + maximumCostCents: 750, + approvalRequirements: { externalWrites: "human" }, + }, + { + id: starterIds.actors.threatIntel, + organisationId: starterIds.organisation, + name: "Parker", + description: + "Builds reproducible operational reports and executive briefings from authoritative records.", + runtime: "codex-subscription", + model: process.env.MUSTER_CODEX_MODEL?.trim() || "configured", + ownerActorId: starterIds.actors.jordan, + systemPromptVersion: "parker-v1", + allowedTools: [ + "alerts.read", + "investigations.read", + "kelpie.cases.read", + "audit.read", + ], + allowedRooms: [starterIds.rooms.soc, starterIds.rooms.parkerDirect], + capabilityRequirements: [ + "alerts.read", + "investigations.read", + "kelpie.cases.read", + "audit.read", + ], + maximumRuntimeSeconds: 1_200, + maximumTokenBudget: 35_000, + maximumCostCents: 600, + approvalRequirements: { externalWrites: "human" }, + }, + ]) + .onConflictDoUpdate({ + target: [ + schema.agentDefinitions.organisationId, + schema.agentDefinitions.name, + ], + set: { + description: sql`excluded.description`, + model: sql`excluded.model`, + allowedTools: sql`excluded.allowed_tools`, + allowedRooms: sql`excluded.allowed_rooms`, + capabilityRequirements: sql`excluded.capability_requirements`, + approvalRequirements: sql`excluded.approval_requirements`, + updatedAt: sql`now()`, + }, + }); + await db .insert(schema.rooms) .values([ { - id: demoIds.rooms.soc, - organisationId: demoIds.organisation, + id: starterIds.rooms.soc, + organisationId: starterIds.organisation, name: "soc-operations", slug: "soc-operations", displayName: "SOC operations", description: "Security operations coordination", topic: "Security operations coordination", roomType: "operations", - createdByActorId: demoIds.actors.jordan, + createdByActorId: starterIds.actors.jordan, }, { - id: demoIds.rooms.triageDirect, - organisationId: demoIds.organisation, + id: starterIds.rooms.triageDirect, + organisationId: starterIds.organisation, name: "dm-alfie", slug: "dm-alfie", displayName: "Alfie", @@ -175,11 +273,11 @@ await db topic: "Threat and technology research", roomType: "direct", visibility: "private", - createdByActorId: demoIds.actors.jordan, + createdByActorId: starterIds.actors.jordan, }, { - id: demoIds.rooms.tawnyDirect, - organisationId: demoIds.organisation, + id: starterIds.rooms.tawnyDirect, + organisationId: starterIds.organisation, name: "dm-jessie", slug: "dm-jessie", displayName: "Jessie", @@ -187,11 +285,11 @@ await db topic: "Threat hunting, enrichment, and analyst guidance", roomType: "direct", visibility: "private", - createdByActorId: demoIds.actors.jordan, + createdByActorId: starterIds.actors.jordan, }, { - id: demoIds.rooms.parkerDirect, - organisationId: demoIds.organisation, + id: starterIds.rooms.parkerDirect, + organisationId: starterIds.organisation, name: "dm-parker", slug: "dm-parker", displayName: "Parker", @@ -199,7 +297,7 @@ await db topic: "Operational reports and executive briefings", roomType: "direct", visibility: "private", - createdByActorId: demoIds.actors.jordan, + createdByActorId: starterIds.actors.jordan, }, ]) .onConflictDoUpdate({ @@ -215,63 +313,63 @@ await db .insert(schema.roomMemberships) .values([ { - organisationId: demoIds.organisation, - roomId: demoIds.rooms.soc, - actorId: demoIds.actors.jordan, + organisationId: starterIds.organisation, + roomId: starterIds.rooms.soc, + actorId: starterIds.actors.jordan, membershipRole: "owner", }, { - organisationId: demoIds.organisation, - roomId: demoIds.rooms.soc, - actorId: demoIds.actors.triage, + organisationId: starterIds.organisation, + roomId: starterIds.rooms.soc, + actorId: starterIds.actors.triage, membershipRole: "agent_member", }, { - organisationId: demoIds.organisation, - roomId: demoIds.rooms.soc, - actorId: demoIds.actors.tawnyHunt, + organisationId: starterIds.organisation, + roomId: starterIds.rooms.soc, + actorId: starterIds.actors.tawnyHunt, membershipRole: "agent_member", }, { - organisationId: demoIds.organisation, - roomId: demoIds.rooms.soc, - actorId: demoIds.actors.threatIntel, + organisationId: starterIds.organisation, + roomId: starterIds.rooms.soc, + actorId: starterIds.actors.threatIntel, membershipRole: "agent_member", }, { - organisationId: demoIds.organisation, - roomId: demoIds.rooms.triageDirect, - actorId: demoIds.actors.jordan, + organisationId: starterIds.organisation, + roomId: starterIds.rooms.triageDirect, + actorId: starterIds.actors.jordan, membershipRole: "owner", }, { - organisationId: demoIds.organisation, - roomId: demoIds.rooms.triageDirect, - actorId: demoIds.actors.triage, + organisationId: starterIds.organisation, + roomId: starterIds.rooms.triageDirect, + actorId: starterIds.actors.triage, membershipRole: "agent_member", }, { - organisationId: demoIds.organisation, - roomId: demoIds.rooms.tawnyDirect, - actorId: demoIds.actors.jordan, + organisationId: starterIds.organisation, + roomId: starterIds.rooms.tawnyDirect, + actorId: starterIds.actors.jordan, membershipRole: "owner", }, { - organisationId: demoIds.organisation, - roomId: demoIds.rooms.tawnyDirect, - actorId: demoIds.actors.tawnyHunt, + organisationId: starterIds.organisation, + roomId: starterIds.rooms.tawnyDirect, + actorId: starterIds.actors.tawnyHunt, membershipRole: "agent_member", }, { - organisationId: demoIds.organisation, - roomId: demoIds.rooms.parkerDirect, - actorId: demoIds.actors.jordan, + organisationId: starterIds.organisation, + roomId: starterIds.rooms.parkerDirect, + actorId: starterIds.actors.jordan, membershipRole: "owner", }, { - organisationId: demoIds.organisation, - roomId: demoIds.rooms.parkerDirect, - actorId: demoIds.actors.threatIntel, + organisationId: starterIds.organisation, + roomId: starterIds.rooms.parkerDirect, + actorId: starterIds.actors.threatIntel, membershipRole: "agent_member", }, ]) diff --git a/packages/database/src/seed-data.ts b/packages/database/src/seed-data.ts index 4f5691d..a99aedf 100644 --- a/packages/database/src/seed-data.ts +++ b/packages/database/src/seed-data.ts @@ -1,4 +1,4 @@ -export const demoIds = { +export const starterIds = { organisation: "018f55d8-c4c7-7c3e-88ef-000000000001", actors: { jordan: "018f55d8-c4c7-7c3e-88ef-000000000010", @@ -49,3 +49,5 @@ export const demoIds = { priyaParent: "018f55d8-c4c7-7c3e-88ef-000000000705", }, } as const; + +export const demoIds = starterIds; diff --git a/packages/database/src/seed.ts b/packages/database/src/seed.ts index f7d78e4..b7f7a6e 100644 --- a/packages/database/src/seed.ts +++ b/packages/database/src/seed.ts @@ -2,6 +2,10 @@ import { database, closeDatabase, schema } from "./index.ts"; import { demoIds } from "./seed-data.ts"; import { sql } from "drizzle-orm"; +if (process.env.MUSTER_DEMO_MODE !== "true") { + throw new Error("Demonstration seed refused. Set MUSTER_DEMO_MODE=true explicitly."); +} + const db = database(); const allCapabilities = ["administration.manage", "rooms.read", "rooms.create", "rooms.manage", "messages.create", "alerts.read", "alerts.acknowledge", "alerts.promote", "investigations.read", "investigations.create", "investigations.update", "investigations.promote", "tasks.read", "tasks.create", "tasks.update", "tasks.assign", "workflows.approve", "agents.invoke", "audit.read"]; diff --git a/packages/database/src/verify-clean-install.ts b/packages/database/src/verify-clean-install.ts new file mode 100644 index 0000000..da12354 --- /dev/null +++ b/packages/database/src/verify-clean-install.ts @@ -0,0 +1,52 @@ +import { count } from "drizzle-orm"; +import { closeDatabase, database, schema } from "./index.ts"; + +const operationalTables = { + alerts: schema.alerts, + investigations: schema.investigations, + messages: schema.messages, + reactions: schema.reactions, + hypotheses: schema.hypotheses, + findings: schema.findings, + decisions: schema.decisions, + approvals: schema.approvals, + agentRuns: schema.agentRuns, + agentMemories: schema.agentMemories, + agentSkills: schema.agentSkills, + agentSkillVersions: schema.agentSkillVersions, + agentSkillEvaluations: schema.agentSkillEvaluations, + workflowRuns: schema.workflowRuns, + evidence: schema.evidence, + timelineEvents: schema.timelineEvents, + notifications: schema.notifications, + tasks: schema.tasks, + integrationRecords: schema.integrationRecords, + integrationEntities: schema.integrationEntities, + integrationDeliveries: schema.integrationDeliveries, + idempotencyRecords: schema.idempotencyRecords, + outboxEvents: schema.outboxEvents, + auditEvents: schema.auditEvents, +} as const; + +const db = database(); +const counts = await Promise.all( + Object.entries(operationalTables).map(async ([name, table]) => { + const [result] = await db.select({ value: count() }).from(table); + return [name, result?.value ?? 0] as const; + }), +); +const populated = counts.filter(([, value]) => value !== 0); + +await closeDatabase(); + +if (populated.length > 0) { + throw new Error( + `Clean-install verification failed: ${populated + .map(([name, value]) => `${name}=${value}`) + .join(", ")}`, + ); +} + +process.stdout.write( + `Clean-install verification passed (${counts.length} operational tables empty).\n`, +); diff --git a/playwright.clean.config.ts b/playwright.clean.config.ts new file mode 100644 index 0000000..4fc9d98 --- /dev/null +++ b/playwright.clean.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests", + testMatch: "clean-install.spec.ts", + globalSetup: "./tests/global-setup.ts", + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? "github" : "list", + use: { + baseURL: process.env.MUSTER_BASE_URL ?? "http://127.0.0.1:3000", + storageState: ".playwright/auth.json", + trace: "retain-on-failure", + screenshot: "only-on-failure", + }, + webServer: { + command: + "BETTER_AUTH_SECRET=muster-playwright-secret-at-least-32-characters AUTH_RATE_LIMIT_MAX=10000 DATABASE_URL=postgresql://muster:muster@localhost:5432/muster REDIS_URL=redis://localhost:6379 pnpm --dir apps/web dev", + url: "http://127.0.0.1:3000/api/v1/health", + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + projects: [{ name: "clean-chromium", use: { ...devices["Desktop Chrome"] } }], +}); diff --git a/scripts/generate-screenshots.sh b/scripts/generate-screenshots.sh index 1d61786..c86d270 100755 --- a/scripts/generate-screenshots.sh +++ b/scripts/generate-screenshots.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")/.." -pnpm exec playwright test tests/screenshots.spec.ts --project=chromium +MUSTER_DEMO_MODE=true NEXT_PUBLIC_MUSTER_DEMO_MODE=true \ + pnpm exec playwright test tests/screenshots.spec.ts --project=chromium diff --git a/tests/clean-install.spec.ts b/tests/clean-install.spec.ts new file mode 100644 index 0000000..fb6e255 --- /dev/null +++ b/tests/clean-install.spec.ts @@ -0,0 +1,45 @@ +import { expect, test } from "@playwright/test"; + +test("fresh workspace is empty and accepts first message and task", async ({ + page, +}) => { + await page.goto("/"); + await expect(page).toHaveURL(/\/rooms\/soc-operations$/); + await expect( + page.getByRole("heading", { name: "Start the conversation" }), + ).toBeVisible(); + + const workspace = page.locator("body"); + for (const syntheticIdentity of [ + "Jordan Blake", + "Maya Chen", + "Daniel Brooks", + "Priya Nair", + "Alex Morgan", + "WS-1042", + "INV-2026-0178", + ]) { + await expect(workspace).not.toContainText(syntheticIdentity); + } + + const message = `First clean-install message ${Date.now()}`; + const composer = page.locator(".tiptap"); + await composer.fill(message); + await page.keyboard.press("Enter"); + await expect(page.getByText(message)).toBeVisible(); + await page.reload(); + await expect(page.getByText(message)).toBeVisible(); + + await page.goto("/tasks"); + await expect(page.getByText("0 shown")).toBeVisible(); + const task = `First clean-install task ${Date.now()}`; + await page.getByRole("button", { name: "New task" }).click(); + await page.getByPlaceholder("What needs doing?").fill(task); + await page + .getByPlaceholder("Context, constraints, and deliverable") + .fill("Verify real work can start without demonstration activity."); + await page.getByRole("button", { name: "Create", exact: true }).click(); + await expect(page.getByText(task)).toBeVisible(); + await page.reload(); + await expect(page.getByText(task)).toBeVisible(); +}); From a2f9c5c77a31c31481ffd73c3d5dfe5bc9f87cc0 Mon Sep 17 00:00:00 2001 From: Justin Middler Date: Sun, 26 Jul 2026 23:13:30 +1000 Subject: [PATCH 002/129] feat: polish the Muster workspace UI --- apps/web/app/globals.css | 1 - apps/web/app/login/page.tsx | 53 ++- apps/web/components/agents-view.tsx | 410 +++++++++++++++++++-- apps/web/components/app-shell.tsx | 82 +++-- apps/web/components/approval-view.tsx | 76 +++- apps/web/components/command-palette.tsx | 55 ++- apps/web/components/integration-view.tsx | 163 +++++++- apps/web/components/investigation-view.tsx | 329 ++++++++++++++--- apps/web/components/login-form.tsx | 57 ++- apps/web/components/page-header.tsx | 14 +- apps/web/components/room-composer.tsx | 76 +++- apps/web/components/room-view.tsx | 365 +++++++++++++----- apps/web/components/search-view.tsx | 81 +++- apps/web/components/settings-view.tsx | 92 ++++- apps/web/components/severity.tsx | 11 +- apps/web/components/tasks-view.tsx | 228 +++++++++--- apps/web/components/ui-policy.test.ts | 57 +++ apps/web/components/ui/avatar.tsx | 4 +- apps/web/components/ui/badge.tsx | 2 +- apps/web/components/workflows-view.tsx | 231 ++++++++++-- apps/web/package.json | 1 - package.json | 1 + pnpm-lock.yaml | 76 +--- screenshots/agent-direct-message.png | Bin 151391 -> 152300 bytes screenshots/dark-mode-room.png | Bin 171235 -> 0 bytes screenshots/direct-message.png | Bin 145154 -> 145570 bytes screenshots/light-mode-room.png | Bin 174443 -> 0 bytes screenshots/mobile-alerts-channel.png | Bin 78390 -> 74596 bytes screenshots/mobile-room.png | Bin 77990 -> 0 bytes screenshots/room-agent-collaboration.png | Bin 173985 -> 175067 bytes screenshots/room-alerts.png | Bin 168864 -> 168221 bytes screenshots/room-dark-1280.png | Bin 0 -> 205579 bytes screenshots/room-dark-1920.png | Bin 0 -> 213325 bytes screenshots/room-dark-375.png | Bin 0 -> 82585 bytes screenshots/room-dark-768.png | Bin 0 -> 122263 bytes screenshots/room-incident.png | Bin 173171 -> 172745 bytes screenshots/room-light-1280.png | Bin 0 -> 208775 bytes screenshots/room-light-1920.png | Bin 0 -> 216533 bytes screenshots/room-light-375.png | Bin 0 -> 83659 bytes screenshots/room-light-768.png | Bin 0 -> 124118 bytes screenshots/room-soc-operations.png | Bin 169243 -> 170401 bytes screenshots/search.png | Bin 127664 -> 126196 bytes screenshots/settings.png | Bin 108144 -> 106614 bytes screenshots/workspace.png | Bin 169247 -> 170406 bytes tests/muster.spec.ts | 165 +++++++-- tests/screenshots.spec.ts | 45 ++- 46 files changed, 2249 insertions(+), 426 deletions(-) create mode 100644 apps/web/components/ui-policy.test.ts delete mode 100644 screenshots/dark-mode-room.png delete mode 100644 screenshots/light-mode-room.png delete mode 100644 screenshots/mobile-room.png create mode 100644 screenshots/room-dark-1280.png create mode 100644 screenshots/room-dark-1920.png create mode 100644 screenshots/room-dark-375.png create mode 100644 screenshots/room-dark-768.png create mode 100644 screenshots/room-light-1280.png create mode 100644 screenshots/room-light-1920.png create mode 100644 screenshots/room-light-375.png create mode 100644 screenshots/room-light-768.png diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 09e1c1d..3090195 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -145,7 +145,6 @@ pre { .sidebar-active { background: var(--color-accent-soft); - box-shadow: inset 2px 0 0 var(--color-accent); } .context-mobile-trigger, diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index b358dfd..f3e8ab4 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -5,22 +5,55 @@ export default function LoginPage() { return (
- Muster + Muster
-

Bring the signal together.

-

The shared workspace for human and agent-driven security operations.

-

Muster connects application telemetry, endpoint detections, security investigations and incident case management in one auditable workspace.

+

+ Bring the signal together. +

+

+ The shared workspace for human and agent-driven security operations. +

+

+ Muster connects application telemetry, endpoint detections, security + investigations and incident case management in one auditable + workspace. +

-

Self-hosted · organisation scoped · auditable by design

+

+ Self-hosted · organisation scoped · auditable by design +

- Muster -

Self-hosted security operations

-

Sign in to Muster

-

Sign in with your organisation account.

+ Muster +

+ Self-hosted security operations +

+

+ Sign in to Muster +

+

+ Sign in with your organisation account. +

-

MFA, recovery codes, passkeys, OIDC, and Entra policies are supported by the authentication architecture.

+

+ MFA, recovery codes, passkeys, OIDC, and Entra policies are + supported by the authentication architecture. +

diff --git a/apps/web/components/agents-view.tsx b/apps/web/components/agents-view.tsx index 26543ed..6c32a29 100644 --- a/apps/web/components/agents-view.tsx +++ b/apps/web/components/agents-view.tsx @@ -22,29 +22,93 @@ import { demoAgents, demoMode } from "@/lib/demo-data"; export function AgentsView() { const [query, setQuery] = useState(""); - const agents = demoAgents.filter((agent) => agent.name.toLowerCase().includes(query.toLowerCase())); + const agents = demoAgents.filter((agent) => + agent.name.toLowerCase().includes(query.toLowerCase()), + ); return ( - New agent} /> + + + New agent + + } + />
- - Gateway healthy + + + Gateway healthy +
{agents.map((agent) => ( - +
-

{agent.name}

Agent

{agent.purpose}

+
+
+

+ {agent.name} +

+ Agent +
+

+ {agent.purpose} +

+
-
Runtime
{agent.runtime}
-
Model
{agent.model}
-
Last run
{agent.lastRun}
-
Success
{agent.successRate}
+
+
Runtime
+
+ {agent.runtime} +
+
+
+
Model
+
{agent.model}
+
+
+
Last run
+
{agent.lastRun}
+
+
+
Success
+
{agent.successRate}
+
-
{agent.tools.length} tools · {agent.rooms} rooms{agent.status}
+
+ + {agent.tools.length} tools · {agent.rooms} rooms + + + {agent.status} + +
))}
@@ -53,17 +117,90 @@ export function AgentsView() { ); } -const agentTabs = ["Overview", "Instructions", "Tools", "Permissions", "Rooms", "Runs", "Learning", "Evaluations", "Versions", "Audit"]; +const agentTabs = [ + "Overview", + "Instructions", + "Tools", + "Permissions", + "Rooms", + "Runs", + "Learning", + "Evaluations", + "Versions", + "Audit", +]; export function AgentDetailView({ tab = "overview" }: { tab?: string }) { const agent = demoAgents[0]!; return ( - {demoMode && }} /> -
AgentActive{demoMode ? `${agent.successRate} success · last run ${agent.lastRun}` : "No runs yet"}Kill switch off
- + + {demoMode && ( + + )} + + + } + /> +
+ + Agent + + Active + + + {demoMode + ? `${agent.successRate} success · last run ${agent.lastRun}` + : "No runs yet"} + + + Kill switch off + +
+
-
{demoMode ? (tab === "learning" ? : ) : (tab === "learning" ? : )}
+
+ {demoMode ? ( + tab === "learning" ? ( + + ) : ( + + ) + ) : tab === "learning" ? ( + + ) : ( + + )} +
); @@ -74,7 +211,9 @@ function CleanAgentOverview({ purpose }: { purpose: string }) {

Purpose

-

{purpose}

+

+ {purpose} +

No runs yet

@@ -113,10 +252,81 @@ function AgentOverview() { return (
-

Purpose

Correlates alert evidence, searches prior organisational security memory, and returns a typed disposition recommendation. It cannot execute response actions.

-

Recent runs

{[["RUN-1048","Legacy portal credential access","Completed","3 min ago","94%"],["RUN-1041","Impossible travel triage","Completed","41 min ago","87%"],["RUN-1038","Bower policy drift","Failed","2 h ago","—"]].map(([id,title,status,time,confidence]) =>
{id}

{title}

{status}{time}{confidence}
)}
+
+

Purpose

+

+ Correlates alert evidence, searches prior organisational security + memory, and returns a typed disposition recommendation. It cannot + execute response actions. +

+
+
+
+

Recent runs

+
+ {[ + [ + "RUN-1048", + "Legacy portal credential access", + "Completed", + "3 min ago", + "94%", + ], + [ + "RUN-1041", + "Impossible travel triage", + "Completed", + "41 min ago", + "87%", + ], + ["RUN-1038", "Bower policy drift", "Failed", "2 h ago", "—"], + ].map(([id, title, status, time, confidence]) => ( +
+ {id} +

{title}

+ + {status} + + + {time} + + {confidence} +
+ ))} +
- +
); } @@ -125,27 +335,165 @@ function LearningPanel() { return (
-

Governed continuous learning

Run reviews produce evidence-linked notes and immutable skill proposals. Nothing enters trusted instructions until evaluation and human approval.

+
+ +
+

+ Governed continuous learning +

+

+ Run reviews produce evidence-linked notes and immutable skill + proposals. Nothing enters trusted instructions until evaluation + and human approval. +

+
+
-

Skill proposals

Self-authored changes awaiting review

1 pending
+
+
+

+ Skill proposals +

+

+ Self-authored changes awaiting review +

+
+ + 1 pending + +
-
correlate-legacy-auth@3ProposedRUN-1048 · 3 min ago
-

Bound identity correlation to explicit evidence

-

Require a matching identity plus at least one of source IP, owned endpoint, or a ten-minute window. Record contradictory matches.

-
Evaluation92 / 100
Baseline88 / 100
Regressions0
-
+
+ + correlate-legacy-auth@3 + + Proposed + + + RUN-1048 · 3 min ago + +
+

+ Bound identity correlation to explicit evidence +

+

+ Require a matching identity plus at least one of source IP, owned + endpoint, or a ten-minute window. Record contradictory matches. +

+
+
+ Evaluation + 92 / 100 +
+
+ Baseline + 88 / 100 +
+
+ Regressions + 0 +
+
+
+ + + +
-

Recent learning notes

- {[["lesson","Legacy portal events use canonical identity after redaction","98%","RUN-1048","3 evidence"],["failure","Historical search must exclude closed false positives","87%","RUN-1041","2 evidence"],["procedure_hint","Tawny endpoint ownership resolves ambiguous usernames","91%","RUN-1032","4 evidence"]].map(([kind,title,confidence,run,evidence]) =>
{kind}

{title}

{confidence}{run}{evidence}
)} +
+

+ Recent learning notes +

+
+ {[ + [ + "lesson", + "Legacy portal events use canonical identity after redaction", + "98%", + "RUN-1048", + "3 evidence", + ], + [ + "failure", + "Historical search must exclude closed false positives", + "87%", + "RUN-1041", + "2 evidence", + ], + [ + "procedure_hint", + "Tawny endpoint ownership resolves ambiguous usernames", + "91%", + "RUN-1032", + "4 evidence", + ], + ].map(([kind, title, confidence, run, evidence]) => ( +
+ {kind} +

{title}

+ {confidence} + + {run} + + + {evidence} + +
+ ))}
); diff --git a/apps/web/components/app-shell.tsx b/apps/web/components/app-shell.tsx index 1225871..6f59481 100644 --- a/apps/web/components/app-shell.tsx +++ b/apps/web/components/app-shell.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import Link from "next/link"; -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useEffect, useState, type ReactNode } from "react"; import { Bell, @@ -33,13 +33,7 @@ import { } from "@/lib/demo-data"; import { cn } from "@/lib/utils"; -function NavGroup({ - label, - children, -}: { - label: string; - children: ReactNode; -}) { +function NavGroup({ label, children }: { label: string; children: ReactNode }) { const [expanded, setExpanded] = useState(true); return (
@@ -47,10 +41,13 @@ function NavGroup({ type="button" aria-expanded={expanded} onClick={() => setExpanded((current) => !current)} - className="mb-1 flex min-h-7 w-full items-center gap-1 rounded px-2 text-left text-[11px] font-semibold text-muted-foreground hover:bg-muted hover:text-foreground" + className="mb-1 flex min-h-7 w-full items-center gap-1 rounded px-2 text-left text-xs font-semibold text-muted-foreground hover:bg-muted hover:text-foreground" >
@@ -235,6 +234,7 @@ export function AppShell({ children: ReactNode; context?: ReactNode; }) { + const router = useRouter(); const [paletteOpen, setPaletteOpen] = useState(false); const [mobileNavOpen, setMobileNavOpen] = useState(false); const [mobileContextOpen, setMobileContextOpen] = useState(false); @@ -280,38 +280,59 @@ export function AppShell({
- -
- + Connected - - {context && ( @@ -339,10 +360,17 @@ export function AppShell({ @@ -109,21 +210,53 @@ function Overview() { } function Hypotheses() { - const statuses = ["unverified", "supported", "contradicted", "inconclusive"] as const; + const statuses = [ + "unverified", + "supported", + "contradicted", + "inconclusive", + ] as const; return (
{statuses.map((status) => (
-

{status}

{activeInvestigation.hypotheses.filter((item) => item.status === status).length}
- {activeInvestigation.hypotheses.filter((item) => item.status === status).map((hypothesis) => ( -
-

{hypothesis.id}

-

{hypothesis.statement}

-
-
{hypothesis.confidence}% confidence+{hypothesis.support} / −{hypothesis.contradict}
-

{hypothesis.owner}

-
- ))} +
+

{status}

+ + { + activeInvestigation.hypotheses.filter( + (item) => item.status === status, + ).length + } + +
+ {activeInvestigation.hypotheses + .filter((item) => item.status === status) + .map((hypothesis) => ( +
+

+ {hypothesis.id} +

+

+ {hypothesis.statement} +

+
+
+
+
+ {hypothesis.confidence}% confidence + + +{hypothesis.support} / −{hypothesis.contradict} + +
+

+ {hypothesis.owner} +

+
+ ))}
))}
@@ -137,13 +270,60 @@ function Findings() {
-

{finding.title}

- {finding.authorType === "agent" ? "Agent finding" : "Human finding"} - {finding.reviewed ? "Human reviewed" : "Review required"} +

+ {finding.title} +

+ + {finding.authorType === "agent" + ? "Agent finding" + : "Human finding"} + + + {finding.reviewed ? "Human reviewed" : "Review required"} +
-

{finding.summary}

Recommended action

{finding.action}

-
Author
{finding.author}
{"runtime" in finding &&
Runtime / model
{finding.runtime}
}
Confidence
{finding.confidence}%
Evidence
{finding.evidence} references
+
+

{finding.summary}

+
+

+ Recommended action +

+

{finding.action}

+
+
+
+
+
Author
+
{finding.author}
+
+ {"runtime" in finding && ( +
+
Runtime / model
+
{finding.runtime}
+
+ )} +
+
Confidence
+
{finding.confidence}%
+
+
+
Evidence
+
{finding.evidence} references
+
+
))} @@ -160,7 +340,22 @@ export function InvestigationView({ tab = "overview" }: { tab?: string }) { eyebrow={`Investigation · ${activeInvestigation.number}`} title={activeInvestigation.title} description={`Created ${activeInvestigation.created} · Last activity ${activeInvestigation.lastActivity}`} - actions={<>} + actions={ + <> + + + + } /> {promotionOpen && (
- {activeInvestigation.status} - Lead {activeInvestigation.lead} - {activeInvestigation.linkedAlerts} alerts - {activeInvestigation.linkedCase} -
{activeInvestigation.participants.map((initials) => )}
+ + {activeInvestigation.status} + + + Lead{" "} + + {activeInvestigation.lead} + + + + {activeInvestigation.linkedAlerts} alerts + + + {activeInvestigation.linkedCase} + +
+ {activeInvestigation.participants.map((initials) => ( + + ))} +
-