@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
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Trusted-source registry — the DB-backed `TrustedSourceStore` for the
3
+ * `firewall` capability, and the composition of the trusted-subnet set the
4
+ * renderer consumes.
5
+ *
6
+ * A trusted source is an ORIGIN subnet permitted to initiate into every managed
7
+ * zone (the admin VPN's client range being the motivating case). It is not a
8
+ * port forward, so `exposeService` cannot express it — which is why the VPN's
9
+ * rules belonged to no module and every converge removed them.
10
+ */
11
+
12
+ import type {
13
+ RegisterTrustedSourceRequest,
14
+ TrustedSource,
15
+ TrustedSourceStore,
16
+ } from '@celilo/capabilities';
17
+ import { and, eq } from 'drizzle-orm';
18
+ import type { DbClient } from '../db/client';
19
+ import { trustedSources } from '../db/schema';
20
+
21
+ /**
22
+ * Build the store bound to the module that will call through the capability.
23
+ * `registeredBy` is stamped here, not supplied by the provider, so a
24
+ * registration can never be attributed to the wrong module.
25
+ */
26
+ export function buildTrustedSourceStore(db: DbClient, registeredBy: string): TrustedSourceStore {
27
+ return {
28
+ list(firewallIp: string): TrustedSource[] {
29
+ return db
30
+ .select()
31
+ .from(trustedSources)
32
+ .where(eq(trustedSources.firewallIp, firewallIp))
33
+ .all()
34
+ .map((r) => ({
35
+ subnet: r.subnet,
36
+ description: r.description,
37
+ registeredBy: r.registeredBy,
38
+ }));
39
+ },
40
+
41
+ add(firewallIp: string, source: RegisterTrustedSourceRequest): void {
42
+ // Idempotent upsert on (firewall, subnet) — matches buildPortForwardStore.
43
+ db.delete(trustedSources)
44
+ .where(
45
+ and(eq(trustedSources.firewallIp, firewallIp), eq(trustedSources.subnet, source.subnet)),
46
+ )
47
+ .run();
48
+ db.insert(trustedSources)
49
+ .values({
50
+ firewallIp,
51
+ subnet: source.subnet,
52
+ description: source.description,
53
+ registeredBy,
54
+ })
55
+ .run();
56
+ },
57
+
58
+ remove(firewallIp: string, subnet: string): void {
59
+ db.delete(trustedSources)
60
+ .where(and(eq(trustedSources.firewallIp, firewallIp), eq(trustedSources.subnet, subnet)))
61
+ .run();
62
+ },
63
+ };
64
+ }
65
+
66
+ /** Where a trusted subnet came from — reach into every tier must be attributable. */
67
+ export type TrustedSubnetOrigin = 'derived-control-plane' | 'registered' | 'operator-override';
68
+
69
+ export interface TrustedSubnetEntry {
70
+ subnet: string;
71
+ origin: TrustedSubnetOrigin;
72
+ /** Module id for `registered`; undefined for the derived and override origins. */
73
+ registeredBy?: string;
74
+ description?: string;
75
+ }
76
+
77
+ /**
78
+ * The operator's explicit trusted sources, as a comma-separated CIDR list in
79
+ * system config. ADDITIVE rather than replacing the derived control-plane
80
+ * subnet: dropping control-plane trust would block the SSH celilo uses to run
81
+ * every hook and converge, so an operator cannot strand celilo's own management
82
+ * by naming a subnet here.
83
+ */
84
+ export const TRUSTED_SUBNETS_CONFIG_KEY = 'firewall.trusted_subnets';
85
+
86
+ export function parseOperatorTrustedSubnets(raw: string | undefined): string[] {
87
+ if (!raw) return [];
88
+ return raw
89
+ .split(',')
90
+ .map((s) => s.trim())
91
+ .filter((s) => s.length > 0);
92
+ }
93
+
94
+ /**
95
+ * Compose the trusted-subnet set from its three origins, deduped by subnet with
96
+ * the FIRST origin winning (derived beats registered beats override), so the
97
+ * report names the strongest claim on a subnet rather than an arbitrary one.
98
+ *
99
+ * With nothing registered and no override this returns exactly the derived
100
+ * control-plane subnet — the property that makes composing safe to ship ahead of
101
+ * any module that registers.
102
+ */
103
+ export function composeTrustedSubnets(inputs: {
104
+ controlPlaneSubnet?: string;
105
+ registered: TrustedSource[];
106
+ operatorOverride: string[];
107
+ }): TrustedSubnetEntry[] {
108
+ const entries: TrustedSubnetEntry[] = [];
109
+ const seen = new Set<string>();
110
+ const push = (entry: TrustedSubnetEntry): void => {
111
+ if (seen.has(entry.subnet)) return;
112
+ seen.add(entry.subnet);
113
+ entries.push(entry);
114
+ };
115
+
116
+ if (inputs.controlPlaneSubnet) {
117
+ push({ subnet: inputs.controlPlaneSubnet, origin: 'derived-control-plane' });
118
+ }
119
+ for (const source of inputs.registered) {
120
+ push({
121
+ subnet: source.subnet,
122
+ origin: 'registered',
123
+ registeredBy: source.registeredBy,
124
+ description: source.description,
125
+ });
126
+ }
127
+ for (const subnet of inputs.operatorOverride) {
128
+ push({ subnet, origin: 'operator-override' });
129
+ }
130
+ return entries;
131
+ }
132
+
133
+ /** The registered trusted sources for one firewall. Read-only; no binding needed. */
134
+ export function listTrustedSourcesFor(db: DbClient, firewallIp: string): TrustedSource[] {
135
+ return db
136
+ .select()
137
+ .from(trustedSources)
138
+ .where(eq(trustedSources.firewallIp, firewallIp))
139
+ .all()
140
+ .map((r) => ({
141
+ subnet: r.subnet,
142
+ description: r.description,
143
+ registeredBy: r.registeredBy,
144
+ }));
145
+ }
146
+
147
+ /** All registered trusted sources, across every firewall — for reporting. */
148
+ export function listAllTrustedSources(db: DbClient): Array<TrustedSource & { firewallIp: string }> {
149
+ return db
150
+ .select()
151
+ .from(trustedSources)
152
+ .all()
153
+ .map((r) => ({
154
+ firewallIp: r.firewallIp,
155
+ subnet: r.subnet,
156
+ description: r.description,
157
+ registeredBy: r.registeredBy,
158
+ }));
159
+ }
@@ -82,6 +82,7 @@ const cleanAudit: AuditDeps = {
82
82
  secretsDecryptable: { results: [] },
83
83
  servicesReachable: { results: [] },
84
84
  machinesReachable: { results: [] },
85
+ trustedSources: { firewalls: [] },
85
86
  };
86
87
 
87
88
  describe('consumerSkipReason', () => {
@@ -916,35 +916,12 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
916
916
  };
917
917
  }
918
918
 
919
- // Execution: Store derived variables in module_configs
920
- // Variables with derive_from are resolved in the context but not stored in module_configs.
921
- // Store them so hooks and host_vars can access them.
922
- if (manifest.variables?.owns) {
923
- for (const variable of manifest.variables.owns) {
924
- if (!variable.derive_from) continue;
925
- const derivedValue = context.selfConfig[variable.name];
926
- if (derivedValue === undefined) continue;
927
-
928
- const existing = db
929
- .select()
930
- .from(moduleConfigs)
931
- .where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, variable.name)))
932
- .get();
933
-
934
- if (!existing) {
935
- // Insert-only (not upsert) — derived values only seed on first
936
- // generate; subsequent runs preserve whatever the user has set.
937
- // Routes through upsertModuleConfig anyway because the helper
938
- // is the only path that maintains the valueJson invariant.
939
- upsertModuleConfig(
940
- db,
941
- moduleId,
942
- variable.name,
943
- derivedValue as string | number | boolean | unknown[] | Record<string, unknown>,
944
- );
945
- }
946
- }
947
- }
919
+ // NOTE: derived variables are NOT persisted here. `buildResolutionContext`
920
+ // (called above) already wrote them at variables/context.ts, so the
921
+ // insert-only block that used to live here could never fire — it read as a
922
+ // second, competing persistence policy while being unreachable. The single
923
+ // write lives in context.ts; see openspec/changes/derived-value-recomputation
924
+ // for the plan to remove it there too.
948
925
 
949
926
  // Execution: Resolve capability 'imports' variables into module_configs
950
927
  // This makes capability-derived values available in host_vars for Ansible