@celilo/cli 0.13.3 → 0.14.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 (85) hide show
  1. package/CELILO_CORE_MODULES.md +3 -0
  2. package/CELILO_SUBSYSTEMS.md +71 -2
  3. package/docs/ALERTING.md +298 -0
  4. package/docs/INDEX.md +103 -0
  5. package/drizzle/0016_trusted_sources.sql +10 -0
  6. package/drizzle/0017_alerting.sql +127 -0
  7. package/drizzle/meta/_journal.json +15 -1
  8. package/package.json +3 -2
  9. package/schemas/system_config.json +9 -0
  10. package/src/cli/commands/alerts-act.ts +107 -0
  11. package/src/cli/commands/alerts-list.ts +62 -0
  12. package/src/cli/commands/alerts-poll.ts +129 -0
  13. package/src/cli/commands/alerts-sweep.ts +156 -0
  14. package/src/cli/commands/module-list.ts +50 -3
  15. package/src/cli/commands/monitor.ts +178 -0
  16. package/src/cli/commands/notify-config.ts +453 -0
  17. package/src/cli/commands/system-audit.ts +2 -0
  18. package/src/cli/commands/system-update.ts +1 -0
  19. package/src/cli/completion.ts +26 -0
  20. package/src/cli/generate-zsh-completion.ts +2 -0
  21. package/src/cli/index.ts +58 -0
  22. package/src/cli/tui/audit-state.ts +2 -0
  23. package/src/db/schema.ts +358 -0
  24. package/src/hooks/capability-loader.ts +158 -46
  25. package/src/hooks/capability-map-coverage.test.ts +101 -0
  26. package/src/manifest/schema.ts +60 -1
  27. package/src/services/alerting/ack.test.ts +212 -0
  28. package/src/services/alerting/ack.ts +119 -0
  29. package/src/services/alerting/builtin-monitors.test.ts +132 -0
  30. package/src/services/alerting/builtin-monitors.ts +84 -0
  31. package/src/services/alerting/builtin-source.ts +82 -0
  32. package/src/services/alerting/coverage-source.ts +38 -0
  33. package/src/services/alerting/deferral.test.ts +161 -0
  34. package/src/services/alerting/delivery-loop.test.ts +396 -0
  35. package/src/services/alerting/deploy-hooks.test.ts +125 -0
  36. package/src/services/alerting/deploy-hooks.ts +111 -0
  37. package/src/services/alerting/escalation.test.ts +207 -0
  38. package/src/services/alerting/escalation.ts +151 -0
  39. package/src/services/alerting/format.test.ts +193 -0
  40. package/src/services/alerting/format.ts +150 -0
  41. package/src/services/alerting/health-coverage.ts +81 -0
  42. package/src/services/alerting/inbound-poller.test.ts +298 -0
  43. package/src/services/alerting/inbound-poller.ts +236 -0
  44. package/src/services/alerting/inbound.test.ts +201 -0
  45. package/src/services/alerting/inbound.ts +112 -0
  46. package/src/services/alerting/interview-responder.test.ts +169 -0
  47. package/src/services/alerting/interview-responder.ts +158 -0
  48. package/src/services/alerting/keys.test.ts +155 -0
  49. package/src/services/alerting/keys.ts +190 -0
  50. package/src/services/alerting/monitors.ts +185 -0
  51. package/src/services/alerting/notification-responder.test.ts +290 -0
  52. package/src/services/alerting/notification-responder.ts +260 -0
  53. package/src/services/alerting/notifier.ts +219 -0
  54. package/src/services/alerting/people.ts +178 -0
  55. package/src/services/alerting/quiet-hours.test.ts +140 -0
  56. package/src/services/alerting/quiet-hours.ts +99 -0
  57. package/src/services/alerting/reconcile.test.ts +190 -0
  58. package/src/services/alerting/reconcile.ts +166 -0
  59. package/src/services/alerting/run-monitor.test.ts +185 -0
  60. package/src/services/alerting/run-monitor.ts +177 -0
  61. package/src/services/alerting/store.test.ts +222 -0
  62. package/src/services/alerting/store.ts +289 -0
  63. package/src/services/alerting/suppression.test.ts +228 -0
  64. package/src/services/alerting/suppression.ts +142 -0
  65. package/src/services/alerting/sweep-runner.test.ts +229 -0
  66. package/src/services/alerting/sweep-runner.ts +204 -0
  67. package/src/services/alerting/sweep.test.ts +61 -0
  68. package/src/services/alerting/sweep.ts +41 -0
  69. package/src/services/alerting/tokens.test.ts +152 -0
  70. package/src/services/alerting/tokens.ts +119 -0
  71. package/src/services/alerting/transport-loader.ts +48 -0
  72. package/src/services/audit/index.test.ts +1 -0
  73. package/src/services/audit/index.ts +3 -0
  74. package/src/services/audit/trusted-sources.test.ts +137 -0
  75. package/src/services/audit/trusted-sources.ts +124 -0
  76. package/src/services/audit/types.ts +2 -1
  77. package/src/services/firewall-reach.ts +83 -0
  78. package/src/services/health-runner.test.ts +50 -0
  79. package/src/services/health-runner.ts +116 -82
  80. package/src/services/module-deploy.ts +17 -0
  81. package/src/services/system-config-validator.test.ts +31 -1
  82. package/src/services/trusted-sources.test.ts +221 -0
  83. package/src/services/trusted-sources.ts +159 -0
  84. package/src/services/update/orchestrator.test.ts +1 -0
  85. package/src/templates/generator.ts +6 -29
