plan: rev 2 — add thin-client API distribution and compute entity provisioning
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.
This commit is contained in:
@@ -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/<slug>/*` | 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.
|
||||
- 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.
|
||||
Reference in New Issue
Block a user