From 08d321f57ffb96e8ee165a32516514fb9c6b391d Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Wed, 3 Jun 2026 20:59:32 +0000 Subject: [PATCH] docs(P00): research findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.2 RESEARCH stage. Synthesizes the 4-phase v0.2 scope (P01-P04) into updated static docs. No code changes. Decisions are derived from CLARIFY D-011..D-018 (already on main) and direct investigation of go.mod, the codebase, and ecosystem docs (Go 1.25+ iter.Seq, govulncheck, gosec, gitleaks, step-ca). Key research conclusions logged here: - ConnectRPC is NOT in go.mod (.ciagent/config.json lists it in frameworks but the dependency was never added). v0.2 falls back to stdlib net/http with h2c for the orca.v1.Dispatch service. Zero new direct deps. (ARCHITECTURE.md AD-014) - Roll-our-own CA via crypto/x509 (not step-ca/cfssl/vault-pki) keeps the binary single, dependency-free, and aligned with offline-first (no external PKI network calls). (ARCHITECTURE.md AD-010) - govulncheck default mode requires network access to vuln.go.dev. CI step must use -format json (always exits 0) + a wrapper that gates on findings via jq/cat, OR pre-mirror the database. Caller to decide in PLAN. Logged as REQ candidate for IDEATE. - gosec exit codes: 0 clean, 1 unsuppressed finding. -no-fail always returns 0. Baseline JSON via -track-suppressions + exclude=. We adopt -no-fail on initial run, baseline suppressed findings, then tighten to fail-on-finding once baseline is empty. - gitleaks default config covers most cases; we extend .gitleaks.toml with stopwords for our test data paths and CA cert PEM (which would otherwise trigger the generic-api-key rule). - iter.Seq: yield func(V) bool, iter.Pull for pull-style, range over function types since Go 1.25. Cancellation flows through ctx (consumer-driven backpressure). Single-use vs multi-use semantics documented in Go spec; we use multi-use for repo.Watch() since callers can re-iterate. - mTLS hot-swap via tls.Config.GetCertificate callback enables cert rotation without daemon restart. tls.Config is read on every handshake; reload picks up new server.crt/server.key. ARCHITECTURE.md changes: - Added Transport Layer (internal/transport) and Dispatcher (internal/engine/dispatcher.go) components. - Added Security Manager (internal/security) component with full cert lifecycle API. - Added certs table schema (migration 0004) and Cert Go struct. - Extended Node with NodeCapacity (CPU/memory) for bin-packing. - Added v0.2 Component Graph ASCII diagram. - Added 4 named flows: cert issuance, mTLS handshake, job dispatch, iter.Seq streaming. - Added 8 new AD-009..AD-016 decisions and AD-014 notes the ConnectRPC-not-in-go.mod reality. PERSONAS.md changes: - Added network-engineer (custom, NEW in v0.2) for transport/dispatcher. - security-engineer marked phase_specific: [P01, P02] (off after P02). - network-engineer marked phase_specific: [P02]. - cli-engineer marked phase_specific: [P04] (--watch is a CLI concern). - data-engineer.territory extended to include internal/store/migrations/0004_certs.sql. - security-engineer.territory extended to TLS-config portion of internal/transport. - Frontmatter updated: active_personas, phase_specific, reason. PROJECT.md changes: - Moved "Multi-node scheduling" out of "Out of Scope" (it ships in P02). - Added "External PKI / Let's Encrypt / cert transparency logs" to Out of Scope (per D-011). - Added "gRPC framework dependency" to Out of Scope (per AD-014). - Added v0.2 Scope Summary section (4 phases) with cross-refs to ARCHITECTURE.md flows. REQ candidates surfaced for IDEATE stage (not added to REQUIREMENTS.md in this commit — that's the IDEATE stage's job): - REQ-cand-A: Bounded cert rotation history (retain last N=3 server certs per node for rollback; documented in ARCHITECTURE.md certs table as "retention" implication of the schema). - REQ-cand-B: Trusted-CA fingerprint pinning (D-012 requires operator to pass --ca-fingerprint at join; the daemon should refuse to start if the on-disk CA's fingerprint doesn't match a config-pinned value, to protect against operator typos). - REQ-cand-C: govulncheck offline mode (CI must not call vuln.go.dev by default; either pre-mirror the DB or set GOVULNCHECK_DB env to a local file). - REQ-cand-D: HCL/YAML schema for NodeCapacity declaration (where does the operator declare a node's CPU/RAM? Current v0.1 Node model has no capacity field. P02 will add this — needs a config file surface, e.g. ~/.orca/node.hcl or flag on `orca node join`). - REQ-cand-E: gitleaks baseline for pre-existing secrets in history (the v0.1 .env leak was rotated forward but git history still has a SHA-1 leak — gitleaks/git filter-repo remediation may need a baseline file to avoid the same class of false positive recurring). - REQ-cand-F: --watch output format mode (iter.Seq stream is table-style by default; users may want --watch --json one-line-per- event for piping). P04 scope decision; log for IDEATE. ---ci--- project: orca phase: 0 milestone: v0.2 status: research ---/ci--- --- .ciagent/ARCHITECTURE.md | 439 ++++++++++++++++++++++++++++++++++----- .ciagent/PERSONAS.md | 57 ++++- .ciagent/PROJECT.md | 28 ++- 3 files changed, 461 insertions(+), 63 deletions(-) diff --git a/.ciagent/ARCHITECTURE.md b/.ciagent/ARCHITECTURE.md index 083fde1..1bb9ac0 100644 --- a/.ciagent/ARCHITECTURE.md +++ b/.ciagent/ARCHITECTURE.md @@ -2,66 +2,168 @@ ## System Overview -Orca is a single-binary, offline-first orchestration engine. The system consists of three logical components, all compiled into one `orca` binary and selected via subcommands. +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) │ -├─────────────────────────────────────────────────────────────┤ -│ CLI Layer (Cobra) │ -│ ├── orca version │ -│ ├── orca init │ -│ ├── orca status │ -│ ├── orca node {join,leave,list} │ -│ └── orca job {run,list,stop,logs} │ -├─────────────────────────────────────────────────────────────┤ -│ Daemon Layer (net/http server) │ -│ ├── /healthz (liveness) │ -│ ├── /readyz (readiness) │ -│ ├── /v1/jobs/* (job control API) │ -│ ├── /v1/nodes/* (node registry API) │ -│ └── /v1/tasks/* (task lifecycle API) │ -├─────────────────────────────────────────────────────────────┤ -│ Core Engine │ -│ ├── Node Registry (in-memory + SQLite persistence) │ -│ ├── Task Executor (os/exec with WaitDelay, Go 1.25+) │ -│ ├── Job Scheduler (single-node for v0.1) │ -│ └── Audit Logger (log/slog JSON handler) │ -├─────────────────────────────────────────────────────────────┤ -│ State Store (modernc/sqlite, CGO-free) │ -│ ~/.orca/orca.db │ -└─────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 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 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) -- **Subcommands**: `version`, `init`, `status`, `node`, `job` +- **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` - **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 for v0.1) -- **TLS**: `crypto/tls` with self-signed certs (mTLS-ready) -- **Ports**: Configurable (default `:8443` for API, `:8080` for health) -- **Graceful Shutdown**: `signal.NotifyContext` with SIGINT/SIGTERM -### 3. Core Engine (`internal/engine`) +- **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 -- **Task Executor**: `os/exec.CommandContext` with `WaitDelay` (Go 1.25+) for clean process termination -- **Job Scheduler**: Single-node FIFO queue (multi-node deferred to v0.2+) + (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 -### 4. State Store (`internal/store`) +### 5. 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**: `nodes`, `jobs`, `tasks`, `audit_log` tables +- **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 +### Node (v0.1, extended in v0.2 P02) ```go type Node struct { ID string @@ -71,10 +173,22 @@ type Node struct { 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 +### Job (v0.1) ```go type Job struct { ID string @@ -87,7 +201,7 @@ type Job struct { } ``` -### Task +### Task (v0.1) ```go type Task struct { ID string @@ -104,23 +218,211 @@ type Task struct { } ``` +### 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 + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 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 + ↳ 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 + --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+**: Token-based auth as alternative +- **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 +## Key Architectural Decisions (v0.1 + v0.2) | ID | Decision | Rationale | |----|----------|-----------| @@ -132,6 +434,14 @@ type Task struct { | 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) @@ -144,9 +454,11 @@ type Task struct { - No cloud provider integrations - No auto-scaling - No admission controllers -- No complex scheduling algorithms +- 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) +## Dependency Map (minimal — v0.2 adds zero direct deps) ``` github.com/spf13/cobra # CLI framework @@ -155,18 +467,37 @@ modernc.org/sqlite # SQLite (pure Go) github.com/google/uuid # UUID generation ``` -Total: ~4 direct dependencies. No web framework, no ORM, no RPC framework. +Total: ~4 direct dependencies. No web framework, no ORM, no RPC framework, +no PKI library. mTLS via `crypto/tls` and `crypto/x509` (stdlib). -## Deployment Model +> **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) ``` -User Machine Server Node -┌──────────┐ ┌──────────────────┐ -│ orca CLI │─────── mTLS ──────────▶│ orca daemon │ -│ │ │ ├── API server │ -│ │ │ ├── Engine │ -│ │ │ └── SQLite store │ -└──────────┘ └──────────────────┘ +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.1, the CLI and daemon can be the same binary on the same machine. Multi-node is deferred. +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. diff --git a/.ciagent/PERSONAS.md b/.ciagent/PERSONAS.md index a462cb4..9beec01 100644 --- a/.ciagent/PERSONAS.md +++ b/.ciagent/PERSONAS.md @@ -5,10 +5,14 @@ active_personas: - data-engineer - cli-engineer - security-engineer + - network-engineer deactivated_personas: - frontend-engineer - devops-sre -phase_specific: [] +phase_specific: + - security-engineer + - network-engineer + - cli-engineer reason: | Orca is a CLI-first, offline-first orchestration engine with no web UI and a single-binary distribution model. The persona roster reflects this: @@ -17,12 +21,18 @@ reason: | - backend-engineer: core engine and API handlers - data-engineer: SQLite state store and migrations - cli-engineer: Cobra subcommands and CLI UX - - security-engineer: mTLS, audit logging, input validation + - security-engineer: mTLS, cert lifecycle, audit logging, input validation + - network-engineer: transport layer, dispatcher, peer-to-peer resilience Deactivated: - frontend-engineer: no web UI in v0.1 - devops-sre: no container/cloud integrations; release flow is handled by CoreCI (not a persona territory) + + Phase-specific (v0.2): + - security-engineer: P01 (mTLS/CA) + P02 (peer transport hardening) + - network-engineer: P02 only (multi-node scheduling & dispatch) + - cli-engineer: P04 only (--watch flag is a CLI concern) --- # Personas: Orca @@ -47,7 +57,7 @@ reason: | - **Domain**: data - **Frameworks**: `modernc/sqlite` - **Constraints**: `schema-first`, `migration-safe`, `local-storage-only` -- **Territory**: `**/store/**`, `**/model.go`, `**/migration*`, `migrations/**` +- **Territory**: `**/store/**`, `**/model.go`, `**/migration*`, `migrations/**`, `internal/store/migrations/0004_certs.sql` - **Active**: true ### cli-engineer (custom) @@ -60,11 +70,21 @@ reason: | ### security-engineer (custom) - **Domain**: security -- **Frameworks**: `crypto/tls`, `slog` -- **Constraints**: `no-panic-in-production`, `structured-audit-logging`, `no-secret-in-logs`, `input-validation` -- **Territory**: `**/auth/**`, `**/audit/**`, `internal/security/**` +- **Frameworks**: `crypto/tls`, `crypto/x509`, `slog` +- **Constraints**: `no-panic-in-production`, `structured-audit-logging`, `no-secret-in-logs`, `input-validation`, `least-privilege` +- **Territory**: `**/auth/**`, `**/audit/**`, `internal/security/**`, `internal/transport/**` (TLS config only) - **Active**: true - **Reason**: mTLS, audit logging, and input validation are first-class concerns. +- **Phase scope**: P01 (mTLS + internal CA), P02 (transport hardening for peer handshakes). Deactivates after P02 ships — P03/P04 have lighter security needs. + +### network-engineer (custom, NEW in v0.2) +- **Domain**: networking +- **Frameworks**: `net/http`, `crypto/tls` (via `internal/security`), `iter` +- **Constraints**: `connection-resilience`, `retry-with-backoff`, `graceful-disconnect`, `context-propagation` +- **Territory**: `**/transport/**`, `**/engine/dispatcher*`, `**/engine/peer*`, `internal/engine/dispatcher.go`, `internal/transport/**` +- **Active**: true +- **Reason**: v0.2 introduces cross-node dispatch and peer-to-peer transport. This persona owns the transport layer, dispatcher, and peer lifecycle concerns that are distinct from the API-handler territory of `backend-engineer`. +- **Phase scope**: P02 only. Deactivates after P02 ships. ### frontend-engineer - **Active**: false @@ -80,6 +100,27 @@ reason: | - **Behavior**: Out-of-territory file changes log a warning but do not block. - **Rationale**: Allows flexibility during early development; tighten to `strict` post-v0.1. -## Phase-Specific Personas +## Phase-Specific Personas (v0.2) -None for v0.1. All personas persist across all 6 phases. +| Persona | Active in | Reason | +|---------|-----------|--------| +| `security-engineer` | P01, P02 | mTLS/CA in P01, transport hardening in P02. Lighter security needs in P03 (CI scanning) and P04 (streaming UX). | +| `network-engineer` | P02 | Multi-node dispatch is a P02 concern only. P01 builds the transport primitives but P02 wires them into cross-node scheduling. | +| `cli-engineer` | P04 | The `--watch` flag is a CLI surface; P01-P03 don't add new CLI commands. | + +In full-autonomy mode, all personas are auto-accepted and the phase-scope +assignments are applied automatically when a phase is committed. + +## Migration from v0.1 + +- `backend-engineer` territory unchanged: `internal/daemon/**` still owns HTTP + handlers. The new `internal/transport/**` package is shared with + `network-engineer` but `transport` owns the *connection lifecycle* (dial, + retry, close) while `daemon` owns the *request handlers*. +- `data-engineer` territory expanded to include the new + `internal/store/migrations/0004_certs.sql` migration in P01. +- `security-engineer` territory extended from `internal/security/**` to + include the TLS-config portion of `internal/transport/**` (the + `NewServerTLSConfig` / `NewClientTLSConfig` helpers). +- `cli-engineer` territory unchanged; the new `orca cert` subcommands in P01 + fall under the existing `internal/cli/**` glob. diff --git a/.ciagent/PROJECT.md b/.ciagent/PROJECT.md index 1bb02e8..c68fb0f 100644 --- a/.ciagent/PROJECT.md +++ b/.ciagent/PROJECT.md @@ -48,7 +48,33 @@ Build a lightweight system to manage and execute workloads across a set of nodes - Full-blown Kubernetes-compatible API. - Complex cloud-provider integrations. - GUI-based management consoles. -- Multi-node scheduling. - 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.