@celilo/cli 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/AGENTS.md +7 -4
  2. package/CELILO_CORE_MODULES.md +7 -6
  3. package/CELILO_SUBSYSTEMS.md +52 -6
  4. package/MODULE_PRIMITIVES.md +164 -0
  5. package/README.md +5 -5
  6. package/drizzle/0015_port_forwards.sql +12 -0
  7. package/drizzle/meta/_journal.json +7 -0
  8. package/package.json +14 -6
  9. package/schemas/system_config.json +1 -1
  10. package/src/ansible/inventory.test.ts +1 -1
  11. package/src/ansible/inventory.ts +3 -3
  12. package/src/api/protocol.test.ts +1 -1
  13. package/src/api/remote-client.test.ts +1 -1
  14. package/src/api/serve.ts +5 -5
  15. package/src/capabilities/public-web-publish.test.ts +209 -0
  16. package/src/capabilities/validation.test.ts +1 -1
  17. package/src/capabilities/well-known.test.ts +1 -1
  18. package/src/capabilities/well-known.ts +2 -2
  19. package/src/cli/command-tree-parser.ts +3 -1
  20. package/src/cli/commands/api.ts +1 -1
  21. package/src/cli/commands/apt-upgrade.test.ts +33 -0
  22. package/src/cli/commands/apt-upgrade.ts +63 -0
  23. package/src/cli/commands/backup-pull.ts +1 -1
  24. package/src/cli/commands/commands-json.ts +29 -0
  25. package/src/cli/commands/completion.ts +1 -1
  26. package/src/cli/commands/events.ts +1 -1
  27. package/src/cli/commands/module-changeset.ts +1 -1
  28. package/src/cli/commands/module-import.ts +1 -1
  29. package/src/cli/commands/module-list.ts +16 -2
  30. package/src/cli/commands/module-publish.ts +1 -1
  31. package/src/cli/commands/module-remove.ts +26 -1
  32. package/src/cli/commands/module-types.ts +1 -1
  33. package/src/cli/commands/module-upgrade.ts +1 -1
  34. package/src/cli/commands/module-version.ts +1 -1
  35. package/src/cli/commands/module-where.test.ts +26 -0
  36. package/src/cli/commands/module-where.ts +130 -0
  37. package/src/cli/commands/proxmox-node-list.ts +1 -1
  38. package/src/cli/commands/publish/alpha.test.ts +1 -1
  39. package/src/cli/commands/publish/alpha.ts +3 -3
  40. package/src/cli/commands/publish/changesets.ts +1 -1
  41. package/src/cli/commands/publish/global-install.ts +6 -2
  42. package/src/cli/commands/publish/helpers.ts +21 -14
  43. package/src/cli/commands/publish/index.ts +4 -4
  44. package/src/cli/commands/publish/plan.ts +3 -3
  45. package/src/cli/commands/publish/preflight.ts +2 -2
  46. package/src/cli/commands/publish/types.ts +2 -2
  47. package/src/cli/commands/publish/workspace.test.ts +1 -1
  48. package/src/cli/commands/publish/workspace.ts +2 -2
  49. package/src/cli/commands/registry-owner.test.ts +166 -0
  50. package/src/cli/commands/registry-owner.ts +124 -0
  51. package/src/cli/commands/registry-token.test.ts +109 -0
  52. package/src/cli/commands/registry-token.ts +194 -0
  53. package/src/cli/commands/restore.ts +1 -1
  54. package/src/cli/commands/service-add-proxmox.ts +1 -1
  55. package/src/cli/commands/service-list.ts +15 -2
  56. package/src/cli/commands/subscribers-list.ts +1 -1
  57. package/src/cli/commands/system-apply-config-equivalence.test.ts +1 -1
  58. package/src/cli/commands/system-apply-config.ts +1 -1
  59. package/src/cli/commands/system-init-deprecation.test.ts +1 -1
  60. package/src/cli/commands/system-init.ts +2 -2
  61. package/src/cli/commands/token.test.ts +26 -0
  62. package/src/cli/commands/token.ts +160 -0
  63. package/src/cli/completion.ts +21 -0
  64. package/src/cli/generate-zsh-completion.test.ts +22 -4
  65. package/src/cli/generate-zsh-completion.ts +7 -3
  66. package/src/cli/index.ts +170 -7
  67. package/src/cli/parser.ts +1 -1
  68. package/src/cli/restore-command.test.ts +1 -1
  69. package/src/db/client.ts +1 -1
  70. package/src/db/schema.ts +50 -7
  71. package/src/hooks/capability-loader-firewall.test.ts +4 -3
  72. package/src/hooks/capability-loader.ts +100 -10
  73. package/src/hooks/define-hook.test.ts +24 -0
  74. package/src/hooks/executor.test.ts +1 -1
  75. package/src/hooks/executor.ts +2 -2
  76. package/src/hooks/types.ts +1 -1
  77. package/src/manifest/contracts/v1.ts +2 -2
  78. package/src/manifest/schema.ts +13 -13
  79. package/src/manifest/template-validator.ts +1 -1
  80. package/src/module/packaging/build.ts +19 -0
  81. package/src/module/packaging/workspace-deps.test.ts +94 -0
  82. package/src/module/packaging/workspace-deps.ts +185 -0
  83. package/src/module/versioning/changeset-version.ts +1 -1
  84. package/src/policy/no-hand-built-ssh.test.ts +90 -0
  85. package/src/registry/client.test.ts +86 -0
  86. package/src/registry/client.ts +67 -1
  87. package/src/services/api-access.ts +1 -1
  88. package/src/services/aspect-approvals.ts +1 -1
  89. package/src/services/aspect-runner.ts +3 -3
  90. package/src/services/aspect-template-resolver.test.ts +1 -1
  91. package/src/services/aspect-template-resolver.ts +1 -1
  92. package/src/services/build-bus/delivery-events.ts +1 -1
  93. package/src/services/build-bus/fan-out.ts +1 -1
  94. package/src/services/build-bus/hook-dispatch.ts +1 -1
  95. package/src/services/build-bus/receiver-server.ts +1 -1
  96. package/src/services/build-bus/status.test.ts +1 -1
  97. package/src/services/build-bus/status.ts +1 -1
  98. package/src/services/build-bus/subscriber-store.ts +1 -1
  99. package/src/services/bus-interview.ts +1 -1
  100. package/src/services/celilo-events.ts +1 -1
  101. package/src/services/celilo-mgmt-hooks.test.ts +1 -1
  102. package/src/services/cross-module-read.ts +1 -1
  103. package/src/services/deploy-posture.ts +1 -1
  104. package/src/services/deployed-systems.test.ts +1 -1
  105. package/src/services/deployed-systems.ts +4 -4
  106. package/src/services/dns-provider-backfill.ts +2 -2
  107. package/src/services/events-daemon.ts +1 -1
  108. package/src/services/machine-pool.ts +3 -3
  109. package/src/services/module-deploy.ts +93 -8
  110. package/src/services/module-subscriptions.ts +1 -1
  111. package/src/services/module-types-generator.ts +1 -1
  112. package/src/services/module-validator/git-hygiene.ts +1 -1
  113. package/src/services/port-forwards.test.ts +92 -0
  114. package/src/services/port-forwards.ts +86 -0
  115. package/src/services/proxmox-reconcile.ts +2 -2
  116. package/src/services/public-web-republish.test.ts +189 -0
  117. package/src/services/public-web-republish.ts +84 -0
  118. package/src/services/restore-from-file.ts +1 -1
  119. package/src/services/restore-preflight.ts +1 -1
  120. package/src/services/system-identity.ts +1 -1
  121. package/src/services/system-init.test.ts +2 -2
  122. package/src/services/system-init.ts +2 -2
  123. package/src/services/web-route-cleanup.test.ts +250 -0
  124. package/src/services/web-route-cleanup.ts +144 -0
  125. package/src/templates/generator.ts +2 -2
  126. package/src/types/infrastructure.ts +1 -1
  127. package/src/variables/computed/computed-integration.test.ts +1 -1
  128. package/src/variables/computed/evaluate.ts +1 -1
  129. package/src/variables/computed/parse.ts +1 -1
  130. package/src/variables/context.test.ts +1 -1
  131. package/src/variables/context.ts +5 -5
  132. package/src/variables/lxc-nameserver.test.ts +1 -1
  133. package/src/variables/resolver.ts +2 -2
  134. package/src/variables/types.ts +1 -1
  135. package/src/api/protocol.ts +0 -159
  136. package/src/api/remote-client.ts +0 -218
  137. package/src/cli/command-registry.ts +0 -1488
