# Project: Orca ## What This Is A minimalist, offline-first, CLI-first orchestration engine inspired by HashiCorp Nomad, prioritizing stability, security, and simplicity over feature richness. Single-binary distribution, no container runtime, no cloud dependencies, no K8s-level complexity. ## Vision A minimalist, offline-first, CLI-first orchestration engine inspired by HashiCorp Nomad, prioritizing stability, security, and simplicity over feature richness. ## Objective Build a lightweight system to manage and execute workloads across a set of nodes, keeping complexity far below that of Kubernetes. ## Requirements - **CLI First**: Primary interaction through a CLI tool. - **Offline First**: Functional without constant internet connectivity. - **AI First**: Designed to be easily discoverable and manageable by AI agents. - **Stability & Security**: Prioritize security fixes and bug fixes over new features. - **Language**: Written in Go 1.25+. - **Simplicity**: Minimalist implementation, avoiding the "K8s complexity trap". ## Constraints - No web UI as a primary requirement. - Must not implement K8s-level complexity. - Feature development must move slowly to ensure stability. - Only CI system allowed: CoreCI (git.cloudinit.dev/coreci/coreci). - Gitea remote: git.cloudinit.dev/coreci/orca. ## Clarified Decisions (D-series, full autonomy) | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-001 | Single binary or multi-binary distribution? | **Single binary** | Simpler distribution; subcommands baked into one `orca` binary. Aligns with simplicity pillar. | 0.95 | | D-002 | Local state store technology? | **modernc/sqlite (pure Go, CGO-free)** | Cross-compile friendly, no CGO dependency, single file on disk, mature. | 0.92 | | D-003 | Inter-node communication? | **Embedded HTTP (net/http) over loopback, mTLS for cross-node** | No external RPC framework needed for v0.1. HTTP suffices. | 0.85 | | D-004 | Scheduling algorithm for v0.1? | **Single-node only (no scheduling)** | Multi-node scheduling is out of scope for v0.1. Tasks run on the node they're submitted to. | 0.90 | | D-005 | CLI output format? | **Human-readable by default, `--json` flag for machine consumption** | Serves both humans and AI agents. | 0.95 | | D-006 | Job/task definition format? | **HCL or YAML in `.hcl`/`.yaml` files** | Familiar to Nomad/HashiCorp users; simpler than JSON for humans. | 0.88 | | D-186 | Bash scripts coverage gate: count toward Go gate or exempt? | **Exempt from Go coverage gate; compensating control: bats tests (C-15) + shellcheck + shfmt in CI; every script must have >=1 happy-path and >=1 failure-path bats test** | Bash is a different language surface from Go; the 70%/50% Go coverage gate (D-042/D-047) is Go-specific. Forcing bash into the Go gate would require a coverage tool that does not exist for bash. The compensating control (bats + shellcheck + shfmt) provides equivalent discipline. | 0.82 | | D-007 | Authentication? | **mTLS for v0.1, token-based deferred** | mTLS is the most secure default. Tokens can be added later if needed. | 0.80 | | D-008 | Container runtime? | **Direct process execution (no container runtime) for v0.1** | Avoids the Docker/container dependency. Pure process management. | 0.85 | | D-009 | Configuration file location? | **`~/.orca/config.hcl` and `/etc/orca/orca.hcl`** | Standard XDG-style paths. | 0.90 | | D-010 | Logging format? | **Structured JSON via `log/slog`** | Native Go 1.21+ slog, no external dependency. | 0.95 | | D-011 | v0.2 mTLS cert authority model? | **Internal CA with CSR join** | One node bootstraps a local CA; peers generate CSRs and submit them to the CA for signing. CA cert is the trust anchor. More secure than self-signed per-node (single trust root) without the operational complexity of an external PKI. | 0.92 | | D-012 | v0.2 CA bootstrap & cert distribution? | **Operator-mediated, fingerprint-verified** | Bootstrap node writes `~/.orca/ca.crt` and `~/.orca/ca.key` (mode 0600). Operator copies `ca.crt` to peers; peers verify by SHA-256 fingerprint at `orca node join --ca-fingerprint `. No automated secret distribution. | 0.85 | | D-013 | v0.2 cert validity & rotation? | **Server certs 90 days, CA cert 10 years, rotate 30 days before expiry** | Server certs are short-lived (compromise window small); CA is long-lived (manual rotation is expensive). `orca cert renew` reissues server certs automatically. | 0.90 | | D-014 | v0.2 mTLS handshake timing? | **Eager — at `orca node join` time** | Fail fast on bad certs, misconfigurations, or CA mismatches. Lazy handshake would let stale configs run until first request, complicating debugging. | 0.88 | | D-015 | v0.2 minimum TLS version & cipher suites? | **TLS 1.3 only; AEAD cipher allowlist** | MinVersion=tls.VersionTLS13, CipherSuites limited to TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_GCM_SHA256. No TLS 1.2 fallback. | 0.92 | | D-016 | v0.2 security-scanning placement? | **`validate` pipeline of `.coreci.yml`, gates merges to main** | `gosec` baseline JSON checked into repo; new findings fail the build. `govulncheck ./...` exit-on-known. Pre-commit hook with `gitleaks` is opt-in (developer machine). | 0.85 | | D-017 | v0.2 `iter.Seq` API surface? | **`orca job list --watch` and `orca node list --watch`** | Pull-based `iter.Seq[Job]` / `iter.Seq[Node]`; cancellation via `context.Context`; `signal.NotifyContext` on ctrl-c. Backpressure is implicit (consumer-driven). | 0.90 | | D-018 | v0.2 multi-node scheduling algorithm? | **Bin-packing by available CPU/memory, FIFO within a node** | Simple, deterministic, matches D-004 minimalism. Cross-node dispatch via ConnectRPC `orca.v1.Dispatch` service. Retry on transient failures with exponential backoff. | 0.85 | ## Out of Scope - Full-blown Kubernetes-compatible API. - Complex cloud-provider integrations. - GUI-based management consoles. - Container runtime integration. - Service mesh / sidecar injection. - Auto-scaling / horizontal pod autoscaler. - External PKI / Let's Encrypt / cert transparency logs. - gRPC framework dependency (ConnectRPC in `config.json` frameworks but not in `go.mod`; v0.2 uses stdlib `net/http` with h2c for `orca.v1.Dispatch` — see ARCHITECTURE.md AD-014). ## v0.2 Scope Summary v0.2 is a focused 4-phase milestone that turns Orca from a single-node process executor into a small cluster engine with strong transport security and richer I/O. The 4 phases are: - **P01 — mTLS handshake + internal CA with CSR join.** Internal CA, CSR join, eager handshake at `node join`, TLS 1.3 + AEAD allowlist. See ARCHITECTURE.md Flow 1 + Flow 2. - **P02 — Multi-node scheduling & job dispatch.** Best-fit bin-packing by CPU/memory, FIFO within a node, `orca.v1.Dispatch` over mTLS. See ARCHITECTURE.md Flow 3. - **P03 — `gosec` + `govulncheck` + `gitleaks` in CI.** `gosec` baseline JSON in repo, `govulncheck ./...` in `validate` pipeline, `gitleaks` in pre-commit (opt-in). - **P04 — `iter.Seq` streaming for `--watch` flags.** Go 1.25+ range-over-func semantics, `context.Context` cancellation, `signal.NotifyContext` on ctrl-c. See ARCHITECTURE.md Flow 4. The vision ("minimalist, offline-first, CLI-first orchestration engine") is unchanged. v0.2 is a hardening + small-cluster extension, not a direction change. ## Key Decisions The 18 D-series decisions (D-001..D-018) are recorded in the "Clarified Decisions" table above. The 10 v0.1 decisions (D-001..D-010) are stable and unchanged in v0.2. The 8 v0.2 decisions (D-011..D-018) were auto-resolved under full autonomy and are summarized here: - **D-011: Internal CA with CSR join** (vs. self-signed per-node or SPIFFE). Single trust root, no external PKI, CSR workflow. - **D-012: Operator-mediated CA cert distribution with fingerprint verify** (no automated secret distribution — matches offline-first principle). - **D-013: 90d server certs, 10y CA cert, 30d pre-expiry rotation.** - **D-014: Eager mTLS handshake at `orca node join` time** (fail fast). - **D-015: TLS 1.3 only, AEAD cipher allowlist** (no TLS 1.2 fallback). - **D-016: `gosec`+`govulncheck` in `validate` pipeline of `.coreci.yml`** (gates merges to main). `gitleaks` in pre-commit (opt-in). - **D-017: `iter.Seq` for `orca job list --watch` and `orca node list --watch`** (pull-based, ctx cancellation, ctrl-c via `signal.NotifyContext`). - **D-018: Bin-packing by CPU/memory with FIFO within node; JSON-over-HTTP orca.v1.Dispatch for cross-node** (no ConnectRPC dep). ## v0.3 Clarified Decisions (D-series, full autonomy) v0.3 is a lean 2-execution-phase milestone completing the streaming and doctor work deferred from v0.2. The 6 v0.3 decisions (D-019..D-024) were auto-resolved under full autonomy: | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-019 | Watch refresh mechanism? | **Poll-based, 1s ticker** | Simpler than event channel; no daemon coupling for CLI; matches offline-first. | 0.90 | | D-020 | Watch output format (REQ-030)? | **Table by default; `--watch --json` streams one-line JSON per event** | Consistent with D-005 `--json` convention; serves humans + AI agents. | 0.92 | | D-021 | Doctor network check scope? | **Probe configured peer addresses via mTLS `/healthz` handshake; PASS/WARN/FAIL per peer** | Reuses existing transport client; read-only. | 0.85 | | D-022 | Doctor db check scope? | **`PRAGMA integrity_check` + migration version query** | Already specced in ARCHITECTURE.md §5; minimal surface. | 0.95 | | D-023 | iter.Seq cancellation? | **`signal.NotifyContext` on SIGINT/SIGTERM** | Per D-017 + ARCHITECTURE Flow 4. | 0.95 | | D-024 | `--watch` applies to job list only, or node list too? | **Both `orca job list --watch` and `orca node list --watch`** | Per ARCHITECTURE.md CLI layer + D-017. | 0.92 | ## v0.3 Scope Summary v0.3 is a focused 2-execution-phase milestone completing the work deferred from v0.2 that was NOT already shipped in P08-P10. A codebase audit during re-init SPECIFY confirmed that REQ-014, REQ-027, REQ-028, REQ-029, REQ-031, REQ-037, REQ-039, REQ-040 all shipped in P08-P10 despite stale REQUIREMENTS.md marking them Pending. The remaining work: - **P01 — `iter.Seq` streaming for `--watch` flags.** Go 1.25+ range-over-func semantics, pull-based `iter.Seq[Job]` / `iter.Seq[Node]`, `context.Context` cancellation, `signal.NotifyContext` on ctrl-c. Applies to both `orca job list --watch` and `orca node list --watch`. Covers REQ-022, REQ-030. - **P02 — `orca doctor` network + db full implementation.** Replaces the P01 stubs (`NetworkStub`, `DBStub`) with real checks: peer reachability via mTLS `/healthz` probe; SQLite `PRAGMA integrity_check` + migration version. Covers REQ-032 (completion). The vision ("minimalist, offline-first, CLI-first orchestration engine") is unchanged. v0.3 is a completion milestone, not a direction change. ## v0.5 Scope Summary — Distribution v0.5 is a 3-execution-phase milestone that makes Orca installable, distributable, and containerized. The engine functionality from v0.1–v0.3 is unchanged; this milestone is purely about **delivery surface**: - **P01 — Namespace unification.** A single `ORCA_HOME` environment variable becomes the namespace root for *all* on-disk state (db, certs, init, daemon). A `--system` flag on the root command selects the system-level namespace root `/root/.orca`. Backward compatible: empty `ORCA_HOME` → `~/.orca`. Covers REQ-041, REQ-042. - **P02 — `install.sh` + in-place update.** A 1-liner installer pulls the release binary from the public Gitea release URL, installs at user level by default (`~/.local/bin/orca`) or system level (`/usr/local/bin/orca`) with `--system`. Re-running updates the binary in place while preserving config/db/certs in the namespace dir. Idempotent. Covers REQ-043, REQ-044. Also updates README quickstart (REQ-016 completion). - **P03 — Docker release.** A multi-stage `Dockerfile` builds a distroless image; `scripts/release.sh` and `.coreci.yml` publish the image to the Gitea container registry per release. Covers REQ-046. - **P04 — Final review + ship + audit.** Milestone release. The vision ("minimalist, offline-first, CLI-first orchestration engine") is unchanged. v0.5 is a distribution milestone, not a direction change. ## v0.5 Clarified Decisions (D-series, full autonomy) The 5 v0.5 decisions (D-025..D-029) were auto-resolved under full autonomy during the CLARIFY stage: | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-025 | System-level namespace path layout? | **`/root/.orca`** (mirror of user-level `~/.orca`) | Consistent shape with user-level; just a different root. Matches the user's "starts at /root" wording. Single dir keeps it simple. | 0.90 | | D-026 | Namespace override mechanism at runtime? | **Unify on `ORCA_HOME`** as single namespace root for all components (db, certs, init, daemon). Add `--system` flag that sets root to `/root/.orca`. | `ORCA_HOME` already exists for certs; extend to all components. Backward compatible (empty → `~/.orca`). One knob, not many. | 0.92 | | D-027 | Docker registry target? | **Gitea built-in container registry** (`git.cloudinit.dev/coreci/orca`) | Keeps everything in one forge; uses Gitea's native registry. Consistent with REQ-045 (public repo → public image pulls). | 0.88 | | D-028 | How to make releases publicly accessible (REQ-045)? | **Flip repo visibility to public** via `tea repos edit coreci/orca --private=false` during P0 ship | Simplest path to anonymous downloads; enables both install.sh pulls and docker pulls. Pre-existing `.env` leak already suppressed via gitleaks baseline + rotate-forward (commit 00127ce). | 0.85 | | D-029 | install.sh default version? | **Latest release** (query Gitea releases API), optional `--version vX.Y.Z` to pin | Matches typical 1-liner installer UX; users get newest by default, can pin for reproducibility. | 0.92 | ### v0.5 Operational prerequisite (P0 ship) The Gitea repo `coreci/orca` is currently **private** (returns 404 unauthenticated). P0 ship flips visibility to public via `tea repos edit coreci/orca --private=false` so that `install.sh` can pull release binaries unauthenticated (REQ-045). This is an operational step performed during the P0 ship, verified by an unauth `curl` against the releases API. ## v0.6 Scope Summary — Node Bootstrap & Proxmox v0.6 is a 3-execution-phase milestone that turns `orca init` from a bare `mkdir` into a full single-node cluster bootstrap, and adds Proxmox 8 & 9 as a first-class remote node type joined over SSH with least-privilege role delegation. The engine functionality from v0.1–v0.5 is unchanged; this milestone is about **bootstrap ergonomics** and **heterogeneous node support**: - **P01 — `orca init` full bootstrap.** A single `orca init` call now: (a) creates the namespace dir (`~/.orca` or `/root/.orca` with `--system`); (b) runs all DB migrations including the new 0006 (`nodes.kind`, `nodes.os` — backward-compatible nullable columns); (c) bootstraps the internal CA via `security.CAInit` if `ca.crt` is absent; (d) generates the server cert via `security.GenerateCSR` + `ca.SignCSR` if `server.crt` is absent; (e) auto-detects the local OS via `/etc/os-release` `ID=` field (ubuntu/debian/alpine); (f) registers a `localhost` node with `kind=localhost`, `os=`, `addr=localhost:8443` if no localhost node exists yet. After `orca init`, `orca doctor` MUST pass with zero FAILs. Idempotent: re-running `orca init` is a no-op (or refresh) for already-provisioned artifacts. Covers REQ-047, REQ-048, REQ-049. - **P02 — Proxmox SSH join.** `orca node join --type proxmox --host --user root --password ` (password via flag or `$ORCA_PROXMOX_PASSWORD`, **never persisted**) bootstraps a remote Proxmox 8/9 host via `golang.org/x/crypto/ssh` (new direct dep). Steps: (1) SSH password-auth; (2) generate or load orca's SSH keypair (`~/.orca/orca_ssh_key` / `.pub`, 0600/0644); (3) deploy pubkey to remote `~orca/.ssh/authorized_keys`; (4) create `orca` user (config-overridable name via `--proxmox-user`, default `orca`); (5) create PVE custom role `OrcaOperator` (config-overridable via `--proxmox-role`) with privileges `VM.Audit`, `Datastore.AllocateSpace`, `SDN.Use`; (6) assign role to `orca` user on `/`; (7) drop `/etc/sudoers.d/orca` allowlist (`pct`, `qm`, `pvesh`, `apt-get`, `dpkg` — no shell-escape commands); (8) record node row `kind=proxmox`, `os=pve`, audit log. Idempotent re-run. Covers REQ-050, REQ-051. - **P03 — `doctor os` + `doctor proxmox`.** Extends `orca doctor` with two new checks: `doctor os` re-runs `/etc/os-release` detection and verifies it matches the stored localhost node row's `os` field (drift = WARN); `doctor proxmox` iterates `kind=proxmox` nodes and SSH-probes each with `pveversion` / `pvecmd status` (3s timeout per peer per D-038 pattern), reporting PASS/WARN/FAIL per node. All bootstrap + join actions emit structured audit-log entries. Covers REQ-052. - **P04 — Final review + ship + audit.** Milestone release. The vision ("minimalist, offline-first, CLI-first orchestration engine") is unchanged. v0.6 is a bootstrap-ergonomics + heterogeneous- nodes milestone, not a direction change. ## v0.6 Clarified Decisions (D-series, full autonomy) The 8 v0.6 decisions (D-030..D-037) were resolved during the CLARIFY stage — D-030..D-034 confirmed by the operator in plan mode, D-035..D-037 auto-resolved at full autonomy within the `clarify_budget`: | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-030 | SSH library for Proxmox join? | **`golang.org/x/crypto/ssh`** | Stdlib-adjacent, well-maintained, single new direct dep. Matches orca's minimal-deps ethos. Shell-out to `/usr/bin/ssh` would require openssh-client on the orca host and complicate password-auth + idempotent pubkey deploy. | 0.92 (operator-confirmed) | | D-031 | Proxmox join password handling? | **Flag/env only, never persisted** | `--password` flag or `$ORCA_PROXMOX_PASSWORD` is used once to deploy the orca pubkey + create the `orca` user; the password is never written to SQLite. Subsequent orca→Proxmox access uses the deployed SSH key. | 0.95 (operator-confirmed) | | D-032 | Localhost OS auto-detect signal? | **`/etc/os-release` `ID=` field** | Parse `ID=` from `/etc/os-release`; map `ubuntu`/`debian`/`alpine` → node `os`. Falls back to `linux` (unknown) if none match. Simplest reliable signal across the three target distros. | 0.93 (operator-confirmed) | | D-033 | Least-privilege Proxmox role granularity? | **Custom PVE role `OrcaOperator`** with `VM.Audit`, `Datastore.AllocateSpace`, `SDN.Use` + `/etc/sudoers.d/orca` allowlist (`pct`, `qm`, `pvesh`, `apt-get`, `dpkg`) | Config-overridable role + user names. Sufficient for "manage the host, VMs/CTs, storage, packages" without granting root shell. Built-in `PVEAuditor` is too read-only; full `Administrator` is too broad. | 0.88 (operator-confirmed) | | D-034 | Node kind/os schema? | **Add `nodes.kind` + `nodes.os` columns via migration 0006** | Schema-first, queryable, doctor can branch on kind. Nullable with `localhost`/`""` defaults for existing rows (backward-compatible). data-engineer owns the migration. | 0.94 (operator-confirmed) | | D-035 | SSH host-key verification on first Proxmox connect? | **TOFU: pin on first connect, refuse on mismatch thereafter** | First connect uses `ssh.InsecureIgnoreHostKey` to capture the host key; it is then persisted to `~/.orca/known_hosts` (or the nodes metadata) and all subsequent connects require a match. Balances first-run ergonomics against MITM risk on subsequent runs. Switching to pre-pinned keys is a future enhancement. | 0.82 (auto) | | D-036 | `orca init` idempotency semantics for already-provisioned artifacts? | **Skip-and-refresh, never overwrite** | If `ca.crt` exists → load it (no regen). If `server.crt` exists → keep it (no reissue). If a localhost node row exists → update `last_seen` + re-detect `os`, never insert a duplicate. If DB migrations are ahead → no-op. If `~/.orca` exists → MkdirAll is a no-op. Idempotent re-run is a hard requirement (REQ-047). | 0.95 (auto) | | D-037 | orca SSH keypair location + algorithm? | **`~/.orca/orca_ssh_key` (0600) + `~/.orca/orca_ssh_key.pub` (0644), Ed25519** | Ed25519 keys are smaller, faster, and more secure than RSA for SSH auth. Stored in the orca namespace dir alongside ca.crt/server.crt so `ORCA_HOME` relocation works. File modes mirror the cert file-mode discipline (REQ-033 spirit). Generated lazily on first `orca node join --type proxmox`, not at `orca init` (localhost doesn't need SSH). | 0.90 (auto) | ### v0.6 clarification notes - **D-035 TOFU caveat**: TOFU (trust-on-first-use) is the standard SSH UX and matches the operator-mediated model from D-012 (CA cert distribution). The operator is expected to verify the host key fingerprint out-of-band on first connect if the network is untrusted. A future milestone may add `--host-key-fingerprint` pin flag to `orca node join --type proxmox` for pre-pinned deployments. - **D-036 idempotency**: re-running `orca init` on a node that already has a localhost row updates `last_seen` and re-detects `os` (in case the host OS was upgraded) but does NOT change the node `ID` or `joined_at`. This makes `orca init` safe to put in a systemd ExecStartPre or a config-management runbook. - **D-037 Ed25519**: `golang.org/x/crypto/ssh` + `golang.org/x/crypto/ed25519` are in the same module; no additional direct dep beyond D-030. ## v0.7 Clarified Decisions (D-series, full autonomy) The 5 v0.7 decisions (D-038..D-042) were auto-resolved at full autonomy within the `clarify_budget` (10): | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-038 | Config file format — HCL or YAML? | **HCL** | D-009 already specced `config.hcl`. HCL is already a direct dep (hashicorp/hcl/v2 for jobspec). Adding YAML would introduce a second parser dep — violates minimal-deps. Use the existing `hclparse` pkg from jobspec. | 0.93 | | D-039 | Config precedence order (flag vs env vs file vs default)? | **flag > env > file > default** | Standard layered config: the most explicit (flag) wins, then the runtime (env), then the persisted (file), then the built-in default. Matches cobra/viper convention without the viper dep. | 0.92 | | D-040 | pprof security — bind to localhost only, or operator-chosen addr? | **Operator-chosen `--pprof ` (default disabled)** | Default disabled keeps the minimalist posture. Operator picks the addr — localhost for dev, unix socket for prod. Separate mux so it never touches the mTLS daemon listener. No auth (pprof is operator-only, addr is the gate). | 0.85 | | D-041 | cert command registration — where in root command order? | **After `cert` is unreachable today, append after `node` in rootCmd.AddCommand order** | Alphabetical-ish with the existing cluster (audit, daemon, doctor, init, job, node, cert, status, version). No behavior change to existing commands. | 0.88 | | D-042 | Coverage target — 50% floor or higher? | **50% floor per package, 70% target for new packages** | 50% is achievable for the concurrent packages (engine, transport) without heroic mock effort; 70% is the floor for new code in P02/P04. Avoids a "raise coverage everywhere" rathole. | 0.85 | ## v0.7 Scope Summary — Hardening & Completion v0.7 is a 4-execution-phase **NFR milestone** that closes out gaps surfaced by the v0.7 IDEATE stage: an unreachable command tree, a missing config file layer, low test coverage in core packages, and the long-deferred pprof endpoint. The engine functionality from v0.1–v0.6 is unchanged; this milestone is purely about **correctness, coverage, and operability**: - **P01 — Register `orca cert` command tree + cert_repo tests.** The `internal/cli/cert.go` command (`cert ca-init`, `cert gen`, `cert show`, `cert renew`, `cert fingerprint`) is fully implemented but never wired into `rootCmd`. This phase adds the missing `rootCmd.AddCommand(newCertCmd(...))` and adds the missing `internal/store/cert_repo_test.go`. Covers REQ-053. - **P02 — HCL config file parsing (`config.hcl`).** D-009 specified `~/.orca/config.hcl` and `/etc/orca/orca.hcl` as config locations, but no HCL config-file parser exists — the CLI relies entirely on flags and env vars. This phase adds a minimal `internal/config` package that loads `config.hcl` (keys: `db_path`, `listen_addr`, `ca_path`, `server_cert_path`, `server_key_path`, `node_capacity`), merges with env/flag overrides (flag > env > file > default), and surfaces it via `--config` flag on the root command. Covers REQ-054. - **P03 — Test coverage uplift.** Adds tests for the lowest-coverage packages: `internal/engine` (executor, dispatcher, peer — currently 8.3%), `internal/transport` (mtls, dispatch, handshake_log — currently 26.3%), `internal/proxmox` (bootstrap SSH path — currently 5.1%), and `internal/audit` (no tests). Target: every package ≥ 50% coverage. Covers REQ-055. - **P04 — `--pprof` opt-in on `orca daemon`.** Adds the long-deferred I-308 pprof endpoint behind an opt-in `--pprof ` flag (default disabled). `net/http/pprof` mounted on a separate mux so it never touches the mTLS daemon listener. Covers REQ-056. - **P05 — Final review + ship + audit.** Milestone release. The vision ("minimalist, offline-first, CLI-first orchestration engine") is unchanged. v0.7 is a hardening milestone, not a direction change. Milestone type: NFR (all phases are fix/test/chore); the final phase's progressive patch IS the deliverable per `run.md` versioning logic. Tags run on the v0.6.x patch line: `v0.6.0` (P0) … `v0.6.5` (P05 = milestone release). ## v0.8 Scope Summary — Coverage & Trust Hardening v0.8 is a 3-execution-phase **NFR milestone** that continues the hardening theme opened by v0.7. v0.7 P03 (REQ-055) lifted four packages to ≥ 50%, but a coverage re-baseline after v0.7 ship shows the floor was insufficient: `internal/engine` regressed to 8.3%, `internal/proxmox` to 5.1%, and four more packages sit between 26% and 48%. Three packages (`internal/audit`, `internal/certpaths`, `cmd/orca`) still have **no test files at all**. v0.8 also closes the two "future enhancement" hooks explicitly deferred in v0.6 — SSH host-key pre-pinning (D-035 caveat) and `orca node key-reset` (RESEARCH_v0.6 §80) — and adds a requirements-hygiene gate so the stale-REQ-status drift seen in REQUIREMENTS.md after v0.7 ship cannot recur: - **P01 — Coverage uplift round 2.** Raise six under-50% packages to ≥ 70% and add first tests for the three zero-test packages. Covers REQ-057. - **P02 — SSH trust hardening.** `--host-key-fingerprint` pre-pin flag on `orca node join --type proxmox` + `orca node key-reset ` command. Covers REQ-058, REQ-059. - **P03 — Requirements-hygiene gate.** `make verify-reqs` target + verify-stage assertion that ROADMAP `Complete` ↔ REQUIREMENTS `Complete`. Covers REQ-060. - **P04 — Final review + ship + audit.** Milestone release. The vision is unchanged. v0.8 is a hardening milestone, not a direction change. Milestone type: NFR (all phases are test/feat-chore on the trust surface — see CLARIFY D-043 for the `feat` vs `chore` classification of P02); the final phase's progressive patch IS the deliverable per `run.md` versioning logic. Tags run on the **v0.7.x** patch line: `v0.7.0` (P0) … `v0.7.4` (P04 = milestone release). ## v0.8 Clarified Decisions (D-series, full autonomy) The 5 v0.8 decisions (D-043..D-047) were auto-resolved at full autonomy within the `clarify_budget` (10): | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-043 | Is P02 (SSH trust hardening) a `feat` phase or a `chore` phase? It adds a new flag + a new subcommand. | **`chore` (trust-surface hardening), not `feat`** | Both `--host-key-fingerprint` and `orca node key-reset` refine the *existing* `orca node join --type proxmox` flow and the existing TOFU `known_hosts` store (D-035). No new orchestration capability, no new node kind, no new API. They close a security gap explicitly deferred in v0.6, not open new surface area. Per `run.md` versioning logic this keeps v0.8 NFR (all phases fix/test/chore/perf/refactor). | 0.84 | | D-044 | Where does `--host-key-fingerprint` live — on `orca node join` or only on `--type proxmox`? | **On `orca node join` (root of the join subcommand), validated when `--type proxmox`** | The flag is generic (any future SSH-joined node kind will use it); gating it to `--type proxmox` only would require re-adding it later. Validation (`flag requires --type proxmox today`) happens in `RunE`, not in the flag declaration, so the flag is declared once on `node join` and the type check emits a clear error for non-proxmox types until other SSH-joined kinds exist. | 0.86 | | D-045 | `--host-key-fingerprint` format — raw hex, `sha256:`-prefixed, or OpenSSH `SHA256:base64`? | **OpenSSH `SHA256:base64` (the format `ssh-keyscan -E sha256 -D -` emits and operators expect)** | Matches the fingerprint format operators already see from `ssh-keyscan` and `orca node join`'s own `Result.HostKeyFingerprint` output. Accept only `SHA256:`-prefixed base64; reject raw hex with a clear error. Internally decode base64 → compare against `ssh.PublicKey` Marshal + sha256. | 0.88 | | D-046 | Does `orca node key-reset ` also revoke the orca pubkey on the remote host, or only clear the local `known_hosts` entry? | **Local `known_hosts` entry only** | Revoking the remote authorized_keys entry would orphan a working node (next dispatch would fail auth). `key-reset` is the local "forget this host's key" operation (mirrors `ssh-keygen -R host`); re-establishing trust is a separate `orca node join` re-run. Audit-log the reset with `actor`, `node`, `event=node.key_reset`. | 0.90 | | D-047 | Coverage target for P01 — 70% floor or higher? | **70% floor for the 6 under-50% packages; 50% floor for the 3 zero-test packages (`internal/audit`, `internal/certpaths`, `cmd/orca`) as a first-toe-hold** | 70% across the board for the already-tested packages matches D-042's "70% target for new packages" and is achievable without heroic mock effort. For the zero-test packages, going 0→50% is the realistic single-phase step (0→70% risks a coverage rathole on `cmd/orca` which is glue code); a future milestone can lift them to 70%. | 0.82 | --- # v0.9/v0.10 — Re-architecture Scope Summary (Supersedes v0.1–v0.8 architecture) v0.9 is the first DIRECTION-CHANGE milestone in the project's history. It supersedes the shipped v0.1–v0.8 architecture per the adopted PRD (`.ciagent/PRD_v0.9.md`). The re-architecture deprecates the daemon/ transport/internal-CA/HCL/single-namespace stack and builds a CLI-only/ SSH-push/step-ca/Markdown-frontmatter/multi-namespace stack plus 8 net-new subsystems. ## Override Justification (Re-architecture Justification axis) The ci-griller returned REPLAN (0.70) on the Re-architecture Justification axis, noting the PRD reverses 6 documented decisions without new evidence and that the incremental-additive path was not evaluated. The user reviewed the fork and overrode the *direction* with a six-part evidence basis. The override is recorded verbatim below; each part addresses a reversal that the grill flagged as unjustified. 1. **The v0.8 daemon model is operationally failing** in the target environment — R-001 ("no orca binary on any server") is a response to measured pain, not preference. 2. **step-ca is externally mandated** (D-101) — the operator environment requires an external CA; AD-010's "too heavyweight" rationale is no longer operative. 3. **Multi-tenancy is a hard product requirement** (R-002) — real multi-tenant use cases cannot be served by the single-namespace layout; the "no multi-tenancy" anti-pattern is obsolete. 4. **WASM is a hard workload requirement** (D-088) — workloads are WASM, not processes; `os/exec` is insufficient; the "no container runtime" anti-pattern is reversed. 5. **SSH-push is the only viable deployment target** for the operator's bare-Linux/Proxmox environment — installing/maintaining an orca daemon on every peer is operationally infeasible. 6. **Simplicity/vision correction** — the v0.1-v0.8 daemon model was a wrong turn against the original CLI-first vision; the re-architecture corrects the vision. ## Supersession Table (AD-series reversals, recorded per grill PC-09) | Old decision | Was | Superseded by | Evidence basis | |---|---|---|---| | AD-010 (ARCHITECTURE.md:463) | step-ca/cfssl/vault-pki "too heavyweight" | **D-101** (step-ca) | Override ground 2 (external mandate) | | SPIFFE rejection (PROJECT.md:94) | internal CA chosen over SPIFFE | **D-068** (SPIFFE SVIDs) | Override ground 3 (multi-tenancy requires per-workload identity) | | No-container-runtime (ARCHITECTURE.md:477) | explicit anti-pattern | **D-088** (5 runtimes; wasmtime primary) | Override ground 4 (WASM is the workload profile) | | No-multi-tenancy (ARCHITECTURE.md:478) | explicit anti-pattern | **D-158 / R-002** (many namespaces under ORCA_HOME) | Override ground 3 (hard multi-tenant product req) | | AD-007 (HCL canonical) | HCL for jobspec | **R-013 / R-014** (Markdown canonical; HCL legacy) | PRD §8 (Markdown + body preservation is the operator-facing format) | | Daemon-on-every-node | `orca daemon` on all peers | **R-001** (no orca binary on any server) | Override grounds 1 + 5 (daemon failing; SSH-push only viable target) | The 19 binding conditions (C-01..C-19) and 10 phase challenges (PC-01..PC-10) from `GRILL_v0.9.md` are adopted as execution gates. The 30 net-new requirements (REQ-061..REQ-090) from `IDEATION_v0.9.md` are recorded in `REQUIREMENTS.md`. The reordered phase plan is in `ROADMAP.md`. ## v0.9 Clarified Decisions (D-series, full autonomy — Phase 0 pre-execution) | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-101 | Cluster CA: internal Go CA (AD-010) or step-ca (external)? | **step-ca (apt-installed)** | Externally mandated per override ground 2; AD-010's "too heavyweight" rationale reversed. CLI wraps `step` CLI via SSH (no Go step-ca client library — keep zero-new-dep posture if possible, or add `github.com/smallstep/cli` as a dep). **Gated by C-07** (CA migration spec). | 0.74 | | D-068 | Workload identity: internal X.509 CA or SPIFFE SVIDs? | **SPIFFE SVIDs minted at submit time via step-ca** | Multi-tenancy (override ground 3) requires per-workload identity model; SPIFFE is the standard. SPIFFE ID `spiffe://orca/ns//job//alloc/` as SAN. **Gated by C-08** (mint spike in v0.10-P01.5; fallback to mTLS identity if spike fails). | 0.72 | | D-088 | Runtime: direct os/exec only (D-008) or multi-runtime? | **5 runtimes: wasm (wasmtime primary), podman, process, pve-vm, pve-ct** | WASM is the primary workload (override ground 4). `processRuntime` wraps existing `executor.go`; others are net-new. Split P07a/b/c per grill PC-10. **P07b gated by C-01** (wasmtime/CGO eval). | 0.82 | | D-158 | Namespace model: single flat root or multi-namespace? | **Multi-namespace under ORCA_HOME (R-002)** | Hard multi-tenant product requirement (override ground 3). `_defaults/` implicit root; `cluster/` for cluster-wide; per-namespace `db/`, `.env`, `.env.secrets`, `jobs/`, `alloc/`, `ns.md`. No namespace column in SQLite. | 0.84 | | D-179 | Jobspec format: HCL canonical (AD-007) or Markdown? | **Markdown with YAML frontmatter canonical (R-013); HCL legacy** | PRD §8 — Markdown + body preservation is the operator-facing format. HCL adapter (REQ-064) preserves `orca job run old-spec.hcl` during migration. | 0.85 | | D-185 | Re-architecture justification: incremental additive or full re-architecture? | **Full re-architecture (overridden by user)** | Six-part evidence basis above; the grill's REPLAN mechanics (PC-01..PC-10, C-01..C-19) adopted as gates. The incremental-additive path was evaluated and rejected on grounds 1 + 5 (daemon failing; SSH-push only viable). | 0.88 | | D-187 | wasmtime Go binding (bytecodealliance/wasmtime-go) is CGO-based — does adopting it revoke D-002 (modernc/sqlite CGO-free cross-compile story)? | **Use the wasmtime CLI (apt-installed on peer) via SSH exec; do NOT import wasmtime-go.** | The Go binding links libwasmtime via cgo and would revoke D-002's CGO-free cross-compile story. The CLI-via-SSH approach (same pattern as podman/qm/pct) avoids CGO entirely. `internal/runtime/wasm.go` imports only stdlib + sshpush. `CGO_ENABLED=0 go build ./...` succeeds. C-01 grill gate SATISFIED; D-002 NOT revoked. Full evaluation in `internal/runtime/C01_WASMTIME_CGO_EVAL.md`. | 0.90 | --- # v0.10 Docs & Install Milestone — Scope Summary v0.10 is a focused milestone that closes the documentation gap left by the v0.9 re-architecture and fixes the release/install pipeline bug that caused `install.sh` to resolve to v0.4.5 instead of the latest release. The v0.9 re-architecture shipped a complete CLI surface (markdown jobspec, `orca ns`, `orca node capacity`, CLI-side scheduler, emitters, Traefik ingress) but no operator-facing reference documentation. This milestone ships that documentation plus a worked full-stack example with ingress configured, and hardens the release pipeline so every Gitea release carries a Linux binary asset. ## Root cause of the v0.4.5 install The v0.8.x releases (v0.8.0 through v0.8.15) shipped with **zero binary assets attached** to their Gitea releases. `scripts/install.sh` resolves "latest" by hitting `/releases/latest` (returns v0.8.15), then looks for `orca-v0.8.15-linux-amd64.tar.gz` in that release's assets. Since the asset is missing, install.sh errors out — there is no fallback walk to older releases that DO carry a binary. The user's v0.4.5 install came from an earlier run or a pinned `--version`. The fix is forward: harden `scripts/release.sh` to cross-build the amd64 tarball and verify the asset attached post-create; harden `scripts/install.sh` to walk backward through releases if the latest lacks the asset. ## v0.10 Phases - **Phase 0 (pre-execution)**: specify → clarify → research → ideate → plan → grill. Tag `v0.9.0`. - **Phase P1 — release/install fix** (REQ-097, REQ-098): cross-build amd64 tarball in release.sh, post-create asset verification, install.sh fallback walk. Tag `v0.9.1`. - **Phase P2 — CLI + jobspec + ingress docs** (REQ-091, REQ-092, REQ-093): `docs/cli.md`, `docs/jobspec.md`, `docs/ingress.md`. Tag `v0.9.2`. - **Phase P3 — full-stack examples** (REQ-094): `examples/full-stack/` with 5 valid jobspecs + rendered artifacts + walkthrough README. Tag `v0.9.3`. - **Phase P4 — README + namespace.md refresh** (REQ-095, REQ-096): README subcommand table + install example + docs/examples sections; `docs/namespace.md` v0.9 layout. Tag `v0.9.4`. - **Phase P5 — final review + ship + audit** (milestone release). Tag `v0.9.5` = v0.10.0 milestone release. **Milestone type**: feature (P1 ships `fix` phases; P2/P3/P4 ship `docs` phases; at least one non-docs phase makes this a feature milestone per the versioning logic). Tags run on the v0.9.x patch line. The milestone branch label is `milestone/v0.10-docs-cli-examples`. The vision ("minimalist, offline-first, CLI-first orchestration engine") is unchanged. v0.10 is a documentation + install-hardening milestone, not a direction change. It builds on the v0.9 re-architecture foundation without modifying any Go orchestration code. ## v0.10 Clarified Decisions (D-series, full autonomy — Phase 0 pre-execution) | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-188 | Should the CLI docs be a single `docs/cli.md` reference or a per-command `docs/cli/` subdirectory? | **Single `docs/cli.md` reference** | Mirrors the existing flat `docs/` pattern (install.md, docker.md, namespace.md, security-scanning.md). One file is more discoverable for a CLI tool and avoids navigation overhead. A per-command subdirectory diverges from the established layout. | 0.92 | | D-189 | Should the examples live in `examples/full-stack/` or in `testdata/`? | **`examples/full-stack/` as a new top-level directory** | `testdata/` holds legacy HCL fixtures (`hello.hcl`, `fail.hcl`) used by Go tests; mixing operator-facing examples with test fixtures conflates audiences. A new `examples/` directory is the conventional location for worked examples and is what an operator expects to find. | 0.93 | | D-190 | How deep should the ingress/Traefik documentation go? | **Dedicated `docs/ingress.md` plus a worked example in `examples/full-stack/`** | Ingress is the user's explicit ask ("full stack with ingress configured") and the Traefik/service-block model (R-007 socket vs TCP, atomic reload, drain, TLS) is non-trivial. A dedicated doc is the clearest answer; a section buried in `docs/cli.md` would be less discoverable. | 0.90 | | D-191 | Should the docs frame the v0.9 canonical path or document both v0.8 and v0.9 equally? | **Document the v0.9 canonical path; flag deprecated surface with callout boxes** | The v0.8 daemon/mTLS/HCL path is deprecated and scheduled for removal in v0.10-P14. Documenting it as primary misleads new operators; documenting both equally doubles the surface and risks documenting soon-removed code. Callout boxes with "deprecated in v0.9, removed in v0.10" point operators to the canonical path. | 0.91 | | D-192 | Should the existing v0.8.15 release be backfilled with a binary asset, or only fix the pipeline forward? | **Fix forward only; no backfill** | Backfilling a past release is an ops task, not a docs milestone deliverable. The next tagged phase (this milestone's P1 ship at v0.9.1) will be the first correctly-asseted release; install.sh's new fallback walk handles the gap until then. | 0.88 | | D-193 | Should `release.sh` build only `linux-amd64` or also `linux-arm64`? | **Cross-build `linux-amd64` explicitly (host-arch-independent); arm64 deferred to a follow-up** | The install.sh user base is amd64 today (the `.coreci.yml` release step hardcodes `--asset orca-${VERSION}-linux-amd64.tar.gz`). Building amd64 regardless of host arch (via `GOOS=linux GOARCH=amd64 go build`) guarantees the asset the install script expects. arm64 support is a separate enhancement. | 0.85 | | D-194 | Should `install.sh` add a `--check` dry-run mode? | **Yes, lightweight** | A dry-run mode (`--check`) that prints the version + asset URL + install path without writing is cheap to add and useful for debugging the "which release will I get?" question that the v0.4.5 incident surfaced. | 0.80 | ## v0.11 Clarified Decisions (D-series, full autonomy — Phase 0 pre-execution) The following 23 decisions (D-215…D-237) extend the locked D-series (ends at D-206). They derive from 5 research documents ingested 2026-08-07 covering ingress hardening, drift detection, platform-engineer positioning, strategic framing, and the systemd Path unit implementation. Operator decisions Q1=A, Q2=C, Q3=A, Q4=A, Q5=A are adopted. ### Ingress hybrid (D-215…D-226, from research doc 1) | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-215 | Public-binding default? | **Hybrid: nft DNAT → Traefik on `127.0.0.1:8443`** | Defense-in-depth (kernel + app layer); mature pattern (kube-proxy, Linkerd2-proxy, F5/HAProxy+nginx). Smaller Traefik attack surface. R-017. | 0.93 | | D-216 | Opt-out? | **`orca cluster config --public-binding=traefik-on-public-ip` for the simple case** | Operators who want simplicity get it with a one-line config change. | 0.94 | | D-217 | nftables emitter? | **Yes; renders `/etc/nftables.d/orca.nft`; idempotent `nft -f` apply** | Same emitter pattern as Traefik/systemd emitters (R-001-clean). | 0.93 | | D-218 | nftables tool vs iptables? | **`nft` (modern) over legacy `iptables`** | Atomic rule-set swap; modern kernel API. | 0.96 | | D-219 | Cross-node cluster mesh? | **Stays bound on private IP `192.168.x.x:8443`; unchanged** | Avoids adding iptables rules for cross-node mesh; keeps mesh logic unchanged. | 0.94 | | D-220 | Traefik `address` in static config? | **`127.0.0.1:8443` in default, `:443` in opt-out** | Single line change; certs/mTLS/dynamic config unchanged. | 0.97 | | D-221 | `orca doctor nft`? | **Yes; checks table, expected rules, file hash; drift detection via hash comparison** | Parity with `orca doctor traefik`; integrates with R-018 critical_paths. | 0.95 | | D-222 | Rate-limit meter? | **`ora_rl` set as part of the default rule set; configurable via `orca nft rate limit set`** | Kernel-level line-rate rate limiting; defense against SYN floods. | 0.91 | | D-223 | GeoIP blocking? | **Operator-opt-in via `orca nft country block add`**; cli + ipset extension | Not a default; operators opt in. | 0.88 | | D-224 | `nftables` not `iptables` in `.coreci.yml` pipelines? | **Yes; integration tests use `nft` exclusively** | Matches D-218. | 0.94 | | D-225 | Per-workload `ingress: native` coexists with hybrid default? | **Yes; `service { ingress: native }` opts into pure iptables + stunnel sidecars** | Workload-level opt-in; doesn't affect cluster default. | 0.93 | | D-226 | `nftables` rule hash baseline? | **`cluster/state/baseline.nft.hash` per peer; drift detection per §17** | Integrates with R-018 drift detection. | 0.90 | ### Drift detection (D-227…D-237, from research doc 5) | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-227 | Drift detection architecture? | **systemd Path units for critical paths + 60s polling backstop + auto-remediation** | R-001-clean (systemd is OS, not Orca); ~10s event-driven latency on critical paths. R-018/R-019. | 0.94 | | D-228 | Path unit event payload? | **Oneshot service; receives path via `%f`; computes sha256; writes event JSON to `/etc/orca/state/drift-events/`** | Stateless, self-contained, idempotent. | 0.93 | | D-229 | Lead-side pickup? | **Aggregator timer reads each peer's drift-events/, validates against applied txn hashes, triggers remediation** | Reuses existing 10s aggregator cadence (C-11); single SSH pull per tick. | 0.94 | | D-230 | Critical path polling cadence? | **5s backstop; systemd Path unit provides ~10s event-driven latency** | Closes the gap to K8s-comparable drift detection on critical paths. | 0.92 | | D-231 | Auto-remediation policy? | **Per-path config; critical paths default to auto; systemd units default to require-approval** | Config files are safe to re-push; service units may need careful ordering (don't restart serving workloads). | 0.93 | | D-232 | Remediation rate limit? | **5-minute cooldown per path; applies only on SUCCESSFUL remediation; transient failures retry on next aggregator tick** | Prevents loops from buggy external actors; avoids a 30s network blip blocking re-remediation for 5 min (refined per CLARIFY C4). | 0.92 | | D-233 | NFS path detection? | **`orca node setup` detects NFS mounts; falls back to polling for affected paths** | systemd Path units use inotify which doesn't work across NFS. | 0.90 | | D-234 | Secrets path exclusion? | **`/etc/orca/credentials/*` excluded from drift detection** | Re-remediating secrets might clobber intentional out-of-band rotation. | 0.95 | | D-235 | EnvironmentFile drift? | **Triggers `orca job restart ` instead of file-level remediation** | Workload already running won't pick up env changes without a restart. | 0.89 | | D-236 | `orca drift watch` semantics? | **`iter.Seq2[Event, error]` per D-017; `signal.NotifyContext` per D-023; default 2s poll** | Consistent with existing `--watch` pattern (D-017/D-023). | 0.95 | | D-237 | Aggregator timer changes? | **Existing 10s cadence; extended to also pull drift-events/ and remediate** | Reuses C-11 aggregator; no new timer. | 0.95 | ### P01.5 — SPIFFE SVID minting spike (gate C-08, D-068) **C-08 SPIFFE mint spike: PASS.** The `step` CLI (smallstep step-ca) accepts a `spiffe://` URI in `--san` and emits a cert whose URI SAN (x509 subjectAltName URI entry) carries the SPIFFE URI. The fallback to mTLS identity (per D-068 / C-08) is NOT needed; D-068 stands. - **SPIFFE URI format (locked):** `spiffe://orca.local/ns//sa//` — trust domain `orca.local`; `ns/` scopes the workload to an Orca namespace (R-002); `sa/` is the service-account; `` makes the SVID unique per allocation. - **step CLI command (locked):** `step ca certificate --san --not-after 24h --provisioner orca-admin --password-file /dev/stdin --force` - **Cert parsing (locked):** `pem.Decode` → `x509.ParseCertificate` → iterate `cert.URIs` and match the expected SPIFFE URI (parsed as `*url.URL`, compared by canonical string). Missing URI SAN → `ErrSpiffeURIMissing` (cert rejected before reaching the workload). - **Implementation:** `internal/identity/spiffe.go` — `SpiffeURI`, `MintSVID`, `VerifySVID`, `SpiffeIDFromCert`, `SubjectFromSpiffe`. - **Tests:** `internal/identity/spiffe_test.go` — mock transport (`execer`) returns a self-signed cert minted in-process via `crypto/x509.CreateCertificate` with `URIs: []*url.URL{spiffeURI}`, exercising the exact production parsing path. 15 tests, all pass. - **Spike result record:** `internal/identity/SPIFFE_SPIKE_RESULT.md`. ## v0.12 Scope Summary — Security Hardening (Zero-Trust Identity) v0.12 is a 27-execution-phase feature milestone dedicated to comprehensive security hardening across the entire attack surface, **including the operating system itself**. The threat-model review (v0.11 closeout + Phase 0 RESEARCH) surfaced 25 distinct findings (F1..F25) spanning injection, traversal, ACL, audit, crypto, OS scripts, emitters, sudoers, system users, file modes, daemon auth, backup, SQLite, install.sh, and migration. v0.12 closes all of them and adopts a **zero-trust identity model** as the load-bearing architectural change. ### Load-bearing rule adopted in Phase 0 **R-021**: *Orca never issues, stores, or accepts human-identity credentials. Human identity is exclusively external (OIDC). Machine identity is exclusively mTLS/SPIFFE. No passwords, no Orca-issued tokens, no CA-key passphrases.* ### Zero-trust identity model Two identity layers, zero overlap: - **Human operators** → OIDC (external IdP, BYO) OR the **bundled Dex** with a **WebAuthn (passkeys) connector** as the default password-free authenticator. `orca auth login` / `orca auth register` open the default browser to the Dex WebAuthn endpoint via OIDC authorization-code + PKCE + local loopback redirect. After the WebAuthn ceremony (biometric/security key), Dex redirects back with an auth code; CLI exchanges for a short-lived ID token (1h) + refresh. Headless/CI fallback: device-code flow. - **Machine-to-machine** → mTLS + SPIFFE SVIDs (unchanged from v0.11). The "no Orca credentials" invariant holds: passkeys are public-key credentials (the private key never leaves the authenticator); the WebAuthn credential DB stores only public keys + credential IDs + sign counts. No passwords, no Orca-issued tokens, no CA-key passphrases anywhere in the system. ### Master key sealing The secrets master key (32 random bytes) is **sealed to OIDC** — wrapped by a key derived from an OIDC token exchange at unseal time. `orca cluster unseal` (operator authenticates via OIDC → token exchange → unwrap master key into memory → zeroed on shutdown). The raw master key never touches disk. **Shamir 3-of-5 recovery**: at seal time, 5 shards are printed and the operator stores them offline. If the IdP is permanently lost AND a quorum of shards is unavailable, the cluster is unrecoverable by design (documented residual risk; no backdoor). ### New requirements (REQ-119..REQ-148) 30 net-new requirements derived from the threat-model findings and the zero-trust identity model. See REQUIREMENTS.md and ROADMAP.md for the full mapping. Highlights: - REQ-119..121: command injection, path traversal, txn path allowlist - REQ-144: OIDC client + bundled Dex (BYO-IdP override) - REQ-145: ACL rewrite (remove KindToken, add KindOidc, enforce) - REQ-146: remove all password/token paths (breaking) - REQ-147: master key seal-to-OIDC + Shamir recovery - REQ-148: WebAuthn connector for Dex (passkeys, browser auth+register) - REQ-122..143: integrity, crypto, OS scripts, emitters, sudoers, system users, SQLite, migration, dual-write closure, transport, drift auth, integration tests, docs, final review ### v0.12 Clarified Decisions (D-series, full autonomy) The 10 v0.12 decisions (D-238..D-247) were resolved during CLARIFY under full autonomy (autonomy.level=full, workflow.no_hitl=true): | ID | Question | Decision | Rationale | Confidence | |----|----------|----------|-----------|------------| | D-238 | Milestone version? | **v0.12 (minor, not v1.0)** | v1.0.0 stays deferred for post-UAT per v0.11 PRD; v0.12 is a minor feature milestone. Tags on v0.11.x patch line. | 0.95 | | D-239 | OIDC provider model? | **Bundled Dex by default + BYO external IdP override** | Zero-trust out of the box without external setup; `oidc.issuer` repoint switches to BYO. | 0.90 | | D-240 | Bundled Dex upstream authenticator (password-free)? | **WebAuthn (passkeys) connector** | Public-key credentials; private key never leaves authenticator; reinforces "no passwords" invariant (R-021). | 0.88 | | D-241 | Master key sealing model? | **Seal to OIDC + Shamir 3-of-5 recovery** | No password anywhere; quorum recovery if IdP lost; no backdoor. | 0.85 | | D-242 | CLI browser flow? | **OIDC auth-code + PKCE + local loopback redirect** | Standard OIDC browser flow; secure for public clients; headless fallback via device-code. | 0.92 | | D-243 | WebAuthn RP ID / secure context? | **Traefik-served cluster domain (step-ca cert, R-017)** | WebAuthn requires HTTPS; Traefik already provides it; RP ID configurable via `orca auth init-idp`. | 0.90 | | D-244 | Passkey storage? | **SQLite at ClusterDir()/webauthn-credentials.db (0600); public keys only** | Public keys are not secrets; 0600 file mode for integrity; no passphrase wrapping needed. | 0.92 | | D-245 | Headless/CI auth fallback? | **Device-code flow** | No browser in CI; device-code is the standard OIDC headless path. | 0.90 | | D-246 | Token storage at rest? | **~/.orca/credentials.json (0600); short-lived (1h) + refresh** | Standard OIDC token storage; 0600; refresh handles rotation; no long-lived Orca-issued tokens. | 0.92 | | D-247 | Breaking-change handling for password/token removal? | **`orca upgrade` refuses v0.11 clusters using --password/bare-tokens without --accept-identity-migration** | No silent breakage; explicit migration gate; documented cutover. | 0.90 | ### v0.12 is a HARDENING + IDENTITY milestone, not a direction change The vision ("minimalist, offline-first, CLI-first orchestration engine inspired by HashiCorp Nomad") is unchanged. v0.12 closes the security-surface gaps surfaced by the v0.11 threat model and adopts a zero-trust identity model. The offline-first principle (R-003) is preserved: the bundled Dex can run on the lead (offline), and the mTLS-only path remains for the single-operator fully-offline case (no human authn needed — the operator holds the pre-staged SSH key + mTLS cert; no password, no token).