Files
Jon Chery 77dcb32054 docs(P00): create phase plans
3 execution phases + final review (PLAN_v0.6.md):
- P01 (Wave 1): orca init full bootstrap + schema 0006 (REQ-047/048/049)
  - data-engineer: migration 0006, Node.Kind/OS, NodeRepo extension
  - backend-engineer: init.go full bootstrap orchestration
  - cli-engineer: osdetect.go, init output UX
- P02 (Wave 1, depends on P01): Proxmox SSH join (REQ-050/051)
  - security-engineer: sshkey.go (Ed25519), TOFU, sudoers, PVE role
  - backend-engineer: proxmox/bootstrap.go SSH session sequence
  - cli-engineer: --type/--host/--password flag wiring
- P03 (Wave 2, depends on P01+P02): doctor extensions (REQ-052)
  - backend-engineer: doctor OS() + Proxmox() checks
  - cli-engineer: doctor os/proxmox subcommands
  - security-engineer: audit logging of bootstrap/join actions
- P04 (Wave 3): final review + ship + audit (milestone release v0.5.4)

Wave ordering: P01 -\u003e P02 -\u003e P03 -\u003e P04 (sequential, parallelization off).
Tags: v0.5.0 (P0) .. v0.5.4 (P4 final = milestone release).

---ci---
project: orca
phase: 0
milestone: v0.6
status: plan
---/ci---
2026-08-03 19:40:21 +00:00

16 KiB

Phase Plans: Orca v0.6 — Node Bootstrap & Proxmox

All 3 execution phases + final review with vertical-slice structure, wave ordering, and REQ-ID mapping. v0.6 scope: Node Bootstrap & Proxmoxorca init full bootstrap, Proxmox SSH join, doctor extensions.

Branching: branches numbered from phase 12 onward (v0.1 used 01-07, v0.2 used 08-11, v0.3 used 00+01-03, v0.5 used 00+01-04). v0.6 uses phase/01-*..phase/04-* on the milestone/v0.6-node-bootstrap-proxmox branch (numbering restarts per milestone per branch-strategy.md).


Phase 1: orca init Full Bootstrap + Schema 0006 (Wave 1)

Branch: phase/01-init-bootstrap REQ Coverage: REQ-047, REQ-048, REQ-049 Persona leads: data-engineer (schema), backend-engineer (init orchestration), cli-engineer (output UX)

Must-Haves

data-engineer territory

  • internal/store/migrations/0006_node_kind_os.sqlALTER TABLE nodes ADD COLUMN kind TEXT; ALTER TABLE nodes ADD COLUMN os TEXT; (nullable, backward-compatible)
  • internal/model/node.go — add Kind string json:"kind,omitempty" + `OS string `json:"os,omitempty" fields; add NodeKind constants (NodeKindLocalhost, NodeKindLinux, NodeKindProxmox)
  • internal/store/node_repo.go — extend Insert/Get/List/Watch/scanNode for kind, os columns (use sql.NullString, map NULL → ""); add GetByName(ctx, name) (*Node, error) and UpdateLastSeenAndOS(ctx, id, os string) error helpers
  • internal/store/node_repo_test.go — extend tests for new columns + helpers; assert NULL → "" mapping; assert GetByName returns ErrNotFound for missing; assert UpdateLastSeenAndOS refreshes last_seen + os without changing id/joined_at

backend-engineer territory

  • internal/cli/init.go — full bootstrap sequence (replace current 35-line mkdir-only impl):
    • MkdirAll(certpaths.Dir(), 0o755) — keep
    • store.Open(certpaths.DBPath()) — runs migrations 0001..0006
    • security.CAInit(certpaths.Dir(), "orca-internal-ca") — idempotent (existing fast-path)
    • if !exists(certpaths.ServerCertPath()): GenerateCSR("localhost", ["localhost","127.0.0.1"]) → ca.SignCSR → WriteCert + WriteKey
    • detectOS() from /etc/os-release (see cli-engineer territory)
    • localhost node upsert: GetByName("localhost") → if found UpdateLastSeenAndOS; else Insert with kind=localhost, os=, name="localhost", addr="localhost:8443"
    • print summary (CA fp, server cert fp, os, node id, db path)
  • internal/cli/init_test.go — idempotency test: run init twice, assert no duplicate localhost node, last_seen refreshed, os unchanged; assert CA/cert not regenerated on re-run; assert doctor passes after init