package/AGENTS.md CHANGED
@@ -77,10 +77,13 @@ celilo module deploy <id> / update / health # deploy + verify on the fleet
77
77
  implements (IPAM, capabilities, firewall/DNS, generator, …), with entry-point files.
78
78
  - `./CELILO_CORE_MODULES.md` — the production modules celilo ships in `modules/`
79
79
  (what each provides/requires, its role in the fleet).
80
- - `../../design/MODULE_DEVELOPMENT_GUIDE.md` — author a module (start here).
80
+ - `../../reference/MODULE_DEVELOPMENT_GUIDE.md` — author a module (start here).
81
+ - `./MODULE_PRIMITIVES.md` — the remote-ops primitives a hook uses to touch a box
82
+ (probe / serviceCtl / applyRenderedConfig / waitFor / …). **Modules never
83
+ hand-build SSH** — read this before writing any hook that reaches a remote host.
81
84
  - `./CLI_USAGE.md` — full CLI reference + workflows.
82
- - `../../v2/BUILD_BUS.md` — how a change reaches the fleet.
83
- - `../../v2/APP_CI_REFERENCE.md` — wire an app's CI/CD (the copy-me recipe).
84
- - `../../v2/MODULE_VERSIONING.md` — what `version` means; `version_source`.
85
+ - `../../openspec/changes/build-bus-poll-cd/proposal.md` — how a change reaches the fleet.
86
+ - `../../reference/APP_CI_REFERENCE.md` — wire an app's CI/CD (the copy-me recipe).
87
+ - `../../openspec/changes/module-version-semantics/proposal.md` — what `version` means; `version_source`.
85
88
  - `../../schemas/module-manifest.schema.json` — the manifest contract.
86
89
  - `https://celilo.computer/docs` — hosted docs (LAN; the repo docs lead).
@@ -22,7 +22,7 @@ Each entry: `module id` — what it is — **provides** / **requires** capabilit
22
22
  ## Network fabric (DNS / firewall / DHCP)
23
23
 
24
24
  - **greenwave** — GreenWave C4000XG ISP router driver; port-forwarding + public-IP discovery via REST. **provides:** `firewall`, `dhcp_server`.
25
- - **iptables** — iptables firewall + NAT; cross-VLAN port exposure with recursive upstream delegation. **provides:** `firewall`.
25
+ - **iptables** — iptables firewall + NAT; cross-VLAN port exposure with recursive upstream delegation. Converge model: `exposeService` registers into the shared-core port-forward registry, then renders the complete ruleset and applies it atomically via `iptables-restore` (default-DROP FORWARD + coarse zone-tier matrix; SSH-free). **provides:** `firewall`.
26
26
  - **knot-unbound-internal** — split-horizon internal DNS via Knot (authoritative) + Unbound (recursive); lightweight, plain apt, no .NET. **provides:** `dns_internal`. Ships a base-module-aspect (`modules/knot-unbound-internal/base-module-aspect/`).
27
27
  - **technitium** — internal split-horizon DNS resolver + authoritative server (web UI + HTTP API); heavier alternative to knot-unbound. **provides:** `dns_internal`. Ships a base-module-aspect (`modules/technitium/base-module-aspect/`).
28
28
  - **namecheap** — public DNS A-record management via Namecheap Dynamic DNS API (HTTP, no browser automation). **provides:** `dns_registrar`.
@@ -34,17 +34,17 @@ Each entry: `module id` — what it is — **provides** / **requires** capabilit
34
34
 
35
35
  ## Celilo's own infrastructure (self-hosted)
36
36
 
37
- - **celilo-mgmt** — the celilo management server itself, deployed as a module (replaces install.sh + `system init`; ships daemon, runs migrations, self-registers). **provides:** `celilo_event_bus`, `celilo_module_deploy_worker`. **requires:** `cross_module_read`. See `v2/MANAGEMENT_AS_NETAPP.md`.
38
- - **celilo-registry** — module registry server (Cargo sparse protocol); stores `.netapp` files, serves index + search/download API. **provides:** `registry_publish`. **requires:** `public_web`, `dns_registrar`.
37
+ - **celilo-mgmt** — the celilo management server itself, deployed as a module (replaces install.sh + `system init`; ships daemon, runs migrations, self-registers). **provides:** `celilo_event_bus`, `celilo_module_deploy_worker`. **requires:** `cross_module_read`. See `openspec/specs/management-as-module/spec.md`.
38
+ - **celilo-registry** — module registry server (Cargo sparse protocol); stores `.netapp` files, serves index + search/download API. On install it provisions a confidential introspection OIDC client via `idp.create_oidc_client` (SECURE_MODULE_PUBLISH.md §5[D-A]) and converges its issuer + introspection endpoint + creds onto the box for RFC 7662 token verification. **provides:** `registry_publish`. **requires:** `public_web`, `dns_registrar`, `idp`.
39
39
  - **celilo-apt-repo** — Debian apt repository (reprepro + Bun HTTP server) serving the celilo `.deb` at apt.celilo.computer. **provides:** `apt_publish`. **requires:** `public_web`, `dns_registrar`.
40
40
  - **celilo-website** — public docs site (static Astro) served via Caddy on celilo.computer. **requires:** `public_web`, `dns_registrar`.
41
41
 
