Files
orca/.ciagent/ARCHITECTURE.md
T
Jon Chery 1ee82fc2e2 docs(P00): ideation - 34 ideas accepted
v0.2 IDEATE stage. 29 new ideas generated (10 Tier 1 mechanical + 11
Tier 2 backend-enriched + 8 Tier 3 cross-project) plus 6 research-stage
candidates (REQ-cand-A..F from commit 08d321f) = 35 considered. Under
full autonomy, all 35 with confidence >= 0.60 are auto-accepted; 1
explicitly deferred to v0.3 (I-308 pprof). 34 accepted into v0.2.

Resulting net-new REQs (REQ-025..REQ-040) span P01-P04:
- P01 (mTLS): REQ-025 (cert rotation history), REQ-026 (CA fingerprint
  pinning), REQ-032 (orca doctor), REQ-033 (file mode enforcement),
  REQ-034 (rotation alarm), REQ-035 (cert show redaction), REQ-036
  (SAN validation), REQ-038 (mTLS failure log fields)
- P02 (multi-node): REQ-028 (NodeCapacity HCL schema, P02 enabler),
  REQ-037 (X-Orca-Idempotency-Key)
- P03 (security CI): REQ-027 (govulncheck offline mode -- changes P03
  scope: CI must not call vuln.go.dev), REQ-029 (gitleaks baseline for
  pre-existing .env leak), REQ-039 (.gitleaks.toml stopwords),
  REQ-040 (.golangci.yml)
- P04 (iter.Seq): REQ-030 (--watch --json mode)
- Cross-cutting: REQ-031 (go test -race)

Total v0.2 REQs: 20 (4 carried from v0.1 + 16 net-new).

ARCHITECTURE.md: added `internal/doctor/` component (§5) with
orca doctor {cert,network,db} subcommands; ASCII diagram updated.
ROADMAP.md: per-phase REQ coverage matrix added; P03 scope change
documented (govulncheck offline mode).
PROJECT.md: unchanged (vision is stable).

---ci---
project: orca
phase: 0
milestone: v0.2
status: ideate
---/ci---
2026-06-03 21:03:59 +00:00

529 lines
30 KiB
Markdown

