@celilo/cli 0.3.30 → 0.4.0-alpha.1
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.
- package/drizzle/0005_module_operations.sql +12 -0
- package/drizzle/0006_base_module_aspects.sql +15 -0
- package/drizzle/0007_module_systems.sql +17 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +6 -5
- package/schemas/system_config.json +14 -28
- package/src/ansible/inventory.test.ts +46 -62
- package/src/ansible/inventory.ts +48 -25
- package/src/capabilities/registration.ts +25 -7
- package/src/capabilities/validation.test.ts +30 -0
- package/src/capabilities/validation.ts +8 -0
- package/src/cli/backup-rename.test.ts +95 -0
- package/src/cli/cli.test.ts +17 -23
- package/src/cli/command-registry.ts +199 -0
- package/src/cli/commands/backup-list.ts +1 -1
- package/src/cli/commands/events.ts +96 -0
- package/src/cli/commands/machine-add.ts +103 -59
- package/src/cli/commands/module-import.ts +153 -4
- package/src/cli/commands/module-remove.ts +86 -17
- package/src/cli/commands/module-status.ts +6 -2
- package/src/cli/commands/publish/alpha.test.ts +185 -0
- package/src/cli/commands/publish/alpha.ts +226 -0
- package/src/cli/commands/publish/changesets.test.ts +89 -0
- package/src/cli/commands/publish/changesets.ts +144 -0
- package/src/cli/commands/publish/consumer-pins.test.ts +155 -0
- package/src/cli/commands/publish/consumer-pins.ts +149 -0
- package/src/cli/commands/publish/execute.ts +131 -0
- package/src/cli/commands/publish/global-install.test.ts +154 -0
- package/src/cli/commands/publish/global-install.ts +171 -0
- package/src/cli/commands/publish/helpers.ts +227 -0
- package/src/cli/commands/publish/index.ts +365 -0
- package/src/cli/commands/publish/module-registry.test.ts +40 -0
- package/src/cli/commands/publish/module-registry.ts +64 -0
- package/src/cli/commands/publish/plan.ts +107 -0
- package/src/cli/commands/publish/preflight.ts +238 -0
- package/src/cli/commands/publish/types.ts +264 -0
- package/src/cli/commands/publish/workspace.test.ts +323 -0
- package/src/cli/commands/publish/workspace.ts +596 -0
- package/src/cli/commands/restore.ts +126 -0
- package/src/cli/commands/storage-add-local.ts +1 -1
- package/src/cli/commands/storage-add-s3.ts +1 -1
- package/src/cli/commands/subscribers-add.ts +68 -0
- package/src/cli/commands/subscribers-list.ts +48 -0
- package/src/cli/commands/subscribers-remove.ts +38 -0
- package/src/cli/commands/subscribers-serve.ts +77 -0
- package/src/cli/commands/subscribers-status.ts +33 -0
- package/src/cli/commands/subscribers-test.ts +71 -0
- package/src/cli/commands/system-apply-config-equivalence.test.ts +108 -0
- package/src/cli/commands/system-apply-config.test.ts +70 -0
- package/src/cli/commands/system-apply-config.ts +130 -0
- package/src/cli/commands/system-audit.ts +2 -1
- package/src/cli/commands/system-init-deprecation.test.ts +90 -0
- package/src/cli/commands/system-init.ts +36 -70
- package/src/cli/commands/system-update.ts +3 -2
- package/src/cli/completion.ts +22 -1
- package/src/cli/index.ts +214 -6
- package/src/cli/interactive-config.test.ts +19 -0
- package/src/cli/restore-command.test.ts +131 -0
- package/src/db/client.ts +42 -0
- package/src/db/schema.test.ts +13 -16
- package/src/db/schema.ts +161 -9
- package/src/hooks/capability-loader-firewall.test.ts +6 -15
- package/src/hooks/capability-loader.test.ts +2 -3
- package/src/hooks/capability-loader.ts +36 -2
- package/src/hooks/define-hook.test.ts +4 -0
- package/src/hooks/executor.test.ts +18 -0
- package/src/hooks/executor.ts +21 -2
- package/src/hooks/load-hook-config.test.ts +26 -24
- package/src/hooks/load-hook-config.ts +11 -2
- package/src/hooks/run-named-hook.ts +16 -0
- package/src/hooks/types.ts +9 -1
- package/src/manifest/contracts/v1.ts +70 -0
- package/src/manifest/schema.ts +262 -16
- package/src/manifest/validate-privileged.test.ts +84 -0
- package/src/manifest/validate.test.ts +156 -0
- package/src/manifest/validate.ts +69 -0
- package/src/module/import.ts +12 -0
- package/src/services/aspect-approvals.test.ts +231 -0
- package/src/services/aspect-approvals.ts +120 -0
- package/src/services/aspect-runner.test.ts +493 -0
- package/src/services/aspect-runner.ts +438 -0
- package/src/services/aspect-template-resolver.test.ts +101 -0
- package/src/services/aspect-template-resolver.ts +122 -0
- package/src/services/backup-create.ts +104 -25
- package/src/services/backup-envelope-roundtrip.test.ts +199 -0
- package/src/services/backup-in-flight-refusal.test.ts +163 -0
- package/src/services/backup-manifest.test.ts +115 -0
- package/src/services/backup-manifest.ts +163 -0
- package/src/services/backup-restore.ts +154 -19
- package/src/services/build-bus/delivery-events.ts +92 -0
- package/src/services/build-bus/event-factory.ts +54 -0
- package/src/services/build-bus/fan-out.test.ts +279 -0
- package/src/services/build-bus/fan-out.ts +161 -0
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +157 -0
- package/src/services/build-bus/hook-dispatch.test.ts +207 -0
- package/src/services/build-bus/hook-dispatch.ts +198 -0
- package/src/services/build-bus/hook-dispatcher.ts +115 -0
- package/src/services/build-bus/index.ts +41 -0
- package/src/services/build-bus/receiver-server.test.ts +179 -0
- package/src/services/build-bus/receiver-server.ts +159 -0
- package/src/services/build-bus/status.test.ts +212 -0
- package/src/services/build-bus/status.ts +213 -0
- package/src/services/build-bus/subscriber-store.ts +113 -0
- package/src/services/celilo-events.test.ts +70 -0
- package/src/services/celilo-events.ts +92 -0
- package/src/services/celilo-mgmt-hooks.test.ts +296 -0
- package/src/services/config-interview.ts +13 -95
- package/src/services/cross-module-data-manager.ts +2 -31
- package/src/services/cross-module-read.test.ts +250 -0
- package/src/services/cross-module-read.ts +232 -0
- package/src/services/deploy-validation.ts +7 -0
- package/src/services/deployed-systems.test.ts +235 -0
- package/src/services/deployed-systems.ts +308 -0
- package/src/services/dns-provider-backfill.ts +75 -0
- package/src/services/health-runner.ts +19 -3
- package/src/services/infrastructure-variable-resolver.test.ts +6 -32
- package/src/services/infrastructure-variable-resolver.ts +3 -13
- package/src/services/machine-detector.ts +104 -48
- package/src/services/machine-pool.ts +145 -2
- package/src/services/module-config.ts +78 -120
- package/src/services/module-deploy.ts +113 -40
- package/src/services/module-operations.test.ts +154 -0
- package/src/services/module-operations.ts +154 -0
- package/src/services/module-subscriptions.test.ts +58 -0
- package/src/services/module-subscriptions.ts +24 -1
- package/src/services/module-types-generator.test.ts +3 -3
- package/src/services/module-types-generator.ts +7 -2
- package/src/services/proxmox-reconcile.test.ts +333 -0
- package/src/services/proxmox-reconcile.ts +156 -0
- package/src/services/proxmox-state-recovery.ts +3 -24
- package/src/services/restore-from-file.test.ts +177 -0
- package/src/services/restore-from-file.ts +355 -0
- package/src/services/restore-preflight.test.ts +127 -0
- package/src/services/restore-preflight.ts +118 -0
- package/src/services/storage-providers/s3.ts +10 -2
- package/src/services/system-identity.ts +30 -0
- package/src/services/system-init.test.ts +64 -21
- package/src/services/system-init.ts +28 -26
- package/src/templates/generator.test.ts +7 -16
- package/src/templates/generator.ts +28 -115
- package/src/test-utils/integration.ts +5 -2
- package/src/types/infrastructure.ts +8 -0
- package/src/variables/computed/computed-integration.test.ts +191 -0
- package/src/variables/computed/computed.test.ts +177 -0
- package/src/variables/computed/evaluate.ts +271 -0
- package/src/variables/computed/marker.ts +53 -0
- package/src/variables/computed/parse.ts +262 -0
- package/src/variables/computed/provider-lookup.ts +130 -0
- package/src/variables/context.test.ts +89 -28
- package/src/variables/context.ts +196 -191
- package/src/variables/parser.ts +3 -3
- package/src/variables/resolver.test.ts +61 -0
- package/src/variables/resolver.ts +81 -0
- package/src/variables/types.ts +23 -1
- package/src/services/dns-auto-register.ts +0 -211
package/src/db/schema.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { sql } from 'drizzle-orm';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
integer,
|
|
4
|
+
primaryKey,
|
|
5
|
+
sqliteTable,
|
|
6
|
+
text,
|
|
7
|
+
unique,
|
|
8
|
+
uniqueIndex,
|
|
9
|
+
} from 'drizzle-orm/sqlite-core';
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Module lifecycle states
|
|
@@ -33,16 +40,25 @@ export const modules = sqliteTable('modules', {
|
|
|
33
40
|
});
|
|
34
41
|
|
|
35
42
|
/**
|
|
36
|
-
* Module configuration - user-provided key-value pairs
|
|
37
|
-
* Stores configuration like container_ip, zone, etc.
|
|
43
|
+
* Module configuration - user-provided key-value pairs.
|
|
38
44
|
*
|
|
39
|
-
*
|
|
40
|
-
* -
|
|
41
|
-
*
|
|
45
|
+
* Two columns, with strictly different roles:
|
|
46
|
+
* - `valueJson`: JSON-encoded canonical value, ALWAYS populated by
|
|
47
|
+
* the write path. This is the source of truth — readers MUST
|
|
48
|
+
* consume `valueJson` and `JSON.parse` it. Type fidelity is
|
|
49
|
+
* preserved here: `number` round-trips as `number`, `boolean` as
|
|
50
|
+
* `boolean`, arrays/objects as their JSON shape.
|
|
51
|
+
* - `value`: human-readable string form, used only by the CLI for
|
|
52
|
+
* display (`module config get`). NEVER used as a source of typed
|
|
53
|
+
* data — that path leaked stringly-typed numbers through to
|
|
54
|
+
* capability calls (see commit history for the forgejo SSH:2222
|
|
55
|
+
* case, where ssh_external_port was read back as "2222" the
|
|
56
|
+
* string and silently broke firewall.exposeService).
|
|
42
57
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
58
|
+
* Historically `value` was the canonical storage for primitives and
|
|
59
|
+
* `valueJson` only for arrays/objects. That was Defect 1 — TS types
|
|
60
|
+
* generated from the manifest claimed `number` while the runtime
|
|
61
|
+
* value was string. Now closed.
|
|
46
62
|
*/
|
|
47
63
|
export const moduleConfigs = sqliteTable(
|
|
48
64
|
'module_configs',
|
|
@@ -298,6 +314,13 @@ export const machines = sqliteTable('machines', {
|
|
|
298
314
|
.default(sql`'[]'`),
|
|
299
315
|
/** Module ID this machine is earmarked for. If set, only this module can use this machine. */
|
|
300
316
|
earmarkedModule: text('earmarked_module'),
|
|
317
|
+
/**
|
|
318
|
+
* Appliance machines (e.g., greenwave = ISP modem) where celilo
|
|
319
|
+
* has no shell-level access — only API calls. Base-module aspects
|
|
320
|
+
* cannot Ansible to these systems, so the aspect runner skips
|
|
321
|
+
* them. See v2/CELILO_BASE.md D8.
|
|
322
|
+
*/
|
|
323
|
+
apiOnly: integer('api_only', { mode: 'boolean' }).notNull().default(false),
|
|
301
324
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
302
325
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
303
326
|
});
|
|
@@ -317,9 +340,66 @@ export const moduleInfrastructure = sqliteTable('module_infrastructure', {
|
|
|
317
340
|
machineId: text('machine_id').references(() => machines.id),
|
|
318
341
|
serviceId: text('service_id').references(() => containerServices.id),
|
|
319
342
|
containerMetadata: text('container_metadata', { mode: 'json' }).$type<Record<string, unknown>>(), // VMID, droplet ID, etc.
|
|
343
|
+
/**
|
|
344
|
+
* Mirrors machines.api_only — set when the container_service
|
|
345
|
+
* driver registers an entry that has no SSH surface (rare today,
|
|
346
|
+
* but exists for future API-only providers). Aspect runner reads
|
|
347
|
+
* this to decide whether the system is reachable via Ansible.
|
|
348
|
+
* See v2/CELILO_BASE.md D8.
|
|
349
|
+
*/
|
|
350
|
+
apiOnly: integer('api_only', { mode: 'boolean' }).notNull().default(false),
|
|
320
351
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
321
352
|
});
|
|
322
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Module systems table
|
|
356
|
+
*
|
|
357
|
+
* A module deploys onto 0..N *systems*, each an addressed host. This is the
|
|
358
|
+
* deployment-STATE counterpart to module_configs (declared inputs): it records,
|
|
359
|
+
* per deployed system, the hostname + IPv4 + zone + where it lives. Replaces the
|
|
360
|
+
* old scalar `target_ip`/`vmid` rows in module_configs, which baked in the
|
|
361
|
+
* one-module-one-host assumption. See v2/MODULE_SYSTEMS_ADDRESSING.md.
|
|
362
|
+
*
|
|
363
|
+
* - 0 rows: API-only modules (e.g. namecheap — no host).
|
|
364
|
+
* - 1 row: the common case (technitium, homebridge, …).
|
|
365
|
+
* - N rows: multi-system modules (e.g. app + db).
|
|
366
|
+
*
|
|
367
|
+
* Keyed by (module_id, name): `name` is the stable, authoring-time handle from
|
|
368
|
+
* the manifest's `requires.systems[].name` — what templates reference via
|
|
369
|
+
* `$infra:<name>.…`. `hostname` is the runtime DNS hostname (often == name, but
|
|
370
|
+
* user/well-known-assignable), used by DNS and events. See
|
|
371
|
+
* v2/MODULE_SYSTEMS_ADDRESSING.md.
|
|
372
|
+
*/
|
|
373
|
+
export const moduleSystems = sqliteTable(
|
|
374
|
+
'module_systems',
|
|
375
|
+
{
|
|
376
|
+
moduleId: text('module_id')
|
|
377
|
+
.notNull()
|
|
378
|
+
.references(() => modules.id, { onDelete: 'cascade' }),
|
|
379
|
+
/** Stable authoring-time handle (requires.systems[].name) — the per-system key. */
|
|
380
|
+
name: text('name').notNull(),
|
|
381
|
+
/** Runtime DNS hostname (often == name). */
|
|
382
|
+
hostname: text('hostname').notNull(),
|
|
383
|
+
/** CIDR-stripped IPv4 — the canonical address. */
|
|
384
|
+
ipv4Address: text('ipv4_address').notNull(),
|
|
385
|
+
/** Network zone the host sits in; CIDR is derived from this + ipv4Address. */
|
|
386
|
+
zone: text('zone').$type<NetworkZone>().notNull(),
|
|
387
|
+
/** Where the system lives: a pool machine or a celilo-provisioned container. */
|
|
388
|
+
infraType: text('infra_type').$type<'machine' | 'container_service'>().notNull(),
|
|
389
|
+
/** FK machines.id — set for machine-pool deploys (null otherwise). */
|
|
390
|
+
machineId: text('machine_id').references(() => machines.id),
|
|
391
|
+
/** FK container_services.id — set for container_service deploys (null otherwise). */
|
|
392
|
+
serviceId: text('service_id').references(() => containerServices.id),
|
|
393
|
+
/** Proxmox VMID — set only for proxmox containers. */
|
|
394
|
+
vmid: integer('vmid'),
|
|
395
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
396
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
397
|
+
},
|
|
398
|
+
(table) => ({
|
|
399
|
+
pk: primaryKey({ columns: [table.moduleId, table.name] }),
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
402
|
+
|
|
323
403
|
/**
|
|
324
404
|
* Web routes table
|
|
325
405
|
* Tracks routes registered by modules via public_web capability functions.
|
|
@@ -404,6 +484,74 @@ export const backups = sqliteTable('backups', {
|
|
|
404
484
|
completedAt: integer('completed_at', { mode: 'timestamp' }),
|
|
405
485
|
});
|
|
406
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Module operations - in-flight tracking for deploy/uninstall/backup/restore.
|
|
489
|
+
*
|
|
490
|
+
* Used by `checkInFlight()` to refuse a backup or restore while another
|
|
491
|
+
* operation is active on any module. A row is created with status='in_progress'
|
|
492
|
+
* when an operation starts and updated to 'completed' or 'failed' when it ends.
|
|
493
|
+
*
|
|
494
|
+
* The `pid` column carries the process that started the operation; a row whose
|
|
495
|
+
* pid is no longer alive is treated as abandoned (the process crashed before
|
|
496
|
+
* the completion update landed) and ignored by in-flight checks.
|
|
497
|
+
*/
|
|
498
|
+
export type ModuleOperationKind = 'deploy' | 'uninstall' | 'backup' | 'restore';
|
|
499
|
+
export type ModuleOperationStatus = 'in_progress' | 'completed' | 'failed';
|
|
500
|
+
|
|
501
|
+
export const moduleOperations = sqliteTable('module_operations', {
|
|
502
|
+
id: text('id').primaryKey(), // UUID
|
|
503
|
+
moduleId: text('module_id').notNull(), // not a FK — survives module deletion (uninstall flow)
|
|
504
|
+
operation: text('operation').$type<ModuleOperationKind>().notNull(),
|
|
505
|
+
status: text('status').$type<ModuleOperationStatus>().notNull().default('in_progress'),
|
|
506
|
+
pid: integer('pid').notNull(),
|
|
507
|
+
startedAt: integer('started_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
508
|
+
completedAt: integer('completed_at', { mode: 'timestamp' }),
|
|
509
|
+
errorMessage: text('error_message'),
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Aspect approvals table
|
|
514
|
+
*
|
|
515
|
+
* Records operator consent for a module's base-module aspect. The
|
|
516
|
+
* operator approves the aspect's scope (applicable_zones +
|
|
517
|
+
* triggers) at `celilo module import` time; that consent is
|
|
518
|
+
* captured here and consulted before any aspect fan-out.
|
|
519
|
+
*
|
|
520
|
+
* `scopeHash` is a stable digest of `applicable_zones` + `triggers`
|
|
521
|
+
* — it lets the framework detect scope-changing upgrades (D7).
|
|
522
|
+
* If a new module version has the same hash, the prior approval
|
|
523
|
+
* still covers it; if the hash differs, the upgrade is blocked
|
|
524
|
+
* until the operator re-approves.
|
|
525
|
+
*
|
|
526
|
+
* Unique on (moduleId, version) — there's at most one approval
|
|
527
|
+
* per module version. Cascade delete on module removal so stale
|
|
528
|
+
* approvals don't linger.
|
|
529
|
+
*
|
|
530
|
+
* See v2/CELILO_BASE.md D2 + D7.
|
|
531
|
+
*/
|
|
532
|
+
export const aspectApprovals = sqliteTable(
|
|
533
|
+
'aspect_approvals',
|
|
534
|
+
{
|
|
535
|
+
id: text('id').primaryKey(), // UUID
|
|
536
|
+
moduleId: text('module_id')
|
|
537
|
+
.notNull()
|
|
538
|
+
.references(() => modules.id, { onDelete: 'cascade' }),
|
|
539
|
+
/** Module version this approval covers (matches modules.version). */
|
|
540
|
+
version: text('version').notNull(),
|
|
541
|
+
/** Hash of `applicable_zones` + `triggers` — see table comment. */
|
|
542
|
+
scopeHash: text('scope_hash').notNull(),
|
|
543
|
+
approvedAt: integer('approved_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
544
|
+
/** Operator identifier (e.g., $USER at approval time). Null when --accept-aspects was used in a context with no USER. */
|
|
545
|
+
approver: text('approver'),
|
|
546
|
+
},
|
|
547
|
+
(table) => ({
|
|
548
|
+
uniqueModuleVersion: unique('aspect_approvals_module_version').on(
|
|
549
|
+
table.moduleId,
|
|
550
|
+
table.version,
|
|
551
|
+
),
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
554
|
+
|
|
407
555
|
/**
|
|
408
556
|
* Type exports for use in application code
|
|
409
557
|
*/
|
|
@@ -441,3 +589,7 @@ export type BackupStorage = typeof backupStorages.$inferSelect;
|
|
|
441
589
|
export type NewBackupStorage = typeof backupStorages.$inferInsert;
|
|
442
590
|
export type Backup = typeof backups.$inferSelect;
|
|
443
591
|
export type NewBackup = typeof backups.$inferInsert;
|
|
592
|
+
export type ModuleOperation = typeof moduleOperations.$inferSelect;
|
|
593
|
+
export type NewModuleOperation = typeof moduleOperations.$inferInsert;
|
|
594
|
+
export type AspectApproval = typeof aspectApprovals.$inferSelect;
|
|
595
|
+
export type NewAspectApproval = typeof aspectApprovals.$inferInsert;
|
|
@@ -10,6 +10,7 @@ import { tmpdir } from 'node:os';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import type { HookLogger } from '@celilo/capabilities';
|
|
12
12
|
import type { DbClient } from '../db/client';
|
|
13
|
+
import { upsertModuleConfig } from '../services/module-config';
|
|
13
14
|
import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database';
|
|
14
15
|
import { loadCapabilityFunctions } from './capability-loader';
|
|
15
16
|
|
|
@@ -102,9 +103,7 @@ describe('Firewall Chain Building', () => {
|
|
|
102
103
|
db.$client.run(
|
|
103
104
|
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('greenwave', 'firewall', '1.0.0', '{"has_external":true}', '["internal"]')`,
|
|
104
105
|
);
|
|
105
|
-
db
|
|
106
|
-
`INSERT INTO module_configs (module_id, key, value) VALUES ('greenwave', 'router_ip', '192.168.0.1')`,
|
|
107
|
-
);
|
|
106
|
+
upsertModuleConfig(db, 'greenwave', 'router_ip', '192.168.0.1');
|
|
108
107
|
|
|
109
108
|
// Set up encrypted secrets
|
|
110
109
|
const { encryptSecret } = await import('../secrets/encryption');
|
|
@@ -152,9 +151,7 @@ describe('Firewall Chain Building', () => {
|
|
|
152
151
|
db.$client.run(
|
|
153
152
|
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('greenwave', 'firewall', '1.0.0', '{"has_external":true}', '["internal"]')`,
|
|
154
153
|
);
|
|
155
|
-
db
|
|
156
|
-
`INSERT INTO module_configs (module_id, key, value) VALUES ('greenwave', 'router_ip', '192.168.0.1')`,
|
|
157
|
-
);
|
|
154
|
+
upsertModuleConfig(db, 'greenwave', 'router_ip', '192.168.0.1');
|
|
158
155
|
|
|
159
156
|
const { encryptSecret } = await import('../secrets/encryption');
|
|
160
157
|
const { getOrCreateMasterKey } = await import('../secrets/master-key');
|
|
@@ -181,15 +178,9 @@ describe('Firewall Chain Building', () => {
|
|
|
181
178
|
db.$client.run(
|
|
182
179
|
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('iptables', 'firewall', '1.0.0', '{}', '["dmz","app","secure"]')`,
|
|
183
180
|
);
|
|
184
|
-
db
|
|
185
|
-
|
|
186
|
-
);
|
|
187
|
-
db.$client.run(
|
|
188
|
-
`INSERT INTO module_configs (module_id, key, value) VALUES ('iptables', 'nat_ip', '192.168.0.253')`,
|
|
189
|
-
);
|
|
190
|
-
db.$client.run(
|
|
191
|
-
`INSERT INTO module_configs (module_id, key, value) VALUES ('iptables', 'upstream_zone', 'internal')`,
|
|
192
|
-
);
|
|
181
|
+
upsertModuleConfig(db, 'iptables', 'firewall_ip', '192.168.0.254');
|
|
182
|
+
upsertModuleConfig(db, 'iptables', 'nat_ip', '192.168.0.253');
|
|
183
|
+
upsertModuleConfig(db, 'iptables', 'upstream_zone', 'internal');
|
|
193
184
|
|
|
194
185
|
const result = await loadCapabilityFunctions('test-consumer', db, noopLogger);
|
|
195
186
|
expect(result.firewall).toBeTruthy();
|
|
@@ -8,6 +8,7 @@ import { tmpdir } from 'node:os';
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import type { HookLogger } from '@celilo/capabilities';
|
|
10
10
|
import type { DbClient } from '../db/client';
|
|
11
|
+
import { upsertModuleConfig } from '../services/module-config';
|
|
11
12
|
import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database';
|
|
12
13
|
import { loadCapabilityFunctions } from './capability-loader';
|
|
13
14
|
|
|
@@ -78,9 +79,7 @@ describe('Capability Loader', () => {
|
|
|
78
79
|
db.$client.run(
|
|
79
80
|
`INSERT INTO capabilities (module_id, capability_name, version, data, registered_at) VALUES ('namecheap', 'dns_registrar', '2.0.0', '{}', unixepoch())`,
|
|
80
81
|
);
|
|
81
|
-
db
|
|
82
|
-
`INSERT INTO module_configs (module_id, key, value) VALUES ('namecheap', 'primary_domain', 'example.com')`,
|
|
83
|
-
);
|
|
82
|
+
upsertModuleConfig(db, 'namecheap', 'primary_domain', 'example.com');
|
|
84
83
|
|
|
85
84
|
// Set encrypted secrets
|
|
86
85
|
const { encryptSecret } = await import('../secrets/encryption');
|
|
@@ -23,6 +23,7 @@ import type { DbClient } from '../db/client';
|
|
|
23
23
|
import { capabilities, modules, secrets, webRoutes } from '../db/schema';
|
|
24
24
|
import { decryptSecret } from '../secrets/encryption';
|
|
25
25
|
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
26
|
+
import { getModuleSystems } from '../services/deployed-systems';
|
|
26
27
|
import { loadHookConfigMap } from './load-hook-config';
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -184,6 +185,7 @@ export async function loadCapabilityFunctions(
|
|
|
184
185
|
const capabilityInterface = exported({
|
|
185
186
|
config: providerConfig,
|
|
186
187
|
secrets: providerSecrets,
|
|
188
|
+
systems: getModuleSystems(capability.moduleId, db),
|
|
187
189
|
logger,
|
|
188
190
|
});
|
|
189
191
|
result[capName] = capabilityInterface;
|
|
@@ -281,17 +283,48 @@ export async function loadCapabilityFunctions(
|
|
|
281
283
|
// apps/celilo/designs/DNS_REGISTRAR_MULTI_DOMAIN.md). Older
|
|
282
284
|
// registrars that haven't been migrated still use the split shape;
|
|
283
285
|
// we keep that fallback so a stale config can still be read.
|
|
286
|
+
let foundAny = false;
|
|
284
287
|
if (Array.isArray(drConfig.domains)) {
|
|
285
288
|
for (const d of drConfig.domains) {
|
|
286
|
-
if (typeof d === 'string' && d)
|
|
289
|
+
if (typeof d === 'string' && d) {
|
|
290
|
+
dnsManagedDomains.push(d);
|
|
291
|
+
foundAny = true;
|
|
292
|
+
}
|
|
287
293
|
}
|
|
288
294
|
} else {
|
|
289
295
|
if (typeof drConfig.primary_domain === 'string' && drConfig.primary_domain) {
|
|
290
296
|
dnsManagedDomains.push(drConfig.primary_domain);
|
|
297
|
+
foundAny = true;
|
|
291
298
|
}
|
|
292
299
|
if (Array.isArray(drConfig.additional_domains)) {
|
|
293
300
|
for (const d of drConfig.additional_domains) {
|
|
294
|
-
if (typeof d === 'string' && d)
|
|
301
|
+
if (typeof d === 'string' && d) {
|
|
302
|
+
dnsManagedDomains.push(d);
|
|
303
|
+
foundAny = true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// namecheap 3.1.1+ derives its managed-domain list from
|
|
309
|
+
// `Object.keys(secret.ddns_passwords)` (a JSON-encoded
|
|
310
|
+
// domain→password map) and no longer exposes a `domains` config.
|
|
311
|
+
// Mirror that derivation here so the cross-module flow knows
|
|
312
|
+
// which zones the registrar covers. See
|
|
313
|
+
// apps/celilo/designs/STRING_LIST_AND_DERIVED_KEYS.md.
|
|
314
|
+
if (!foundAny) {
|
|
315
|
+
const drSecrets = await loadModuleSecrets(drp.moduleId, masterKey, db);
|
|
316
|
+
const raw = drSecrets.ddns_passwords;
|
|
317
|
+
if (typeof raw === 'string' && raw) {
|
|
318
|
+
try {
|
|
319
|
+
const parsed = JSON.parse(raw);
|
|
320
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
321
|
+
for (const k of Object.keys(parsed)) {
|
|
322
|
+
if (k) dnsManagedDomains.push(k);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
// Bad JSON → empty list; consumer will throw
|
|
327
|
+
// MissingProviderInputError naming this registrar.
|
|
295
328
|
}
|
|
296
329
|
}
|
|
297
330
|
}
|
|
@@ -462,6 +495,7 @@ async function buildFirewallChain(
|
|
|
462
495
|
leafFirewall = leafExported({
|
|
463
496
|
config: leafConfig,
|
|
464
497
|
secrets: leafSecrets,
|
|
498
|
+
systems: getModuleSystems(hasExternal.moduleId, db),
|
|
465
499
|
logger,
|
|
466
500
|
});
|
|
467
501
|
debugLog(
|
|
@@ -44,6 +44,7 @@ function makeContext(overrides: Partial<HookContext> = {}): HookContext {
|
|
|
44
44
|
return {
|
|
45
45
|
config: {},
|
|
46
46
|
secrets: {},
|
|
47
|
+
systems: [],
|
|
47
48
|
logger: makeLogger(),
|
|
48
49
|
debug: false,
|
|
49
50
|
screenshotDir: '',
|
|
@@ -329,6 +330,7 @@ describe('defineCapabilityFunction', () => {
|
|
|
329
330
|
const methods = factory({
|
|
330
331
|
config: { target_ip: '10.0.0.5' },
|
|
331
332
|
secrets: { token: 'abc' },
|
|
333
|
+
systems: [],
|
|
332
334
|
logger: makeLogger(),
|
|
333
335
|
});
|
|
334
336
|
|
|
@@ -354,6 +356,7 @@ describe('defineCapabilityFunction', () => {
|
|
|
354
356
|
const methods = factory({
|
|
355
357
|
config: {},
|
|
356
358
|
secrets: {},
|
|
359
|
+
systems: [],
|
|
357
360
|
logger: makeLogger(),
|
|
358
361
|
});
|
|
359
362
|
|
|
@@ -381,6 +384,7 @@ describe('defineCapabilityFunction', () => {
|
|
|
381
384
|
const methods = factory({
|
|
382
385
|
config: {},
|
|
383
386
|
secrets: {},
|
|
387
|
+
systems: [],
|
|
384
388
|
logger: makeLogger(),
|
|
385
389
|
});
|
|
386
390
|
|
|
@@ -112,6 +112,7 @@ describe('Hook Executor', () => {
|
|
|
112
112
|
const result = await executeHookScript(scriptPath, {
|
|
113
113
|
config: { username: 'testuser' },
|
|
114
114
|
secrets: { password: 'secret' },
|
|
115
|
+
systems: [],
|
|
115
116
|
logger,
|
|
116
117
|
debug: false,
|
|
117
118
|
screenshotDir: '/tmp',
|
|
@@ -131,6 +132,7 @@ describe('Hook Executor', () => {
|
|
|
131
132
|
executeHookScript('/nonexistent/hook.ts', {
|
|
132
133
|
config: {},
|
|
133
134
|
secrets: {},
|
|
135
|
+
systems: [],
|
|
134
136
|
logger,
|
|
135
137
|
debug: false,
|
|
136
138
|
screenshotDir: '/tmp',
|
|
@@ -147,6 +149,7 @@ describe('Hook Executor', () => {
|
|
|
147
149
|
executeHookScript(scriptPath, {
|
|
148
150
|
config: {},
|
|
149
151
|
secrets: {},
|
|
152
|
+
systems: [],
|
|
150
153
|
logger,
|
|
151
154
|
debug: false,
|
|
152
155
|
screenshotDir: '/tmp',
|
|
@@ -167,6 +170,7 @@ describe('Hook Executor', () => {
|
|
|
167
170
|
executeHookScript(scriptPath, {
|
|
168
171
|
config: {},
|
|
169
172
|
secrets: {},
|
|
173
|
+
systems: [],
|
|
170
174
|
logger,
|
|
171
175
|
debug: false,
|
|
172
176
|
screenshotDir: '/tmp',
|
|
@@ -182,6 +186,7 @@ describe('Hook Executor', () => {
|
|
|
182
186
|
const result = await executeHookScript(scriptPath, {
|
|
183
187
|
config: {},
|
|
184
188
|
secrets: {},
|
|
189
|
+
systems: [],
|
|
185
190
|
logger,
|
|
186
191
|
debug: false,
|
|
187
192
|
screenshotDir: '/tmp',
|
|
@@ -199,6 +204,7 @@ describe('Hook Executor', () => {
|
|
|
199
204
|
executeHookScript(scriptPath, {
|
|
200
205
|
config: {},
|
|
201
206
|
secrets: {},
|
|
207
|
+
systems: [],
|
|
202
208
|
logger,
|
|
203
209
|
debug: false,
|
|
204
210
|
screenshotDir: '/tmp',
|
|
@@ -458,5 +464,17 @@ describe('Hook Executor', () => {
|
|
|
458
464
|
expect(result).toContain("'idp'");
|
|
459
465
|
expect(result).toContain('each missing capability');
|
|
460
466
|
});
|
|
467
|
+
|
|
468
|
+
test('skips framework-granted privileges (not provider-loaded)', () => {
|
|
469
|
+
// cross_module_read is a privilege granted by the framework to the
|
|
470
|
+
// hooks that use it (backup/restore), not a provider-loaded
|
|
471
|
+
// capability. It must not block other hooks like on_install
|
|
472
|
+
// (regression: celilo-mgmt deploy, v2/NETWORK_CONFIG_TO_FIREWALL.md).
|
|
473
|
+
expect(checkRequiredCapabilities('on_install', ['cross_module_read'], {})).toBeNull();
|
|
474
|
+
// Real missing providers still flagged alongside a privilege.
|
|
475
|
+
const mixed = checkRequiredCapabilities('on_install', ['cross_module_read', 'idp'], {});
|
|
476
|
+
expect(mixed).toContain("'idp'");
|
|
477
|
+
expect(mixed).not.toContain('cross_module_read');
|
|
478
|
+
});
|
|
461
479
|
});
|
|
462
480
|
});
|
package/src/hooks/executor.ts
CHANGED
|
@@ -21,12 +21,17 @@
|
|
|
21
21
|
|
|
22
22
|
import { existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
23
23
|
import { join, resolve } from 'node:path';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
type DeployedSystem,
|
|
26
|
+
isCompiledHook,
|
|
27
|
+
isMissingProviderInputError,
|
|
28
|
+
} from '@celilo/capabilities';
|
|
25
29
|
import {
|
|
26
30
|
type ContractHookSignature,
|
|
27
31
|
resolveContract,
|
|
28
32
|
supportedContractVersions,
|
|
29
33
|
} from '../manifest/contracts';
|
|
34
|
+
import { isPrivilegedCapability } from '../manifest/validate';
|
|
30
35
|
import type { HookContext, HookDefinition, HookLogger, HookResult } from './types';
|
|
31
36
|
|
|
32
37
|
/** Default total timeout: 60 seconds */
|
|
@@ -256,6 +261,13 @@ export interface InvokeHookOptions {
|
|
|
256
261
|
debug?: boolean;
|
|
257
262
|
/** Pre-loaded capability function interfaces to inject into context */
|
|
258
263
|
capabilities?: Record<string, unknown>;
|
|
264
|
+
/**
|
|
265
|
+
* The module's 0..N deployed systems, injected as `ctx.systems`
|
|
266
|
+
* (v2/MODULE_SYSTEMS_ADDRESSING.md). Callers load these from the DB via
|
|
267
|
+
* `getModuleSystems` — the executor stays decoupled from the database, the
|
|
268
|
+
* same way `capabilities` is loaded by the caller. Defaults to `[]`.
|
|
269
|
+
*/
|
|
270
|
+
systems?: DeployedSystem[];
|
|
259
271
|
/**
|
|
260
272
|
* Names of capabilities the hook's module declares in
|
|
261
273
|
* `requires.capabilities`. The executor checks each one is present in
|
|
@@ -280,7 +292,13 @@ export function checkRequiredCapabilities(
|
|
|
280
292
|
requiredCapabilities: string[],
|
|
281
293
|
loadedCapabilities: Record<string, unknown>,
|
|
282
294
|
): string | null {
|
|
283
|
-
|
|
295
|
+
// Framework-granted privileges (e.g. cross_module_read) aren't loaded as
|
|
296
|
+
// provider capability-functions — they're granted by the framework to the
|
|
297
|
+
// specific hooks that use them (backup/restore), via a separate mechanism.
|
|
298
|
+
// Don't treat them as missing providers for every hook (e.g. on_install).
|
|
299
|
+
const missing = requiredCapabilities.filter(
|
|
300
|
+
(name) => !isPrivilegedCapability(name) && !(name in loadedCapabilities),
|
|
301
|
+
);
|
|
284
302
|
if (missing.length === 0) return null;
|
|
285
303
|
|
|
286
304
|
const lines = [
|
|
@@ -404,6 +422,7 @@ export async function invokeHook(
|
|
|
404
422
|
...inputs,
|
|
405
423
|
config,
|
|
406
424
|
secrets,
|
|
425
|
+
systems: options.systems ?? [],
|
|
407
426
|
logger,
|
|
408
427
|
debug,
|
|
409
428
|
screenshotDir,
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
20
20
|
import type { DbClient } from '../db/client';
|
|
21
|
-
import { machines,
|
|
21
|
+
import { machines, moduleInfrastructure, modules } from '../db/schema';
|
|
22
|
+
import { upsertModuleConfig } from '../services/module-config';
|
|
22
23
|
import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database';
|
|
23
24
|
import { loadHookConfigMap } from './load-hook-config';
|
|
24
25
|
|
|
@@ -48,39 +49,42 @@ describe('loadHookConfigMap', () => {
|
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
test('returns scalar string config values from module_configs', async () => {
|
|
51
|
-
db
|
|
52
|
-
|
|
53
|
-
{ moduleId: 'mod', key: 'hostname', value: 'caddy', valueJson: null },
|
|
54
|
-
{ moduleId: 'mod', key: 'acme_email', value: 'admin@example.com', valueJson: null },
|
|
55
|
-
])
|
|
56
|
-
.run();
|
|
52
|
+
upsertModuleConfig(db, 'mod', 'hostname', 'caddy');
|
|
53
|
+
upsertModuleConfig(db, 'mod', 'acme_email', 'admin@example.com');
|
|
57
54
|
|
|
58
55
|
const result = await loadHookConfigMap('mod', db);
|
|
59
56
|
expect(result).toEqual({ hostname: 'caddy', acme_email: 'admin@example.com' });
|
|
60
57
|
});
|
|
61
58
|
|
|
62
59
|
test('parses valueJson for complex types (arrays, objects)', async () => {
|
|
63
|
-
db.
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
moduleId: 'mod',
|
|
67
|
-
key: 'hostnames',
|
|
68
|
-
value: '',
|
|
69
|
-
valueJson: '["www.example.com","example.com"]',
|
|
70
|
-
},
|
|
71
|
-
{ moduleId: 'mod', key: 'plain', value: 'string-val', valueJson: null },
|
|
72
|
-
])
|
|
73
|
-
.run();
|
|
60
|
+
upsertModuleConfig(db, 'mod', 'hostnames', ['www.example.com', 'example.com']);
|
|
61
|
+
upsertModuleConfig(db, 'mod', 'plain', 'string-val');
|
|
74
62
|
|
|
75
63
|
const result = await loadHookConfigMap('mod', db);
|
|
76
64
|
expect(result.hostnames).toEqual(['www.example.com', 'example.com']);
|
|
77
65
|
expect(result.plain).toBe('string-val');
|
|
78
66
|
});
|
|
79
67
|
|
|
68
|
+
test('preserves primitive types (numbers, booleans) — Defect 1 round-trip', async () => {
|
|
69
|
+
// The headline defect that motivated Commit C: primitives used to
|
|
70
|
+
// stringify on write and come back as strings (e.g. ssh_external_port:
|
|
71
|
+
// 2222 → stored "2222" → read "2222" → silently broke
|
|
72
|
+
// firewall.exposeService that expected a number). With upsertModuleConfig
|
|
73
|
+
// routing through JSON, types round-trip cleanly.
|
|
74
|
+
upsertModuleConfig(db, 'mod', 'port', 2222);
|
|
75
|
+
upsertModuleConfig(db, 'mod', 'enabled', true);
|
|
76
|
+
upsertModuleConfig(db, 'mod', 'disabled', false);
|
|
77
|
+
|
|
78
|
+
const result = await loadHookConfigMap('mod', db);
|
|
79
|
+
expect(result.port).toBe(2222);
|
|
80
|
+
expect(typeof result.port).toBe('number');
|
|
81
|
+
expect(result.enabled).toBe(true);
|
|
82
|
+
expect(typeof result.enabled).toBe('boolean');
|
|
83
|
+
expect(result.disabled).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
80
86
|
test('preserves an explicit target_ip in module_configs (skips machine fallback)', async () => {
|
|
81
|
-
db.
|
|
82
|
-
.values({ moduleId: 'mod', key: 'target_ip', value: '10.0.10.10', valueJson: null })
|
|
83
|
-
.run();
|
|
87
|
+
upsertModuleConfig(db, 'mod', 'target_ip', '10.0.10.10');
|
|
84
88
|
// A machine record exists but should NOT override the explicit value.
|
|
85
89
|
db.insert(machines)
|
|
86
90
|
.values({
|
|
@@ -137,9 +141,7 @@ describe('loadHookConfigMap', () => {
|
|
|
137
141
|
});
|
|
138
142
|
|
|
139
143
|
test('leaves target_ip unset when there is no infrastructure record', async () => {
|
|
140
|
-
db
|
|
141
|
-
.values({ moduleId: 'mod', key: 'hostname', value: 'caddy', valueJson: null })
|
|
142
|
-
.run();
|
|
144
|
+
upsertModuleConfig(db, 'mod', 'hostname', 'caddy');
|
|
143
145
|
// no moduleInfrastructure row inserted
|
|
144
146
|
|
|
145
147
|
const result = await loadHookConfigMap('mod', db);
|
|
@@ -12,7 +12,15 @@
|
|
|
12
12
|
* code, easy to miss.
|
|
13
13
|
*
|
|
14
14
|
* The shape:
|
|
15
|
-
* - Every row from `module_configs`, parsed from `valueJson`
|
|
15
|
+
* - Every row from `module_configs`, parsed from `valueJson` via
|
|
16
|
+
* the shared `parseStoredConfigValue` helper. This preserves the
|
|
17
|
+
* types declared in each module's manifest: `number` reads as
|
|
18
|
+
* `number`, `boolean` as `boolean`, complex types as their
|
|
19
|
+
* parsed JSON shape. Pre-Defect-1, this path returned raw strings
|
|
20
|
+
* for primitives (because `valueJson` was NULL for them); that
|
|
21
|
+
* broke capability calls like `firewall.exposeService({ports:[...]})`
|
|
22
|
+
* that did a `typeof === 'number'` check downstream. Fixed in
|
|
23
|
+
* v2 by always populating valueJson on write.
|
|
16
24
|
* - If `target_ip` isn't in the row set, look up the deployment
|
|
17
25
|
* machine and inject both `target_ip` AND `ip.primary` from
|
|
18
26
|
* `machines.ipAddress`. Two keys because consumers historically used
|
|
@@ -26,6 +34,7 @@
|
|
|
26
34
|
import { eq } from 'drizzle-orm';
|
|
27
35
|
import type { DbClient } from '../db/client';
|
|
28
36
|
import { machines, moduleConfigs, moduleInfrastructure } from '../db/schema';
|
|
37
|
+
import { parseStoredConfigValue } from '../services/module-config';
|
|
29
38
|
|
|
30
39
|
export async function loadHookConfigMap(
|
|
31
40
|
moduleId: string,
|
|
@@ -39,7 +48,7 @@ export async function loadHookConfigMap(
|
|
|
39
48
|
|
|
40
49
|
const configMap: Record<string, unknown> = {};
|
|
41
50
|
for (const c of configRecords) {
|
|
42
|
-
configMap[c.key] =
|
|
51
|
+
configMap[c.key] = parseStoredConfigValue(c);
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
if (configMap.target_ip) return configMap;
|
|
@@ -18,6 +18,7 @@ import { modules, secrets } from '../db/schema';
|
|
|
18
18
|
import type { ModuleManifest } from '../manifest/schema';
|
|
19
19
|
import { decryptSecret } from '../secrets/encryption';
|
|
20
20
|
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
21
|
+
import { getModuleSystems } from '../services/deployed-systems';
|
|
21
22
|
import { loadCapabilityFunctions } from './capability-loader';
|
|
22
23
|
import { invokeHook } from './executor';
|
|
23
24
|
import { loadHookConfigMap } from './load-hook-config';
|
|
@@ -81,6 +82,20 @@ export async function runNamedHook(
|
|
|
81
82
|
notDefined: true,
|
|
82
83
|
};
|
|
83
84
|
}
|
|
85
|
+
// Defensive narrow: `on_upstream_publish` is also under
|
|
86
|
+
// `manifest.hooks` (build-bus, Phase 4) but is an ARRAY of
|
|
87
|
+
// match-rule hooks dispatched by the receiver daemon — NOT
|
|
88
|
+
// runnable via `celilo module run-hook`. The HookName type
|
|
89
|
+
// excludes it, but the indexed-access return type can't prove
|
|
90
|
+
// that, so narrow at the value level.
|
|
91
|
+
if (Array.isArray(hookDef)) {
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
outputs: {},
|
|
95
|
+
duration: Date.now() - startedAt,
|
|
96
|
+
notDefined: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
84
99
|
|
|
85
100
|
const configMap = await loadHookConfigMap(moduleId, db);
|
|
86
101
|
|
|
@@ -123,6 +138,7 @@ export async function runNamedHook(
|
|
|
123
138
|
debug: options.debug ?? false,
|
|
124
139
|
capabilities: capabilityFunctions,
|
|
125
140
|
requiredCapabilities,
|
|
141
|
+
systems: getModuleSystems(moduleId, db),
|
|
126
142
|
},
|
|
127
143
|
);
|
|
128
144
|
}
|