cli-engineer territory

  • internal/cli/osdetect.go (NEW) — detectOS() string: read /etc/os-release then fall back to /usr/lib/os-release; parse KEY=VALUE lines via bufio.Scanner + strings.SplitN; strip surrounding quotes; return ID value or "linux" fallback. Map ubuntu/debian/alpine → verbatim; unknown values stored verbatim (not masked).
  • internal/cli/osdetect_test.go — test parsing with sample os-release content (ubuntu, debian, alpine, missing file, missing ID=, unknown ID, quoted values)
  • internal/cli/init.go output UX — multi-step progress lines: "✓ Namespace dir: ...", "✓ Database initialized: ...", "✓ CA provisioned: ... (fp=...)", "✓ Server cert provisioned: ... (fp=...)", "✓ OS detected: ubuntu", "✓ Localhost node registered: "; --json outputs a single JSON summary object

Verification

  • go build ./... PASS
  • go test ./internal/store/... ./internal/cli/... ./internal/model/... PASS
  • go test -race ./... PASS
  • orca init on a fresh namespace → creates dir, db, CA, server cert, localhost node; orca doctor passes with zero FAILs
  • orca init re-run → no duplicate localhost node, last_seen refreshed, CA/cert not regenerated (idempotent, D-036)
  • orca init --json → valid JSON summary
  • orca node list shows the localhost node with kind=localhost, os=
  • Migration 0006 applies cleanly on existing dbs (existing rows get NULL kind/os → scanned as "")

Phase 2: Proxmox SSH Join (Wave 1)

Branch: phase/02-proxmox-join REQ Coverage: REQ-050, REQ-051 Persona leads: security-engineer (SSH key, TOFU, sudoers, PVE role), backend-engineer (SSH session orchestration), cli-engineer (flag wiring) Depends on: Phase 1 (migration 0006 + Node.Kind/OS fields)

Must-Haves

dependency + security-engineer territory

  • go.mod / go.sum — add golang.org/x/crypto v0.54.0; bump golang.org/x/sys to v0.47.0; add golang.org/x/term v0.45.0 (indirect). Run go mod tidy.
  • internal/certpaths/certpaths.go — add SSHKeyPath() → Dir()/orca_ssh_key, SSHPubPath() → Dir()/orca_ssh_key.pub, KnownHostsPath() → Dir()/known_hosts
  • internal/security/sshkey.go (NEW) — GenerateOrLoadSSHKey(dir string) (keyPEM, pubLine []byte, err error):
    • If orca_ssh_key + .pub exist → load + return (idempotent)
    • Else: ed25519.GenerateKey(rand.Reader)x509.MarshalPKCS8PrivateKey → PEM encode → writeAtomic(keyPath, 0600, keyPEM); ssh.NewPublicKey(pub)ssh.MarshalAuthorizedKeywriteAtomic(pubPath, 0644, pubLine)
    • Return keyPEM (for ssh.ParsePrivateKey) + pubLine (authorized_keys line)
  • internal/security/sshkey_test.go — test generate → load round-trip; test idempotent re-load; test file modes (0600/0644); test ssh.ParsePrivateKey accepts the PKCS8 PEM

