@celilo/cli 2.2.0 → 2.3.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.
- package/CELILO_CORE_MODULES.md +1 -0
- package/CELILO_SUBSYSTEMS.md +1 -1
- package/README.md +1 -1
- package/drizzle/0032_module_jail_policies.sql +28 -0
- package/drizzle/0033_build_bus_hook_runs.sql +37 -0
- package/drizzle/meta/_journal.json +15 -1
- package/package.json +3 -3
- package/schemas/system_config.json +5 -0
- package/src/__integration__/container-services-cli.integration.test.ts +1 -1
- package/src/ansible/inventory.test.ts +2 -1
- package/src/api/sessions.test.ts +5 -4
- package/src/api-clients/proxmox.ts +10 -7
- package/src/cli/backup-rename.test.ts +5 -4
- package/src/cli/cli.test.ts +3 -2
- package/src/cli/commands/console-get-chain.test.ts +2 -1
- package/src/cli/commands/events.test.ts +2 -2
- package/src/cli/commands/firewall-interface-list.test.ts +4 -3
- package/src/cli/commands/machine-list.test.ts +57 -0
- package/src/cli/commands/machine-list.ts +35 -4
- package/src/cli/commands/module-config.test.ts +2 -1
- package/src/cli/commands/module-deploy.ts +8 -2
- package/src/cli/commands/module-generate.test.ts +53 -0
- package/src/cli/commands/module-generate.ts +31 -26
- package/src/cli/commands/module-health.ts +1 -0
- package/src/cli/commands/module-import-aspect.test.ts +2 -1
- package/src/cli/commands/module-import-registry.test.ts +3 -2
- package/src/cli/commands/module-jail.test.ts +242 -0
- package/src/cli/commands/module-jail.ts +227 -0
- package/src/cli/commands/module-list-jail.test.ts +136 -0
- package/src/cli/commands/module-list.ts +30 -3
- package/src/cli/commands/module-operations.test.ts +2 -1
- package/src/cli/commands/module-publish.test.ts +11 -18
- package/src/cli/commands/module-update.test.ts +28 -12
- package/src/cli/commands/module-update.ts +4 -1
- package/src/cli/commands/module-upgrade.test.ts +130 -1
- package/src/cli/commands/module-upgrade.ts +77 -3
- package/src/cli/commands/module-verify.test.ts +3 -4
- package/src/cli/commands/module-verify.ts +0 -1
- package/src/cli/commands/notify-config.test.ts +5 -3
- package/src/cli/commands/publish/execute.ts +4 -1
- package/src/cli/commands/publish/index.ts +11 -1
- package/src/cli/commands/publish/module-registry.test.ts +24 -1
- package/src/cli/commands/publish/module-registry.ts +23 -4
- package/src/cli/commands/publish/plan.ts +1 -1
- package/src/cli/commands/publish/types.ts +7 -0
- package/src/cli/commands/registry-owner.test.ts +3 -2
- package/src/cli/commands/registry-token.test.ts +3 -2
- package/src/cli/commands/system-apply-config-equivalence.test.ts +5 -7
- package/src/cli/commands/system-audit.ts +20 -6
- package/src/cli/commands/system-config.test.ts +148 -0
- package/src/cli/commands/system-config.ts +26 -1
- package/src/cli/commands/system-doctor-remediation-gate.test.ts +295 -0
- package/src/cli/commands/system-doctor.test.ts +143 -6
- package/src/cli/commands/system-doctor.ts +142 -25
- package/src/cli/commands/system-init-deprecation.test.ts +7 -4
- package/src/cli/commands/system-migrate.test.ts +2 -1
- package/src/cli/commands/system-update.ts +5 -0
- package/src/cli/completion.ts +8 -2
- package/src/cli/flag-surface-gate.test.ts +279 -0
- package/src/cli/index.ts +70 -3
- package/src/cli/parser.test.ts +37 -1
- package/src/cli/restore-command.test.ts +5 -4
- package/src/cli/restore-migration-failure.test.ts +4 -3
- package/src/cli/tui/audit-state.ts +2 -0
- package/src/config/paths.test.ts +22 -22
- package/src/db/client.test.ts +46 -1
- package/src/db/client.ts +26 -0
- package/src/db/migrate.test.ts +2 -1
- package/src/db/migrate.ts +16 -16
- package/src/db/schema.ts +63 -0
- package/src/hooks/capability-loader-firewall.test.ts +9 -1
- package/src/hooks/capability-loader.test.ts +78 -0
- package/src/hooks/capability-loader.ts +45 -2
- package/src/hooks/executor.test.ts +161 -1
- package/src/hooks/executor.ts +87 -21
- package/src/hooks/hook-jail-unreachability.test.ts +17 -18
- package/src/hooks/hook-store.test.ts +6 -0
- package/src/hooks/hook-trespass.test.ts +26 -9
- package/src/hooks/jail.test.ts +152 -5
- package/src/hooks/jail.ts +93 -4
- package/src/manifest/contracts/v1.ts +13 -0
- package/src/module/packaging/audit.ts +9 -26
- package/src/module/packaging/build-paths.test.ts +127 -0
- package/src/module/packaging/build-paths.ts +175 -0
- package/src/module/packaging/build.test.ts +71 -1
- package/src/module/packaging/build.ts +60 -0
- package/src/module/packaging/extract.ts +1 -5
- package/src/policy/module-script-scan.ts +51 -107
- package/src/policy/no-hand-built-ssh.test.ts +5 -1
- package/src/policy/no-swallowed-refusal.test.ts +14 -15
- package/src/registry/client.test.ts +2 -2
- package/src/secrets/storage.test.ts +1 -1
- package/src/services/alerting/ack.test.ts +2 -1
- package/src/services/alerting/cadence-migration.test.ts +3 -2
- package/src/services/alerting/coverage-source.test.ts +2 -1
- package/src/services/alerting/deferral.test.ts +2 -1
- package/src/services/alerting/delivery-loop.test.ts +2 -1
- package/src/services/alerting/deploy-hooks.test.ts +2 -1
- package/src/services/alerting/inbound-poller.test.ts +2 -1
- package/src/services/alerting/inbound.test.ts +2 -1
- package/src/services/alerting/keys.test.ts +4 -0
- package/src/services/alerting/keys.ts +12 -2
- package/src/services/alerting/notification-responder.test.ts +2 -1
- package/src/services/alerting/run-monitor.test.ts +2 -1
- package/src/services/alerting/store.test.ts +2 -1
- package/src/services/alerting/sweep-runner.test.ts +2 -1
- package/src/services/alerting/tokens.test.ts +2 -1
- package/src/services/aspect-approvals.test.ts +2 -1
- package/src/services/aspect-reconcile.test.ts +4 -3
- package/src/services/aspect-runner.test.ts +2 -1
- package/src/services/audit/health.test.ts +97 -2
- package/src/services/audit/health.ts +64 -2
- package/src/services/audit/index.test.ts +22 -0
- package/src/services/audit/index.ts +8 -2
- package/src/services/audit/jail-exemptions.test.ts +42 -0
- package/src/services/audit/jail-exemptions.ts +44 -0
- package/src/services/audit/module-integrity.test.ts +23 -22
- package/src/services/audit/module-integrity.ts +7 -16
- package/src/services/audit/types.ts +2 -1
- package/src/services/backup-age-agreement.test.ts +2 -1
- package/src/services/backup-create.ts +9 -0
- package/src/services/backup-envelope-roundtrip.test.ts +3 -2
- package/src/services/backup-in-flight-refusal.test.ts +3 -2
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +48 -0
- package/src/services/build-bus/hook-dispatcher.ts +49 -1
- package/src/services/bus-ensure-flow.test.ts +3 -2
- package/src/services/bus-interview-park.test.ts +4 -3
- package/src/services/bus-interview.test.ts +2 -2
- package/src/services/bus-secret-flow.test.ts +3 -2
- package/src/services/capability-table-rows.test.ts +2 -1
- package/src/services/celilo-events.test.ts +1 -1
- package/src/services/celilo-mgmt-hooks.test.ts +23 -5
- package/src/services/consumer-cleanup.test.ts +3 -2
- package/src/services/container-service.test.ts +3 -2
- package/src/services/control-plane-bootstrap.test.ts +2 -1
- package/src/services/cross-module-read.test.ts +4 -3
- package/src/services/deploy-preflight.ts +7 -0
- package/src/services/deploy-validation.test.ts +3 -2
- package/src/services/deployed-systems.ts +1 -1
- package/src/services/dns-internal-records.test.ts +3 -2
- package/src/services/dns-provider-backfill.test.ts +2 -1
- package/src/services/dns-registrations.test.ts +2 -1
- package/src/services/ensure-interview.test.ts +3 -2
- package/src/services/fleet-checks.test.ts +126 -6
- package/src/services/fleet-checks.ts +176 -3
- package/src/services/fleet-key.test.ts +3 -2
- package/src/services/health-runner.test.ts +87 -2
- package/src/services/health-runner.ts +40 -11
- package/src/services/infrastructure-selector.test.ts +3 -2
- package/src/services/infrastructure-variable-resolver.test.ts +2 -1
- package/src/services/jail-exemptions.test.ts +125 -0
- package/src/services/jail-exemptions.ts +81 -0
- package/src/services/machine-pool.test.ts +3 -2
- package/src/services/module-config.test.ts +2 -1
- package/src/services/module-deploy.dns-repoint.test.ts +2 -1
- package/src/services/module-deploy.ts +48 -24
- package/src/services/module-operations.test.ts +2 -1
- package/src/services/module-subscriptions.test.ts +4 -3
- package/src/services/network-discovery.test.ts +64 -1
- package/src/services/network-discovery.ts +30 -5
- package/src/services/port-forwards.test.ts +2 -1
- package/src/services/programmatic-responder.aspect.test.ts +2 -1
- package/src/services/proxmox-reconcile.test.ts +2 -1
- package/src/services/responder-probe.test.ts +1 -1
- package/src/services/restore-from-file.test.ts +7 -6
- package/src/services/restore-preflight.test.ts +3 -2
- package/src/services/secret-schema-loader.test.ts +2 -1
- package/src/services/ssh-key-manager.test.ts +4 -3
- package/src/services/static-content-converge.test.ts +5 -4
- package/src/services/static-content-converge.ts +33 -10
- package/src/services/system-state-stage.test.ts +4 -3
- package/src/services/trusted-sources.test.ts +3 -2
- package/src/services/update/orchestrator.test.ts +2 -0
- package/src/services/zone-detector.test.ts +2 -1
- package/src/test-utils/bus-responder.ts +6 -4
- package/src/test-utils/db-path.ts +25 -0
- package/src/test-utils/integration.ts +7 -0
- package/src/test-utils/module-fixtures.ts +5 -6
- package/src/module/packaging/generated-plane.test.ts +0 -79
- package/src/module/packaging/generated-plane.ts +0 -134
|
@@ -293,6 +293,8 @@ describe('Firewall Chain Building', () => {
|
|
|
293
293
|
upsertModuleConfig(db, 'iptables', 'nat_ip', '192.168.0.253');
|
|
294
294
|
upsertModuleConfig(db, 'iptables', 'default_route_zone', 'isp-transit');
|
|
295
295
|
upsertModuleConfig(db, 'iptables', 'isolate_transit_network', true);
|
|
296
|
+
upsertModuleConfig(db, 'iptables', 'interface_baseline', 'eth0,eth2');
|
|
297
|
+
upsertModuleConfig(db, 'iptables', 'interface_zone_map', 'eth0=isp-transit,eth2=internal');
|
|
296
298
|
|
|
297
299
|
const config = await loadedConfig();
|
|
298
300
|
expect(config.defaultRouteZone).toBe('isp-transit');
|
|
@@ -300,6 +302,8 @@ describe('Firewall Chain Building', () => {
|
|
|
300
302
|
// preserves the manifest-declared type, and the module tests
|
|
301
303
|
// `state.isolateTransitNetwork && ...` — where 'false' would be truthy.
|
|
302
304
|
expect(config.isolateTransitNetwork).toBe(true);
|
|
305
|
+
expect(config.interfaceBaseline).toEqual(['eth0', 'eth2']);
|
|
306
|
+
expect(config.interfaceZoneMap).toBe('eth0=isp-transit,eth2=internal');
|
|
303
307
|
});
|
|
304
308
|
|
|
305
309
|
test('single provider: the recorded interface_baseline is forwarded as a parsed list', async () => {
|
|
@@ -358,13 +362,15 @@ describe('Firewall Chain Building', () => {
|
|
|
358
362
|
upsertModuleConfig(db, 'iptables', 'default_route_zone', 'internal');
|
|
359
363
|
upsertModuleConfig(db, 'iptables', 'isolate_transit_network', true);
|
|
360
364
|
upsertModuleConfig(db, 'iptables', 'interface_baseline', 'eth0,eth1');
|
|
365
|
+
upsertModuleConfig(db, 'iptables', 'interface_zone_map', 'eth0=isp-transit,eth1=internal');
|
|
361
366
|
|
|
362
367
|
const config = await loadedConfig();
|
|
363
368
|
expect(config.defaultRouteZone).toBe('internal');
|
|
364
369
|
expect(config.isolateTransitNetwork).toBe(true);
|
|
365
|
-
// The downstream construction site already forwarded
|
|
370
|
+
// The downstream construction site already forwarded these; pinned so the
|
|
366
371
|
// two sites cannot drift apart again.
|
|
367
372
|
expect(config.interfaceBaseline).toEqual(['eth0', 'eth1']);
|
|
373
|
+
expect(config.interfaceZoneMap).toBe('eth0=isp-transit,eth1=internal');
|
|
368
374
|
});
|
|
369
375
|
|
|
370
376
|
test('unset settings arrive undefined, not as a wrong default', async () => {
|
|
@@ -378,6 +384,8 @@ describe('Firewall Chain Building', () => {
|
|
|
378
384
|
// would be silently overridden by a stale copy here.
|
|
379
385
|
expect(config.defaultRouteZone).toBeUndefined();
|
|
380
386
|
expect(config.isolateTransitNetwork).toBeUndefined();
|
|
387
|
+
expect(config.interfaceBaseline).toBeUndefined();
|
|
388
|
+
expect(config.interfaceZoneMap).toBeUndefined();
|
|
381
389
|
});
|
|
382
390
|
});
|
|
383
391
|
});
|
|
@@ -7,7 +7,9 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import type { HookLogger, RouteReadView } from '@celilo/capabilities';
|
|
10
|
+
import { and, eq } from 'drizzle-orm';
|
|
10
11
|
import type { DbClient } from '../db/client';
|
|
12
|
+
import { moduleConfigs } from '../db/schema';
|
|
11
13
|
import { upsertModuleConfig } from '../services/module-config';
|
|
12
14
|
import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database';
|
|
13
15
|
import { loadCapabilityFunctions } from './capability-loader';
|
|
@@ -40,6 +42,36 @@ export default function createDhcpServer(context) {
|
|
|
40
42
|
}
|
|
41
43
|
`;
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* A factory that WRITES through the hook-owned-state accessor, so the loader's
|
|
47
|
+
* store view is proven end to end: the method's write lands as a hook-owned
|
|
48
|
+
* row, and an undeclared name throws instead of landing. Branded
|
|
49
|
+
* (defineCapabilityFunction, the Phase 8 pattern) — the branded path is the
|
|
50
|
+
* one the loader hands the store views to. The temp dir the fixture is
|
|
51
|
+
* dropped into has no node_modules link, so the import uses the workspace
|
|
52
|
+
* package by absolute path, the same trick capability-loader-firewall uses.
|
|
53
|
+
*/
|
|
54
|
+
const TEST_STORE_WRITING_DHCP_MODULE = `
|
|
55
|
+
import { defineCapabilityFunction } from '${join(__dirname, '../../../..', 'packages/capabilities/src/index.ts')}';
|
|
56
|
+
|
|
57
|
+
export default defineCapabilityFunction({
|
|
58
|
+
capability: 'dhcp_server',
|
|
59
|
+
handler: (context) => ({
|
|
60
|
+
async setDnsServers() {},
|
|
61
|
+
async getDnsServers() { return [context.config.marker]; },
|
|
62
|
+
async setDomainName() {},
|
|
63
|
+
async getDomainName() { return context.config.marker; },
|
|
64
|
+
async claimIp() {
|
|
65
|
+
await context.config.set('public_ip', '203.0.113.7');
|
|
66
|
+
return 'claimed';
|
|
67
|
+
},
|
|
68
|
+
async claimUndeclared() {
|
|
69
|
+
await context.config.set('no_such_key', 'x');
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
`;
|
|
74
|
+
|
|
43
75
|
describe('Capability Loader', () => {
|
|
44
76
|
let db: DbClient;
|
|
45
77
|
let tempDir: string;
|
|
@@ -134,6 +166,52 @@ describe('Capability Loader', () => {
|
|
|
134
166
|
expect(await dhcp.getDomainName()).toBe('internal-dhcp');
|
|
135
167
|
});
|
|
136
168
|
|
|
169
|
+
test('a provider factory writes hook-owned config through the injected store view', async () => {
|
|
170
|
+
// The store view must do more than carry the plain record a factory reads.
|
|
171
|
+
// A method writing `context.config.set` lands a hook-owned row (validated
|
|
172
|
+
// against the PROVIDER's manifest, source column 'hook'), and an
|
|
173
|
+
// undeclared name throws instead of writing a phantom row.
|
|
174
|
+
const modulePath = join(tempDir, 'storewriting-vpn');
|
|
175
|
+
const scriptsDir = join(modulePath, 'scripts');
|
|
176
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
177
|
+
writeFileSync(join(scriptsDir, 'dhcp-server-functions.ts'), TEST_STORE_WRITING_DHCP_MODULE);
|
|
178
|
+
const manifest = JSON.stringify({
|
|
179
|
+
variables: { owns: [{ name: 'public_ip', type: 'string', source: 'hook' }] },
|
|
180
|
+
});
|
|
181
|
+
db.$client.run(
|
|
182
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('storewriting-vpn', 'StoreWritingVpn', '1.0.0', '${modulePath}', '${manifest.replace(/'/g, "''")}')`,
|
|
183
|
+
);
|
|
184
|
+
db.$client.run(
|
|
185
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, registered_at) VALUES ('storewriting-vpn', 'dhcp_server', '1.0.0', '{}', unixepoch())`,
|
|
186
|
+
);
|
|
187
|
+
upsertModuleConfig(db, 'storewriting-vpn', 'marker', 'storewriting-vpn');
|
|
188
|
+
|
|
189
|
+
const result = await loadCapabilityFunctions('store-consumer', db, noopLogger);
|
|
190
|
+
const dhcp = result.dhcp_server as {
|
|
191
|
+
claimIp(): Promise<string>;
|
|
192
|
+
claimUndeclared(): Promise<void>;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
expect(await dhcp.claimIp()).toBe('claimed');
|
|
196
|
+
const row = db
|
|
197
|
+
.select()
|
|
198
|
+
.from(moduleConfigs)
|
|
199
|
+
.where(
|
|
200
|
+
and(eq(moduleConfigs.moduleId, 'storewriting-vpn'), eq(moduleConfigs.key, 'public_ip')),
|
|
201
|
+
)
|
|
202
|
+
.get();
|
|
203
|
+
expect(row?.valueJson).toBe('"203.0.113.7"');
|
|
204
|
+
expect(row?.source).toBe('hook');
|
|
205
|
+
|
|
206
|
+
// The store round-trips text, so the map read the NEXT load takes sees the
|
|
207
|
+
// same string the write sent.
|
|
208
|
+
const reloaded = await loadCapabilityFunctions('store-consumer', db, noopLogger);
|
|
209
|
+
const dhcpAgain = reloaded.dhcp_server as { getDnsServers(): Promise<string[]> };
|
|
210
|
+
expect(await dhcpAgain.getDnsServers()).toEqual(['storewriting-vpn']);
|
|
211
|
+
|
|
212
|
+
await expect(dhcp.claimUndeclared()).rejects.toThrow(/no declared hook-owned config/);
|
|
213
|
+
});
|
|
214
|
+
|
|
137
215
|
test('falls back to a zone-agnostic provider when no explicit zone provider exists', async () => {
|
|
138
216
|
const modulePath = join(tempDir, 'upstream-dhcp');
|
|
139
217
|
const scriptsDir = join(modulePath, 'scripts');
|
|
@@ -60,6 +60,9 @@ import {
|
|
|
60
60
|
import { resolveComputedFields } from '../variables/computed/evaluate';
|
|
61
61
|
import { containsComputedMarker } from '../variables/computed/marker';
|
|
62
62
|
import { buildProviderLookup } from '../variables/computed/provider-lookup';
|
|
63
|
+
import type { BufferedStoreOp } from './hook-protocol';
|
|
64
|
+
import { type HookStores, createHookStores } from './hook-store';
|
|
65
|
+
import { type StoreTransport, buildStoreView } from './hook-store-proxy';
|
|
63
66
|
import { loadHookConfigMap } from './load-hook-config';
|
|
64
67
|
|
|
65
68
|
/**
|
|
@@ -260,6 +263,13 @@ export async function loadCapabilityFunctions(
|
|
|
260
263
|
// This allows modules to optionally consume capabilities at runtime
|
|
261
264
|
// (e.g., caddy uses dns_registrar if available, falls back if not).
|
|
262
265
|
const verbose = process.env.CELILO_DEBUG === '1';
|
|
266
|
+
|
|
267
|
+
// The broker-side stores, resolved from the PROVIDER on the first store
|
|
268
|
+
// call this consumer load makes, and shared across every capability it
|
|
269
|
+
// builds. Nothing pays for the manifest read until a factory actually
|
|
270
|
+
// writes or reads through the accessor. See the storeTransport construction
|
|
271
|
+
// at the factory-invocation site below.
|
|
272
|
+
let storesPromise: Promise<HookStores> | undefined;
|
|
263
273
|
const debugLog = verbose
|
|
264
274
|
? (msg: string) => process.stderr.write(`[capability-loader] ${msg}\n`)
|
|
265
275
|
: (_msg: string) => {};
|
|
@@ -392,10 +402,35 @@ export async function loadCapabilityFunctions(
|
|
|
392
402
|
// Phase 8 path: branded defineCapabilityFunction factory. Call with
|
|
393
403
|
// the canonical { config, secrets, logger } context. The factory
|
|
394
404
|
// applies wrapWithLogging internally so we don't double-wrap.
|
|
405
|
+
//
|
|
406
|
+
// Both maps arrive as the hook-owned-state view (buildStoreView): the
|
|
407
|
+
// plain record the factory reads, PLUS get/set/delete/transaction that
|
|
408
|
+
// reach the broker-side stores (createHookStores), validated against
|
|
409
|
+
// the PROVIDER module's own manifest. The transport is in-process — a
|
|
410
|
+
// capability factory runs here in celilo's process, not in the jail —
|
|
411
|
+
// but the view is the same object a jailed hook sees, so a provider's
|
|
412
|
+
// self-config write goes through the same validated door regardless of
|
|
413
|
+
// which process it runs in (hook-owned-state tasks 0.7 and 5.5 shape).
|
|
395
414
|
if (isCompiledCapabilityFactory(exported)) {
|
|
415
|
+
const storeTransport: StoreTransport = (store, method, args) => {
|
|
416
|
+
storesPromise ??= createHookStores(db, capability.moduleId);
|
|
417
|
+
return storesPromise.then((backend: HookStores): Promise<unknown> => {
|
|
418
|
+
const one = backend[store];
|
|
419
|
+
switch (method) {
|
|
420
|
+
case 'transaction':
|
|
421
|
+
return one.applyTransaction(args[0] as readonly BufferedStoreOp[]);
|
|
422
|
+
case 'set':
|
|
423
|
+
return one.set(args[0] as string, args[1] as string);
|
|
424
|
+
case 'delete':
|
|
425
|
+
return one.delete(args[0] as string);
|
|
426
|
+
default:
|
|
427
|
+
return one.get(args[0] as string);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
};
|
|
396
431
|
const capabilityInterface = exported({
|
|
397
|
-
config: providerConfig,
|
|
398
|
-
secrets: providerSecrets,
|
|
432
|
+
config: buildStoreView('config', providerConfig, storeTransport),
|
|
433
|
+
secrets: buildStoreView('secrets', providerSecrets, storeTransport),
|
|
399
434
|
systems: getModuleSystems(capability.moduleId, db),
|
|
400
435
|
logger,
|
|
401
436
|
// WHO IS CALLING, so a provider can scope per-consumer state to them.
|
|
@@ -803,6 +838,10 @@ function buildCapabilityInterface(
|
|
|
803
838
|
// baseline was recorded but never read back, so every converge stayed
|
|
804
839
|
// in onboarding mode and refused instead of isolating (ce-qzxm).
|
|
805
840
|
interfaceBaseline: parseInterfaceBaseline(config.interface_baseline),
|
|
841
|
+
// The learned zone-to-leg map, same round trip and the same site that
|
|
842
|
+
// dropped it. The module writes it at the end of a clean converge and
|
|
843
|
+
// reads it back to keep a managed /32 on its declared zone leg.
|
|
844
|
+
interfaceZoneMap: config.interface_zone_map as string | undefined,
|
|
806
845
|
// Operator settings, forwarded verbatim. `parseStoredConfigValue`
|
|
807
846
|
// preserves each manifest-declared type, so the boolean arrives as a
|
|
808
847
|
// boolean and needs no coercion here.
|
|
@@ -1326,6 +1365,10 @@ async function buildFirewallChain(
|
|
|
1326
1365
|
// Absent means the box has never converged cleanly, so an interface
|
|
1327
1366
|
// celilo cannot attribute refuses rather than being disabled.
|
|
1328
1367
|
interfaceBaseline: parseInterfaceBaseline(provConfig.interface_baseline),
|
|
1368
|
+
// The managed-address resolver consults this before it reads the live
|
|
1369
|
+
// address table. Dropping it makes the fallback select the management
|
|
1370
|
+
// (SSH) interface, after which the bad alias can corrupt the learned map.
|
|
1371
|
+
interfaceZoneMap: provConfig.interface_zone_map as string | undefined,
|
|
1329
1372
|
// Which declared zone carries this firewall's default route, and
|
|
1330
1373
|
// whether fronted zones may initiate into it. Both are operator
|
|
1331
1374
|
// settings and both are read HERE or nowhere: the module declares them
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
2
|
import { existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { closeDb, getDb } from '../db/client';
|
|
6
|
+
import { runMigrations } from '../db/migrate';
|
|
7
|
+
import { modules, systemConfig } from '../db/schema';
|
|
5
8
|
import type { ContractHookSignature } from '../manifest/contracts';
|
|
9
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
6
10
|
import {
|
|
7
11
|
checkRequiredCapabilities,
|
|
8
12
|
declaredPathInputs,
|
|
@@ -329,6 +333,162 @@ describe('Hook Executor', () => {
|
|
|
329
333
|
});
|
|
330
334
|
});
|
|
331
335
|
|
|
336
|
+
describe('the executor resolves the jail policy through stored config (hook-jail-config-surface slice 4)', () => {
|
|
337
|
+
// Each test gets its own migrated scratch database and its own jail-mode
|
|
338
|
+
// store, so a planted row here never leaks into another test (or another
|
|
339
|
+
// host). With no module tree in `options.jail`, `planJailedSpawn` treats a
|
|
340
|
+
// `required` policy as a hard failure on EVERY host — which makes the
|
|
341
|
+
// resolution outcome observable without depending on what jail backend a
|
|
342
|
+
// given machine happens to have.
|
|
343
|
+
let dir: string;
|
|
344
|
+
const saved: Record<string, string | undefined> = {};
|
|
345
|
+
|
|
346
|
+
const plantPolicyRow = (value: string): void => {
|
|
347
|
+
getDb().insert(systemConfig).values({ key: 'hooks.jail_policy', value }).run();
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// The per-module row is keyed by module id, but the executor is handed
|
|
351
|
+
// the module's tree — the join the production read makes (task 1.4). The
|
|
352
|
+
// path is fake: nothing in this describe touches the filesystem beyond
|
|
353
|
+
// the scratch DB, and `existsSync(scriptPath)` fails before any policy
|
|
354
|
+
// read when it is not fake.
|
|
355
|
+
const moduleTree = (): string => join(dir, 'modules', 'fixture-module');
|
|
356
|
+
const plantModulePolicyRow = (moduleId: string, policy: string): void => {
|
|
357
|
+
getDb()
|
|
358
|
+
.insert(modules)
|
|
359
|
+
.values({
|
|
360
|
+
id: moduleId,
|
|
361
|
+
name: moduleId,
|
|
362
|
+
version: '1.0.0',
|
|
363
|
+
manifestData: {},
|
|
364
|
+
sourcePath: moduleTree(),
|
|
365
|
+
})
|
|
366
|
+
.run();
|
|
367
|
+
// Raw SQL, not the drizzle table, on purpose: the drizzle type only
|
|
368
|
+
// accepts 'auto' | 'off' | 'required', and the bad-value test models a
|
|
369
|
+
// row that arrived past every typed writer — a restore or a hand edit,
|
|
370
|
+
// the untrusted path the resolver throws on (Rule 3.7).
|
|
371
|
+
getDb()
|
|
372
|
+
.$client.prepare('INSERT INTO module_jail_policies (module_id, policy) VALUES (?, ?)')
|
|
373
|
+
.run(moduleId, policy);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const runVoidHook = (moduleTree?: string): Promise<Record<string, unknown>> => {
|
|
377
|
+
const { logger } = createCapturingLogger();
|
|
378
|
+
return executeHookScript(
|
|
379
|
+
join(FIXTURES_DIR, 'void-hook.ts'),
|
|
380
|
+
{
|
|
381
|
+
config: configStore(),
|
|
382
|
+
secrets: secretStore(),
|
|
383
|
+
systems: [],
|
|
384
|
+
logger,
|
|
385
|
+
debug: false,
|
|
386
|
+
screenshotDir: '/tmp',
|
|
387
|
+
stateDir: '/tmp',
|
|
388
|
+
capabilities: {},
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
// Absent means the system-config-only path: exactly what the tests
|
|
392
|
+
// above exercise. Present means the executor reads the module's row
|
|
393
|
+
// and resolves through it.
|
|
394
|
+
jail: moduleTree === undefined ? undefined : { modulePath: moduleTree, pathInputs: [] },
|
|
395
|
+
},
|
|
396
|
+
);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
beforeEach(async () => {
|
|
400
|
+
for (const key of ['CELILO_HOOK_JAIL', 'CELILO_HOOK_JAIL_MODE_PATH']) {
|
|
401
|
+
saved[key] = process.env[key];
|
|
402
|
+
}
|
|
403
|
+
dir = mkdtempSync(join(tmpdir(), 'celilo-executor-policy-'));
|
|
404
|
+
process.env.CELILO_DB_PATH = join(dir, 'celilo.db');
|
|
405
|
+
process.env.CELILO_HOOK_JAIL_MODE_PATH = join(dir, 'mode.json');
|
|
406
|
+
delete process.env.CELILO_HOOK_JAIL;
|
|
407
|
+
await runMigrations(process.env.CELILO_DB_PATH);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
afterEach(() => {
|
|
411
|
+
closeDb();
|
|
412
|
+
// The db path resets to the scratch path rather than restoring the
|
|
413
|
+
// saved value: that value may be another suite's temp database, or
|
|
414
|
+
// unset — which sends the next var-less reader to the operator's real
|
|
415
|
+
// celilo.db (celilo#1315).
|
|
416
|
+
resetTestDbPath();
|
|
417
|
+
for (const key of ['CELILO_HOOK_JAIL', 'CELILO_HOOK_JAIL_MODE_PATH']) {
|
|
418
|
+
if (saved[key] === undefined) delete process.env[key];
|
|
419
|
+
else process.env[key] = saved[key];
|
|
420
|
+
}
|
|
421
|
+
rmSync(dir, { recursive: true, force: true });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('a stored row decides when the env is silent', async () => {
|
|
425
|
+
plantPolicyRow('required');
|
|
426
|
+
await expect(runVoidHook()).rejects.toThrow(/no hook jail is available/);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('env wins over a conflicting stored row', async () => {
|
|
430
|
+
process.env.CELILO_HOOK_JAIL = 'off';
|
|
431
|
+
plantPolicyRow('required');
|
|
432
|
+
await expect(runVoidHook()).resolves.toBeDefined();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test('env=required still reaches the planner when the row says off', async () => {
|
|
436
|
+
process.env.CELILO_HOOK_JAIL = 'required';
|
|
437
|
+
plantPolicyRow('off');
|
|
438
|
+
await expect(runVoidHook()).rejects.toThrow(/no hook jail is available/);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('a bad stored row fails the hook instead of running it unjailed', async () => {
|
|
442
|
+
plantPolicyRow('alwayssafe');
|
|
443
|
+
await expect(runVoidHook()).rejects.toThrow(/hooks\.jail_policy='alwayssafe'/);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('nothing set resolves to the default, off (peba’s ruling on ce-rez7)', async () => {
|
|
447
|
+
// Migrated DB with no row: the stored-row read returns undefined and the
|
|
448
|
+
// resolver falls through to the default, which jails nobody.
|
|
449
|
+
await expect(runVoidHook()).resolves.toBeDefined();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('the module row beats system config (per-module-jail-policy task 1.4)', async () => {
|
|
453
|
+
// System says jail hard; the module is exempted. The exempted module's
|
|
454
|
+
// hook runs unjailed while every other module of this host would fail —
|
|
455
|
+
// the spec's own "A module runs unjailed while the fleet is jailed".
|
|
456
|
+
plantPolicyRow('required');
|
|
457
|
+
plantModulePolicyRow('exempted-module', 'off');
|
|
458
|
+
await expect(runVoidHook(moduleTree())).resolves.toBeDefined();
|
|
459
|
+
// And the same host still jails a module with no row: `required` with
|
|
460
|
+
// no jail backend is a hard failure, so the system row stays in force.
|
|
461
|
+
// Which surface the failure takes is host-determined, and both prove
|
|
462
|
+
// the hook did not run: on a host with a jail backend it dies at mount
|
|
463
|
+
// planning (the fake module tree does not exist, "cannot be built");
|
|
464
|
+
// on a host with no backend it dies at backend detection ("no hook
|
|
465
|
+
// jail is available"). The point under test is policy precedence, not
|
|
466
|
+
// the surface. (celilo#1342: the CI runner has no bubblewrap, so the
|
|
467
|
+
// mount-planning surface is unreachable there.)
|
|
468
|
+
await expect(runVoidHook(join(dir, 'modules', 'other-module'))).rejects.toThrow(
|
|
469
|
+
/cannot be built|no hook jail is available/,
|
|
470
|
+
);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('a bad per-module row fails the hook instead of running it unjailed', async () => {
|
|
474
|
+
plantModulePolicyRow('broken-module', 'alwayssafe');
|
|
475
|
+
await expect(runVoidHook(moduleTree())).rejects.toThrow(/module's jail policy='alwayssafe'/);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test('env beats a conflicting module row', async () => {
|
|
479
|
+
plantModulePolicyRow('exempted-module', 'off');
|
|
480
|
+
process.env.CELILO_HOOK_JAIL = 'required';
|
|
481
|
+
// The point is that the env overrode the module's `off`, so the hook
|
|
482
|
+
// is jailed and fails, not exempted and successful. Which failure it
|
|
483
|
+
// hits is host-determined: mount planning (the fake module tree does
|
|
484
|
+
// not exist) on a host with a jail backend, backend detection on a
|
|
485
|
+
// host without one. Either proves the hook did not run.
|
|
486
|
+
await expect(runVoidHook(moduleTree())).rejects.toThrow(
|
|
487
|
+
/cannot be built|no hook jail is available/,
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
332
492
|
describe('resolveHookTimeouts', () => {
|
|
333
493
|
test('no declaration: 60s total, 30s idle heuristic', () => {
|
|
334
494
|
expect(resolveHookTimeouts(undefined, false)).toEqual({
|
package/src/hooks/executor.ts
CHANGED
|
@@ -51,6 +51,8 @@ import {
|
|
|
51
51
|
moduleArtifactDir,
|
|
52
52
|
moduleStateDir,
|
|
53
53
|
} from '@celilo/capabilities';
|
|
54
|
+
import { getDbPath } from '../config/paths';
|
|
55
|
+
import { getDb } from '../db/client';
|
|
54
56
|
import {
|
|
55
57
|
type ContractHookSignature,
|
|
56
58
|
contractHookSignature,
|
|
@@ -73,10 +75,10 @@ import {
|
|
|
73
75
|
import {
|
|
74
76
|
type JailPlan,
|
|
75
77
|
detectJailBackend,
|
|
76
|
-
jailPolicy,
|
|
77
78
|
planJailedSpawn,
|
|
78
79
|
realpathRequest,
|
|
79
80
|
recordJailMode,
|
|
81
|
+
resolveJailPolicy,
|
|
80
82
|
runtimeModulePathsFor,
|
|
81
83
|
} from './jail';
|
|
82
84
|
import {
|
|
@@ -171,32 +173,47 @@ const FORWARDED_ENV = [
|
|
|
171
173
|
] as const;
|
|
172
174
|
|
|
173
175
|
/**
|
|
174
|
-
* The
|
|
175
|
-
*
|
|
176
|
+
* The directory holding the `celilo` bin of the running CLI package, resolved
|
|
177
|
+
* from this file's own location: src/hooks -> the package's bin/. Every
|
|
178
|
+
* distribution layout lands there. The .deb wrapper execs
|
|
179
|
+
* `$CELILO_HOME/node_modules/@celilo/cli/bin/celilo` directly (packaging/celilo/
|
|
180
|
+
* scripts/wrapper.sh), and a bun global install symlinks ~/.bun/bin/celilo into
|
|
181
|
+
* that same bin directory, whose script self-locates its package dir.
|
|
182
|
+
*/
|
|
183
|
+
export const CLI_BIN_DIR = join(import.meta.dir, '..', '..', 'bin');
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The PATH a hook child gets: the CLI package's own bin directory, then the
|
|
187
|
+
* runtime's, ahead of the inherited one — so the child can exec the SAME
|
|
188
|
+
* `celilo` the parent runs, and that celilo can still find its interpreter.
|
|
176
189
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* `
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
190
|
+
* `dirname(process.execPath)` alone is NOT the CLI's directory under the fleet
|
|
191
|
+
* layout: the .deb runs the CLI through a wrapper at /usr/local/bin/celilo that
|
|
192
|
+
* execs bun on `node_modules/@celilo/cli/src/cli/index.ts`, so the directory
|
|
193
|
+
* beside the bun binary holds only bun (measured on the fleet, 2026-09-07,
|
|
194
|
+
* ce-y0fd). Hooks then found `celilo` only because the systemd unit happened to
|
|
195
|
+
* inherit /usr/local/bin on its PATH — the same accident that broke every hook
|
|
196
|
+
* spawning `celilo` by name in the e2e management image (celilo#1300).
|
|
197
|
+
* CLI_BIN_DIR resolves where the bin actually lives, whatever layout launched
|
|
198
|
+
* the parent, and comes FIRST so `celilo` is the package's own bin rather than
|
|
199
|
+
* an operator's wrapper. The runtime directory stays second because bin/celilo
|
|
200
|
+
* needs `command -v bun` (it is bash, not a shebanged TS file).
|
|
187
201
|
*
|
|
188
|
-
* Already-present wins: an operator who has
|
|
189
|
-
* unchanged value. An absent inherited PATH still yields
|
|
190
|
-
*
|
|
202
|
+
* Already-present wins: an operator who has a directory on PATH sees an
|
|
203
|
+
* unchanged value. An absent inherited PATH still yields both directories, so
|
|
204
|
+
* a hook can always reach `celilo` even from a stripped parent; bash resolves
|
|
205
|
+
* through the loader's default search.
|
|
191
206
|
*
|
|
192
207
|
* Exported for the allow-list tests (hook-trespass.test.ts), which pin this
|
|
193
208
|
* contract the same way they pin the rest of the child environment.
|
|
194
209
|
*/
|
|
195
210
|
export function childPath(inherited: string | undefined): string {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
211
|
+
const base = inherited === undefined ? [] : inherited.split(':');
|
|
212
|
+
const prepended: string[] = [];
|
|
213
|
+
for (const dir of [CLI_BIN_DIR, dirname(process.execPath)]) {
|
|
214
|
+
if (!base.includes(dir) && !prepended.includes(dir)) prepended.push(dir);
|
|
215
|
+
}
|
|
216
|
+
return [...prepended, ...base].join(':');
|
|
200
217
|
}
|
|
201
218
|
|
|
202
219
|
/** The shim celilo spawns. Resolved from here so an npm install finds it too. */
|
|
@@ -330,6 +347,51 @@ export interface ExecuteHookOptions {
|
|
|
330
347
|
hookStores?: HookStoresProvider;
|
|
331
348
|
}
|
|
332
349
|
|
|
350
|
+
/**
|
|
351
|
+
* The stored `hooks.jail_policy` value, or undefined when there is no row or
|
|
352
|
+
* no celilo DB to ask (a dev box). Same row-read pattern as
|
|
353
|
+
* `ipam/auto-allocator.ts` and the executor's fellow consumer
|
|
354
|
+
* `cli/commands/system-doctor.ts`; the drizzle table is `systemConfig` in
|
|
355
|
+
* `db/schema.ts`.
|
|
356
|
+
*
|
|
357
|
+
* Unlike `remoteAccess`, this read happens here rather than through a
|
|
358
|
+
* caller-built input because D2 names the executor itself as one of the two
|
|
359
|
+
* consumers that fetch the stored row and hand it to the pure resolver.
|
|
360
|
+
*/
|
|
361
|
+
function readStoredJailPolicy(): string | undefined {
|
|
362
|
+
if (!existsSync(getDbPath())) return undefined;
|
|
363
|
+
const row = getDb()
|
|
364
|
+
.$client.prepare('SELECT value FROM system_config WHERE key = ?')
|
|
365
|
+
.get('hooks.jail_policy') as { value: string } | undefined;
|
|
366
|
+
return row?.value;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The module's own recorded jail policy, or undefined when there is no row
|
|
371
|
+
* (per-module-jail-policy task 1.4). The system-key read stays its own
|
|
372
|
+
* function because the two answers come from different tables.
|
|
373
|
+
*
|
|
374
|
+
* The executor is handed the module's tree, `options.jail.modulePath`, which
|
|
375
|
+
* is `mod.sourcePath` — not its id, which the caller holds. The row is found
|
|
376
|
+
* the way the jail inputs identify the module: by joining `modules` on
|
|
377
|
+
* `source_path`, the exact string the caller passed, so no second identity
|
|
378
|
+
* is invented and none of invokeHook's callers need plumbing. Absent jail
|
|
379
|
+
* inputs (an invocation that cannot be jailed) also means no module identity
|
|
380
|
+
* and no module row to read.
|
|
381
|
+
*
|
|
382
|
+
* A bad stored value is not coerced here — `resolveJailPolicy` throws on it,
|
|
383
|
+
* the same rule the system key follows (peba's ruling on ce-8832).
|
|
384
|
+
*/
|
|
385
|
+
function readStoredModuleJailPolicy(modulePath: string | undefined): string | undefined {
|
|
386
|
+
if (!modulePath || !existsSync(getDbPath())) return undefined;
|
|
387
|
+
const row = getDb()
|
|
388
|
+
.$client.prepare(
|
|
389
|
+
'SELECT p.policy AS policy FROM module_jail_policies p JOIN modules m ON m.id = p.module_id WHERE m.source_path = ?',
|
|
390
|
+
)
|
|
391
|
+
.get(modulePath) as { policy: string } | undefined;
|
|
392
|
+
return row?.policy;
|
|
393
|
+
}
|
|
394
|
+
|
|
333
395
|
/**
|
|
334
396
|
* Execute a hook script
|
|
335
397
|
*
|
|
@@ -394,7 +456,11 @@ export async function executeHookScript(
|
|
|
394
456
|
[process.execPath, HOOK_RUNNER_SPAWN_PATH],
|
|
395
457
|
mountSet,
|
|
396
458
|
detectJailBackend(),
|
|
397
|
-
|
|
459
|
+
resolveJailPolicy(
|
|
460
|
+
process.env.CELILO_HOOK_JAIL,
|
|
461
|
+
readStoredModuleJailPolicy(options.jail?.modulePath),
|
|
462
|
+
readStoredJailPolicy(),
|
|
463
|
+
).policy,
|
|
398
464
|
);
|
|
399
465
|
reportJail(jail, logger);
|
|
400
466
|
// Only a real module hook says anything about whether this HOST jails. An
|
|
@@ -33,7 +33,7 @@ import { join, resolve } from 'node:path';
|
|
|
33
33
|
/** Uniquely named so it never collides with a real key on a jailed dev box. */
|
|
34
34
|
const PLANTED_KEY_NAME = 'id_celilo_jail_probe';
|
|
35
35
|
import { executeHookScript } from './executor';
|
|
36
|
-
import { detectJailBackend
|
|
36
|
+
import { detectJailBackend } from './jail';
|
|
37
37
|
import { createCapturingLogger } from './logger';
|
|
38
38
|
import { configStore, secretStore } from './test-fixtures/store-backed';
|
|
39
39
|
import type { HookContext } from './types';
|
|
@@ -46,12 +46,15 @@ interface Probe {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const availability = detectJailBackend();
|
|
49
|
-
//
|
|
50
|
-
// operator switch
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
49
|
+
// Gated on the BACKEND alone, not on `jailPolicy()`. The ambient policy is an
|
|
50
|
+
// operator switch (`CELILO_HOOK_JAIL=off` is the default), and reading it here
|
|
51
|
+
// made the suite skip on every host with the default — measuring nothing, on
|
|
52
|
+
// any platform, forever. The suite does not need the operator's blessing: it
|
|
53
|
+
// sets the policy to `required` inside `runProbe()` and restores it after, so
|
|
54
|
+
// the jail under test is this suite's own act, deterministic wherever a
|
|
55
|
+
// backend exists. (celilo#1359: since the default became `off`, all three
|
|
56
|
+
// jailed suites skipped everywhere.)
|
|
57
|
+
const jailed = availability.backend !== 'none';
|
|
55
58
|
|
|
56
59
|
interface Rig {
|
|
57
60
|
outputs: {
|
|
@@ -208,18 +211,14 @@ describe.skipIf(!jailed)(`the jail is real (backend: ${availability.backend})`,
|
|
|
208
211
|
describe.skipIf(jailed)('this run is not jailed', () => {
|
|
209
212
|
test('says so, rather than reporting a pass it did not earn', () => {
|
|
210
213
|
// Not an assertion about the product. It is the line that stops a green
|
|
211
|
-
// run on
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
// run on a backend-less host reading as "the jail was proven". The only
|
|
215
|
+
// way here is `backend === 'none'` — an operator who set the policy off
|
|
216
|
+
// does NOT land in this companion, because the live suite above now pins
|
|
217
|
+
// the policy itself and runs anyway (celilo#1359).
|
|
218
|
+
const why = availability.reason ?? 'no backend';
|
|
216
219
|
console.log(
|
|
217
|
-
`\nhook jail: SKIPPED the live suite — ${why}\nThese properties are proven wherever a backend is available
|
|
220
|
+
`\nhook jail: SKIPPED the live suite — ${why}\nThese properties are proven wherever a jail backend is available. The hermetic half runs everywhere (jail.test.ts).`,
|
|
218
221
|
);
|
|
219
|
-
|
|
220
|
-
// disjunction rather than one of them. Asserting `backend === 'none'` alone
|
|
221
|
-
// went red the day macOS got a backend AND the operator turned it off — a
|
|
222
|
-
// combination that is not a defect in anything.
|
|
223
|
-
expect(availability.backend === 'none' || jailPolicy() === 'off').toBe(true);
|
|
222
|
+
expect(availability.backend === 'none').toBe(true);
|
|
224
223
|
});
|
|
225
224
|
});
|
|
@@ -14,6 +14,7 @@ import { join } from 'node:path';
|
|
|
14
14
|
import { type DbClient, closeDb, createDbClient } from '../db/client';
|
|
15
15
|
import { runMigrations } from '../db/migrate';
|
|
16
16
|
import { moduleConfigs, modules, secrets } from '../db/schema';
|
|
17
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
17
18
|
import { createHookStores } from './hook-store';
|
|
18
19
|
|
|
19
20
|
describe('hook stores', () => {
|
|
@@ -54,6 +55,11 @@ describe('hook stores', () => {
|
|
|
54
55
|
|
|
55
56
|
afterEach(() => {
|
|
56
57
|
closeDb();
|
|
58
|
+
// The variable pointed at this suite's (now removed) temp database;
|
|
59
|
+
// leaving it set sends the next var-less reader at a dangling path.
|
|
60
|
+
// Reset to the scratch path so no later reader reaches the operator's
|
|
61
|
+
// real celilo.db (celilo#1315).
|
|
62
|
+
resetTestDbPath();
|
|
57
63
|
rmSync(testDir, { recursive: true, force: true });
|
|
58
64
|
delete process.env.CELILO_MASTER_KEY_PATH;
|
|
59
65
|
});
|