@@ -26,10 +26,12 @@ Each entry: `module id` — what it is — **provides** / **requires** capabilit
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`.
29
+ - **wireguard** — owns the admin WireGuard tunnel on the firewall host: interface, listen port, peers (as records), and client subnet are module config rather than hand-maintained state. Exposes the listen port and **registers the client subnet as a trusted source**, so VPN reach into the managed zones is in the firewall registry and every converge re-emits it. Records `network.vpn.subnet`, which the internal resolver's split-horizon view also consumes. Adopts a running tunnel in place (existing key and peers retained; `wg syncconf`, never `wg-quick down`) because that tunnel is the operator's recovery path. **requires:** `firewall` (and the provider must support trusted-source registration — `iptables` does, `greenwave` does not).
29
30
 
30
31
  ## Public edge (ingress / identity)
31
32
 
32
33
  - **caddy** — reverse proxy with automatic HTTPS (HTTP-01 ACME); the standard HTTPS ingress. **provides:** `public_web`. **requires:** `dns_registrar`, `firewall`.
34
+ - **generic-cpanel-hosting-provider** — publishes static sites into a subfolder of a domain on a cPanel/SSH web host celilo does **not** govern (the host owns the domain, DNS, TLS and web server). Systemless like **namecheap** — no `requires.system`, no zone, no IPAM; it holds credentials for an external party. One provider serves many accounts, resolved by hostname; onboarding takes the account password once, `ssh-copy-id`s celilo's key, then discards it. **provides:** `external_web`.
33
35
  - **authentik** — Authentik identity provider with OIDC (Docker Compose: server, worker, Postgres, Redis). **provides:** `idp`. **requires:** `public_web`, `dns_registrar`, `firewall`.
34
36
 
35
37
  ## Celilo's own infrastructure (self-hosted)
@@ -37,6 +39,7 @@ Each entry: `module id` — what it is — **provides** / **requires** capabilit
37
39
  - **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
40
  - **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
41
  - **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`.
42
+ - **signal** — bidirectional Signal transport for alerts and deploy-interview questions; runs signal-cli in daemon mode with its JSON-RPC socket bound to the host's own address (never public). Enrolled as a SECONDARY DEVICE of an existing Signal account rather than registering its own number — Signal blocks most VOIP ranges and bans bot-ish registrations. Recipient addresses live on celilo routes, not in module config, so adding a person never requires a redeploy. Runs on x86_64 and aarch64. `libsignal-client` ships no linux-aarch64 native, so celilo builds one (`modules/signal/build/`) and installs it as a `libsignal-jni` .deb on ARM hosts; x86_64 uses the JAR's bundled native. **provides:** `notification` (`send`, `receive`). **requires:** nothing — a transport that depended on the proxy, registrar or firewall could not tell you those were broken. See `openspec/changes/add-alerting/`.
40
43
  - **celilo-website** — public docs site (static Astro) served via Caddy on celilo.computer. **requires:** `public_web`, `dns_registrar`.
41
44
 
42
45
  ## Git forge & CI pipeline
