Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture Decision Records

These ADRs mirror the accepted decisions of the Quoro project ledger. They are generated from the maintainer’s decision ledger; do not edit them by hand. To change a decision, open an issue proposing a new decision that supersedes it; once the maintainer accepts it, a new ADR is generated.

Some decisions about internal coordination or business planning are kept private, so numbering has gaps.

ADRTitleStatusTagsDate
0001Project name is QuoroAcceptedproduct2026-11-02
0002Quoro unifies hypervisor and app deploy in one self-hosted tool, operable by AI agentsAcceptedproduct2026-11-02
0003Start as a self-use / experimenter tool; enterprise or managed offering laterAcceptedproduct2026-11-02
0004License: AGPL-3.0 core, enterprise code in ee/ under a separate license, CLA from the first external contributorAcceptedproduct, oss2026-11-02
0005Repos: public quoro repository (English) + private coordination repositoryAcceptedproduct, process2026-11-02
0007Own engine on Cloud Hypervisor + KVM (not Incus, not an own VMM)Acceptedengine, architecture2026-11-02
0008One microVM per app; no Docker on nodesAcceptedengine, architecture2026-11-02
0009MVP requires a KVM-capable host; container fallback is a later Engine implementationAcceptedengine, product2026-11-02
0010Independent nodes orchestrated by Quoro (Dokploy-style), not Incus-style clusteringAcceptedarchitecture, node-agent2026-11-02
0011Nodes are autonomous and dial outAcceptedarchitecture, node-agent, reliability2026-11-02
0012Same binary on every node; one primary is the single source of truth, others are replicasAcceptedarchitecture, control-plane, reliability2026-11-02
0013State replication is continuous over an append-only, numbered, hash-chained logAcceptedarchitecture, control-plane, reliability2026-11-02
0014Telemetry (app logs, metrics) stays on each node; only state is replicatedAcceptedarchitecture, observability2026-11-02
0015SQLite first; state stored as a numbered change logAcceptedarchitecture, control-plane2026-11-02
0016Leader election roadmap: single node in MVP, manual promotion in M5, Raft auto-failover with >=3 votes laterAcceptedreliability, control-plane2026-11-02
0017HA requires at least 3 votes; with 2 nodes Quoro tells the user and offers a witnessAcceptedreliability, witness2026-11-02
0018The edge/LB can be the third voter (role witness+edge of the same binary)Acceptedreliability, witness, ingress2026-11-02
0019Fencing is mandatory and lease-based; node agent self-fences from M0Acceptedreliability, node-agent, fencing2026-11-02
0020Per-app HA policies: none / cold / warm / hot, with cost shown in the panelAcceptedreliability, ha-policy2026-11-02
0021N+1 capacity indicator: ‘if node A dies, do its apps fit on the others?’Acceptedreliability, ha-policy, panel2026-11-02
0022Failback is never automatic or immediate; returning node rejoins as replica and handover is plannedAcceptedreliability, failback2026-11-02
0023Sync engine is our own: control-plane log replication + a content-addressed chunk engine for volumesAcceptedsync, reliability, backups2026-11-02
0024Ingress: reverse proxy with ACME for public-IP hosts, Cloudflare Tunnel for homelabs behind NATAcceptedingress2026-11-02
0025Provider interfaces (DNS, LB, S3-compatible storage) with Cloudflare as the first implementation, not the only oneAcceptedproviders, oss2026-11-02
0026Curated catalog in the repo instead of running an own image registry (for now)Acceptedcatalog, security, oss2026-11-02
0027Signed binaries and reproducible builds are mandatoryAcceptedsecurity, release, oss2026-11-02
0028Baseline stack (to be confirmed in the architecture doc)Superseded by 0056stack2026-11-02
0029Agents are first-class identities, separate from humansAcceptedagents, security2026-11-02
0030Grants: scope + resource selector + TTL (+ optional max uses); incident-scoped; JIT approval for destructive actionsAcceptedagents, security, auth2026-11-02
0031Audit log: append-only, hash-chained, optionally copied to S3/R2 with object lock; MCP uses the same pathAcceptedagents, audit, security2026-11-02
0032AI-native loop: detect -> notify -> wake agent -> act within limitsAcceptedagents, events2026-11-02
0033Milestones M0-M6; HA and backups out of MVP but the MVP architecture prepares themAcceptedroadmap2026-11-02
0034Out of scope for nowAcceptedroadmap, scope2026-11-02
0036Anti-drift rulesAcceptedprocess2026-11-02
0037Git conventionsAcceptedprocess, git2026-11-02
0040Write the bases before any codeAcceptedprocess, docs2026-11-02
0042Repo AGENTS.md must carry the maintainer’s working rules, because non-Claude agents never see the maintainer’s CLAUDE.mdAcceptedprocess, agents-md2026-11-02
0044Realtime transport (WebSockets vs SSE) stays open until the architecture docSuperseded by 0057roadmap, stack2026-11-02
0047Leases have two modes: local (HA none/cold, never expire on control-plane loss) and expiring (warm/hot, self-fence on expiry)Acceptedreliability, node-agent, fencing2026-11-02
0048API contract source of truth: hand-written docs/contracts/openapi.yaml is frozen; utoipa output is diffed against it in CI; orval generates the panel clientAcceptedstack, api2026-11-02
0049Agent runner (D-0032 wake): reference runner in examples/agent-runner/, owned by the mcp workstream, outside the quoro binaryAcceptedagents, mcp, events2026-11-02
0050Cluster keys: separate Ed25519 signing key and XChaCha20-Poly1305 data-encryption key, generated at setup, stored as 0600 files outside SQLite, plus a node-local keyAcceptedsecurity, control-plane, node-agent2026-11-02
0051Provider credentials: a human-only providers resource (API + replicated table with secret_enc) added to the contract before the M0 freezeAcceptedproviders, ingress, security2026-11-02
0052Local persistent volumes (non-replicated) ship with M5a, before the cold-backup DoD; until then the PRD states no persistent app dataAcceptedengine, sync, roadmap2026-11-02
0053Off-site audit-log copy (D-0031) ships in M5a with the S3-compatible path, gated by an object-lock probe per providerAcceptedaudit, roadmap, security2026-11-02
0054Host layout and CLI: one TLS listener for API/panel/MCP on 8443, self-signed primary certificate pinned by join commands, /var/lib/quoro + /etc/quoro/quoro.toml, one-time setup token for /setupAcceptedinstaller, control-plane, security2026-11-02
0055Lab TLS source for the M2 deploy DoD: Cloudflare Tunnel with a test domain; ACME exercised separately via DNS-01 against a test zoneAcceptedingress, lab, roadmap2026-11-02
0056Baseline stack without noVNC: consoles are text-only (xterm.js) because Cloud Hypervisor has no VNC or display deviceAcceptedstack, engine, console2026-11-02
0057Realtime transport: SSE for one-way streams, WebSockets for bidirectional sessionsAcceptedstack, architecture2026-11-02
0059Q-0001: embedded Rust reverse proxy behind the Ingress interface; Caddy admin API as fallback backendAcceptedingress2026-11-02
0060Q-0002: Dockerfile-only builds in M4, run inside a builder microVM; re-verify Railpack/Nixpacks before M4Acceptedbuilds2026-11-02
0061Q-0003: write our own minimal init and OCI-to-ext4 pipeline, with in-root path resolution as a required hardening itemAcceptedengine2026-11-02
0062Q-0004: reflink snapshots + full chunk scan for M5a cold backups; spike lvm-thin vs btrfs before M5b warmAcceptedsync, engine2026-11-02
0063Q-0005: kernel WireGuard configured by Quoro over netlink; mesh deferred to M5a when cross-node traffic startsAcceptednetworking2026-11-02
0064First public release is an M3 preview (v0.1, experimental, not production); M4 complete is the first general releaseAcceptedproduct, roadmap2026-11-02
0065Docs stay Markdown in the repo until the first public release; pick docs-site tooling during M3Accepteddocs2026-11-02
0067Manual promotion of a replica node to primary ships in M5b, alongside leases and fencingAcceptedroadmap, reliability2026-11-02
0068PRD HA table costs confirmed: none = no reserved resources; cold = backup storage and restore timeAcceptedproduct, reliability2026-11-02
0069SQLite and catalog-template backups use Quoro’s own chunk engine with client-side encryption, from consistent snapshots (SQLite backup API / VACUUM INTO)Acceptedsync, security2026-11-02
0070Curated catalog format, owner, location and signature scheme are decided at M4 planning, before the first template shipsAcceptedcatalog2026-11-02
0071Engine trait async methods return Send futures (fn … -> impl Future<Output = …> + Send), via a standalone contract PR before the M0 freezeAcceptedengine, contracts2026-11-02
0072Lease timing: a node renews immediately after a first grant and treats the lease as held only after that answer; held expiring leases survive an agent restart within the same boot by persisting remaining validity against boot_id and CLOCK_BOOTTIMEAcceptedreliability, contracts2026-11-02
0073Fencing tokens change only on ownership change: renewals by the same holder keep the token; a grant to a new holder or after expiry or revoke needs a strictly higher tokenAcceptedreliability, contracts2026-11-02
0074A failed forced stop escalates: SIGKILL the VMM process; if the VM may still run, the node never emits node.fenced, emits node.fence_failed, marks itself failed and keeps the lease unstartableAcceptedreliability, contracts2026-11-02
0075Concurrent fences and a mid-fence crash test are mandatory before expiring leases ship (M5b)Acceptedreliability2026-11-02
0076License identifier is AGPL-3.0-onlyAcceptedproduct, governance2026-11-02
0077Governance defaults for the public repo (Q-0023 items 2-6, 8, 9)Acceptedgovernance, docs2026-11-02
0078A promoted replica obtains the cluster signing and data-encryption keys because both are replicated to every replica node, wrapped to that node’s node-local keyAcceptedsecurity, reliability2026-11-02
0079Add protocol error code fence_failed and a human-only operator acknowledgement that clears a node’s failed markAcceptedreliability, contracts2026-11-02
0080Dependency license policy for the AGPL-3.0-only core: an allowlist of permissive and AGPL-compatible licenses enforced in CI with cargo-deny, starting in M1Acceptedgovernance, security2026-11-02
0081Release signing: keyless GitHub artifact attestations (Sigstore, SLSA build provenance) verified by a sha256-pinned gh in install.shAcceptedinstaller, security, release2026-11-02
0082One-off workflow_dispatch dry run of the release workflow: it attests and uploads workflow artifacts only, with no tag and no releaseAcceptedinstaller, security, release2026-11-02
0083VM disk_gb minimum is 1 in openapi.yaml (CreateVmRequest and Vm), matching db-schema CHECK (disk_gb >= 1)Acceptedcontrol-plane, contracts2026-11-02
0084M1 keeps every microVM isolated from every other; an additive contract change carrying a project or isolation group on NetAttach and guest DNS on NodeNetwork lands in a standalone PR at the start of M2, before any multi-VM app featureAcceptedengine, networking, contracts2026-11-02
0086cargo-deny checks the targets Quoro ships (x86_64 and aarch64 Linux, gnu and musl), not every target; the ADR 0080 license allowlist is unchangedAcceptedgovernance, security2026-11-02
0088quoro.service runs as root while the Cloud Hypervisor engine is in-process (M1)Superseded by 0096installer, engine, security2026-11-02
0089Privilege split in M2: the API and panel run unprivileged and only the node agent keeps the privileges the engine needsAcceptedsecurity, installer, engine2026-11-02
0090Release builds and reproduce-release.sh run inside the same digest-pinned Ubuntu 24.04 container image with snapshot-pinned packages, so any host with podman or docker reproduces the published artifact byte for byteAcceptedrelease, security, installer2026-11-02
0094Non-primary nodes get only a per-node tunnel run token from the primary; the account API token never leaves the primaryAcceptedingress, providers, contract2026-11-03
0095Add provider_id to domains and audit_exports and a node-to-tunnel-provider mapping so DELETE /providers/{id} can return 409 while referencedAcceptedproviders, contract2026-11-03
0096M2 process model: quoro.service is the unprivileged control plane (User=quoro-api, empty capability bounding set) and quoro-node.service is the root node agent with KillMode=process; node data in /var/lib/quoro-nodeAcceptedsecurity, installer, engine, m22026-11-03
0098AppResources.disk_gb minimum is 1 in openapi.yaml, matching vms CHECK (disk_gb >= 1)Acceptedcontracts, control-plane, apps2026-11-03
0099Add domain_id to desired_state spec.routes[] so node certificate.* events can name their domainAcceptedcontracts, apps, ingress, m22026-11-03
0102M3 demo agent runner uses the built-in deterministic policy; an LLM backend (Claude, Anthropic API) stays optional, configured at the runner with the operator’s own key, never required by the DoDAcceptedagents, m32026-11-03
0103A webhook delivery from a webhook with wake carries data.wake = {agent_id, grant_id} in the incident.opened payload (additive, schema_version 1)Acceptedcontracts, agents, m32026-11-03
0104Ratify the provisional webhook and event contract choices from #116Acceptedcontracts, m32026-11-03
0105M3 ships without deleteAgent; disabling an agent (PATCH) is the lockout; delete semantics are decided laterAcceptedagents, m32026-11-03
0106Add GET /api/v1/agents/{agent_id}/keys to openapi.yaml so the panel can list an agent’s keysAcceptedcontracts, panel, agents, m32026-11-03
0107Let operators and viewers resolve user ids to names (a read path for users)Acceptedcontracts, panel, m32026-11-03
0109Docs site: mdBook built from the repo’s Markdown, published to GitHub Pages together with the v0.1.0 tagAccepteddocs, m32026-11-03
0111user:read stays human-only; WebhookDelivery exposes the test flag; creating users is M4 workAcceptedcontracts, m32026-11-03
0113M4 web terminal path: exec through quoro-init over virtio-vsock, relayed by the root node agentAcceptedm4, terminal, engine, contracts2026-11-04
0114M4 terminal and console access: app:terminal human-only (owner, operator); vm:console grantable read-only (owner, operator); agents get a non-interactive app:exec later via approvalAcceptedm4, terminal, console, security, contracts2026-11-04
0115M4 session audit: an open record plus a session-end record carrying a SHA-256 of a node-local, size-capped transcriptAcceptedm4, terminal, audit, contracts2026-11-04
0116M4 VM console: read-only; serial in socket mode with the node agent as its only client from vm.create and a node-side ring per VM; viewers fan out from the nodeAcceptedm4, console, engine, contracts2026-11-04
0117Builder interface: a node-agent Builder trait (Dockerfile first, Railpack later) that returns build events and a node-local OCI layoutAcceptedbuilds, m4, contracts2026-11-04
0118Builds run on the target node in an ephemeral builder microVM (one per build) with a persistent per-app work disk for the BuildKit cacheAcceptedbuilds, m4, contracts2026-11-04
0119Build log transport: tagged serial-console lines out of the builder VM, kept on the node, streamed to the panel over SSE via a new stream_open kind build_logsAcceptedbuilds, m4, contracts2026-11-04
0120M4 builds and metrics reuse existing scopes; M4 adds no MCP toolAcceptedcontracts, m42026-11-04
0121Built image reference build.quoro.invalid/<app_id>:<build_id>; Git app image is empty before first buildAcceptedcontracts, m42026-11-04
0122Git apps: createApp takes exactly one of image/source; source cannot be added or removed in M4; createDeployment 409 for Git appsAcceptedcontracts, m42026-11-04
0123M4 build scope: public HTTPS repos only, trigger api only, one active build per app, fixed default limitsAcceptedcontracts, m42026-11-04
0124Build lifecycle runs through desired state; primary emits build.* events; Deployment.phase build is never reportedAcceptedcontracts, m42026-11-04
0125Builder VM networking: egress_only with no isolation groupAcceptedcontracts, m42026-11-04
0126Terminal exec channel: vsock port 1024, framed [u8 type][u32 BE len][payload], quoro-init config v2 with exec_portAcceptedcontracts, m42026-11-04
0127Console: Engine::console superseded by serial_output; node ring of two 512 KiB segments; console streams outlive VM restartsAcceptedcontracts, m42026-11-04
0128Transcripts: terminal only, 16 MiB cap closes session (transcript_limit), 30 days and 1 GiB per node, fetch human-only with audit:read and always auditedAcceptedcontracts, m42026-11-04
0129Metrics: host-side VMM RSS and CPU, 5 s sampling, 10-minute in-memory ring, SSE samples carry no idAcceptedcontracts, m42026-11-04
0130Public site layout on quoro.dev: the mdBook docs site serves quoro.dev from GitHub Pages at the v0.1.0 release; the installer gets a short URL quoro.dev/install.sh laterAccepteddocs, release2026-11-04
0132M4 interactive: ratify contract readings taken while implementing terminal and console sessionsAcceptedterminal, console, m4, contracts2026-11-04
0133Keep an app’s last failed or cancelled build in spec.builds so the node keeps its logAcceptedbuilds, m4, contracts2026-11-04
0134Curated catalog: one TOML file per template under catalog/ in the public repo, embedded in the signed release binary; images pinned by digest; upstream cosign signatures verified in CI when a template is added or bumped, not on nodesAcceptedcatalog, security, m42026-11-04
0135Q-0099: invited users change their own password through a human-only self-service operation (current and new password), added in a standalone contract PR during M4; no must_change_password flag yetAcceptedcontracts, users, m42026-11-04
0136Q-0101: pre-M4 VMs get the terminal and console only after a redeploy; the release notes say so and the panel shows a ‘redeploy to enable’ hint instead of the action; the node agent never recreates a VM on its ownAcceptedm4, engine, panel2026-11-04