42
42
  ## Git forge & CI pipeline
43
43
 
44
- - **forgejo** — self-hosted Forgejo git forge (git-over-SSH, OIDC, public ingress). **provides:** `source_forge`. **requires:** `public_web`, `idp`, `firewall`, `dns_registrar`. See `v2/CI_PIPELINE.md`.
45
- - **forgejo-runner** — host-mode (LXC) Forgejo Actions runner for unit/lint jobs; outbound-only, lives in dmz. **requires:** `source_forge`. See `v2/FORGEJO_RUNNER.md`.
44
+ - **forgejo** — self-hosted Forgejo git forge (git-over-SSH, OIDC, public ingress). **provides:** `source_forge`. **requires:** `public_web`, `idp`, `firewall`, `dns_registrar`. See `openspec/specs/forgejo-runner/spec.md`.
45
+ - **forgejo-runner** — host-mode (LXC) Forgejo Actions runner for unit/lint jobs; outbound-only, lives in dmz. **requires:** `source_forge`. See `openspec/specs/forgejo-runner/spec.md`.
46
46
  - **forgejo-builder** — VM-based, Docker-capable Forgejo Actions runner (`requires.system.type: vm`) for hermetic release builds + the cele2e suite. **requires:** `source_forge`.
47
- - **npm-cache-node** — self-hosted npm registry: a pass-through disk cache (proxies upstream) plus a `PUT /<pkg>` publish endpoint that stores locally published `@celilo/*` tarballs as authoritative local-origin (never overwritten by upstream). PUT is gated on a static operator-set publish token (`publish_tokens` secret, SHA-256 model; fail-closed — absent/bad token → 401); reads stay open (it's a mirror). **requires:** `public_web`, `dns_registrar`. See `v2/NPM_CACHE_NODE.md`.
47
+ - **npm-cache-node** — self-hosted npm registry: a pass-through disk cache (proxies upstream) plus a `PUT /<pkg>` publish endpoint that stores locally published `@celilo/*` tarballs as authoritative local-origin (never overwritten by upstream). PUT is gated on a static operator-set publish token (`publish_tokens` secret, SHA-256 model; fail-closed — absent/bad token → 401); reads stay open (it's a mirror). **requires:** `public_web`, `dns_registrar`. See `openspec/changes/private-npm-registry/proposal.md`.
48
48
 
49
49
  ## Applications
50
50
 
@@ -52,6 +52,7 @@ Each entry: `module id` — what it is — **provides** / **requires** capabilit
52
52
 
53
53
  ## E2E fixtures & probes (not production apps)
54
54
 
55
+ - **hello-foo** / **hello-bar** — hostless single-page sites (one hand-written `site/index.html`, no build step) published at a configurable `domain` + `path`. They exist so the caddy route-lifecycle e2e can put several independent routes on one caddy and watch them interact — celilo-website is the only other static-site module, and one site can't answer "does removing `/` break `/foo`?". **requires:** `public_web`, `dns_registrar`.
55
56
  - **source-forge-probe** — hostless e2e probe that exercises the `source_forge` capability (issue/revoke runner token) against a deployed Forgejo. **requires:** `source_forge`.
56
57
  - **test-ansible-output** — e2e fixture that runs 25×1s sleep tasks to assert Ansible output streams in real time. Not a real app.
57
58
  - **vm-smoke** — throwaway module proving celilo's generic `type: vm` deploy path end-to-end (template clone → cloud-init → Ansible reach). No capabilities.
@@ -11,7 +11,7 @@ inventory / the codebase and cite what you found. Only raise a question if the s
11
11
  is genuinely empty, or the real choice is between existing mechanisms.
12
12
 
13
13
  This is an *implemented-feature* map. For terminology see `GLOSSARY.md`; for architecture
14
- see `design/README.md`. Companion doc: [CELILO_CORE_MODULES.md](./CELILO_CORE_MODULES.md)
14
+ see `openspec/specs/`. Companion doc: [CELILO_CORE_MODULES.md](./CELILO_CORE_MODULES.md)
15
15
  (the production modules celilo ships).
16
16
 
17
17
  > **All file paths below are relative to the celilo source-repo root** (e.g.
@@ -26,6 +26,7 @@ see `design/README.md`. Companion doc: [CELILO_CORE_MODULES.md](./CELILO_CORE_MO
26
26
  - **IPAM (IP/VMID allocation)** — `apps/celilo/src/ipam/allocator.ts` — `allocateIPFromSubnet`, `allocateVMID`, `reserveIP`/`unreserveIP`, `inferZoneFromIP`, `getAllocation`. Auto-wrapper: `apps/celilo/src/ipam/auto-allocator.ts` — `allocateForModule` / `deallocateForModule`.
27
27
  - **Infrastructure selection (container-service vs machine pool)** — `apps/celilo/src/services/machine-pool.ts` (`getMachineByHostname`, `addMachine`, `assignModuleToMachine`) and `apps/celilo/src/services/container-service.ts` (`getContainerServiceByName`, `addContainerService`, `verifyContainerService`). Provider API clients: `apps/celilo/src/api-clients/proxmox.ts`, `apps/celilo/src/api-clients/digitalocean.ts`.
28
28
  - **Zone detection / system config** — `apps/celilo/src/services/zone-detector.ts` — `detectZoneFromIp` reads `network.<zone>.subnet` from the `systemConfig` table.
29
+ - **Host discovery ("which host serves module X?")** — `apps/celilo/src/cli/commands/module-where.ts` (`celilo module where <id> [--json]`, MCP `celilo_module_where`) — reads deployed hosts from `module_systems` via `getModuleSystems`, reconciles the live Proxmox node via `reconcilePlacement`, and adds a role-based reachability hint per zone. CI/build infra (builder VM, Forgejo runners) is out of scope (not in `module_systems`).
29
30
 
30
31
  ## Capability system (cross-module data & functions)
31
32
 
@@ -38,16 +39,53 @@ see `design/README.md`. Companion doc: [CELILO_CORE_MODULES.md](./CELILO_CORE_MO
38
39
 
39
40
  | capability | impl | provider module(s) |
40
41
  |---|---|---|
41
- | `public_web` | `packages/capabilities/src/public-web.ts` (`createPublicWeb`) | caddy |
42
+ | `public_web` | `packages/capabilities/src/public-web.ts` (`createPublicWeb`). **Route lifecycle** is framework-owned at both ends, not per-module: removing a consumer runs `apps/celilo/src/services/web-route-cleanup.ts` (delete rows → reclaim `/srv/www/<slug>` → emit `routes_changed`) whether or not the module has an `on_uninstall`; deploying a provider runs `apps/celilo/src/services/public-web-republish.ts` (re-run every static consumer's `on_install`) so a provider rebuild refills the web roots it destroyed. | caddy |
42
43
  | `idp` | `packages/capabilities/src/idp.ts` | authentik |
43
44
  | `dns_registrar` | `packages/capabilities/src/dns-registrar.ts` (`registerHost`) | namecheap |
44
- | `firewall` | `packages/capabilities/src/firewall.ts` (`exposeService`, `unexposeService`, `listExposedServices`) | greenwave, iptables |
45
+ | `firewall` | `packages/capabilities/src/firewall.ts` (`exposeService`, `unexposeService`, `listExposedServices`). **Converge model**: register into the shared-core port-forward registry → render the complete ruleset → apply atomically (see the firewall converge note below). | greenwave, iptables |
45
46
  | `dns_internal` | `packages/capabilities/src/dns-internal.ts` | knot-unbound-internal, technitium |
46
47
  | `dhcp_server` | `packages/capabilities/src/dhcp-server.ts` | greenwave |
47
48
  | `source_forge` | `packages/capabilities/src/source-forge.ts` | forgejo |
48
49
  | `registry_publish` | `packages/capabilities/src/registry-publish.ts` | celilo-registry |
49
50
  | `cross_module_read` | `packages/capabilities/src/cross-module-read.ts` | framework (read other modules' capability data) |
50
51
 
52
+ ## Remote-ops primitives (the SSH seam — modules never hand-build SSH)
53
+
54
+ Module hooks reach a remote box ONLY through these typed primitives
55
+ (openspec/changes/unified-management-no-ssh/proposal.md); a raw `ssh` string / `node:child_process` in
56
+ `modules/**/scripts/` is a defect. Impl: `packages/capabilities/src/remote.ts`
57
+ (exported from `packages/capabilities/src/index.ts`).
58
+
59
+ - **`remoteExec`** — the ONE ssh seam (`ssh root@<target> <cmd>`); everything else builds on it.
60
+ - **`probe`** — read-only health checks: `systemd` / `http` / `command`.
61
+ - **`serviceCtl`** — systemctl start/stop/restart/reload/enable/disable.
62
+ - **`runAppCommand` / `runAppCommandWithSecret`** — escape-hatch on-box command; the secret variant feeds the secret on **stdin** (`$SECRET`), never argv.
63
+ - **`streamBackup` / `streamRestore` / `fetchFile` / `pushFile`** — binary-safe streaming via local shell redirect/pipe.
64
+ - **`waitFor`** — predicate-poll combinator.
65
+ - **`applyRenderedConfig`** — converge: write rendered config → validate → apply → rollback (one round-trip). Used by caddy (Caddyfile), knot (views), iptables (ruleset).
66
+ - **`tailLog` / `grepLog`** — journald reads (regex / ignoreCase).
67
+
68
+ Target = `RemoteTarget` (`{ ipv4_address }`; `DeployedSystem` satisfies it). The
69
+ runner seam (`execRunner` real / `createMockRunner` for tests) lives in
70
+ `packages/capabilities/src/testing.ts`. Authoring guide: `MODULE_PRIMITIVES.md`
71
+ (ships in `@celilo/cli`).
72
+
73
+ ## Firewall converge & port-forward registry
74
+
75
+ - **Port-forward registry (desired state)** — `port_forwards` DB table
76
+ (`apps/celilo/src/db/schema.ts`, migration `0014`) + the injectable
77
+ `PortForwardStore` (`apps/celilo/src/services/port-forwards.ts`,
78
+ `buildPortForwardStore`). Shared-core so any firewall provider reconciles
79
+ against one store; injected by the capability-loader into the firewall factory.
80
+ - **Ruleset renderer (pure)** — `modules/iptables/scripts/ruleset-renderer.ts`
81
+ (`renderRuleset`): registry + firewall state → a complete `iptables-restore`
82
+ file. Default-DROP FORWARD + established/related + egress + the coarse
83
+ zone-tier matrix (dmz→app, app→secure from `network.<zone>.subnet`) + per-service DNAT allows.
84
+ - **Converge** — `modules/iptables/scripts/firewall-functions.ts` (`converge`):
85
+ `exposeService`/`unexposeService` register into the store, then render + apply
86
+ atomically via `applyRenderedConfig` (`iptables-restore`). Replaces the old
87
+ per-rule `iptables -A`; the registry (not `iptables -L`) is the source of truth.
88
+
51
89
  ## Hooks & deploy
52
90
 
53
91
  - **Hook executor / ABI** — `apps/celilo/src/hooks/executor.ts` (`invokeHook`, `executeHookScript`, `checkRequiredCapabilities`), types in `apps/celilo/src/hooks/types.ts` (`HookContext`, `HookDefinition`, `HookName`). Named-hook runner: `apps/celilo/src/hooks/run-named-hook.ts`. Manifest hook config: `apps/celilo/src/hooks/load-hook-config.ts`.
@@ -68,6 +106,10 @@ see `design/README.md`. Companion doc: [CELILO_CORE_MODULES.md](./CELILO_CORE_MO
68
106
 
69
107
  - **Module packaging** — `apps/celilo/src/module/packaging/` — `build.ts` (`buildModule`), `extract.ts`, `checksum.ts`, `signature.ts` (`signChecksums`/`verifySignature`), `release-metadata.ts`, `audit.ts`.
70
108
  - **Publish driver** — `scripts/publish.ts` shims to `apps/celilo/src/cli/commands/publish/` (workspace npm packages via `bun publish` + module registry; preflight stale-version/stale-manifest gates).
109
+ - **Registry publish-token admin (append-safe)** — `apps/celilo/src/cli/commands/registry-token.ts` — `celilo registry token add/rm <token>` read-modify-write the celilo-registry `publish_tokens` secret (newline-separated bootstrap/admin list). Avoids the `module secret set` full-overwrite that clobbered other holders. Runs on-mgr where the master key lives; runtime-minted scoped tokens are handled separately by the registry-server.
110
+ - **Contributor identity tokens (idp-issued, per-user)** — `apps/celilo/src/cli/commands/token.ts` — `celilo token obtain|list|revoke` mints/lists/revokes per-user API tokens via the `idp` capability (`create_token`/`list_tokens`/`revoke_token` on authentik). A module contributor authenticates publishes AS THEMSELVES (SECURE_MODULE_PUBLISH.md §6) — no admin/shared token on their machine; the token feeds `celilo author init`. Shown once at mint; the idp stores it hashed, celilo persists nothing. Runs on-mgr where the idp provider lives. Distinct from `registry token add/rm` (raw bootstrap list, different trust model).
111
+ - **Registry token verification (opaque + idp introspection)** — `packages/registry-server/src/auth.ts` (`TokenAuth` — opaque SHA-256 publish tokens, admin/per-package scope) + `packages/registry-server/src/introspection.ts` (`IntrospectionVerifier` — RFC 7662 verify-bridge, SECURE_MODULE_PUBLISH.md §5[D-A]). `authorizePackage()` in `server.ts` tries the opaque set first (unchanged), then, for a token unknown to it, `identify()`s it via the idp introspection endpoint using the registry's confidential OIDC client creds (`OIDC_INTROSPECTION_ENDPOINT`/`OIDC_CLIENT_ID`/`OIDC_CLIENT_SECRET`, provisioned on install — ce-7aa), reading `{active, sub, groups, exp}`. Fails CLOSED on any introspection error; never logs tokens/secrets. Instant revocation: revoke at the idp → next publish sees `active:false` → 401.
112
+ - **Registry module-owner table (hybrid group + owner authz — ce-1ch, D-C)** — `packages/registry-server/src/module-owner-store.ts` (`ModuleOwnerStore` — JSON-persisted `{moduleName, ownerSub, claimedAt, sourceGroup}`, `REGISTRY_OWNERS_FILE`/`dataDir/module-owners.json`). The verified `groups` claim gates *whether* an identity may publish (`REGISTRY_ADMIN_GROUP`→publish/reassign anything; `REGISTRY_PUBLISHER_GROUP`, default `celilo-authors`, configurable→claim+publish owned); the owner table gates *which names*. First-publish-claims: the first verified publisher of an unclaimed name owns it; a *different* publisher is then DENIED (confused-deputy defense — Author-A cannot publish Author-B's module). Admin HTTP endpoints `GET /api/v1/modules/owners`, `GET|POST /api/v1/modules/owners/{name}` (reassign). Operator front door: `celilo registry owner list|show|set` (`apps/celilo/src/cli/commands/registry-owner.ts`), admin token resolved from the local `publish_tokens` bootstrap list.
71
113
 
72
114
  ## Persistence
73
115
 
@@ -79,14 +121,18 @@ see `design/README.md`. Companion doc: [CELILO_CORE_MODULES.md](./CELILO_CORE_MO
79
121
 
80
122
  ## Remote API (drive the CLI over the wire)
81
123
 
82
- Run any celilo command on celilo-mgr over SSH instead of screen-scraping `ssh <host> celilo …`. Typed, streamed, per-operation authz, mid-run interviews. Design: `v2/API_COMMUNICATION.md`.
124
+ Run any celilo command on celilo-mgr over SSH instead of screen-scraping `ssh <host> celilo …`. Typed, streamed, per-operation authz, mid-run interviews. Design: `openspec/changes/replace-ssh-cli-api/proposal.md`.
83
125
 
84
- - **Wire protocol** — `apps/celilo/src/api/protocol.ts` versioned NDJSON tagged union (`command`/`progress`/`log`/`result`/`error`/`interview`/`answer`) + `translateOutputLine`.
126
+ - **Lightweight core (`@celilo/core`)** — `packages/core/src/` — the transport primitives lifted out of `@celilo/cli` so a consumer (e.g. the MCP server) can reach the wire without dragging Ink/React/drizzle/aws-sdk: `command-registry.ts` (`COMMANDS` + `CommandDef`/`ArgDef`/`FlagDef`), `protocol.ts`, `remote-client.ts`. Public surface: `packages/core/src/index.ts`.
127
+ - **Wire protocol** — `packages/core/src/protocol.ts` (`@celilo/core`) — versioned NDJSON tagged union (`command`/`progress`/`log`/`result`/`error`/`interview`/`answer`) + `translateOutputLine`.
128
+ - **Registry serialization** — `apps/celilo/src/cli/commands/commands-json.ts` — `celilo commands --json` prints the full `COMMANDS` tree as JSON; the live source of truth the MCP fetches to generate its tool surface (so it mirrors whatever celilo version the server runs). `service list --json` similarly exposes configured providers for MCP auto-detect. `module list --json` prints the module roster (id/version/state) as stable JSON — the backbone the MCP composite troubleshooting tools correlate `audit --json` findings against.
85
129
  - **Server** — `apps/celilo/src/api/serve.ts` (`apiServeMode`); the `celilo api-serve --principal=<id>` sshd forced-command entry point (dispatched in `apps/celilo/src/cli/index.ts`). Authorizes per principal, runs the command as a protocol-mode child, streams output, audits to stderr.
86
- - **Client** — `apps/celilo/src/api/remote-client.ts` — `resolveRemote` (`--remote <dest>` / `CELILO_REMOTE`), `runRemoteClient` (`ssh -T`, renders progress via the local ProgressDisplay, answers interviews via clack).
130
+ - **Client** — `packages/core/src/remote-client.ts` (`@celilo/core`) — `resolveRemote` (`--remote <dest>` / `CELILO_REMOTE`), `runRemoteClient` (`ssh -T`, renders progress via the local ProgressDisplay, answers interviews via clack).
87
131
  - **Access control** — `apps/celilo/src/services/api-access.ts` — `grantPrincipal`, `isAuthorized` (deny-by-default, `command:subcommand` grants), `renderAuthorizedKeys`. Table: `api_principals` (`apps/celilo/src/db/schema.ts`). CLI: `apps/celilo/src/cli/commands/api.ts` (`api grant|list|revoke|authorized-keys|key new`).
88
132
  - **Mid-run interview bridge (`kind:daemon` responder)** — `apps/celilo/src/services/remote-responder.ts` — `startRemoteResponder` bridges bus `interview.required.*` ↔ wire.
89
133
  - **Server provisioning** — the `celilo-bootstrap` deb (`packaging/celilo-bootstrap/scripts/postinst`) creates the non-root `celilo-api` landing account + sshd; membership in the `celilo` group + `/etc/sudoers.d/celilo` (`!use_pty`) gives api-serve DB access via the wrapper's sudo-drop.
134
+ - **Self-upgrade (apt)** — `celilo apt-upgrade` (`apps/celilo/src/cli/commands/apt-upgrade.ts`) upgrades the deb-installed `celilo`/`celilo-bootstrap` packages (`apt-get update` → `--only-upgrade install`) then spawns a fresh `celilo system migrate` (ISS-0100). It's the RW target behind the MCP's registry-derived `celilo_apt_upgrade` tool; the celilo user's two apt invocations are scoped-sudo'd by `/etc/sudoers.d/celilo-apt-upgrade`, shipped by `celilo-bootstrap`.
135
+ - **MCP service (`@celilo/mcp`)** — `packages/mcp/src/` — an operator-facing stdio MCP server (official `@modelcontextprotocol/sdk`, bin `celilo-mcp`) that drives a remote celilo server over the Remote API for an AI client. Two-item config (`config.ts`: `server` + `defaultUser`, env or `~/.config/celilo-mcp/config.json`). Dual-principal auth (`auth.ts`: `celilo-mcp auth setup` enrolls read-only `celilo-mcp-ro` + full `celilo-mcp-rw` ed25519 keypairs, prints the exact `celilo api grant` lines the operator runs server-side). Transport (`transport.ts`): reuses `@celilo/core` `runRemoteClient`, selecting the principal by `ssh -i <key>` and capturing structured output. Tool surface is generated LIVE from the server's command registry — `registry-fetch.ts` fetches `celilo commands --json` (+ `service list --json` for configured providers) over the RO principal on connect; `tools-from-registry.ts` (pure) projects that into one tool per runnable leaf, grouped by top-level command (`celilo_module_*`, `celilo_proxmox_*`, …), each with a Zod input schema from the leaf's args/flags and a read/write tag → RO/RW routing, plus a generic `celilo_run` escape hatch. Auto-detect hides provider-gated groups (e.g. `celilo_proxmox_*` until a Proxmox service is configured) and re-detects on a timer, emitting `notifications/tools/list_changed` when the surface changes. Coverage gate (`tests/coverage.test.ts`) asserts every registry leaf maps to a tool. Composite RO troubleshooting tools (`troubleshoot.ts` pure correlation + `troubleshoot-tools.ts` thin adapters): `celilo_assess_module <id>` and `celilo_fleet_status` correlate `celilo audit --json` (the drift backbone) with the `module list --json` roster into a per-module / fleet-wide verdict. Design: `openspec/changes/celilo-mcp-service/proposal.md`. (Distinct from the dev/ops `@celilo/mcp-server` below.)
90
136
 
91
137
  ## E2E simulation
92
138
 
@@ -0,0 +1,164 @@
1
+ # Module Primitives — the remote-ops toolkit for module hooks
2
+
3
+ **The one rule: modules never hand-build SSH.** A module hook that needs to touch
4
+ a deployed box does it through a typed primitive from `@celilo/capabilities` —
5
+ never a raw `ssh root@…` string, never `node:child_process`. SSH lives in exactly
6
+ one place (the `remoteExec` seam) and in Ansible's transport. A raw ssh/exec in
7
+ `modules/**/scripts/` is a defect (and a publish/lint gate flags it).
8
+
9
+ Why: hand-rolled ssh strings re-invent host-key handling, escaping, timeouts, and
10
+ secret-hygiene every time — and can't be unit-tested. The primitives give you one
11
+ correct, mockable implementation of each shape of remote work.
12
+
13
+ ```ts
14
+ import { probe, serviceCtl, applyRenderedConfig } from '@celilo/capabilities';
15
+ ```
16
+
17
+ ---
18
+
19
+ ## The two things every primitive shares
20
+
21
+ **Target** — a `RemoteTarget` is just `{ ipv4_address: string }`. A
22
+ `DeployedSystem` (what your hook gets in `context.systems`) satisfies it directly;
23
+ for a machine addressed by config, pass `{ ipv4_address: ip }`.
24
+
25
+ **Runner** — every primitive takes an optional trailing `runner` (default
26
+ `execRunner`, which really shells out). In unit tests pass `createMockRunner([…])`
27
+ so you assert on the command strings/stdin without touching a box. A non-zero
28
+ remote exit comes back as `{ ok: false, … }` — **failure is data, never a throw** —
29
+ so your hook decides whether it's fatal (a deploy) or best-effort (a teardown).
30
+
31
+ ```ts
32
+ // test
33
+ import { createMockRunner } from '@celilo/capabilities';
34
+ const { run, calls } = createMockRunner([
35
+ { match: 'systemctl is-active caddy', result: { ok: true, stdout: 'active', stderr: '' } },
36
+ ]);
37
+ expect(probe(sys, { kind: 'systemd', unit: 'caddy' }, run).healthy).toBe(true);
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Which primitive when
43
+
44
+ | You want to… | Use | Not |
45
+ |---|---|---|
46
+ | Check a box is healthy (read-only) | `probe` | `ssh … systemctl is-active` |
47
+ | Start/stop/restart a systemd unit | `serviceCtl` | `ssh … systemctl restart` |
48
+ | Push a config whose content you computed from celilo's DB | `applyRenderedConfig` (converge) | `ssh … cat > file` |
49
+ | Wait for a condition to come true | `waitFor` | `sleep` / `setTimeout` |
50
+ | Back up / restore app state (binary) | `streamBackup` / `streamRestore` | `ssh … | base64` |
51
+ | Read a unit's journal | `tailLog` / `grepLog` | `ssh … journalctl` |
52
+ | Run a secret on a command line | `runAppCommandWithSecret` | secret on argv |
53
+ | Anything else with no capability/HTTP/converge path | `runAppCommand` (+ `// escape-hatch:`) | a raw ssh string |
54
+
55
+ **Reach for a capability method first.** If another module already provides the
56
+ thing (open a port → `firewall.exposeService`; register a route →
57
+ `public_web.registerReverseProxy`; mint a token → `idp`/`source_forge`), call
58
+ that, not a primitive. The primitives are for the module's *own* box.
59
+
60
+ ---
61
+
62
+ ## The primitives
63
+
64
+ ### `probe(target, check, runner?, opts?) → { healthy, detail }`
65
+ Read-only health check. Never mutates. `check` is one of:
66
+ - `{ kind: 'systemd', unit }` — unit is `active`.
67
+ - `{ kind: 'http', url, expectStatus?, headers? }` — curl runs **on the box**, so
68
+ it doesn't depend on Caddy/public DNS. `expectStatus` defaults to 200; pass a
69
+ list (e.g. `[200, 308]`) or `headers: { Host: 'app.example.com' }` for a vhost.
70
+ - `{ kind: 'command', command, expectStdoutIncludes? }`.
71
+ ```ts
72
+ probe(sys, { kind: 'http', url: 'http://localhost', headers: { Host: fqdn }, expectStatus: [200, 308] }, run);
73
+ ```
74
+
75
+ ### `serviceCtl(target, unit, action, runner?, opts?) → RunResult`
76
+ systemd control. `action ∈ start|stop|restart|reload|enable|disable`. Mutating —
77
+ for liveness use `probe`.
78
+ ```ts
79
+ serviceCtl(sys, 'caddy', 'reload', run);
80
+ ```
81
+
82
+ ### `applyRenderedConfig({ target, path, content, validate, apply, runner?, timeoutMs? }) → RunResult`
83
+ **The converge primitive.** For config whose *content* your hook computes at
84
+ deploy-time from celilo's DB (caddy's Caddyfile from `web_routes`, knot's views
85
+ from the DNS ledger, the firewall ruleset from the port-forward registry) —
86
+ content a static Ansible render can't know. One atomic on-box script, one SSH
87
+ round-trip: back up → write (`content` rides **stdin**, newline-safe) → `validate`
88
+ (rolled back + not activated if it fails) → `apply` (rolled back if it fails).
89
+ `{path}` is substituted into `validate`/`apply`.
90
+ ```ts
91
+ applyRenderedConfig({
92
+ target: sys, path: '/etc/caddy/Caddyfile', content: rendered,
93
+ validate: 'caddy validate --config {path} --adapter caddyfile',
94
+ apply: 'caddy reload --config {path} --force', runner: run,
95
+ });
96
+ ```
97
+
98
+ ### `waitFor(predicate, { attempts?, intervalMs?, onAttempt? }) → Promise<boolean>`
99
+ Poll an async predicate until true or exhausted (default 30 × 5s). A combinator —
100
+ it never touches the remote; wrap a `probe`/`runAppCommand` thunk. Use
101
+ `onAttempt` to emit a heartbeat so a long wait doesn't trip the hook idle-timeout.
102
+ Never `sleep`.
103
+ ```ts
104
+ await waitFor(() => probe(sys, { kind: 'http', url }, run).healthy,
105
+ { onAttempt: (n) => logger.info(`waiting… ${n * 5}s`) });
106
+ ```
107
+
108
+ ### `tailLog(q & { lines? }) / grepLog(q & { grep }) → RunResult`
109
+ Read a systemd unit's journal. `q = { target, unit, grep?, regex?, ignoreCase? }`.
110
+ `grepLog` requires `grep`; `regex: true` uses `grep -E`.
111
+ ```ts
112
+ grepLog({ target: sys, unit: 'caddy', grep: 'certificate obtained', regex: true });
113
+ ```
114
+
115
+ ### `streamBackup / streamRestore / fetchFile / pushFile → RunResult`
116
+ Binary-safe streaming — bytes flow through the local shell redirect/pipe, never
117
+ Node's string capture, so tar/pg_dump/sqlite snapshots survive byte-for-byte.
118
+ ```ts
119
+ streamBackup(sys, 'tar -cf - -C /var/lib/forgejo repositories', '/backup/repos.tar', run);
120
+ streamRestore(sys, '/restore/repos.tar', 'tar -xf - -C /var/lib/forgejo', run);
121
+ fetchFile(sys, '/etc/app/id', '/local/id', run); // pull
122
+ pushFile(sys, '/local/cert.pem', '/etc/app/cert.pem', run); // push
123
+ ```
124
+
125
+ ### `runAppCommandWithSecret(target, command, secret, runner?, opts?) → RunResult`
126
+ For a command needing a secret on its **command line** (token mint, credential
127
+ register) where the app has no HTTP API. The secret rides **stdin** into shell var
128
+ `SECRET` — never argv, so it's absent from logs/history/the recorded call.
129
+ Reference it as `"$SECRET"`. Escape-hatch discipline applies (comment required).
130
+ ```ts
131
+ // escape-hatch: act_runner registers only via CLI; no HTTP API.
132
+ runAppCommandWithSecret(sys, 'act_runner register --token "$SECRET"', token, run);
133
+ ```
134
+
135
+ ### `runAppCommand(target, command, runner?, opts?) → RunResult`
136
+ **The escape hatch.** Only when the work is neither a capability, HTTP, converge,
137
+ nor a service action (e.g. removing app state on teardown). **Every call site MUST
138
+ carry an inline `// escape-hatch: <why no capability/HTTP/converge path>` comment**
139
+ — the recurrence-gate lint flags an uncommented one.
140
+ ```ts
141
+ // escape-hatch: purge app data dir on teardown; no capability owns this.
142
+ runAppCommand(sys, 'rm -rf /var/lib/app/*', run);
143
+ ```
144
+
145
+ ### `remoteExec(target, command, opts?, runner?) → RunResult`
146
+ The single SSH seam every other primitive builds on. You almost never call this
147
+ directly — prefer the specific primitive. `opts` is `{ input?, timeoutMs? }`.
148
+
149
+ ---
150
+
151
+ ## Rules of thumb
152
+
153
+ - **Failure is data.** Check `.ok`; decide fatal vs best-effort. Don't wrap in
154
+ try/catch expecting throws.
155
+ - **Unit-test with `createMockRunner`.** Assert on `calls[].cmd` and `calls[].input`.
156
+ Controlled identifiers (systemd unit names) are passed unescaped; the outer
157
+ `remoteExec` escapes the whole command, so assert on bare substrings.
158
+ - **Secrets never on argv** — `runAppCommandWithSecret`, or a capability method.
159
+ - **No `sleep`** — `waitFor` on the real condition.
160
+ - **Computed config → `applyRenderedConfig`**, not `ssh cat >`. It validates and
161
+ rolls back; a broken config never goes live.
162
+
163
+ See also: `CELILO_SUBSYSTEMS.md` (the primitive impls + the firewall converge),
164
+ `openspec/changes/unified-management-no-ssh/proposal.md` (the design), `reference/MODULE_DEVELOPMENT_GUIDE.md`.
package/README.md CHANGED
@@ -247,7 +247,7 @@ celilo ipam ip list-reservations
247
247
  - ✅ **Bridge** - From `network.{zone}.bridge` system config
248
248
  - ✅ **VM Resources** - cores, memory, disk, storage from manifest defaults
249
249
 
250
- See [../../design/MODULE_DEVELOPMENT_GUIDE.md](../../design/MODULE_DEVELOPMENT_GUIDE.md) for complete zero-config documentation.
250
+ See [../../reference/MODULE_DEVELOPMENT_GUIDE.md](../../reference/MODULE_DEVELOPMENT_GUIDE.md) for complete zero-config documentation.
251
251
 
252
252
  ### Database Location
253
253
 
@@ -262,7 +262,7 @@ See [../../design/MODULE_DEVELOPMENT_GUIDE.md](../../design/MODULE_DEVELOPMENT_G
262
262
 
263
263
  ### Testing Strategy
264
264
 
265
- **Three test tiers** (see [TESTING_STRATEGY.md](../../design/TESTING_STRATEGY.md) for details):
265
+ **Three test tiers** (see [TESTING_STRATEGY.md](../../reference/TESTING_STRATEGY.md) for details):
266
266
 
267
267
  **Tier 1: Unit Tests** (< 1 second)
268
268
  ```bash
@@ -1539,9 +1539,9 @@ When debugging issues, check:
1539
1539
 
1540
1540
  **Documentation**:
1541
1541
  - [CLAUDE.md](../../CLAUDE.md) - Engineering standards
1542
- - [TESTING_STRATEGY.md](../../design/TESTING_STRATEGY.md) - Testing approach
1543
- - [IDENTIFIER_NAMING_CONVENTIONS.md](../../design/IDENTIFIER_NAMING_CONVENTIONS.md) - Naming rules
1544
- - [TEMPLATE_VARIABLE_SYNTAX.md](../../design/TEMPLATE_VARIABLE_SYNTAX.md) - Variable syntax
1542
+ - [TESTING_STRATEGY.md](../../reference/TESTING_STRATEGY.md) - Testing approach
1543
+ - [IDENTIFIER_NAMING_CONVENTIONS.md](../../reference/IDENTIFIER_NAMING_CONVENTIONS.md) - Naming rules
1544
+ - [TEMPLATE_VARIABLE_SYNTAX.md](../../reference/TEMPLATE_VARIABLE_SYNTAX.md) - Variable syntax
1545
1545
 
1546
1546
  **Debugging Tools**:
1547
1547
  - Drizzle Studio: `bun run db:studio`
@@ -0,0 +1,12 @@
1
+ CREATE TABLE `port_forwards` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `firewall_ip` text NOT NULL,
4
+ `internal_ip` text NOT NULL,
5
+ `port` integer NOT NULL,
6
+ `protocol` text NOT NULL,
7
+ `ingress_ip` text,
8
+ `description` text DEFAULT '' NOT NULL,
9
+ `created_at` integer DEFAULT (unixepoch()) NOT NULL
10
+ );
11
+ --> statement-breakpoint
12
+ CREATE UNIQUE INDEX `port_forwards_unique_idx` ON `port_forwards` (`firewall_ip`,`internal_ip`,`port`,`protocol`,`ingress_ip`);
@@ -106,6 +106,13 @@
106
106
  "when": 1783000000000,
107
107
  "tag": "0014_api_principals",
108
108
  "breakpoints": true
109
+ },
110
+ {
111
+ "idx": 15,
112
+ "version": "6",
113
+ "when": 1783100000000,
114
+ "tag": "0015_port_forwards",
115
+ "breakpoints": true
109
116
  }
110
117
  ]
111
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Celilo — home lab orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,9 +14,16 @@
14
14
  "tsconfig.json",
15
15
  "AGENTS.md",
16
16
  "CELILO_SUBSYSTEMS.md",
17
- "CELILO_CORE_MODULES.md"
17
+ "CELILO_CORE_MODULES.md",
18
+ "MODULE_PRIMITIVES.md"
19
+ ],
20
+ "keywords": [
21
+ "celilo",
22
+ "homelab",
23
+ "orchestration",
24
+ "ansible",
25
+ "terraform"
18
26
  ],
19
- "keywords": ["celilo", "homelab", "orchestration", "ansible", "terraform"],
20
27
  "license": "MIT",
21
28
  "repository": {
22
29
  "type": "git",
@@ -49,9 +56,10 @@
49
56
  },
50
57
  "dependencies": {
51
58
  "@aws-sdk/client-s3": "^3.1024.0",
52
- "@celilo/capabilities": "^0.6.0",
53
- "@celilo/cli-display": "^0.1.9",
54
- "@celilo/event-bus": "^0.1.7",
59
+ "@celilo/capabilities": "workspace:^",
60
+ "@celilo/cli-display": "workspace:^",
61
+ "@celilo/core": "workspace:^",
62
+ "@celilo/event-bus": "workspace:^",
55
63
  "@clack/prompts": "^1.1.0",
56
64
  "ajv": "^8.18.0",
57
65
  "drizzle-orm": "^0.36.4",
@@ -13,7 +13,7 @@
13
13
  "network.dmz.subnet": {
14
14
  "type": "string",
15
15
  "pattern": "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/\\d{1,2}$",
16
- "description": "DMZ subnet CIDR (not defaulted — appears only when a firewall module provides this zone; see v2/NETWORK_CONFIG_TO_FIREWALL.md)"
16
+ "description": "DMZ subnet CIDR (not defaulted — appears only when a firewall module provides this zone; see openspec/specs/progressive-zone-disclosure/spec.md)"
17
17
  },
18
18
  "network.dmz.gateway": {
19
19
  "type": "string",
@@ -97,7 +97,7 @@ describe('generateHostsIni', () => {
97
97
 
98
98
  test('emits ansible_connection=local for a local machine (no SSH host/key)', () => {
99
99
  // The management box deploying to itself uses Ansible's local
100
- // connection (v2/CELILO_BOOTSTRAP_VIRTUAL_PACKAGE.md).
100
+ // connection (openspec/specs/bootstrap-meta-package/spec.md).
101
101
  const hosts: InventoryHost[] = [
102
102
  {
103
103
  hostname: 'celilo-mgr',
@@ -30,7 +30,7 @@ export interface InventoryHost {
30
30
  /**
31
31
  * The management box deploying to itself (127.0.0.1) uses Ansible's
32
32
  * local connection — no SSH, no ansible_host/key. See
33
- * v2/CELILO_BOOTSTRAP_VIRTUAL_PACKAGE.md.
33
+ * openspec/specs/bootstrap-meta-package/spec.md.
34
34
  */
35
35
  local?: boolean;
36
36
  }
@@ -201,7 +201,7 @@ export function buildHostVars(moduleId: string, db: DbClient): Record<string, un
201
201
  }
202
202
 
203
203
  // The host's IP is sourced from the deployed-systems model now, not a
204
- // `target_ip` config row (v2/MODULE_SYSTEMS_ADDRESSING.md). Emit it as the
204
+ // `target_ip` config row (openspec/specs/module-systems-addressing/spec.md). Emit it as the
205
205
  // `target_ip` host_var so Ansible templates that reference `{{ target_ip }}`
206
206
  // (zone files, DNS record tasks) keep working. Single-system modules have one.
207
207
  const recordedSystems = getModuleSystems(moduleId, db);
@@ -266,7 +266,7 @@ export function extractInventoryHost(moduleId: string, db: DbClient): InventoryH
266
266
  }
267
267
 
268
268
  // The host's IP now comes from the deployed-systems model, not a `target_ip`
269
- // config row (v2/MODULE_SYSTEMS_ADDRESSING.md). Inject it so the
269
+ // config row (openspec/specs/module-systems-addressing/spec.md). Inject it so the
270
270
  // ansible_host derivation below and the Ansible `{{ target_ip }}` host_var
271
271
  // (zone files, DNS record tasks) resolve. Single-system modules have one.
272
272
  const recordedSystems = getModuleSystems(moduleId, db);
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { translateOutputLine } from './protocol';
2
+ import { translateOutputLine } from '@celilo/core';
3
3
 
4
4
  describe('translateOutputLine', () => {
5
5
  test('start marker → progress start', () => {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { type RemoteTransport, resolveRemote, runRemoteClient } from './remote-client';
2
+ import { type RemoteTransport, resolveRemote, runRemoteClient } from '@celilo/core';
3
3
 
4
4
  const argv = (rest: string[]) => ['bun', 'celilo', ...rest];
5
5
 
package/src/api/serve.ts CHANGED
@@ -12,16 +12,16 @@
12
12
  */
13
13
 
14
14
  import { createInterface } from 'node:readline';
15
- import { parseArguments } from '../cli/parser';
16
- import { getEventBusPath } from '../config/paths';
17
- import { isAuthorized } from '../services/api-access';
18
- import { type WireInterview, startRemoteResponder } from '../services/remote-responder';
19
15
  import {
20
16
  API_PROTOCOL_VERSION,
21
17
  ClientMessageSchema,
22
18
  type ServerMessage,
23
19
  translateOutputLine,
24
- } from './protocol';
20
+ } from '@celilo/core';
21
+ import { parseArguments } from '../cli/parser';
22
+ import { getEventBusPath } from '../config/paths';
23
+ import { isAuthorized } from '../services/api-access';
24
+ import { type WireInterview, startRemoteResponder } from '../services/remote-responder';
25
25
 
26
26
  /** Exit code returned to the client when authz denies a command. */
27
27
  const EXIT_PERMISSION_DENIED = 126;