backend-engineer territory (with security-engineer co-own)

  • internal/proxmox/bootstrap.go (NEW package) — BootstrapProxmox(ctx context.Context, opts Options) (*Result, error):
    • Options: Host, SSHUser, Password, ProxmoxUser (default "orca"), ProxmoxRole (default "OrcaOperator"), Port (default 22), Logger *slog.Logger
    • Step 1: security.GenerateOrLoadSSHKey(certpaths.Dir()) → keyPEM, pubLine
    • Step 2: Build ssh.ClientConfig with ssh.Password(opts.Password) auth + knownhosts.New(certpaths.KnownHostsPath()) HostKeyCallback (TOFU: captures on first connect, verifies on subsequent)
    • Step 3: ssh.Dial("tcp", host:port, config) with 10s timeout
    • Step 4: Deploy pubkey — session.CombinedOutput("mkdir -p ~orca/.ssh && touch ~orca/.ssh/authorized_keys && chmod 0700 ~orca/.ssh && chmod 0600 ~orca/.ssh/authorized_keys && grep -qF '<publine>' ~orca/.ssh/authorized_keys || echo '<publine>' >> ~orca/.ssh/authorized_keys") (idempotent append)
    • Step 5: Create orca system user — session.CombinedOutput("id -u orca 2>/dev/null || useradd -m -s /bin/bash orca") (idempotent)
    • Step 6: Create PVE role — session.CombinedOutput("pveum role list 2>/dev/null | grep -q '^OrcaOperator' || pveum role add OrcaOperator --privs 'VM.Audit Datastore.AllocateSpace SDN.Use'") (idempotent; use opts.ProxmoxRole for the name)
    • Step 7: Create PVE user — session.CombinedOutput("pveum user list 2>/dev/null | grep -q 'orca@pam' || pveum user add orca@pam -comment 'Orca automation user'") (idempotent; use opts.ProxmoxUser)
    • Step 8: Assign ACL — session.CombinedOutput("pveum acl modify / -user orca@pam -role OrcaOperator") (idempotent)
    • Step 9: Write sudoers — resolve binary paths via command -v pct etc.; write /etc/sudoers.d/orca (mode 0440) with NOEXEC on pct/qm, no NOEXEC on apt-get/dpkg; exclude pvesh (AD-020)
    • Step 10: Validate sudoers — session.CombinedOutput("visudo -cf /etc/sudoers.d/orca"); abort + cleanup if validation fails
    • Step 11: Audit log — logger.Info("proxmox.bootstrap_ok", slog.String("host", opts.Host), slog.String("user", opts.ProxmoxUser), slog.String("role", opts.ProxmoxRole))
    • Result: Node{Kind: "proxmox", OS: "pve", Name: opts.Host, Address: opts.Host + ":8443"}
  • internal/proxmox/bootstrap_test.go — unit tests with a mock SSH server (httptest-style or net.Pipe + manual SSH handshake) OR test the command-builder functions in isolation (probe commands, sudoers content, idempotency checks). Integration test against a real Proxmox host is out of scope for unit tests (flagged as // +build integration).

cli-engineer territory

  • internal/cli/node.go — extend nodeJoinCmd:
    • Add --type flag (values: localhost default, linux, proxmox)
    • Add --host, --ssh-user (default root), --password, --proxmox-user (default orca), --proxmox-role (default OrcaOperator), --ssh-port (default 22) flags
    • When --type proxmox: validate --host + (--password or $ORCA_PROXMOX_PASSWORD) are set; call proxmox.BootstrapProxmox(ctx, opts); insert the returned node via NodeRepo.Insert; print summary
    • When --type localhost (default): existing flow (fingerprint check + registry.Join)
    • Password from --password flag OR $ORCA_PROXMOX_PASSWORD env var (prefer env var per D-031; never log the password; zero the byte slice after use)
  • internal/cli/node_test.go — test flag wiring; test --type proxmox validation (missing host/password → error); test env var fallback

Verification

  • go build ./... PASS
  • go test ./internal/proxmox/... ./internal/security/... ./internal/cli/... PASS
  • go test -race ./... PASS
  • go mod tidy leaves no unused deps; go.sum has golang.org/x/crypto v0.54.0
  • orca node join --type proxmox --host <pve-host> --password <pw> on a real Proxmox 8/9 host:
    • Creates orcaOperator role, orca@pam user, ACL, sudoers file
    • orca@pam can sudo pct list, sudo qm list, sudo apt-get update without password
    • orca@pam CANNOT sudo pvesh (not in sudoers)
    • orca@pam CANNOT sudo bash (not in sudoers)
    • visudo -cf /etc/sudoers.d/orca passes
    • Re-running the join command is idempotent (no duplicate role/user/ACL/sudoers/key)
  • orca node list shows the proxmox node with kind=proxmox, os=pve
  • Audit log contains proxmox.bootstrap_ok entry with host, user, role
  • ~/.orca/orca_ssh_key is 0600, .pub is 0644, known_hosts contains the PVE host key

Phase 3: Doctor Extensions + Audit Logging (Wave 2)

Branch: phase/03-doctor-extensions REQ Coverage: REQ-052 Persona leads: cli-engineer (subcommand wiring), backend-engineer (check logic), security-engineer (audit logging) Depends on: Phase 1 (localhost node + os field), Phase 2 (proxmox nodes + SSH client)

Must-Haves

backend-engineer territory

  • internal/doctor/doctor.go — add OS() check:
    • Re-run detectOS() (from internal/cli/osdetect.go — extract to shared package or pass as param)
    • Load localhost node via NodeRepo.GetByName("localhost")
    • Compare detected OS to stored node.OS; drift → WARN ("OS drift: init=ubuntu, now=debian — re-run orca init to refresh"); match → PASS
    • Missing localhost node → FAIL ("no localhost node — run orca init")
  • internal/doctor/doctor.go — add Proxmox() check (clone Network() pattern):
    • List nodes from NodeRepo, filter kind == "proxmox"
    • Zero proxmox nodes → WARN ("no proxmox nodes registered (single-node?)")
    • Per node: load orca SSH key, build ssh.ClientConfig with ssh.PublicKeys(signer) + knownhosts.New, dial with 3s timeout, run pveversion via session
    • PASS = reachable + pveversion exits 0; FAIL = unreachable or pveversion fails
    • Accumulate per-node lines (clone Network()'s lines []string pattern)
  • internal/doctor/doctor.go — extend All() to include OS() and Proxmox()
  • internal/doctor/doctor_test.go — test OS() with mock node repo (drift, match, missing); test Proxmox() with mock nodes (zero nodes → WARN, reachable → PASS, unreachable → FAIL)

cli-engineer territory

  • internal/cli/doctor.go — add doctorOSCmd + doctorProxmoxCmd subcommands wired to doctor.OS() / doctor.Proxmox(); add to doctorCmd.AddCommand(...)
  • internal/cli/doctor.godoctor os and doctor proxmox honor --json flag (reuse existing pattern)

security-engineer territory

  • internal/audit/audit.go (extend) — emit proxmox.bootstrap_ok, proxmox.bootstrap_fail, node.os_drift events with structured slog fields
  • Audit log entries for all bootstrap + join actions (REQ-052): orca init emits init.bootstrap_ok (os, node_id, ca_fp); orca node join --type proxmox emits proxmox.bootstrap_ok (host, user, role); doctor os drift emits node.os_drift (init_os, current_os)

Verification

  • go build ./... PASS
  • go test ./internal/doctor/... ./internal/cli/... PASS
  • go test -race ./... PASS
  • orca doctor (after orca init) → all checks PASS (cert, db, os, network=zero peers WARN, proxmox=zero nodes WARN)
  • orca doctor os → PASS (OS matches)
  • orca doctor proxmox (no proxmox nodes) → WARN ("no proxmox nodes registered")
  • orca doctor proxmox (after joining a PVE host) → PASS per node
  • orca doctor proxmox (PVE host down) → FAIL per node with error message
  • Audit log contains init.bootstrap_ok and proxmox.bootstrap_ok entries
  • --json output for doctor os and doctor proxmox is valid JSON

Phase 4: Final Review + Ship + Audit (Wave 3)

Branch: phase/04-final-review-ship REQ Coverage: REQ-047, REQ-048, REQ-049, REQ-050, REQ-051, REQ-052 (all) Persona leads: lead-developer (review + audit), all personas (post-hoc review)

Must-Haves

  • Review (delegate to ciagent-review): multi-persona code review across P01-P03
    • Auto-apply P0 fixes; flag P1+ for post-hoc review
    • Review territory discipline (warn mode)
    • Review test coverage for all 6 REQs
  • Audit (delegate to ciagent-audit):
    • Reconstruction test: git log matches .ciagent/ files
    • Branch hygiene: phase branches merged cleanly to milestone
    • Commit discipline: all commits have ---ci--- blocks
    • File discipline: no stale .ciagent/ files
  • Ship (delegate to ciagent-ship):
    • Merge phase/04milestone/v0.6
    • Merge milestone/v0.6main (rebase-then-fast-forward per config.json)
    • Tag v0.5.4 (final phase patch = milestone release per feature-milestone promotion)
    • Create Gitea release with full milestone summary (all phases, all REQs)
  • Complete milestone:
    • Update .ciagent/REQUIREMENTS.md — mark REQ-047..052 as Complete
    • Update .ciagent/ROADMAP.md — mark v0.6 as complete
    • Update .ciagent/CHECKPOINT.jsonmilestone_complete: true
    • Commit: docs(milestone): complete node-bootstrap-proxmox

Verification

  • git log --oneline main..milestone/v0.6 shows all phase commits in order
  • git tag --list v0.5.* shows v0.5.0..v0.5.4
  • main branch contains all v0.6 work (fast-forward merge)
  • orca init && orca doctor on a fresh checkout passes end-to-end
  • Gitea release v0.5.4 exists with milestone summary

Wave Ordering

  • Wave 1 (Phases 1-2): Schema + init bootstrap (P01) is a hard prerequisite for Proxmox join (P02) — P02 depends on the Node.Kind/ OS fields + migration 0006 from P01. parallelization.enabled=false → sequential.
  • Wave 2 (Phase 3): Doctor extensions depend on both P01 (localhost node + os field for doctor os) and P02 (proxmox nodes + SSH client for doctor proxmox).
  • Wave 3 (Phase 4): Final review + ship + audit — covers all execution phases.

For v0.6, parallelization.enabled=false — phases run sequentially.

Versioning

  • Milestone type: feature (P01/P02/P03 ship feat phases)
  • Patch per phase: v0.5.0 (P0), v0.5.1 (P01), v0.5.2 (P02), v0.5.3 (P03), v0.5.4 (P04 final = milestone release)
  • Tags run on the previous minor's patch line (v0.5.x) per branch-strategy.md
  • Milestone branch label: milestone/v0.6-node-bootstrap-proxmox (uses milestone number, not tag line)

Requirement Coverage Matrix

REQ Phase Persona lead Must-haves
REQ-047 P01 backend-engineer init.go full bootstrap (CA + cert + db + localhost node, idempotent)
REQ-048 P01 backend-engineer + cli-engineer detectOS() from /etc/os-release + localhost node registration
REQ-049 P01 data-engineer migration 0006 + Node.Kind/OS + NodeRepo schema extension
REQ-050 P02 security-engineer + backend-engineer proxmox.BootstrapProxmox SSH dance + sshkey.go + certpaths SSH paths
REQ-051 P02 security-engineer OrcaOperator PVE role + orca@pam user + sudoers NOEXEC design
REQ-052 P03 backend-engineer + security-engineer doctor OS() + Proxmox() + audit logging of all bootstrap/join actions