# Architecture: Orca
## System Overview
Orca is a single-binary, offline-first orchestration engine. The system consists
of three logical layers (CLI, Daemon, Engine) compiled into one `orca` binary
and selected via subcommands. v0.2 adds a **cross-node transport layer** (mTLS)
and a **dispatcher** for multi-node job execution.
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ orca (single binary, v0.2) │
├─────────────────────────────────────────────────────────────────────────────┤
│ CLI Layer (Cobra) │
│ ├── orca version │
│ ├── orca init # local node bootstrap │
│ ├── orca cert {init,join,renew,show} # NEW (P01) │
│ ├── orca status │
│ ├── orca node {join,leave,list} # join = mTLS handshake (P01) │
│ │ └── orca node list --watch # NEW iter.Seq (P04) │
│ ├── orca job {run,list,stop,logs} │
│ │ └── orca job list --watch # NEW iter.Seq (P04) │
│ ├── orca doctor # NEW (P01) — diagnostics │
│ │ ├── orca doctor cert │
│ │ ├── orca doctor network │
│ │ └── orca doctor db │
│ └── orca daemon │
├─────────────────────────────────────────────────────────────────────────────┤
│ Daemon Layer (net/http over h2c, mTLS in P01) │
│ ├── /healthz (liveness) │
│ ├── /readyz (readiness) │
│ ├── /v1/jobs/* (job control API) │
│ ├── /v1/nodes/* (node registry API) │
│ ├── /v1/tasks/* (task lifecycle API) │
│ ├── /orca.v1.Dispatch/... # NEW (P02) — cross-node dispatch │
│ └── /orca.v1.Register/... # NEW (P02) — peer join ack │
├─────────────────────────────────────────────────────────────────────────────┤
│ Transport Layer (NEW — internal/transport) │
│ ├── mTLS client (dialer pool per peer) │
│ ├── mTLS server config (TLS 1.3 only, AEAD allowlist) │
│ ├── Retry+backoff (exponential, jittered, capped) │
│ └── Graceful disconnect (ctx-aware Conn.Close) │
├─────────────────────────────────────────────────────────────────────────────┤
│ Core Engine │
│ ├── Node Registry (in-memory + SQLite persistence) │
│ ├── Task Executor (os/exec with WaitDelay, Go 1.25+) │
│ ├── Job Scheduler (single-node FIFO; bin-pack P02) │
│ ├── Dispatcher # NEW internal/engine/dispatcher.go │
│ │ ├── Local decision (does this job fit on this node?) │
│ │ ├── Remote dispatch (POST to peer via transport) │
│ │ └── Streaming callback (iter.Seq[DispatchResult] for CLI) │
│ ├── Security Manager # NEW internal/security (P01) │
│ │ ├── CA lifecycle (init, fingerprint, sign CSR) │
│ │ ├── Server cert lifecycle (issue, renew, rotate) │
│ │ ├── mTLS config builder │
│ │ └── Cert store (filesystem + SQLite metadata) │
│ └── Audit Logger (log/slog JSON handler) │
├─────────────────────────────────────────────────────────────────────────────┤
│ State Store (modernc/sqlite, CGO-free) │
│ ~/.orca/orca.db │
│ ├── nodes, jobs, tasks, audit_log (v0.1) │
│ └── certs # NEW (P01) — CA + server certs │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Component Details
### 1. CLI Layer (`cmd/orca`, `internal/cli`)
- **Framework**: Cobra (industry standard, familiar to operators)
- **v0.1 subcommands**: `version`, `init`, `status`, `node`, `job`, `daemon`
- **v0.2 additions (P01)**: `orca cert {init,join,renew,show}`
- **v0.2 additions (P04)**: `--watch` flag on `orca job list` and `orca node list`
- **v0.2 additions (P01)**: `orca doctor` subcommand (see §5 below)
- **Output**: Human-readable by default; `--json` flag for machine consumption
- **Discovery**: All subcommands self-document via Cobra's auto-generated help
- **Watch semantics (P04)**: `--watch` consumes `iter.Seq[Job|Node]`, exits on
ctrl-c (via `signal.NotifyContext`), refreshes on internal change events.
### 2. Daemon Layer (`internal/daemon`)
- **Server**: `net/http` with `http.ServeMux` (no external router)
- **TLS (P01)**: `crypto/tls` with `MinVersion=tls.VersionTLS13` and
AEAD cipher allowlist
(`TLS_AES_256_GCM_SHA384`, `TLS_CHACHA20_POLY1305_SHA256`,
`TLS_AES_128_GCM_SHA256`)
- **Ports**: Configurable (default `:8443` for API+mTLS, `:8080` for health)
- **Graceful Shutdown**: `signal.NotifyContext` with SIGINT/SIGTERM
- **v0.2 endpoints (P02)**:
- `POST /orca.v1.Dispatch/Submit` — receive cross-node job submission
- `POST /orca.v1.Dispatch/Status` — query dispatched job status
- `POST /orca.v1.Register/Hello` — peer join ack (used during `orca node join`)
### 3. Transport Layer (`internal/transport`, NEW in P01/P02)
- **Client**: `http.Client` with `http.Transport.TLSClientConfig` populated
from `internal/security.NewClientTLSConfig`
- **Server**: `http.Server.TLSConfig` populated from
`internal/security.NewServerTLSConfig`
- **Retry policy**: exponential backoff with jitter (start 100ms, x2, cap 5s,
max 5 attempts); only idempotent verbs (`GET`, `HEAD`, `OPTIONS`) are
retried automatically; `POST` retries require an explicit
`X-Orca-Idempotency-Key` header
- **Conn lifecycle**: `context.Context`-aware dials and reads; graceful
`Close` on `ctx.Done()`
- **Peer dial pool**: small `sync.Map` of `peerID → *http.Client` to reuse
TLS handshakes (TCP keep-alive) within a session
### 4. Core Engine (`internal/engine`)
- **Node Registry**: In-memory map of node IDs → metadata, persisted to SQLite
(CPU/memory capacity, available slots, last-seen)
- **Task Executor**: `os/exec.CommandContext` with `WaitDelay` (Go 1.25+) for
clean process termination
- **Job Scheduler (v0.2 P02)**:
- **Algorithm**: best-fit bin-packing by `available_cpu` and `available_memory`
- **Within-node ordering**: FIFO queue
- **Cross-node**: if local node is full, `Dispatcher.Submit(peer, job)` is
invoked; peers are tried in round-robin order
- **Fallback**: if all peers reject, return `ErrNoFit` and requeue
- **Dispatcher (NEW, P02)**:
- `Submit(peerID, spec) (jobID, error)` — blocking call with retry
- `Watch(peerID) iter.Seq[DispatchEvent]` — pull-style event stream for the
CLI's `--watch` flag
- Stateless: every call uses the latest mTLS client config and peer address
- **Security Manager (NEW, P01)**:
- `InitCA(commonName) (*CA, error)` — generates a self-signed CA, writes
`ca.crt` (0644) and `ca.key` (0600) to `~/.orca/`
- `Fingerprint(certPath) (sha256hex, error)` — used by `orca cert join`
- `SignServerCert(csr, validity) (*cert, error)` — signs a CSR with the CA
- `IssueServerCert(nodeName, dnsNames, ips) (*cert, *key, error)` — generates
a keypair + CSR + signs it, returns PEM bytes for `orca cert join --server`
- `ServerTLSConfig() (*tls.Config, error)` — loads `server.crt`/`server.key`
and the CA pool from disk
- `ClientTLSConfig(caPath) (*tls.Config, error)` — returns a client config
pinned to the supplied CA
- **Rotation policy**: server certs valid 90d; CA cert valid 10y. On
`orca cert renew`, `IssueServerCert` is called and the daemon
gracefully reloads the in-process `tls.Config` via `GetCertificate`
hot-swap (no restart required)
- **Audit Logger**: `slog.NewJSONHandler(os.Stderr, ...)` with structured fields
### 5. Doctor (`internal/doctor`, NEW in P01)
- **Purpose**: operator-facing diagnostics; runs read-only checks against
the local state and reports PASS/WARN/FAIL.
- **Subcommands**:
- `orca doctor` — runs all checks
- `orca doctor cert` — cert/CA health (file modes, expiry windows, SAN
presence, fingerprint pinning match — see REQ-026, REQ-033,
REQ-034, REQ-036)
- `orca doctor network` — peer reachability over mTLS (per-peer handshake
sanity, last-seen delta)
- `orca doctor db` — SQLite integrity check (`PRAGMA integrity_check`)
+ migration version
- **Output**: human-readable by default; `--json` for machine consumption
- **No state changes**: doctor is strictly read-only. It can be run
while the daemon is down (where possible) or while it's up.
- **Initial implementation in P01** (cert checks only); `network` and
`db` checks land in subsequent phases as their state becomes
available.
### 6. State Store (`internal/store`)
- **Driver**: `modernc.org/sqlite` (pure Go, CGO-free)
- **Location**: `~/.orca/orca.db` (user-mode) or `/var/lib/orca/orca.db` (system-mode)
- **Schema (v0.1)**: `nodes`, `jobs`, `tasks`, `audit_log` tables
- **Schema (v0.2 P01)**: NEW `certs` table
```sql
CREATE TABLE certs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL, -- 'ca' | 'server'
node_id TEXT, -- NULL for CA
serial_hex TEXT NOT NULL, -- x509.SerialNumber.Hex()
subject_cn TEXT NOT NULL,
issuer_cn TEXT NOT NULL,
not_before INTEGER NOT NULL, -- unix seconds
not_after INTEGER NOT NULL, -- unix seconds
fingerprint TEXT NOT NULL, -- sha256 of DER, hex
source_path TEXT NOT NULL, -- on-disk PEM path
created_at INTEGER NOT NULL
);
CREATE INDEX idx_certs_node_kind ON certs(node_id, kind);
CREATE INDEX idx_certs_not_after ON certs(not_after);
```
- **Migrations**: Embedded SQL files, applied on startup
- **Migration 0004** is added in P01 with the schema above
## Data Model
### Node (v0.1, extended in v0.2 P02)
```go
type Node struct {
ID string
Name string
Address string
State string
JoinedAt time.Time
LastSeen time.Time
Metadata map[string]string
// v0.2 P02 — capacity for bin-packing
Capacity NodeCapacity
}
type NodeCapacity struct {
CPUMillicores int // total, e.g. 4000 = 4 cores
MemoryBytes int64 // total RAM
CPUUsed int // currently allocated
MemoryUsed int64 // currently allocated
}
func (c NodeCapacity) AvailableCPU() int { return c.CPUMillicores - c.CPUUsed }
func (c NodeCapacity) AvailableMemory() int64 { return c.MemoryBytes - c.MemoryUsed }
```
### Job (v0.1)
```go
type Job struct {
ID string
Name string
Spec string
Status string
CreatedAt time.Time
StartedAt *time.Time
EndedAt *time.Time
}
```
### Task (v0.1)
```go
type Task struct {
ID string
JobID string
Command string
Args []string
Env []string
PID int
ExitCode int
Status string
CreatedAt time.Time
StartedAt *time.Time
EndedAt *time.Time
}
```
### Certificate (NEW, v0.2 P01)
```go
type Cert struct {
Kind CertKind // CertCA | CertServer
NodeID string // empty for CA
SerialHex string
SubjectCN string
IssuerCN string
NotBefore time.Time
NotAfter time.Time
Fingerprint string // sha256 of DER (hex)
SourcePath string // PEM path on disk
CreatedAt time.Time
}
```
## v0.2 Component Graph (ASCII)
```
┌─────────────────┐
│ Operator Host │
│ (orca CLI) │
└────────┬────────┘
│ 1. cert join --ca-fingerprint <sha>
┌──────────────────────────────────────────────────────────────────────────────┐
│ NODE A (Bootstrap / CA holder) │
│ │
│ ┌──────────────┐ CSR ┌────────────────────┐ PEM sign ┌────────┐ │
│ │ orca cert │──────────▶│ internal/security │─────────────▶│ CA │ │
│ │ {init,join} │ │ .SignServerCert() │ │ key │ │
│ └──────────────┘ └────────────────────┘ │ 0600 │ │
│ ┌──└────────┘ │
│ ┌─────────────────┐ │ │
│ │ internal/daemon │ ◀──tls.Config── internal/security │ │
│ │ (http.Server) │ .ServerTLSConfig() │ │
│ │ │ │ │
│ │ /v1/jobs/* │ │ │
│ │ /v1/nodes/* │ │ │
│ │ /orca.v1.* │ │ │
│ └────────┬────────┘ │ │
│ │ Submit(job) (bin-pack) │ │
│ ▼ │ │
│ ┌─────────────────┐ │ │
│ │ internal/engine │ │ │
│ │ .scheduler │──── if local fits → executor │ │
│ │ .dispatcher │──── else → Submit(peer, job) ───────────┼──┐ │
│ └─────────────────┘ │ │ │
│ │ │ mTLS │
└──────────────────────────────────────────────────────────────┼──┼───────────┘
│ │
ORCA NODE NETWORK │ │
│ │
┌──────────────────────────────────────────────────────────────┼──┼───────────┐
│ NODE B (Peer) │ │ │
│ │ │ │
│ ┌─────────────────┐ ◀── TLS 1.3 handshake ─────────────────┘ │ │
│ │ internal/daemon │ │ │
│ │ (http.Server) │ POST /orca.v1.Dispatch/Submit │ │
│ │ │──── 200 + jobID │ │
│ └────────┬────────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────────┐ │ │
│ │ internal/engine │ │ │
│ │ .scheduler (FIFO) │ │
│ │ .executor │ │
│ └─────────────────┘ │ │
└──────────────────────────────────────────────────────────────────────────────┘
```
## v0.2 Flows
### Flow 1: Cert Issuance (CA-init → CSR → sign → install) — P01
```
Operator (Node A) Operator (Node B)
───────────────── ─────────────────
orca cert init
↳ InitCA("orca-ca")
↳ write ca.crt (0644), ca.key (0600)
↳ record in certs table (kind='ca')
orca cert join --ca-fingerprint <sha>
↳ operator copies ca.crt → Node B
↳ verifies fingerprint matches local
--ca-fingerprint arg
↳ IssueServerCert("node-b", SANs)
↳ generate 2048-bit RSA key
↳ build CSR with SANs
↳ read ca.crt + ca.key
↳ sign CSR (90d validity)
↳ write server.crt (0644),
server.key (0600)
↳ record in certs table
(kind='server', node_id='node-b')
```
### Flow 2: mTLS Handshake at `node join` — P01
```
Node B (joiner) Node A (CA holder)
──────────────── ─────────────────
orca node join --name node-b
--ca-fingerprint <sha>
--peer node-a:8443
↳ load ca.crt → verify sha256 == --ca-fingerprint
↳ load server.crt + server.key
↳ tls.Config{MinVersion: TLS1.3, ...}
↳ ClientHello (SNI=node-a)
◀── ServerHello (TLS 1.3)
◀── Certificate (Node A's cert)
↳ verify Node A's cert chains to ca.crt ◀── CertificateRequest
↳ send Certificate (Node B's cert) ◀── Finished
↳ Finished
↳ GET /healthz (over mTLS) — sanity check
↳ POST /v1/nodes (over mTLS) — register
↳ insert into nodes table
↳ audit log
↳ 200 OK
↳ record node_a in peers table
↳ audit log
```
### Flow 3: Job Dispatch (CLI → dispatcher → peer) — P02
```
User (Node A CLI) Node A (scheduler) Node B (peer)
────────────────── ─────────────────── ─────────────
orca job run spec.hcl
↳ parse HCL
↳ POST /v1/jobs (mTLS, local)
↳ scheduler.Submit(spec)
↳ bin-pack: spec.cpu + spec.mem
↳ local node A has 2000mc + 4GiB free → fit!
↳ executor.Run(spec)
↳ 202 Accepted + jobID
↳ returns jobID
```
```
User (Node A CLI) Node A (scheduler) Node B (peer)
────────────────── ─────────────────── ─────────────
↳ scheduler.Submit(spec)
↳ bin-pack: spec.cpu + spec.mem
↳ local node A has 0 free → NO FIT
↳ dispatcher.Submit(peer="node-b", spec)
↳ load transport.Client("node-b")
↳ POST /orca.v1.Dispatch/Submit
↳ mTLS handshake
↳ authenticate cert
↳ scheduler.Submit(spec)
↳ executor.Run(spec)
↳ 200 OK + jobID
↳ 200 OK + jobID
↳ return jobID to local caller
↳ returns jobID
```
### Flow 4: iter.Seq Streaming (`--watch`) — P04
```
User orca job list --watch
──── ─────────────────────
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()
seq := store.Jobs().Watch(ctx)
for job := range seq {
print(job) // human or --json
}
// ctrl-c → ctx.Done() → seq stops yielding
```
Internally `store.Jobs().Watch(ctx) iter.Seq[Job]` polls the
`jobs` table on a 1s ticker (or subscribes to an in-process
notifier channel) and yields the current snapshot of each job
until `ctx.Done()`. The store repo implements `iter.Seq[Job]`
as a function that takes a `yield func(Job) bool` callback.
## Security Architecture
### Authentication
- **v0.1**: mTLS for all API endpoints (self-signed CA)
- **v0.2 P01**: Internal CA with CSR join (see Flow 1 + 2)
- **v0.2+**: Token-based auth deferred to v0.3+
### Cert Rotation
- Server certs: 90-day validity, rotate at 60 days (30d before expiry)
- CA cert: 10-year validity, manual rotation
- Hot-swap: `tls.Config.GetCertificate` callback re-reads the
`server.crt`/`server.key` files on each handshake so `orca cert renew`
takes effect without a daemon restart.
### Audit Logging
- All state-changing operations emit structured log records
- Fields: `timestamp`, `actor`, `action`, `resource`, `result`, `error`
- Stored in SQLite `audit_log` table and stderr (JSON)
- **v0.2 P01 additions**: `cert.issued`, `cert.renewed`, `cert.joined`,
`node.handshake_ok`, `node.handshake_failed`
### Input Validation
- All CLI inputs validated via Cobra's `Args`/`ValidArgs` functions
- All API inputs validated at handler boundary
- HCL/YAML specs parsed with strict schemas
## Key Architectural Decisions (v0.1 + v0.2)
| ID | Decision | Rationale |
|----|----------|-----------|
| AD-001 | Single binary with subcommands | Simpler distribution, aligns with simplicity pillar |
| AD-002 | modernc/sqlite (CGO-free) | Cross-compile friendly, no CGO toolchain needed |
| AD-003 | net/http (no framework) | Stdlib suffices; avoids external router dependency |
| AD-004 | os/exec with WaitDelay (Go 1.25+) | Clean process termination, native to Go |
| AD-005 | Cobra for CLI | Industry standard, familiar to operators |
| AD-006 | slog for logging | Native to Go 1.21+, no external dependency |
| AD-007 | HCL for job specs | Familiar to Nomad/HashiCorp users |
| AD-008 | Single-node scheduling (v0.1) | Multi-node scheduling deferred to v0.2+ |
| AD-009 | Internal CA, no external PKI (v0.2) | Self-contained, no operational PKI requirement |
| AD-010 | Roll-our-own CA in `crypto/x509` (v0.2) | step-ca/cfssl/vault-pki too heavyweight for Orca's footprint |
| AD-011 | Operator-mediated CA cert distribution (v0.2) | No secret distribution over the wire; matches offline-first |
| AD-012 | TLS 1.3 only, AEAD allowlist (v0.2) | Modern crypto only; no downgrade risk |
| AD-013 | Eager mTLS at `node join` (v0.2) | Fail fast; don't defer handshake to first request |
| AD-014 | `orca.v1.Dispatch` via stdlib h2c (v0.2) | ConnectRPC not in go.mod; stdlib suffices for a single-RPC service |
| AD-015 | Best-fit bin-packing (v0.2) | Simple, deterministic, optimal for small fleets |
| AD-016 | iter.Seq for streaming lists (v0.2) | Go 1.25+ native, context-aware, pull semantics |
## Anti-Patterns (Explicitly Avoided)
- No controller/agent split (single binary)
- No CRDs / custom resource definitions
- No web UI (CLI-only)
- No service mesh
- No container runtime integration
- No multi-tenancy
- No cloud provider integrations
- No auto-scaling
- No admission controllers
- No complex scheduling algorithms (best-fit only)
- No gRPC framework dependency (stdlib net/http with h2c, Go 1.25+ native)
- No external PKI / no cert transparency logs (offline-first)
## Dependency Map (minimal — v0.2 adds zero direct deps)
```
github.com/spf13/cobra # CLI framework
github.com/hashicorp/hcl/v2 # HCL parser
modernc.org/sqlite # SQLite (pure Go)
github.com/google/uuid # UUID generation
```
Total: ~4 direct dependencies. No web framework, no ORM, no RPC framework,
no PKI library. mTLS via `crypto/tls` and `crypto/x509` (stdlib).
> **ConnectRPC note**: `.ciagent/config.json` lists `connectrpc` in
> `frameworks`, but the actual `go.mod` does not depend on
> `connectrpc.com/connect`. v0.2 falls back to plain `net/http` with
> HTTP/2 cleartext (h2c) for `orca.v1.Dispatch`. The protocol is a
> simple JSON-over-HTTP POST: client sends
> `{"spec": "..."}` to `/orca.v1.Dispatch/Submit`; server replies
> `{"job_id": "..."}`. This keeps the zero-new-dep promise and the
> codebase coherent with the rest of the daemon's `http.ServeMux`.
## Deployment Model (v0.2)
```
Operator Machine Node A (CA holder) Node B (Peer)
──────────────── ───────────────── ─────────────
orca CLI orca daemon orca daemon
│ │ ▲ │ ▲
│ mTLS handshake │ │ mTLS │ │
│ at `node join` ────────────┼──┘ │ │
│ │ │ │
│ submit job ───POST────────▶│ POST (cross-node) ──────────▶│
│ │ mTLS only │ │
│ │ │ │
│ ◀───────jobID──────────────│ ◀─────jobID (200 OK)─────────│
│ │ │
│ orca job list --watch │ │
│ (iter.Seq stream) ◀────────│── polls local + stream events│
```
For v0.2, one node must be the CA holder (`orca cert init` was run
on it). The CA holder's `ca.crt` is copied to each peer manually by
the operator; peers do not auto-fetch it.