From 79dc87d5845d35480606b4695a81b47047152d39 Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 7 Jul 2026 23:56:07 +0200 Subject: [PATCH] =?UTF-8?q?plan:=20rev=202=20=E2=80=94=20add=20thin-client?= =?UTF-8?q?=20API=20distribution=20and=20compute=20entity=20provisioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two onboarding paths share the same lifecycle state machine: 1. Workstation self-enrollment: curl bootstrap.sh | bash → API enroll → thin client (no git clone, no sync timer). Context poller replaces 5-minute pull. Only AGENTS.md, OIKOS.md, tools/ fetched to disk. 2. Compute entity provisioning: POST /entities/provision → Oikos actuator creates LXC/VM/container on Proxmox host. Validates VMID, IP, capacity, template. Creates relationship edges (hosts, provides, mounts, depends-on) atomically. No self-enrollment, no age key. Adds: context endpoint for agent file deltas, provisioning_steps table, actuator provision methods, type-specific transition checks, full verification matrix covering both paths. --- plans/2026-07-07-client-lifecycle-in-go.md | 424 +++++++++++++++++---- 1 file changed, 357 insertions(+), 67 deletions(-) diff --git a/plans/2026-07-07-client-lifecycle-in-go.md b/plans/2026-07-07-client-lifecycle-in-go.md index 4b8e62f..9e28343 100644 --- a/plans/2026-07-07-client-lifecycle-in-go.md +++ b/plans/2026-07-07-client-lifecycle-in-go.md @@ -1,15 +1,23 @@ # Plan: Client lifecycle — enrollment through deprecation in Oikos Go -**Status:** Planned (2026-07-07) +**Status:** Planned (2026-07-07, rev 2) — rev 2 adds thin-client API distribution model +and compute entity (LXC/VM/container) provisioning flow. ## Goal -Define and implement the complete lifecycle of a homelab client in the Oikos Go -runtime: how a new machine is provisioned, enrolled, given secrets, synced, -operated, and eventually deprecated (or decommissioned or destroyed). Every -state transition feeds the Postgres DB as the authoritative source of truth. -No step depends on the archived Python `secrets-issuance` server or the -non-existent `bin/homelab` CLI. +Define and implement the complete lifecycle of every compute entity in the Oikos +Go runtime — workstations, Proxmox hosts, LXCs, VMs, and containers. Two +onboarding paths share the same lifecycle state machine: + +1. **Workstation self-enrollment**: the machine calls the API to enroll itself +2. **Compute entity provisioning**: Oikos creates the entity (LXC/VM/container) + on a host via the actuator, and the DB is the sole source of truth from the + first API call + +Every state transition feeds the Postgres DB. No step depends on the archived +Python `secrets-issuance` server or the non-existent `bin/homelab` CLI. The +full repo clone + git-sync timer is replaced with a thin API-distribution model +for workstations. ## Current state — what exists vs. what runs @@ -34,31 +42,36 @@ secrets, state, and lifecycle transitions. ``` ┌──────────────────────────────────────────────────────────────┐ -│ NEW CLIENT (bare machine) │ +│ WORKSTATION (bare machine) │ │ │ -│ 1. curl bootstrap.sh | sudo bash │ -│ → clones repo, installs sync timer │ -│ → calls POST /api/v1/clients/enroll (new endpoint) │ -│ → receives age keypair from Oikos API │ -│ → writes /etc/age/key.txt │ -│ → sync timer starts pulling every 5 min │ +│ 1. curl bootstrap.sh | sudo bash (from raw Gitea URL) │ +│ → fetches AGENTS.md, OIKOS.md, tools/ to /opt/homelab/ │ +│ → calls POST /api/v1/clients/enroll │ +│ → receives age keypair + Infisical identity │ +│ → writes /etc/age/key.txt, /etc/infisical/identity │ +│ → polls GET /api/v1/clients/{slug}/context for updates │ +│ (no git clone, no sync timer) │ └──────────────────────────┬───────────────────────────────────┘ │ POST /api/v1/clients/enroll ▼ ┌──────────────────────────────────────────────────────────────┐ │ OIKOS API (Go, :8090) │ │ │ +│ Workstation endpoints: │ │ POST /api/v1/clients/enroll — issue age key, set state │ │ POST /api/v1/clients/{slug}/activate — provisioning→active │ │ POST /api/v1/clients/{slug}/deprecate — active→deprecated │ │ POST /api/v1/clients/{slug}/destroy — deprecated→destroyed │ -│ GET /api/v1/clients/{slug}/secrets — client's accessible │ -│ secrets (Infisical lookup by machine identity) │ -│ MCP whoami(hostname) — client self-introspection │ -│ MCP explain(service) — compact context card │ -│ MCP preflight(service) — risk classification │ -│ MCP get_change_history(entity) — ledger entries │ -│ MCP get_state_snapshot() — last scheduler pass │ +│ GET /api/v1/clients/{slug}/secrets — accessible secrets │ +│ GET /api/v1/clients/{slug}/context — agent context delta │ +│ │ +│ Compute entity endpoints: │ +│ POST /api/v1/entities/provision — create LXC/VM/container │ +│ GET /api/v1/entities/{slug}/provision/status — progress │ +│ │ +│ MCP tools: │ +│ whoami, explain, preflight, get_change_history, │ +│ get_state_snapshot, list_my_secrets │ └──────────────────────────┬───────────────────────────────────┘ │ writes ▼ @@ -73,18 +86,241 @@ secrets, state, and lifecycle transitions. └──────────────────────────────────────────────────────────────┘ ``` -## Client lifecycle: state machine +## Thin client model — why the clone + sync timer goes away + +The repo was the source of truth when Oikos was Python + YAML files. Now the DB +is the source of truth. A full clone of the repo onto every workstation is +unnecessary — the only things a workstation actually needs on disk are: + +| Artifact | Why local? | How delivered | +|----------|-----------|---------------| +| `AGENTS.md` + `OIKOS.md` | Agent reads these at startup; can't query MCP before knowing MCP exists | Fetched once by bootstrap.sh, cached to `/opt/homelab/` | +| `.sops.yaml` | Recipient rules for age decryption (DR fallback if Infisical is down) | Served by `GET /api/v1/clients/{slug}/context` | +| SOPS-encrypted secrets | DR cold recovery (Infisical is primary) | Served by `GET /api/v1/secrets/sops-backups` or fetched on enrollment | +| Age private key | Decrypt SOPS files | Delivered once in enroll response, written to `/etc/age/key.txt` | +| Infisical identity | Authenticate to Infisical for primary secrets | Delivered once in enroll response, written to `/etc/infisical/identity` | +| `tools/` (caveman, hermes-soul) | Auto-setup after pull | Fetched by bootstrap.sh once; API context endpoint serves updates | + +Everything else — topology, knowledge, policy, runbooks, ledger — is queried +live from the DB via MCP. + +### What replaces the sync timer + +The 5-minute `git pull` is replaced by a lightweight API poll: ``` +GET /api/v1/clients/{slug}/context?since=2026-07-07T12:00:00Z +``` + +Returns a JSON delta: +```json +{ + "agent_files_changed": ["AGENTS.md", "OIKOS.md"], + "sops_config_changed": true, + "tools_changed": ["setup-caveman.sh"], + "since": "2026-07-07T12:05:00Z" +} +``` + +The client fetches only changed files. Poll interval: 5 minutes (same as old +timer, but HTTP instead of git). On delta, the client writes updated files to +`/opt/homelab/` and re-runs any changed `tools/*.setup.sh` scripts. + +### What stays a full clone + +The **control plane host** (mac-mini, running the Oikos Docker stack) keeps the +full clone at `/opt/homelab-context/`. It is the deployment target, the seed +source, and the operator's working copy. All other workstations are thin clients. + +### bootstrap.sh changes (thin client) + +```bash +# Old: full clone +git clone "$REPO_HTTPS" "$CLONE_DIR" + +# New: fetch only what the agent needs +mkdir -p /opt/homelab/.agents/shared /opt/homelab/.agents/skills +curl -s "$RAW_URL/AGENTS.md" -o /opt/homelab/AGENTS.md +curl -s "$RAW_URL/.agents/OIKOS.md" -o /opt/homelab/OIKOS.md +curl -s "$RAW_URL/.agents/shared/caveman.md" -o /opt/homelab/caveman.md + +# Then enroll via API (gets age key + Infisical identity) +# Then start the context poller +``` + +No `git` dependency on workstations. No `post-pull.sh`. No sync timer unit +files. Just a cron/launchd job that hits `GET /context` every 5 minutes. + +### Transition for existing enrolled clients + +Existing clients with full clones continue working. The context poller is +additive — it writes updated files to `/opt/homelab/` alongside the clone. +Once the poller is proven, the clone can be removed and the git-based timer +disabled. Backward compatible, no flag day. + +## Compute entity lifecycle — LXC, VM, container provisioning + +Workstations self-enroll. Compute entities (LXCs, VMs, Docker containers) are +**created by Oikos** — an operator (or Hermes) issues an intent, and Oikos +provisions the entity on a Proxmox host via the actuator. The DB is the source +of truth from the first API call; there is no bootstrap.sh, no mesh join, no +age key — the entity exists because Oikos put it there. + +### Provisioning flow + +``` +Operator (or Hermes via MCP): + "create LXC 130 on hubris running jellyfin, 2 cores, 2GB RAM, 20GB disk, + mount /mnt/library, IP 192.168.8.130, privileged" + + │ + ▼ +POST /api/v1/entities/provision +{ + "slug": "lxc:jellyfin", + "type": "lxc", + "name": "Jellyfin", + "host": "host:hubris", + "attributes": { + "vmid": 130, + "cores": 2, + "ram_mb": 2048, + "disk_gb": 20, + "ip": "192.168.8.130", + "privileged": true, + "mounts": [{"source": "/mnt/library", "target": "/mnt/library"}], + "template": "debian-12-standard", + "services": ["jellyfin"] + } +} + + │ Oikos API: + │ 1. Creates entity in DB: lxc:jellyfin, state = planned + │ 2. Validates: IP not in use, VMID not taken, host has capacity + │ 3. Policy classifier: config_mutation → requests operator approval + │ 4. On approval, transitions to provisioning + │ 5. Creates execution record + │ 6. Actuator SSHs to host:hubris: + │ pct create 130 /var/lib/vz/template/cache/debian-12-standard.tar.zst \ + │ --cores 2 --memory 2048 --rootfs local-lvm:20 \ + │ --net0 name=eth0,bridge=vmbr0,ip=192.168.8.130/24,gw=192.168.8.2 \ + │ --unprivileged 0 + │ pct start 130 + │ pct exec 130 -- apt update && apt install -y jellyfin-server + │ # mount, service enable, firewall rules + │ 7. Polls health check until passing + │ 8. Transitions to active + │ 9. Creates relationship edges: host:hubris hosts lxc:jellyfin, + │ lxc:jellyfin provides service:jellyfin, etc. + │ 10. Writes audit log + + ▼ + Entity in DB: lxc:jellyfin, state = active + Relationships: host:hubris → hosts → lxc:jellyfin + lxc:jellyfin → provides → service:jellyfin + lxc:jellyfin → mounts → storage:library +``` + +### State machine (shared by workstations and compute entities) + +``` + ┌─────────────────────────────┐ + │ WORKSTATION │ + │ Operator runs bootstrap.sh │ + │ → self-enrolls via API │ + │ → gets age key + Infisical │ + │ → polls /context for deltas │ + └──────────────┬──────────────┘ + │ [planned] ──→ provisioning ──→ active ──→ migrating ──→ active - │ │ │ + ▲ ▲ │ │ │ │ ├──→ deprecated ──→ destroyed │ │ │ - │ └──→ failed └──→ failed - │ - └──→ destroyed (cancelled) + │ │ └──→ failed + │ │ + │ └──→ failed ┌─────────────────────────────┐ + │ │ COMPUTE ENTITY │ + │ │ POST /entities/provision │ + └──→ destroyed (cancelled) │ → Oikos actuator creates │ + │ LXC/VM/container on host │ + │ → no self-enrollment │ + │ → no mesh join │ + │ → no age key │ + └─────────────────────────────┘ ``` +### Provisioning differences by type + +| Aspect | Workstation | LXC | VM | Docker container | +|--------|------------|-----|----|-----------------| +| Who creates the entity | Operator via API/seed | Operator/Hermes via `POST /entities/provision` | Same as LXC | Same as LXC | +| How it reaches `provisioning` | Self-enrolls via `POST /clients/enroll` | API transitions automatically after operator approval | Same as LXC | Same as LXC | +| Age keypair | Generated, delivered to client | None — no SOPS access needed | None | None | +| Infisical identity | Created, scoped to `/clients//*` | Only if the LXC hosts services that need secrets | Same as LXC | Same as LXC | +| Mesh join | Netbird/Tailscale (if remote access needed) | No — reachable via host LAN IP | Optional | No | +| Health check | Scheduler probes HTTP/TCP/SSH | Scheduler probes service port on host LAN IP | Same as LXC | Scheduler probes container port | +| `provisioning → active` checks | `age-key-enrolled`, `mesh-joined`, `health-check-answering` | `health-check-answering`, `service-running`, `mounts-verified` | Same as LXC + `vm-agent-responding` | `health-check-answering`, `container-running` | +| `deprecated → destroyed` checks | `secrets-revoked`, `no-inbound-edges`, `ingress-dns-removed` | `backups-verified`, `no-inbound-edges`, `ingress-dns-removed` | Same as LXC | `no-inbound-edges` | +| Secrets to revoke on destroy | Age key, Infisical identity | Infisical identity (if any), service tokens | Same as LXC | Infisical identity (if any) | + +### Compute entity attributes schema + +```json +{ + "vmid": 130, + "cores": 2, + "ram_mb": 2048, + "disk_gb": 20, + "ip": "192.168.8.130", + "privileged": true, + "os": "debian-12", + "template": "debian-12-standard", + "mounts": [ + {"source": "/mnt/library", "target": "/mnt/library", "options": "ro"} + ], + "services": ["jellyfin"], + "provisioned_by": "ws:mac-mini", + "provisioned_at": "2026-07-07T14:00:00Z", + "host": "host:hubris" +} +``` + +### API endpoints for compute entity provisioning + +| Method | Path | Scope | Purpose | +|--------|------|-------|---------| +| `POST` | `/api/v1/entities/provision` | operator | Declare intent to create an LXC/VM/container. Creates entity in `planned`, validates constraints, queues execution for operator approval. | +| `GET` | `/api/v1/entities/{slug}/provision/status` | operator | Poll provisioning progress: steps completed, current step, errors | +| `POST` | `/api/v1/entities/{slug}/provision/retry` | operator | Retry a failed provisioning step | + +### What `POST /entities/provision` validates + +1. **Slug is available**: no existing entity with same slug +2. **VMID not in use**: query entities where `attributes->>'vmid'` = requested VMID +3. **IP not in use**: query entities where `attributes->>'ip'` = requested IP +4. **Host exists and is active**: `host:hubris` must be an entity with state `active` +5. **Host has capacity**: check entity_status for CPU/RAM/disk headroom on host +6. **Template exists**: Proxmox template must be available on the host +7. **Mount source exists**: storage entity must exist for each mount +8. **Policy classification**: `config_mutation` → operator approval required (auto-act only if Hermes has autonomy for this specific action type) + +### Relationship edges created on provision + +When an LXC is provisioned, these edges are written to the `relationships` table: + +| Source | Edge type | Target | Why | +|--------|-----------|--------|-----| +| `host:hubris` | `hosts` | `lxc:jellyfin` | Physical colocation | +| `lxc:jellyfin` | `provides` | `service:jellyfin` | Service mapping | +| `lxc:jellyfin` | `mounts` | `storage:library` | Storage dependency | +| `lxc:jellyfin` | `depends-on` | `host:hubris` | Blast radius: hubris down → jellyfin down | +| `service:jellyfin` | `depends-on` | `lxc:jellyfin` | Service availability ties to container | +| `lxc:jellyfin` | `depends-on` | `service:dns` | DNS resolution | +| `lxc:jellyfin` | `depends-on` | `service:caddy` | If ingress-exposed | + +All edges are created atomically during provisioning. The `blast_radius()` CTE +walks these edges to answer "what breaks if hubris goes down?" + ### State: `planned` The operator declares intent. A client entity exists in the DB with state @@ -348,46 +584,68 @@ Add to `api/openapi.yaml`, regenerate with `make generate`, implement in | File | Change | |------|--------| -| `bootstrap.sh` | Replace `secrets.hubris.network/issue` call with `POST /api/v1/clients/enroll`. Remove dead symlinks. | -| `api/openapi.yaml` | Add client enrollment, lifecycle, and secret endpoints | -| `internal/httpapi/impl.go` | Implement client lifecycle handlers | -| `internal/db/queries/clients.sql` | Add client-specific sqlc queries | +| `bootstrap.sh` | Replace git clone with thin fetch of AGENTS.md + OIKOS.md + tools/. Replace `secrets.hubris.network/issue` with `POST /api/v1/clients/enroll`. Replace sync timer with context poller. Remove dead symlinks. | +| `api/openapi.yaml` | Add workstation enrollment, lifecycle, secret, and context endpoints. Add compute entity provision, status, retry endpoints. | +| `internal/httpapi/impl.go` | Implement workstation lifecycle handlers. Implement compute entity provisioning handlers. | +| `internal/db/queries/clients.sql` | Add client-specific sqlc queries (lookup by slug+type, capacity checks) | +| `internal/db/queries/entities.sql` | Add provision validation queries (VMID collision, IP collision, template availability) | | `internal/mcp/server.go` | Register whoami, explain, preflight, get_change_history, get_state_snapshot, list_my_secrets | | `internal/secrets/infisical.go` | Add `CreateMachineIdentity`, `DeleteMachineIdentity`, `StoreClientKey` | -| `internal/ontology/validate.go` | Implement lifecycle transition checks for infrastructure lifecycle | -| `seeds/ontology.yaml` | Add client-specific attributes schema for machine types | -| `migrations/012_client_enrollment.up.sql` | Index for slug+type lookups | -| `AGENTS.md` | Update MCP tool list to match actual implementation | -| `CLIENTS.md` | Update enrollment flow to reference Oikos API, not Python issuance | -| `CONTRIBUTING.md` | Add client lifecycle as a documented extension point | +| `internal/actuator/actuator.go` | Add `ProvisionLXC`, `ProvisionVM` methods (pct create, start, exec, health poll) | +| `internal/ontology/validate.go` | Implement lifecycle transition checks for infrastructure lifecycle (both workstation and compute variants) | +| `seeds/ontology.yaml` | Add workstation-specific and compute-entity attribute schemas. Add provision-entity skill definition. | +| `migrations/012_client_enrollment.up.sql` | Index for slug+type lookups. Add `provisioning_steps` tracking table. | +| `tools/context-poller.sh` | New: lightweight daemon that polls `GET /context` and applies deltas | +| `AGENTS.md` | Update MCP tool list. Document thin client model. | +| `CLIENTS.md` | Update enrollment flow: thin client, API-based, no git clone. | +| `CONTRIBUTING.md` | Add compute entity provisioning as an extension point. | ## Files deleted or deprecated | File | Disposition | |------|-------------| | `archive/secrets-issuance/` | Already archived. Add deprecation notice referencing this plan. | -| `archive/secrets-sops-backup/` | Keep for DR. Add note that new clients use Infisical, SOPS is fallback. | +| `archive/secrets-sops-backup/` | Keep for DR. SOPS backups served via `GET /api/v1/secrets/sops-backups`. | +| `scripts/sync/` (install.sh, linux/*, macos/*) | Deprecated. Replaced by `tools/context-poller.sh`. | +| `tools/post-pull.sh` | Keep on control plane host only. Remove from thin client distribution. | | Any reference to `bin/homelab` | Delete or comment out in bootstrap.sh; CLI doesn't exist. | -| `tools/*.setup.sh` references | Either create the files or remove the auto-setup convention from post-pull.sh. | +| `tools/*.setup.sh` references | Either create the files or remove the auto-setup convention. Context poller can trigger setup scripts on delta. | +| Root `inventory.yaml` | Eventually deprecated. `seeds/inventory.yaml` + DB are authoritative. Keep during transition. | ## Phased implementation ### Phase 1 — API + DB (P0, this week) -1. Write `migrations/012_client_enrollment.up.sql` -2. Add client endpoints to `api/openapi.yaml` +1. Write `migrations/012_client_enrollment.up.sql` (add `provisioning_steps` table) +2. Add workstation + compute entity endpoints to `api/openapi.yaml` 3. Run `make generate` 4. Implement enrollment handler (`POST /api/v1/clients/enroll`): - Age key generation - Infisical machine identity creation - Entity attribute update - Audit log write -5. Implement lifecycle transition handlers (activate, deprecate, destroy, fail) +5. Implement workstation lifecycle transition handlers (activate, deprecate, destroy, fail) 6. Implement `GET /api/v1/clients/{slug}/secrets` -7. Update `seeds/ontology.yaml` with client attribute schemas -8. Add sqlc queries in `internal/db/queries/clients.sql` +7. Implement `GET /api/v1/clients/{slug}/context` (agent file deltas) +8. Update `seeds/ontology.yaml` with workstation and compute attribute schemas +9. Add sqlc queries in `internal/db/queries/clients.sql` -### Phase 2 — MCP tools (P1, next week) +### Phase 2 — Compute entity provisioning (P0, this week) + +1. Implement `POST /api/v1/entities/provision`: + - Entity creation in `planned` state + - Constraint validation (VMID, IP, capacity, template, mounts) + - Policy classification + operator approval flow + - Transition to `provisioning` on approval +2. Implement actuator provisioning methods: + - `ProvisionLXC(slug, attributes)` → `pct create`, `pct start`, package install, mount, service enable + - `ProvisionVM(slug, attributes)` → `qm create`, `qm start` +3. Implement `GET /api/v1/entities/{slug}/provision/status` +4. Implement `POST /api/v1/entities/{slug}/provision/retry` +5. Write relationship edges atomically on provision completion +6. Add validation queries for collision detection + +### Phase 3 — MCP tools (P1, next week) 1. Register `whoami(hostname)` in `internal/mcp/server.go` 2. Register `explain(service)` — compact context card from DB @@ -396,44 +654,71 @@ Add to `api/openapi.yaml`, regenerate with `make generate`, implement in 5. Register `get_state_snapshot()` 6. Register `list_my_secrets(caller_pubkey?)` -### Phase 3 — Bootstrap script cleanup (P1, next week) +### Phase 4 — Thin client distribution (P1, next week) -1. Replace secrets issuance URL with Oikos API endpoint -2. Remove `--no-secrets` / `--no-mesh` or rewire them to degraded modes -3. Remove `bin/homelab` symlink -4. Update Infisical identity file creation -5. Test full enrollment on a fresh machine +1. Write `tools/context-poller.sh` — polls `GET /context`, writes deltas, triggers setup scripts +2. Rewrite `bootstrap.sh`: + - Fetch AGENTS.md, OIKOS.md, tools/ from raw Gitea URL (not full clone) + - Call `POST /api/v1/clients/enroll` + - Install context poller (cron/launchd) + - Remove git dependency for workstations +3. Create missing `tools/setup-caveman.sh` and `tools/setup-hermes-soul.sh` (or serve from context endpoint) +4. Test full enrollment on a fresh machine -### Phase 4 — Transition check enforcement (P2, within 2 weeks) +### Phase 5 — Transition check enforcement (P2, within 2 weeks) -1. Implement all `provisioning → active` checks in `internal/ontology/validate.go` -2. Implement all `deprecated → destroyed` checks +1. Implement all `provisioning → active` checks (workstation and compute variants) +2. Implement all `deprecated → destroyed` checks (workstation and compute variants) 3. Wire checks into lifecycle transition handlers 4. Test that `POST /activate` fails when checks don't pass 5. Test that `POST /destroy` fails when inbound edges exist +6. Test that provision fails when VMID/IP collision detected -### Phase 5 — Cleanup (P2, within 2 weeks) +### Phase 6 — Cleanup + existing client migration (P2, within 2 weeks) 1. Delete or comment-out dead code in bootstrap.sh -2. Recreate `tools/setup-caveman.sh` and `tools/setup-hermes-soul.sh` (or remove references) -3. Update AGENTS.md MCP tool list -4. Update CLIENTS.md enrollment flow -5. Archive Python secrets-issuance with final deprecation note -6. Run `make generate-check` and full test suite +2. Deprecate `scripts/sync/` (install.sh, systemd timer, launchd plist) +3. Keep `tools/post-pull.sh` for control plane host only +4. Update AGENTS.md MCP tool list + thin client model +5. Update CLIENTS.md enrollment flow +6. Archive Python secrets-issuance with final deprecation note +7. Add backward-compat: existing clients with full clones continue working; + context poller runs alongside until operator removes the clone +8. Run `make generate-check` and full test suite ## Verification -- Fresh machine with no prior state: `curl bootstrap.sh | sudo bash` → machine - shows up in DB as `provisioning` with age pubkey, Infisical identity, and sync - timer running +### Workstation enrollment +- Fresh machine with no prior state: `curl bootstrap.sh | sudo bash` → fetches + agent files only (no git clone), enrolls via API, machine shows up in DB as + `provisioning` with age pubkey and Infisical identity +- Context poller running: `GET /context?since=...` returns file deltas - `POST /api/v1/clients/ws:test-machine/activate` → state → `active`, all checks pass - `POST /api/v1/clients/ws:test-machine/deprecate` → state → `deprecated` - `POST /api/v1/clients/ws:test-machine/destroy` → fails if edges exist; succeeds after edges removed, secrets revoked - MCP `whoami(ws:test-machine)` returns client record with peers and health + +### Compute entity provisioning +- `POST /api/v1/entities/provision` with valid LXC spec → entity created in + `planned`, approval requested, on approval → `provisioning`, actuator creates + LXC on host, health check passes → `active` +- `POST /api/v1/entities/provision` with conflicting VMID → 409, error message + with conflicting entity slug +- `POST /api/v1/entities/provision` with IP already in use → 409 +- `POST /api/v1/entities/provision` with unknown host → 400 +- Relationship edges created: `hosts`, `provides`, `mounts`, `depends-on` + all present after provisioning +- `GET /entities/lxc:test-lxc/blast-radius` → returns host, dependent services +- `POST /destroy` on a provisioned LXC → validates backups + edges, deletes + relationships, transitions to `destroyed` + +### Integrated - MCP `explain(service:caddy)` returns context card with relations and risk class - `GET /api/v1/clients/ws:test-machine/secrets` returns secrets list scoped to client -- Existing clients continue working through the sync timer (no regression) +- Thin client survives Infisical outage: SOPS fallback works with local age key + `.sops.yaml` + served via context endpoint +- Existing clients with full clones continue working (no regression) - `make test test-db generate-check` passes ## Related @@ -442,10 +727,15 @@ Add to `api/openapi.yaml`, regenerate with `make generate`, implement in - [seeds/ontology.yaml](../seeds/ontology.yaml) — lifecycle definitions, entity type hierarchy - [seeds/policy.yaml](../seeds/policy.yaml) — risk classes, approval rules - [CLIENTS.md](../CLIENTS.md) — client onboarding guide (update after this plan) -- [bootstrap.sh](../bootstrap.sh) — current enrollment script (rewrite in Phase 3) +- [bootstrap.sh](../bootstrap.sh) — current enrollment script (rewrite in Phase 4) ## Changelog -- 2026-07-07 — initial plan. Replaces Python secrets-issuance, defines full - lifecycle in Go, adds client API endpoints, MCP tools, and Infisical - machine identity integration. \ No newline at end of file +- 2026-07-07 rev 2 — added thin-client API distribution model (no git clone, + no sync timer; context poller replaces 5-minute pull). Added compute entity + provisioning flow (LXC/VM/container created by Oikos actuator, not + self-enrolled). Extended state machine to cover both onboarding paths with + type-specific transition checks, attribute schemas, and relationship edges. +- 2026-07-07 rev 1 — initial plan. Replaces Python secrets-issuance, defines + full workstation lifecycle in Go, adds client API endpoints, MCP tools, + and Infisical machine identity integration. \ No newline at end of file