@@ -45,13 +45,39 @@ see `openspec/specs/`. Companion doc: [CELILO_CORE_MODULES.md](./CELILO_CORE_MOD
45
45
  | `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 |
46
46
  | `idp` | `packages/capabilities/src/idp.ts` | authentik |
47
47
  | `dns_registrar` | `packages/capabilities/src/dns-registrar.ts` (`registerHost`) | namecheap |
48
+ | `external_web` | `packages/capabilities/src/external-web.ts` (`publishStaticSite`). **OFF-FLEET static publishing** — the counterpart to `public_web`. See the in-fleet vs off-fleet note below. | generic-cpanel-hosting-provider |
48
49
  | `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 |
49
50
  | `dns_internal` | `packages/capabilities/src/dns-internal.ts` | knot-unbound-internal, technitium |
50
51
  | `dhcp_server` | `packages/capabilities/src/dhcp-server.ts` | greenwave |
51
52
  | `source_forge` | `packages/capabilities/src/source-forge.ts` | forgejo |
52
53
  | `registry_publish` | `packages/capabilities/src/registry-publish.ts` | celilo-registry |
54
+ | `notification` | `packages/capabilities/src/notification.ts` (`send`, optional `receive`) | signal (planned) — transports are ordinary modules, both self-hosted and credential-only |
53
55
  | `cross_module_read` | `packages/capabilities/src/cross-module-read.ts` | framework (read other modules' capability data) |
54
56
 
57
+ ### In-fleet (`public_web`) vs off-fleet (`external_web`)
58
+
59
+ Both serve a static site; the difference is **control, not file serving**. A
60
+ system is *in-fleet* when celilo governs it — it can run a managed daemon, bind
61
+ a port, drive the firewall, put the box on the VPN. It is **off-fleet** when
62
+ celilo can only place files there: an unprivileged, jailed cPanel account is the
63
+ canonical case. Off-fleet is a governance boundary, not a deploy boundary.
64
+
65
+ | | `public_web` (caddy) | `external_web` |
66
+ |---|---|---|
67
+ | TLS certificate | celilo obtains it (ACME) | already installed by the host |
68
+ | Public + split-horizon DNS | celilo registers | the host's |
69
+ | Firewall ingress | celilo opens | not celilo's to open |
70
+ | `idp` / reverse-proxy composition | yes | no |
71
+ | Route | any path, incl. `/` | a subfolder only; `/` is REJECTED (the docroot holds the host's own site) |
72
+
73
+ `external_web` is module-owned (loaded through `CAPABILITY_MODULE_MAP` like
74
+ `dns_registrar`), not framework-owned like `createPublicWeb` — there is no
75
+ celilo-side machinery to own, only a provider holding credentials. Its
76
+ `publishStaticSite` signature is a subset of `public_web`'s on purpose, so a
77
+ content module targets either with
78
+ `capabilities.external_web ?? capabilities.public_web`. Both are
79
+ **authoritative**: a publish that doesn't reach clients throws (#328).
80
+
55
81
  ## Remote-ops primitives (the SSH seam — modules never hand-build SSH)
56
82
 
57
83
  Module hooks reach a remote box ONLY through these typed primitives
@@ -59,16 +85,17 @@ Module hooks reach a remote box ONLY through these typed primitives
59
85
  `modules/**/scripts/` is a defect. Impl: `packages/capabilities/src/remote.ts`
60
86
  (exported from `packages/capabilities/src/index.ts`).
61
87
 
62
- - **`remoteExec`** — the ONE ssh seam (`ssh root@<target> <cmd>`); everything else builds on it.
88
+ - **`remoteExec`** — the ONE ssh seam (`ssh <user>@<target> <cmd>`); everything else builds on it. `user`/`port`/`identityFile` on the target default to `root`/22/the agent key, so a fleet call is unchanged; an OFF-FLEET account (`external_web`) sets them.
63
89
  - **`probe`** — read-only health checks: `systemd` / `http` / `command`.
64
90
  - **`serviceCtl`** — systemctl start/stop/restart/reload/enable/disable.
65
91
  - **`runAppCommand` / `runAppCommandWithSecret`** — escape-hatch on-box command; the secret variant feeds the secret on **stdin** (`$SECRET`), never argv.
66
92
  - **`streamBackup` / `streamRestore` / `fetchFile` / `pushFile`** — binary-safe streaming via local shell redirect/pipe.
67
93
  - **`waitFor`** — predicate-poll combinator.
68
94
  - **`applyRenderedConfig`** — converge: write rendered config → validate → apply → rollback (one round-trip). Used by caddy (Caddyfile), knot (views), iptables (ruleset).
95
+ - **`installAuthorizedKey`** — ONE-TIME credential bootstrap for an off-fleet account: `ssh-copy-id` under a password taken from the child ENV (`SSH_ASKPASS_REQUIRE=force`, so no `sshpass` dependency and the password never lands in a command string). Idempotent; used by `external_web` onboarding so every later publish is key-based.
69
96
  - **`tailLog` / `grepLog`** — journald reads (regex / ignoreCase).
70
97
 
71
- Target = `RemoteTarget` (`{ ipv4_address }`; `DeployedSystem` satisfies it). The
98
+ Target = `RemoteTarget` (`{ ipv4_address }` plus optional `user` / `port` / `identityFile`; `DeployedSystem` satisfies it). The
72
99
  runner seam (`execRunner` real / `createMockRunner` for tests) lives in
73
100
  `packages/capabilities/src/testing.ts`. Authoring guide: `MODULE_PRIMITIVES.md`
74
101
  (ships in `@celilo/cli`).
@@ -88,6 +115,27 @@ runner seam (`execRunner` real / `createMockRunner` for tests) lives in
88
115
  `exposeService`/`unexposeService` register into the store, then render + apply
89
116
  atomically via `applyRenderedConfig` (`iptables-restore`). Replaces the old
90
117
  per-rule `iptables -A`; the registry (not `iptables -L`) is the source of truth.
118
+ - **Trusted-source registry (desired state)** — `trusted_sources` DB table
119
+ (migration `0016`) + `apps/celilo/src/services/trusted-sources.ts`
120
+ (`buildTrustedSourceStore`, `composeTrustedSubnets`). The sibling primitive to
121
+ port forwards, for the case a port forward cannot express: an ORIGIN subnet
122
+ permitted to initiate into every managed zone. Registered through the OPTIONAL
123
+ `registerTrustedSource` / `withdrawTrustedSource` / `listTrustedSources` trio on
124
+ `packages/capabilities/src/firewall.ts`; `iptables` implements it, `greenwave`
125
+ deliberately does not. A consumer detects support with `supportsTrustedSources`
126
+ and fails loudly via `requireTrustedSources` — a silent no-op is prohibited by
127
+ the contract, because a discarded registration yields a network that believes
128
+ it has reach it does not have.
129
+ - **Composed trusted subnets** — `apps/celilo/src/hooks/capability-loader.ts`
130
+ (`loadTrustedSubnets`): derived control plane + module registrations + the
131
+ `firewall.trusted_subnets` operator override, deduped and origin-tagged. With
132
+ no contributors it equals the derived subnet alone, so the rendered ruleset is
133
+ byte-identical to the pre-composition output.
134
+ - **Unowned-trusted-network audit** — `apps/celilo/src/services/audit/trusted-sources.ts`
135
+ + `apps/celilo/src/services/firewall-reach.ts`: reads each firewall's LIVE table
136
+ (`iptables-save`) and reports reach granted to a network celilo does not
137
+ recognise, instead of removing it silently at the next converge. Surfaces under
138
+ `celilo system audit` as the `trusted_sources` category.
91
139
 
92
140
  ## Hooks & deploy
93
141
 
@@ -114,6 +162,26 @@ runner seam (`execRunner` real / `createMockRunner` for tests) lives in
114
162
  - **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.
115
163
  - **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.
116
164
 
165
+ ## Alerting & notifications
166
+
167
+ Observation and delivery: monitors run checks on a schedule, alerts hold what
168
+ is currently wrong, and routes carry the message to a person's phone. Design:
169
+ `openspec/changes/add-alerting/design.md`.
170
+
171
+ - **Alert identity** — `apps/celilo/src/services/alerting/keys.ts` — the key grammar (`module:<id>[/check:<name>]`, `builtin:<check>[/<kind>:<target>]`) that makes "the same problem" the same alert across runs. `moduleAlertKey`, `moduleCheckAlertKey`, `builtinAlertKey`, `parseAlertKey`.
172
+ - **Reconciliation** — `apps/celilo/src/services/alerting/reconcile.ts` (`reconcile`) — a successful run's failing-key set is authoritative and resolution is by SET DIFFERENCE (absent ⇒ resolved). A run whose outcome is `error` resolves NOTHING and fires a module-level alert instead: the false-all-clear guard.
173
+ - **Monitor execution** — `apps/celilo/src/services/alerting/run-monitor.ts` (`runOneMonitor`) + `sweep.ts` (`selectDueMonitors`) + `builtin-monitors.ts` / `health-coverage.ts` (the built-in checks and the "module with no health check" coverage check).
174
+ - **The sweep** — `apps/celilo/src/services/alerting/sweep-runner.ts` (`runSweep`) — the ordered pass that makes alerting run by itself: run due monitors → promote past-grace alerts → re-evaluate suppression → flush quiet-hours deferrals → notify. Driven by `celilo alerts sweep` on `timer.tick.5m`. Never throws for one bad monitor.
175
+ - **Suppression (topology-derived, never configured)** — `apps/celilo/src/services/alerting/suppression.ts` — `ancestorKeysFor`/`findSuppressor`/`machineAlertKey`. A firing machine explains its modules' failures; a firing capability provider explains its zone's consumers. Derived from `module_systems`, so it cannot drift from reality. Deploy windows: `deploy-hooks.ts` (`openDeployWindow`/`closeDeployWindows` — closed by module, so a crashed deploy self-heals).
176
+ - **Escalation & quiet hours** — `escalation.ts` (`decideEscalation`, every reason to stay silent enumerated) + `quiet-hours.ts` (`isWithinQuietHours`, Intl-based and DST-safe). Quiet hours defer the MESSAGE while the escalation clock keeps running.
177
+ - **Delivery** — `notifier.ts` (`notifyAlert`, `deliverDeferred`, `notifyResolved`, body composition) + `transport-loader.ts` (`loadNotificationTransport` — resolves a route's transport module's `notification` capability).
178
+ - **Inbound replies** — `tokens.ts` (per-DELIVERY reply tokens, so a reply identifies WHO) + `inbound.ts` (`interpretInbound` — two-factor: valid unexpired token AND sender matching the issuing route) + `inbound-poller.ts` (`pollInbound`, `makeReceiver`). celilo-mgr polls the transport; nothing calls in, so the ack path never depends on the public HTTPS it may be paging about.
179
+ - **Ack / silence / resolve** — `ack.ts` — three deliberately distinct operations. An ack is broadcast to every other paged route.
180
+ - **Interviews over a transport** — `interview-responder.ts` (delivery policy; `secret.*` families are REFUSED) + `notification-responder.ts` (queries the bus's events table for unanswered questions: a watch only sees events emitted after it registers, and `alerts poll` is one-shot. Leaves a question alone for 90s so an attached responder — terminal, or the `api-serve` wire bridge — answers first). Alerts and interviews share the send path, the token table, and the inbound path; only `targetId` differs.
181
+ - **People, routes, policies** — `people.ts` — `createPerson`/`createRoute`/`createPolicy`/`addPolicyStep`.
182
+ - **Persistence** — `apps/celilo/src/services/alerting/store.ts`; tables `people`, `routes`, `escalation_policies`, `escalation_steps`, `monitors`, `monitor_runs`, `suppression_windows`, `alerts`, `notification_deliveries` (`apps/celilo/src/db/schema.ts`, migration `drizzle/0017_alerting.sql`).
183
+ - **CLI** — `apps/celilo/src/cli/commands/` — `alerts-list.ts`, `alerts-act.ts` (ack/silence/resolve), `alerts-sweep.ts`, `alerts-poll.ts`, `monitor.ts`, `notify-config.ts` (`person`/`route`/`escalation-policy`).
184
+
117
185
  ## Persistence
118
186
 
119
187
  - **DB schema** — `apps/celilo/src/db/schema.ts`. Client: `apps/celilo/src/db/client.ts`. Migration runner: `apps/celilo/src/db/migrate.ts`. Migrations: `apps/celilo/drizzle/`.
@@ -141,4 +209,5 @@ Run any celilo command on celilo-mgr over SSH instead of screen-scraping `ssh <h
141
209
 
142
210
  - **cele2e harness** — `packages/e2e/src/` — `runner.ts`, `container-manager.ts` (`startNetwork`, `reconnectNetwork`), `network-builder.ts` (`NetworkBuilder`).
143
211
  - **Run wrapper** — `infra/scripts/cele2e-run.sh` lives in the separate `infra/` clone, **not** in this repo. See the cele2e section of the repo-root `CLAUDE.md` for the operator workflow.
212
+ - **Signal simulators** — three containers, three jobs. `docker/Dockerfile.signal-cli` runs the REAL unlinked daemon (`network().withSignalCli()`, reachable at `signal-cli.lab`) so `e2e/tests/signal-contract.test.ts` can re-check celilo's understanding of the JSON-RPC surface against the actual binary — the pebble pattern. `docker/Dockerfile.signal-sim` runs `simulators/signal-cli/server.ts` (`withSignalSim()`, `signal-sim.lab`), the drivable stand-in with a control surface (`/_control/inbound`, `/_control/sent`, `/_control/unlink`). `docker/Dockerfile.signal-release` (`withSignalRelease()`, `signal-release.lab`) serves the signal-cli release tarball so the module's deploy-time download resolves inside the sealed network — the download is served, never skipped. The libsignal aarch64 native is compiled at `build-infra` time from the module's own recipe (`packages/e2e/scripts/stage-libsignal.ts` → `modules/signal/build/`) and staged into the apt-repo pool, so the recipe is exercised for real on every rebuild while the deploy stays fast and the network stays sealed.
144
213
  - **MCP server (agent-driven e2e)** — `packages/mcp-server/src/index.ts` — stdio MCP server exposing `start_run`/`run_status`/`run_result`/`stop_run` (detached cele2e runs read off the event bus, no ANSI scraping) + `env_check` (docker VM, run-lock, shared-infra, mgmt image CLI version, netapps). Dev/ops tool, `private`, not shipped to consumers.
@@ -0,0 +1,298 @@
1
+ # Alerting — operator guide
2
+
3
+ Getting celilo to tell you when something is wrong, on a channel you actually
4
+ read, without teaching you to ignore it.
5
+
6
+ This guide is task-ordered: set up who gets told, how they get told, and in
7
+ what order; then watch a module; then handle an alert when it arrives.
8
+
9
+ ---
10
+
11
+ ## The shape of it
12
+
13
+ ```
14
+ monitor ──runs a check every N minutes──▶ alert ──escalation policy──▶ route ──▶ your phone
15
+ │ │ │
16
+ └─ a module's health_check hook, └─ one per PROBLEM, not per run, └─ you reply
17
+ or a built-in (machines_reachable) identified by a stable key `ack`
18
+ ```
19
+
20
+ Four nouns, and they are worth keeping straight:
21
+
22
+ | noun | what it is |
23
+ |------|-----------|
24
+ | **person** | a human, with a timezone and optional quiet hours |
25
+ | **route** | one way to reach a person (a Signal number). A person may have several |
26
+ | **escalation policy** | an ordered list of *steps*: which route, how many minutes in |
27
+ | **monitor** | a check that runs on a schedule and produces alerts |
28
+
29
+ An alert's identity is its **key** — `module:caddy/check:cert-validity`. The
30
+ same problem across a hundred runs is one alert, so a slow-burning failure
31
+ does not become a hundred messages.
32
+
33
+ ---
34
+
35
+ ## 1. Add yourself
36
+
37
+ ```bash
38
+ celilo person add peba --timezone America/Los_Angeles
39
+ ```
40
+
41
+ Quiet hours are optional and per-person. They defer the *message*, never the
42
+ escalation clock — steps keep advancing while you sleep, so if nobody
43
+ acknowledges by 03:00 the chain has already reached whoever is awake:
44
+
45
+ ```bash
46
+ celilo person add peba --timezone America/Los_Angeles \
47
+ --quiet-hours 22:00-07:00
48
+ ```
49
+
50
+ A policy may be marked to page through quiet hours anyway
51
+ (`escalation_policies.bypass_quiet_hours`); `celilo escalation-policy list`
52
+ shows which do. Use it sparingly — a channel that wakes you for a warning is a
53
+ channel you will mute.
54
+
55
+ ## 2. Add a route
56
+
57
+ A route needs a transport module. Deploy `signal` first (see
58
+ [the signal module's README](../../../modules/signal/README.md)); then:
59
+
60
+ ```bash
61
+ celilo route add peba signal --address +15551234567 --can-ack
62
+ ```
63
+
64
+ `--can-ack` says replies from this address are trusted to acknowledge alerts.
65
+ A route without it is send-only — it still gets paged, it just gets no reply
66
+ token, because a reply instruction nobody can follow is worse than none.
67
+
68
+ `--severity-floor critical` keeps a route out of everything below critical.
69
+ That is how "hazel's phone is for real emergencies only" is expressed.
70
+
71
+ ```bash
72
+ celilo route list
73
+ ```
74
+
75
+ ## 3. Build an escalation policy
76
+
77
+ Steps are `--after` minutes from when escalation *begins*, not from the
78
+ previous step — so the numbers read as a timeline rather than an accumulation.
79
+ A step names a person and a transport, which together identify the route.
80
+
81
+ ```bash
82
+ celilo escalation-policy add oncall
83
+ celilo escalation-policy step oncall peba signal --after 0
84
+ celilo escalation-policy step oncall hazel signal --after 15
85
+ ```
86
+
87
+ That is: page me immediately; if nobody has acknowledged 15 minutes later,
88
+ page hazel. A step whose route's severity floor excludes the alert is
89
+ skipped *immediately* rather than costing its delay in silence.
90
+
91
+ ## 4. Watch something
92
+
93
+ A module that declares `hooks.health_check` gets a monitor automatically on
94
+ its first deploy. To see what is watched:
95
+
96
+ ```bash
97
+ celilo monitor list
98
+ ```
99
+
100
+ To add one by hand, and to point it at a policy:
101
+
102
+ ```bash
103
+ celilo monitor add caddy --interval 5m
104
+ celilo monitor add machines_reachable --interval 5m
105
+ celilo escalation-policy assign oncall caddy
106
+ ```
107
+
108
+ The target says which kind it is: an audit category (`machines_reachable`,
109
+ `health_coverage`) is a built-in check, anything else is a module's
110
+ `health_check` hook. `celilo monitor run <target>` runs one now.
111
+
112
+ `machines_reachable` is deliberately **unsuppressible**: it is how you find
113
+ out the fleet is unreachable, so nothing may explain it away.
114
+
115
+ Modules with no health check at all show up under the built-in
116
+ `health_coverage` check — "not observed" is a state worth being able to see.
117
+
118
+ ---
119
+
120
+ ## Living with it
121
+
122
+ ### What you will see
123
+
124
+ ```bash
125
+ celilo alerts list
126
+ celilo alerts list --json # for scripts
127
+ celilo module list # health column, per module
128
+ ```
129
+
130
+ The health column reads `healthy` / `failing(n)` / `suppressed` / `not observed`.
131
+ "not observed" is not "healthy" and is not printed as if it were.
132
+
133
+ ### When one arrives
134
+
135
+ The message names the key first — on a phone that is what shows in the
136
+ notification, and half-awake you need to know *what* before *why*.
137
+
138
+ ```
139
+ 🔴 module:caddy/check:cert-validity
140
+ certificate for www.example.com expires in 2 days
141
+
142
+ reply K3M9QX
143
+ ```
144
+
145
+ Reply with the token, or just `ack` if only one alert is outstanding for you.
146
+ Everyone else who was paged is told **who** took it, so the secondary can go
147
+ back to bed.
148
+
149
+ From a terminal:
150
+
151
+ ```bash
152
+ celilo alerts ack module:caddy/check:cert-validity --as peba
153
+ ```
154
+
155
+ ### ack vs silence vs resolve
156
+
157
+ These look similar and mean entirely different things. Collapsing any two is
158
+ how an alerting system stops being trusted.
159
+
160
+ | | means | escalation | alert state |
161
+ |---|---|---|---|
162
+ | **ack** | "I have this" | stops | still **firing** — the problem is still happening |
163
+ | **silence** | "I know, stop telling me until X" | stops until expiry | firing, silenced |
164
+ | **resolve** | "the condition is gone" | stops | resolved |
165
+
166
+ ```bash
167
+ celilo alerts silence module:caddy/check:cert-validity --for 2h
168
+ celilo alerts resolve module:caddy/check:cert-validity
169
+ ```
170
+
171
+ `celilo monitor disable <target>` is the bigger hammer: it stops the check
172
+ running at all, rather than silencing what it finds.
173
+
174
+ Silence always expires — that is what makes it safe to offer.
175
+
176
+ ### What `resolve` is actually for
177
+
178
+ Reasonable question: if the check is still failing, what does resolving it
179
+ achieve? Nothing — and that is the point. **Resolve does not override the
180
+ monitor.** The next successful run is authoritative, so if the condition is
181
+ still there the alert comes straight back on the next sweep.
182
+
183
+ Its use is narrower than it looks: **you have just fixed something, and the
184
+ monitor's interval is fifteen minutes.** Without resolve you stare at a stale
185
+ `firing` row — and keep escalating against it — until the next run notices.
186
+ Resolve says "I know this one is done, stop counting" and costs nothing if you
187
+ are wrong, because the monitor corrects you.
188
+
189
+ If you find yourself resolving the same key repeatedly, the check interval is
190
+ too long or the check is wrong. That is the signal, not the resolve.
191
+
192
+ ### Replies can only acknowledge
193
+
194
+ The reply grammar is deliberately one verb. A bare token acknowledges; so does
195
+ `ack`, `ok`, `k`, or 👍:
196
+
197
+ ```
198
+ K3M9QX → acknowledged
199
+ K3M9QX ack → acknowledged
200
+ K3M9QX resolve → REJECTED, and nothing happens
201
+ ```
202
+
203
+ `resolve` and `silence` are terminal operations, not reply verbs — every extra
204
+ verb is another thing to mistype at 3am, and a reply that silently did the wrong
205
+ thing would be worse than one that did nothing. An unrecognised verb is
206
+ rejected and counted, so `celilo alerts poll` reports it rather than swallowing
207
+ it.
208
+
209
+ ### Why didn't I get paged?
210
+
211
+ Every reason to stay silent is recorded rather than being an early return.
212
+
213
+ ```bash
214
+ celilo alerts list --json | jq '.[] | {key, state, suppressedBy: .suppressedByAlertId}'
215
+ celilo monitor run caddy # run one check now, see what it reports
216
+ ```
217
+
218
+ The usual answers:
219
+
220
+ - **within grace** — a new failure waits out a grace window before it may page,
221
+ so a check that flaps for thirty seconds never reaches you.
222
+ - **suppressed** — something upstream explains it. A firing machine explains
223
+ every module on it; a firing capability provider explains its zone's
224
+ consumers. This is derived from the deployment topology, never configured,
225
+ so it cannot drift from reality.
226
+ - **deploy window** — a deploy suppresses its own module. Restarting a service
227
+ fails its own health check, and paging you about the deploy you are
228
+ personally running is how a channel gets muted.
229
+ - **awaiting confirmation** — suppression just lifted. The alert waits for one
230
+ more successful run before paging: if it recovered along with its cause, it
231
+ resolves quietly.
232
+ - **quiet hours** — deferred, not dropped. It arrives when the window ends,
233
+ *if* it is still a problem then.
234
+
235
+ ### What happens when the check itself breaks
236
+
237
+ A check that could not **run** tells you nothing about what it was watching.
238
+ So an errored run resolves nothing — every alert it owns is frozen exactly as
239
+ it was — and raises one module-level alert saying the check is broken.
240
+
241
+ The alternative is the single most dangerous failure an alerting system can
242
+ have: reading "could not run" as "found nothing wrong", clearing every alert,
243
+ and telling you everything is fine at the moment you can see least.
244
+
245
+ ---
246
+
247
+ ## Running it
248
+
249
+ Two commands, both dispatcher-driven, both safe to run by hand:
250
+
251
+ ```bash
252
+ celilo alerts sweep # timer.tick.5m — run due monitors, escalate, notify
253
+ celilo alerts poll # read replies off every bidirectional transport
254
+ ```
255
+
256
+ `poll` runs far more often than the sweep: "I texted ack and nothing happened"
257
+ is the experience the whole design exists to prevent.
258
+
259
+ celilo-mgr polls the transport; nothing calls in. That direction is
260
+ deliberate — an inbound webhook would need public HTTPS, which means caddy,
261
+ DNS and a certificate, so the ack path would depend on the very infrastructure
262
+ being paged about.
263
+
264
+ ---
265
+
266
+ ## Answering deploy questions from your phone
267
+
268
+ Because an alert and an interview question are the same message with different
269
+ reply semantics, the same transport carries both. A headless deploy that needs
270
+ a value can ask you:
271
+
272
+ ```
273
+ ❓ caddy needs: admin_email
274
+
275
+ reply K3M9QX <value>
276
+ ```
277
+
278
+ Two rules, both enforced before anything is sent:
279
+
280
+ 1. **A terminal wins.** If stdin is a TTY, the question is answered there — a
281
+ deploy started from your laptop must not appear to hang while silently
282
+ waiting for a text.
283
+ 2. **Secrets are refused.** `secret.*` questions never travel over a messaging
284
+ transport, on a headless box or anywhere else. Typing a credential into a
285
+ chat puts it in the transport's message store and the device's history
286
+ before the responder ever sees it. Answer those at a terminal.
287
+
288
+ ---
289
+
290
+ ## See also
291
+
292
+ - **Rolling this out to a live fleet**: `openspec/changes/add-alerting/ROLLOUT.md`
293
+ — ordered stages, what to verify at each, and how to back out.
294
+ - **Building the libsignal aarch64 native** (needed on ARM hosts):
295
+ `modules/signal/build/README.md`
296
+ - Design and the decisions behind it: `openspec/changes/add-alerting/design.md`
297
+ - Subsystem map: [CELILO_SUBSYSTEMS.md](../CELILO_SUBSYSTEMS.md) § Alerting & notifications
298
+ - The Signal transport: `modules/signal/README.md`
package/docs/INDEX.md ADDED
@@ -0,0 +1,103 @@
1
+ # Celilo CLI Docs
2
+
3
+ Canonical user-facing docs live at https://celilo.computer/docs. This
4
+ file is the offline-readable summary for engineers working in the repo.
5
+
6
+ ## Alerting
7
+
8
+ See [ALERTING.md](./ALERTING.md) — people, routes, escalation policies,
9
+ watching a module, and what to do when an alert arrives.
10
+
11
+ ## System update runbook
12
+
13
+ Canonical: https://celilo.computer/docs/system-update
14
+
15
+ ### Philosophy
16
+
17
+ `celilo system update` is the single command for bringing a deployment
18
+ back to "READY" — the audit-determined state where every drift category
19
+ is clean. It is built on three guarantees:
20
+
21
+ 1. **Audit first.** `update` runs `system audit` as its first step and
22
+ refuses to proceed if the verdict is `BLOCKED`. The audit is also
23
+ exposed as a standalone read-only command (`system audit`) for use
24
+ in cron, CI, or interactive checks.
25
+ 2. **Backups before mutation.** Every module that runs through the
26
+ update flow gets a fresh backup tagged with the run's `update_id`
27
+ before any change is applied. `--no-backup` skips this and is the
28
+ only way to opt out.
29
+ 3. **Failures isolate to the dependency subtree.** The orchestrator
30
+ walks modules in topological order. A failure on a provider skips
31
+ only its transitive consumers — peers and unrelated modules continue.
32
+ Consumers may also proceed if their declared `requires` version
33
+ still satisfies the running provider (version-aware skip).
34
+
35
+ ### Recommended sequence
36
+
37
+ ```
38
+ celilo system audit # cheap, read-only — run any time
39
+ celilo system update --dry-run # preview the plan
40
+ celilo system update # do it
41
+ ```
42
+
43
+ For a single module:
44
+
45
+ ```
46
+ celilo system update --module <id>
47
+ ```
48
+
49
+ For an audit that returns `BLOCKED` on a destructive Terraform plan:
50
+
51
+ ```
52
+ celilo system update --allow-destructive # only after reviewing the plan
53
+ ```
54
+
55
+ ### Rollback
56
+
57
+ Pre-update backups are tagged with the `update_id` printed in the
58
+ `system update` output. To roll back a single module:
59
+
60
+ ```
61
+ celilo backup list --module <id>
62
+ celilo backup restore <backup-id>
63
+ ```
64
+
65
+ System-state snapshots (the celilo SQLite DB) are taken before the
66
+ first module mutation in a run; restore them via the same flow with
67
+ the system-scoped backup id.
68
+
69
+ ### FAQ
70
+
71
+ **Q. Why did `system update` skip module X?**
72
+ Likely one of:
73
+ - `system audit` reported no drift for X (already at latest).
74
+ - A provider that X depends on failed earlier in the run, AND X's
75
+ `requires` declares a version newer than the still-running provider.
76
+ Output line `skipReason` makes the cause explicit.
77
+ - `--module <id>` was passed and X wasn't the named module.
78
+
79
+ **Q. The audit says `capability_abi: BLOCKED` — what now?**
80
+ A module's manifest declares a capability version that the running
81
+ `@celilo/capabilities` SDK no longer supports (or vice versa). Either
82
+ upgrade the framework (`bun update -g @celilo/cli` then re-run `system
83
+ update`) or rebuild the module against the new SDK and republish.
84
+ `CAPABILITY_CONTRACT_VERSIONS` in `packages/capabilities/src/capability-contract.ts`
85
+ is the source of truth.
86
+
87
+ ### Troubleshooting
88
+
89
+ **Schema migrations fail on update.** `system audit` checks pending
90
+ migrations as part of `schema` drift. If a migration is failing on
91
+ your DB, `bun run drizzle-kit migrate` from `apps/celilo/` will print
92
+ the error directly.
93
+
94
+ **Backups storage is full.** Pre-update backups live in their own
95
+ retention pool tagged with the `update_id`. They are pruned by
96
+ `backup retention` policy independently from regular backups. Inspect
97
+ with `celilo backup list --type pre-update`.
98
+
99
+ ## Related
100
+
101
+ - Design doc: `apps/celilo/designs/CELILO_UPDATE.md`
102
+ - Audit categories: `apps/celilo/src/services/audit/`
103
+ - Orchestrator: `apps/celilo/src/services/update/`
@@ -0,0 +1,10 @@
1
+ CREATE TABLE `trusted_sources` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `firewall_ip` text NOT NULL,
4
+ `subnet` text NOT NULL,
5
+ `description` text DEFAULT '' NOT NULL,
6
+ `registered_by` text NOT NULL,
7
+ `created_at` integer DEFAULT (unixepoch()) NOT NULL
8
+ );
9
+ --> statement-breakpoint
10
+ CREATE UNIQUE INDEX `trusted_sources_unique_idx` ON `trusted_sources` (`firewall_ip`,`subnet`);