Skip to content

domain — Architecture

The @mnemose/domain package is the authoritative type layer for the Mnemose platform. It contains all Zod schemas for commands, domain events, and shared identifiers. No runtime logic — only schemas, type aliases, and the persona definition.

Responsibilities

  • Define the 50 domain events (AnyDomainEventSchema discriminated union) that represent all state changes in the platform
  • Define the 50 commands (AnyCommandSchema discriminated union) that represent all intent signals
  • Provide shared identifier types (TenantId, OrgId, WorkspaceId, ProjectId, GroupId, KernelId, etc.) and enum schemas
  • Model the ownership hierarchy aggregates (Org, Workspace, Project, Group) — see Ownership hierarchy
  • Export the MnemoseAgent persona definition used by the agent service and loop-host

Package Structure

src/
├── commands/
│ └── index.ts — AnyCommandSchema + all individual command schemas
├── events/
│ └── index.ts — AnyDomainEventSchema + all individual event schemas
├── schemas/
│ ├── tenant.ts — TenantId, CloudProvider (carries optional orgId)
│ ├── org.ts — OrgId, Org (ownership root)
│ ├── workspace.ts — WorkspaceId, Workspace (tenant-scoped)
│ ├── project.ts — ProjectId, Project (workspace-scoped)
│ ├── group.ts — GroupId, Group, GroupMemberRef, GroupMembership
│ ├── iam.ts — IamRole, ResourceScope, EscalationStatus
│ ├── resource.ts — ResourceType
│ ├── kernel.ts — KernelId, KernelSessionId, PermissionTier, FleetMode
│ ├── kms-key.ts — KmsKey schemas
│ └── user.ts — User identity schemas
├── personas/
│ └── mnemose-agent.ts — MnemoseAgent persona definition
└── index.ts — Re-exports everything

Contracts

All schemas are exported as committed JSON Schema files via scripts/export-contracts.ts. The generated files live in contracts/ and are the language-agnostic API contract for event/command integration.

Terminal window
pnpm --filter @mnemose/domain contracts:export # regenerate
pnpm --filter @mnemose/domain contracts:check # CI gate

Event Envelope

Every domain event carries a standard envelope defined in EventBase:

{
eventId: string(UUID); // unique event identity
aggregateId: string(UUID); // identity of the aggregate this event belongs to
aggregateType: string; // e.g. "Tenant", "Kernel", "IamEscalation"
sequence: number; // position in the aggregate's event stream
tenantId: TenantId; // tenant scope
correlationId: string(UUID); // links all events from one user action
causationId: string(UUID); // ID of the command that caused this event
actor: string; // agent persona name or user sub
recordedAt: Date;
}

Control-plane events (bootstrap, org setup) use ControlPlaneEventBase which omits tenantId.

Ownership hierarchy

The platform’s ownership and containment model is expressed by four aggregates plus the existing Tenant:

Platform
└── Org ──────────────► Tenant ──► Workspace ──► Project
│ (Org → Tenant via tenant.orgId)
└── Group ──┬──► User
├──► Workspace
└──► Project (many-to-many via GroupMembership)
  • Org (org.ts) — top-level ownership root beneath the platform. Owns tenants and groups. Created/managed by control-plane commands (no tenant scope).
  • Tenant (tenant.ts) — billing and cloud boundary. Carries an optional orgId modeling the Org → Tenant edge (null until attached).
  • Workspace (workspace.ts) — tenant-scoped container for projects.
  • Project (project.ts) — workspace-scoped container; carries the denormalized tenantId of its workspace. Distinct from CloudProject.
  • Group (group.ts) — org-scoped, cross-cutting collection. Members are a polymorphic GroupMemberRef (user | workspace | project) attached via GroupMembership records.

Org and Group lifecycle commands/events extend the control-plane bases (ControlPlaneCommandBase / ControlPlaneEventBase); Workspace and Project commands/events are tenant-scoped.

This hierarchy is the basis for sandbox ownership: a sandbox is owned by exactly one of these nodes (org, tenant, workspace, or project). The sandbox schema’s SandboxOwnerRef discriminated union carries the owner; a null owner denotes implicit tenant ownership, and resolveSandboxOwner() makes ownership total by falling back to { kind: "tenant", id: tenantId }.