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.
| ADR | Title | Status | Tags | Date |
|---|---|---|---|---|
| 0001 | Project name is Quoro | Accepted | product | 2026-11-02 |
| 0002 | Quoro unifies hypervisor and app deploy in one self-hosted tool, operable by AI agents | Accepted | product | 2026-11-02 |
| 0003 | Start as a self-use / experimenter tool; enterprise or managed offering later | Accepted | product | 2026-11-02 |
| 0004 | License: AGPL-3.0 core, enterprise code in ee/ under a separate license, CLA from the first external contributor | Accepted | product, oss | 2026-11-02 |
| 0005 | Repos: public quoro repository (English) + private coordination repository | Accepted | product, process | 2026-11-02 |
| 0007 | Own engine on Cloud Hypervisor + KVM (not Incus, not an own VMM) | Accepted | engine, architecture | 2026-11-02 |
| 0008 | One microVM per app; no Docker on nodes | Accepted | engine, architecture | 2026-11-02 |
| 0009 | MVP requires a KVM-capable host; container fallback is a later Engine implementation | Accepted | engine, product | 2026-11-02 |
| 0010 | Independent nodes orchestrated by Quoro (Dokploy-style), not Incus-style clustering | Accepted | architecture, node-agent | 2026-11-02 |
| 0011 | Nodes are autonomous and dial out | Accepted | architecture, node-agent, reliability | 2026-11-02 |
| 0012 | Same binary on every node; one primary is the single source of truth, others are replicas | Accepted | architecture, control-plane, reliability | 2026-11-02 |
| 0013 | State replication is continuous over an append-only, numbered, hash-chained log | Accepted | architecture, control-plane, reliability | 2026-11-02 |
| 0014 | Telemetry (app logs, metrics) stays on each node; only state is replicated | Accepted | architecture, observability | 2026-11-02 |
| 0015 | SQLite first; state stored as a numbered change log | Accepted | architecture, control-plane | 2026-11-02 |
| 0016 | Leader election roadmap: single node in MVP, manual promotion in M5, Raft auto-failover with >=3 votes later | Accepted | reliability, control-plane | 2026-11-02 |
| 0017 | HA requires at least 3 votes; with 2 nodes Quoro tells the user and offers a witness | Accepted | reliability, witness | 2026-11-02 |
| 0018 | The edge/LB can be the third voter (role witness+edge of the same binary) | Accepted | reliability, witness, ingress | 2026-11-02 |
| 0019 | Fencing is mandatory and lease-based; node agent self-fences from M0 | Accepted | reliability, node-agent, fencing | 2026-11-02 |
| 0020 | Per-app HA policies: none / cold / warm / hot, with cost shown in the panel | Accepted | reliability, ha-policy | 2026-11-02 |
| 0021 | N+1 capacity indicator: ‘if node A dies, do its apps fit on the others?’ | Accepted | reliability, ha-policy, panel | 2026-11-02 |
| 0022 | Failback is never automatic or immediate; returning node rejoins as replica and handover is planned | Accepted | reliability, failback | 2026-11-02 |
| 0023 | Sync engine is our own: control-plane log replication + a content-addressed chunk engine for volumes | Accepted | sync, reliability, backups | 2026-11-02 |
| 0024 | Ingress: reverse proxy with ACME for public-IP hosts, Cloudflare Tunnel for homelabs behind NAT | Accepted | ingress | 2026-11-02 |
| 0025 | Provider interfaces (DNS, LB, S3-compatible storage) with Cloudflare as the first implementation, not the only one | Accepted | providers, oss | 2026-11-02 |
| 0026 | Curated catalog in the repo instead of running an own image registry (for now) | Accepted | catalog, security, oss | 2026-11-02 |
| 0027 | Signed binaries and reproducible builds are mandatory | Accepted | security, release, oss | 2026-11-02 |
| 0028 | Baseline stack (to be confirmed in the architecture doc) | Superseded by 0056 | stack | 2026-11-02 |
| 0029 | Agents are first-class identities, separate from humans | Accepted | agents, security | 2026-11-02 |
| 0030 | Grants: scope + resource selector + TTL (+ optional max uses); incident-scoped; JIT approval for destructive actions | Accepted | agents, security, auth | 2026-11-02 |
| 0031 | Audit log: append-only, hash-chained, optionally copied to S3/R2 with object lock; MCP uses the same path | Accepted | agents, audit, security | 2026-11-02 |
| 0032 | AI-native loop: detect -> notify -> wake agent -> act within limits | Accepted | agents, events | 2026-11-02 |
| 0033 | Milestones M0-M6; HA and backups out of MVP but the MVP architecture prepares them | Accepted | roadmap | 2026-11-02 |
| 0034 | Out of scope for now | Accepted | roadmap, scope | 2026-11-02 |
| 0036 | Anti-drift rules | Accepted | process | 2026-11-02 |
| 0037 | Git conventions | Accepted | process, git | 2026-11-02 |
| 0040 | Write the bases before any code | Accepted | process, docs | 2026-11-02 |
| 0042 | Repo AGENTS.md must carry the maintainer’s working rules, because non-Claude agents never see the maintainer’s CLAUDE.md | Accepted | process, agents-md | 2026-11-02 |
| 0044 | Realtime transport (WebSockets vs SSE) stays open until the architecture doc | Superseded by 0057 | roadmap, stack | 2026-11-02 |
| 0047 | Leases have two modes: local (HA none/cold, never expire on control-plane loss) and expiring (warm/hot, self-fence on expiry) | Accepted | reliability, node-agent, fencing | 2026-11-02 |
| 0048 | API contract source of truth: hand-written docs/contracts/openapi.yaml is frozen; utoipa output is diffed against it in CI; orval generates the panel client | Accepted | stack, api | 2026-11-02 |
| 0049 | Agent runner (D-0032 wake): reference runner in examples/agent-runner/, owned by the mcp workstream, outside the quoro binary | Accepted | agents, mcp, events | 2026-11-02 |
| 0050 | Cluster keys: separate Ed25519 signing key and XChaCha20-Poly1305 data-encryption key, generated at setup, stored as 0600 files outside SQLite, plus a node-local key | Accepted | security, control-plane, node-agent | 2026-11-02 |
| 0051 | Provider credentials: a human-only providers resource (API + replicated table with secret_enc) added to the contract before the M0 freeze | Accepted | providers, ingress, security | 2026-11-02 |
| 0052 | Local persistent volumes (non-replicated) ship with M5a, before the cold-backup DoD; until then the PRD states no persistent app data | Accepted | engine, sync, roadmap | 2026-11-02 |
| 0053 | Off-site audit-log copy (D-0031) ships in M5a with the S3-compatible path, gated by an object-lock probe per provider | Accepted | audit, roadmap, security | 2026-11-02 |
| 0054 | Host 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 /setup | Accepted | installer, control-plane, security | 2026-11-02 |
| 0055 | Lab TLS source for the M2 deploy DoD: Cloudflare Tunnel with a test domain; ACME exercised separately via DNS-01 against a test zone | Accepted | ingress, lab, roadmap | 2026-11-02 |
| 0056 | Baseline stack without noVNC: consoles are text-only (xterm.js) because Cloud Hypervisor has no VNC or display device | Accepted | stack, engine, console | 2026-11-02 |
| 0057 | Realtime transport: SSE for one-way streams, WebSockets for bidirectional sessions | Accepted | stack, architecture | 2026-11-02 |
| 0059 | Q-0001: embedded Rust reverse proxy behind the Ingress interface; Caddy admin API as fallback backend | Accepted | ingress | 2026-11-02 |
| 0060 | Q-0002: Dockerfile-only builds in M4, run inside a builder microVM; re-verify Railpack/Nixpacks before M4 | Accepted | builds | 2026-11-02 |
| 0061 | Q-0003: write our own minimal init and OCI-to-ext4 pipeline, with in-root path resolution as a required hardening item | Accepted | engine | 2026-11-02 |
| 0062 | Q-0004: reflink snapshots + full chunk scan for M5a cold backups; spike lvm-thin vs btrfs before M5b warm | Accepted | sync, engine | 2026-11-02 |
| 0063 | Q-0005: kernel WireGuard configured by Quoro over netlink; mesh deferred to M5a when cross-node traffic starts | Accepted | networking | 2026-11-02 |
| 0064 | First public release is an M3 preview (v0.1, experimental, not production); M4 complete is the first general release | Accepted | product, roadmap | 2026-11-02 |
| 0065 | Docs stay Markdown in the repo until the first public release; pick docs-site tooling during M3 | Accepted | docs | 2026-11-02 |
| 0067 | Manual promotion of a replica node to primary ships in M5b, alongside leases and fencing | Accepted | roadmap, reliability | 2026-11-02 |
| 0068 | PRD HA table costs confirmed: none = no reserved resources; cold = backup storage and restore time | Accepted | product, reliability | 2026-11-02 |
| 0069 | SQLite and catalog-template backups use Quoro’s own chunk engine with client-side encryption, from consistent snapshots (SQLite backup API / VACUUM INTO) | Accepted | sync, security | 2026-11-02 |
| 0070 | Curated catalog format, owner, location and signature scheme are decided at M4 planning, before the first template ships | Accepted | catalog | 2026-11-02 |
| 0071 | Engine trait async methods return Send futures (fn … -> impl Future<Output = …> + Send), via a standalone contract PR before the M0 freeze | Accepted | engine, contracts | 2026-11-02 |
| 0072 | Lease 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_BOOTTIME | Accepted | reliability, contracts | 2026-11-02 |
| 0073 | Fencing 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 token | Accepted | reliability, contracts | 2026-11-02 |
| 0074 | A 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 unstartable | Accepted | reliability, contracts | 2026-11-02 |
| 0075 | Concurrent fences and a mid-fence crash test are mandatory before expiring leases ship (M5b) | Accepted | reliability | 2026-11-02 |
| 0076 | License identifier is AGPL-3.0-only | Accepted | product, governance | 2026-11-02 |
| 0077 | Governance defaults for the public repo (Q-0023 items 2-6, 8, 9) | Accepted | governance, docs | 2026-11-02 |
| 0078 | A 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 key | Accepted | security, reliability | 2026-11-02 |
| 0079 | Add protocol error code fence_failed and a human-only operator acknowledgement that clears a node’s failed mark | Accepted | reliability, contracts | 2026-11-02 |
| 0080 | Dependency 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 M1 | Accepted | governance, security | 2026-11-02 |
| 0081 | Release signing: keyless GitHub artifact attestations (Sigstore, SLSA build provenance) verified by a sha256-pinned gh in install.sh | Accepted | installer, security, release | 2026-11-02 |
| 0082 | One-off workflow_dispatch dry run of the release workflow: it attests and uploads workflow artifacts only, with no tag and no release | Accepted | installer, security, release | 2026-11-02 |
| 0083 | VM disk_gb minimum is 1 in openapi.yaml (CreateVmRequest and Vm), matching db-schema CHECK (disk_gb >= 1) | Accepted | control-plane, contracts | 2026-11-02 |
| 0084 | M1 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 feature | Accepted | engine, networking, contracts | 2026-11-02 |
| 0086 | cargo-deny checks the targets Quoro ships (x86_64 and aarch64 Linux, gnu and musl), not every target; the ADR 0080 license allowlist is unchanged | Accepted | governance, security | 2026-11-02 |
| 0088 | quoro.service runs as root while the Cloud Hypervisor engine is in-process (M1) | Superseded by 0096 | installer, engine, security | 2026-11-02 |
| 0089 | Privilege split in M2: the API and panel run unprivileged and only the node agent keeps the privileges the engine needs | Accepted | security, installer, engine | 2026-11-02 |
| 0090 | Release 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 byte | Accepted | release, security, installer | 2026-11-02 |
| 0094 | Non-primary nodes get only a per-node tunnel run token from the primary; the account API token never leaves the primary | Accepted | ingress, providers, contract | 2026-11-03 |
| 0095 | Add provider_id to domains and audit_exports and a node-to-tunnel-provider mapping so DELETE /providers/{id} can return 409 while referenced | Accepted | providers, contract | 2026-11-03 |
| 0096 | M2 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-node | Accepted | security, installer, engine, m2 | 2026-11-03 |
| 0098 | AppResources.disk_gb minimum is 1 in openapi.yaml, matching vms CHECK (disk_gb >= 1) | Accepted | contracts, control-plane, apps | 2026-11-03 |
| 0099 | Add domain_id to desired_state spec.routes[] so node certificate.* events can name their domain | Accepted | contracts, apps, ingress, m2 | 2026-11-03 |
| 0102 | M3 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 DoD | Accepted | agents, m3 | 2026-11-03 |
| 0103 | A webhook delivery from a webhook with wake carries data.wake = {agent_id, grant_id} in the incident.opened payload (additive, schema_version 1) | Accepted | contracts, agents, m3 | 2026-11-03 |
| 0104 | Ratify the provisional webhook and event contract choices from #116 | Accepted | contracts, m3 | 2026-11-03 |
| 0105 | M3 ships without deleteAgent; disabling an agent (PATCH) is the lockout; delete semantics are decided later | Accepted | agents, m3 | 2026-11-03 |
| 0106 | Add GET /api/v1/agents/{agent_id}/keys to openapi.yaml so the panel can list an agent’s keys | Accepted | contracts, panel, agents, m3 | 2026-11-03 |
| 0107 | Let operators and viewers resolve user ids to names (a read path for users) | Accepted | contracts, panel, m3 | 2026-11-03 |
| 0109 | Docs site: mdBook built from the repo’s Markdown, published to GitHub Pages together with the v0.1.0 tag | Accepted | docs, m3 | 2026-11-03 |
| 0111 | user:read stays human-only; WebhookDelivery exposes the test flag; creating users is M4 work | Accepted | contracts, m3 | 2026-11-03 |
| 0113 | M4 web terminal path: exec through quoro-init over virtio-vsock, relayed by the root node agent | Accepted | m4, terminal, engine, contracts | 2026-11-04 |
| 0114 | M4 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 approval | Accepted | m4, terminal, console, security, contracts | 2026-11-04 |
| 0115 | M4 session audit: an open record plus a session-end record carrying a SHA-256 of a node-local, size-capped transcript | Accepted | m4, terminal, audit, contracts | 2026-11-04 |
| 0116 | M4 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 node | Accepted | m4, console, engine, contracts | 2026-11-04 |
| 0117 | Builder interface: a node-agent Builder trait (Dockerfile first, Railpack later) that returns build events and a node-local OCI layout | Accepted | builds, m4, contracts | 2026-11-04 |
| 0118 | Builds run on the target node in an ephemeral builder microVM (one per build) with a persistent per-app work disk for the BuildKit cache | Accepted | builds, m4, contracts | 2026-11-04 |
| 0119 | Build 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_logs | Accepted | builds, m4, contracts | 2026-11-04 |
| 0120 | M4 builds and metrics reuse existing scopes; M4 adds no MCP tool | Accepted | contracts, m4 | 2026-11-04 |
| 0121 | Built image reference build.quoro.invalid/<app_id>:<build_id>; Git app image is empty before first build | Accepted | contracts, m4 | 2026-11-04 |
| 0122 | Git apps: createApp takes exactly one of image/source; source cannot be added or removed in M4; createDeployment 409 for Git apps | Accepted | contracts, m4 | 2026-11-04 |
| 0123 | M4 build scope: public HTTPS repos only, trigger api only, one active build per app, fixed default limits | Accepted | contracts, m4 | 2026-11-04 |
| 0124 | Build lifecycle runs through desired state; primary emits build.* events; Deployment.phase build is never reported | Accepted | contracts, m4 | 2026-11-04 |
| 0125 | Builder VM networking: egress_only with no isolation group | Accepted | contracts, m4 | 2026-11-04 |
| 0126 | Terminal exec channel: vsock port 1024, framed [u8 type][u32 BE len][payload], quoro-init config v2 with exec_port | Accepted | contracts, m4 | 2026-11-04 |
| 0127 | Console: Engine::console superseded by serial_output; node ring of two 512 KiB segments; console streams outlive VM restarts | Accepted | contracts, m4 | 2026-11-04 |
| 0128 | Transcripts: terminal only, 16 MiB cap closes session (transcript_limit), 30 days and 1 GiB per node, fetch human-only with audit:read and always audited | Accepted | contracts, m4 | 2026-11-04 |
| 0129 | Metrics: host-side VMM RSS and CPU, 5 s sampling, 10-minute in-memory ring, SSE samples carry no id | Accepted | contracts, m4 | 2026-11-04 |
| 0130 | Public 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 later | Accepted | docs, release | 2026-11-04 |
| 0132 | M4 interactive: ratify contract readings taken while implementing terminal and console sessions | Accepted | terminal, console, m4, contracts | 2026-11-04 |
| 0133 | Keep an app’s last failed or cancelled build in spec.builds so the node keeps its log | Accepted | builds, m4, contracts | 2026-11-04 |
| 0134 | Curated 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 nodes | Accepted | catalog, security, m4 | 2026-11-04 |
| 0135 | Q-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 yet | Accepted | contracts, users, m4 | 2026-11-04 |
| 0136 | Q-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 own | Accepted | m4, engine, panel | 2026-11-04 |