@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
|
@@ -31,9 +31,9 @@ import {
|
|
|
31
31
|
listUnpublishedWorkspacePackages,
|
|
32
32
|
planFallbacks,
|
|
33
33
|
} from '../../../../scripts/workspace-fallback';
|
|
34
|
-
import { childPath, executeHookScript, hookChildEnv } from './executor';
|
|
34
|
+
import { CLI_BIN_DIR, childPath, executeHookScript, hookChildEnv } from './executor';
|
|
35
35
|
import { HOOK_PROTOCOL_VERSION } from './hook-protocol';
|
|
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';
|
|
@@ -246,19 +246,29 @@ describe('the child environment is an allow-list', () => {
|
|
|
246
246
|
// celilo#1300: a hook that spawns `celilo` by name must find the same CLI
|
|
247
247
|
// the parent runs, whatever the inherited PATH looks like.
|
|
248
248
|
describe('childPath', () => {
|
|
249
|
-
test('prepends the runtime
|
|
249
|
+
test('prepends the CLI bin and runtime directories so the child reaches the parent CLI', () => {
|
|
250
250
|
const child = childPath('/usr/local/bin:/usr/bin');
|
|
251
|
-
expect(child.startsWith(`${dirname(process.execPath)}:`)).toBe(true);
|
|
251
|
+
expect(child.startsWith(`${CLI_BIN_DIR}:${dirname(process.execPath)}:`)).toBe(true);
|
|
252
252
|
expect(child.endsWith('/usr/local/bin:/usr/bin')).toBe(true);
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
-
test('leaves an inherited PATH that already carries the
|
|
256
|
-
const already = `${dirname(process.execPath)}:/usr/bin`;
|
|
255
|
+
test('leaves an inherited PATH that already carries the directories unchanged', () => {
|
|
256
|
+
const already = `${CLI_BIN_DIR}:${dirname(process.execPath)}:/usr/bin`;
|
|
257
257
|
expect(childPath(already)).toBe(already);
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
-
test('substitutes
|
|
261
|
-
expect(childPath(undefined)).toBe(dirname(process.execPath));
|
|
260
|
+
test('substitutes both directories when the parent has no PATH at all', () => {
|
|
261
|
+
expect(childPath(undefined)).toBe(`${CLI_BIN_DIR}:${dirname(process.execPath)}`);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// The fleet gate (ce-y0fd): the .deb layout runs the CLI through
|
|
265
|
+
// $CELILO_HOME/node_modules/@celilo/cli/bin/celilo, so the directory beside
|
|
266
|
+
// process.execPath holds only bun and holds no celilo. A PATH that resolves
|
|
267
|
+
// `celilo` must therefore come from the CLI package's own bin directory —
|
|
268
|
+
// and that directory must really contain the bin, or every prepended PATH
|
|
269
|
+
// entry is decoration over the same accident celilo#1300 recorded.
|
|
270
|
+
test('prepends a CLI bin directory that actually contains the celilo bin', () => {
|
|
271
|
+
expect(existsSync(join(CLI_BIN_DIR, 'celilo'))).toBe(true);
|
|
262
272
|
});
|
|
263
273
|
});
|
|
264
274
|
});
|
|
@@ -292,7 +302,14 @@ describe('hook process boundary — hello-trespass gate', () => {
|
|
|
292
302
|
});
|
|
293
303
|
|
|
294
304
|
const availability = detectJailBackend();
|
|
295
|
-
|
|
305
|
+
// Gated on the BACKEND alone, not on `jailPolicy()`. The ambient policy is an
|
|
306
|
+
// operator switch (`CELILO_HOOK_JAIL=off` is the default), and reading it here
|
|
307
|
+
// made stage 2 skip on every host with the default — measuring nothing. The
|
|
308
|
+
// jailed run pins the policy to `required` inside `runTrespass(true)` and
|
|
309
|
+
// restores it after, so the jail under test is this suite's own act wherever a
|
|
310
|
+
// backend exists. (celilo#1359: since the default became `off`, all three
|
|
311
|
+
// jailed suites skipped everywhere.)
|
|
312
|
+
const jailed = availability.backend !== 'none';
|
|
296
313
|
|
|
297
314
|
describe.skipIf(!jailed)(
|
|
298
315
|
`stage 2: the same hook, jailed (backend: ${availability.backend})`,
|
package/src/hooks/jail.test.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
readJailMode,
|
|
28
28
|
realpathRequest,
|
|
29
29
|
recordJailMode,
|
|
30
|
+
resolveJailPolicy,
|
|
30
31
|
runtimeModulePathsFor,
|
|
31
32
|
} from './jail';
|
|
32
33
|
import { deriveMountSet } from './mount-set';
|
|
@@ -208,9 +209,13 @@ describe('the three policies', () => {
|
|
|
208
209
|
});
|
|
209
210
|
|
|
210
211
|
test("'required' turns an unavailable jail into a hard failure (task 4.5)", () => {
|
|
211
|
-
expect(
|
|
212
|
-
planJailedSpawn(CMD, deriveMountSet(REQUEST), UNAVAILABLE, 'required', allPresent),
|
|
213
|
-
|
|
212
|
+
expect(
|
|
213
|
+
() => planJailedSpawn(CMD, deriveMountSet(REQUEST), UNAVAILABLE, 'required', allPresent),
|
|
214
|
+
// Not /CELILO_HOOK_JAIL=required/. Since hook-jail-config-surface the
|
|
215
|
+
// policy can come from the stored `hooks.jail_policy` row or the
|
|
216
|
+
// default, so a message naming the variable names a source that may be
|
|
217
|
+
// unset.
|
|
218
|
+
).toThrow(/hook jail policy is 'required'/);
|
|
214
219
|
});
|
|
215
220
|
|
|
216
221
|
test("'required' names the reason, so the operator can act on it", () => {
|
|
@@ -272,8 +277,12 @@ describe('jailPolicy reads the operator’s switch', () => {
|
|
|
272
277
|
}
|
|
273
278
|
};
|
|
274
279
|
|
|
275
|
-
test('unset is
|
|
276
|
-
expect(withEnv(undefined, jailPolicy)).toBe('
|
|
280
|
+
test('unset is off (peba, ce-rez7)', () => {
|
|
281
|
+
expect(withEnv(undefined, jailPolicy)).toBe('off');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('an explicit auto still means auto', () => {
|
|
285
|
+
expect(withEnv('auto', jailPolicy)).toBe('auto');
|
|
277
286
|
});
|
|
278
287
|
|
|
279
288
|
test('a typo is refused rather than silently meaning auto', () => {
|
|
@@ -284,6 +293,144 @@ describe('jailPolicy reads the operator’s switch', () => {
|
|
|
284
293
|
});
|
|
285
294
|
});
|
|
286
295
|
|
|
296
|
+
describe('resolveJailPolicy applies D2 precedence (hook-jail-config-surface)', () => {
|
|
297
|
+
test('an env value set and non-empty wins over stored config', () => {
|
|
298
|
+
expect(resolveJailPolicy('required', undefined, 'off')).toEqual({
|
|
299
|
+
policy: 'required',
|
|
300
|
+
source: 'env',
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('an EMPTY STRING env value counts as unset and falls through to config', () => {
|
|
305
|
+
// jailPolicy() already treats '' as unset, so the resolver agrees rather
|
|
306
|
+
// than inventing a second rule for what counts as set.
|
|
307
|
+
expect(resolveJailPolicy('', undefined, 'off')).toEqual({ policy: 'off', source: 'config' });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('stored config wins when the env var is absent', () => {
|
|
311
|
+
expect(resolveJailPolicy(undefined, undefined, 'off')).toEqual({
|
|
312
|
+
policy: 'off',
|
|
313
|
+
source: 'config',
|
|
314
|
+
});
|
|
315
|
+
expect(resolveJailPolicy(undefined, undefined, 'required')).toEqual({
|
|
316
|
+
policy: 'required',
|
|
317
|
+
source: 'config',
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('all absent resolves to off from the default (peba, ce-rez7)', () => {
|
|
322
|
+
expect(resolveJailPolicy(undefined, undefined, undefined)).toEqual({
|
|
323
|
+
policy: 'off',
|
|
324
|
+
source: 'default',
|
|
325
|
+
});
|
|
326
|
+
expect(resolveJailPolicy('', undefined, undefined)).toEqual({
|
|
327
|
+
policy: 'off',
|
|
328
|
+
source: 'default',
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('an explicit env auto still wins (peba, ce-rez7)', () => {
|
|
333
|
+
expect(resolveJailPolicy('auto', undefined, 'off')).toEqual({ policy: 'auto', source: 'env' });
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('a stored empty jail_policy THROWS, it does not resolve to auto (peba, ce-8832)', () => {
|
|
337
|
+
// The set-time pattern ^(auto|off|required)$ rejects '', so a stored one
|
|
338
|
+
// can only arrive via a restore from file or a hand edit to the DB — the
|
|
339
|
+
// untrusted paths D3 exists to catch. It is outside the enum and fails
|
|
340
|
+
// closed like any other bad stored value.
|
|
341
|
+
expect(() => resolveJailPolicy(undefined, undefined, '')).toThrow(/is not a hook jail policy/);
|
|
342
|
+
expect(() => resolveJailPolicy(undefined, undefined, '')).toThrow(/hooks\.jail_policy=''/);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('a typo in the STORED row is refused, not silently treated as auto (D3)', () => {
|
|
346
|
+
// The DB row is a trust boundary like any other (Rule 3.7): restores carry
|
|
347
|
+
// foreign state, and a typo that silently meant `auto` reads as an armed
|
|
348
|
+
// jail when nothing is armed. The config path is no laxer than the env
|
|
349
|
+
// path.
|
|
350
|
+
expect(() => resolveJailPolicy(undefined, undefined, 'alwayssafe')).toThrow(
|
|
351
|
+
/is not a hook jail policy/,
|
|
352
|
+
);
|
|
353
|
+
expect(() => resolveJailPolicy(undefined, undefined, 'alwayssafe')).toThrow(
|
|
354
|
+
/hooks\.jail_policy='alwayssafe'/,
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('a typo in the env value is still refused (unchanged jailPolicy behaviour)', () => {
|
|
359
|
+
expect(() => resolveJailPolicy('requried', undefined, undefined)).toThrow(
|
|
360
|
+
/is not a hook jail policy/,
|
|
361
|
+
);
|
|
362
|
+
expect(() => resolveJailPolicy('requried', undefined, 'off')).toThrow(
|
|
363
|
+
/CELILO_HOOK_JAIL='requried'/,
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe('resolveJailPolicy applies the four-step precedence (per-module-jail-policy task 1.3)', () => {
|
|
369
|
+
// Each boundary gets its own assertion, not just the ends: the chain is
|
|
370
|
+
// fixed and rendered, and a test that only pins (env, default) cannot tell
|
|
371
|
+
// a reordering of module and config apart from a correct one.
|
|
372
|
+
|
|
373
|
+
test('env beats the module row', () => {
|
|
374
|
+
expect(resolveJailPolicy('required', 'off', 'auto')).toEqual({
|
|
375
|
+
policy: 'required',
|
|
376
|
+
source: 'env',
|
|
377
|
+
});
|
|
378
|
+
expect(resolveJailPolicy('off', 'auto', 'auto')).toEqual({ policy: 'off', source: 'env' });
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('the module row beats system config', () => {
|
|
382
|
+
expect(resolveJailPolicy(undefined, 'off', 'auto')).toEqual({
|
|
383
|
+
policy: 'off',
|
|
384
|
+
source: 'module',
|
|
385
|
+
});
|
|
386
|
+
expect(resolveJailPolicy(undefined, 'required', 'off')).toEqual({
|
|
387
|
+
policy: 'required',
|
|
388
|
+
source: 'module',
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('system config beats the default', () => {
|
|
393
|
+
expect(resolveJailPolicy(undefined, undefined, 'auto')).toEqual({
|
|
394
|
+
policy: 'auto',
|
|
395
|
+
source: 'config',
|
|
396
|
+
});
|
|
397
|
+
expect(resolveJailPolicy(undefined, undefined, 'required')).toEqual({
|
|
398
|
+
policy: 'required',
|
|
399
|
+
source: 'config',
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('the default is off (peba, ce-rez7)', () => {
|
|
404
|
+
expect(resolveJailPolicy(undefined, undefined, undefined)).toEqual({
|
|
405
|
+
policy: 'off',
|
|
406
|
+
source: 'default',
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('an absent module row falls through to system config — absent means follow the system', () => {
|
|
411
|
+
// The everyday case: a fleet with no per-module rows behaves exactly as
|
|
412
|
+
// it does today, which is the spec's own operator guarantee.
|
|
413
|
+
expect(resolveJailPolicy(undefined, undefined, 'auto')).toEqual({
|
|
414
|
+
policy: 'auto',
|
|
415
|
+
source: 'config',
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('a bad stored per-module value THROWS rather than coercing (task 1.3)', () => {
|
|
420
|
+
// The same rule as the system key (peba, ce-8832): a restore or a hand
|
|
421
|
+
// edit is a trust boundary, and a typo that silently meant `off` would
|
|
422
|
+
// jail a module the operator exempted — or the reverse. The error names
|
|
423
|
+
// its source so a reader can tell which stored row is junk.
|
|
424
|
+
expect(() => resolveJailPolicy(undefined, 'alwayssafe', 'off')).toThrow(
|
|
425
|
+
/is not a hook jail policy/,
|
|
426
|
+
);
|
|
427
|
+
expect(() => resolveJailPolicy(undefined, 'alwayssafe', 'off')).toThrow(
|
|
428
|
+
/module's jail policy='alwayssafe'/,
|
|
429
|
+
);
|
|
430
|
+
expect(() => resolveJailPolicy(undefined, '', 'off')).toThrow(/is not a hook jail policy/);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
287
434
|
describe('realpath at the caller (task 4.2k)', () => {
|
|
288
435
|
test('a symlinked module root is resolved before it becomes a bind', () => {
|
|
289
436
|
// `mod.sourcePath` comes out of the database, and a database restored from
|
package/src/hooks/jail.ts
CHANGED
|
@@ -100,7 +100,9 @@ export function autoJailDefers(availability: JailAvailability): boolean {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* What the operator asked for. `CELILO_HOOK_JAIL`, and the default is `
|
|
103
|
+
* What the operator asked for. `CELILO_HOOK_JAIL`, and the default is `off`
|
|
104
|
+
* (peba's ruling on ce-rez7; `resolveJailPolicy`'s doc comment below has the
|
|
105
|
+
* reversal trigger).
|
|
104
106
|
*
|
|
105
107
|
* `required` is D8's end state, reached as an explicit act once stage 2 has
|
|
106
108
|
* been proven on a host: an unavailable jail becomes a hard failure rather
|
|
@@ -418,16 +420,98 @@ function unavailableReason(error: unknown): string {
|
|
|
418
420
|
* `CELILO_HOOK_JAIL=requried` that silently meant `auto` would read as the
|
|
419
421
|
* jail being enforced when it is not, which is the one mistake this variable
|
|
420
422
|
* exists to prevent.
|
|
423
|
+
*
|
|
424
|
+
* The unset/empty default is `off` (peba's ruling on ce-rez7): jailing must
|
|
425
|
+
* begin because someone set a switch, not because a package upgrade installed
|
|
426
|
+
* a backend. Turning the jail ON is a deliberate act.
|
|
421
427
|
*/
|
|
422
428
|
export function jailPolicy(): JailPolicy {
|
|
423
429
|
const raw = process.env.CELILO_HOOK_JAIL;
|
|
424
|
-
if (raw === undefined || raw === '') return '
|
|
430
|
+
if (raw === undefined || raw === '') return 'off';
|
|
425
431
|
if (raw === 'auto' || raw === 'off' || raw === 'required') return raw;
|
|
426
432
|
throw new Error(
|
|
427
433
|
`CELILO_HOOK_JAIL='${raw}' is not a hook jail policy. Use 'auto' (jail when a backend is available), 'required' (an unavailable jail is a hard failure), or 'off'.`,
|
|
428
434
|
);
|
|
429
435
|
}
|
|
430
436
|
|
|
437
|
+
/** Where the effective jail policy came from. Rendered by `system doctor` (D4). */
|
|
438
|
+
export type JailPolicySource = 'env' | 'module' | 'config' | 'default';
|
|
439
|
+
|
|
440
|
+
/** How much jailing a policy does. Lower is weaker (per-module-jail-policy). */
|
|
441
|
+
export const POLICY_STRENGTH: Record<JailPolicy, number> = { off: 0, auto: 1, required: 2 };
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Whether the module's own policy jails LESS than the system's would — the
|
|
445
|
+
* definition of an exemption (per-module-jail-policy task 3.1-3.3).
|
|
446
|
+
*
|
|
447
|
+
* Shared by `module jail`'s weakening interview, `module list`'s marker,
|
|
448
|
+
* `system doctor`'s count and `system audit`'s category, so the four
|
|
449
|
+
* surfaces cannot disagree about what an exemption is.
|
|
450
|
+
*/
|
|
451
|
+
export function isWeakerJailPolicy(modulePolicy: JailPolicy, systemPolicy: JailPolicy): boolean {
|
|
452
|
+
return POLICY_STRENGTH[modulePolicy] < POLICY_STRENGTH[systemPolicy];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Apply the precedence to the four sources (per-module-jail-policy task 1.2):
|
|
457
|
+
* env wins, then the module's own recorded policy, then stored system
|
|
458
|
+
* config, then the default, which is `off` (peba's rulings on ce-rez7 and
|
|
459
|
+
* ce-8832; see openspec/changes/hook-jail-config-surface/design.md and
|
|
460
|
+
* openspec/changes/per-module-jail-policy/ for the fixed chain).
|
|
461
|
+
*
|
|
462
|
+
* Pure, so both consumers (executor.ts and system-doctor.ts) can fetch the
|
|
463
|
+
* stored rows themselves and stay testable without a database.
|
|
464
|
+
*
|
|
465
|
+
* The empty-string env value is treated as unset, matching `jailPolicy()`'s
|
|
466
|
+
* own handling — one rule for "what counts as set", not a second one here.
|
|
467
|
+
*
|
|
468
|
+
* The stored paths are deliberately stricter than the env path: a stored
|
|
469
|
+
* empty string THROWS (peba's ruling on ce-8832). The set-time pattern
|
|
470
|
+
* `^(auto|off|required)$` rejects `''`, so a stored one can only arrive via
|
|
471
|
+
* a restore from file or a hand edit to the DB — exactly the untrusted
|
|
472
|
+
* paths D3 exists to catch. Resolving it to a silent `auto` would let
|
|
473
|
+
* foreign state pick the jailing default with nobody told, and failing
|
|
474
|
+
* closed is the whole point. An empty CELILO_HOOK_JAIL stays "not set",
|
|
475
|
+
* because `jailPolicy()` relies on that and slice 1's safety rests on the
|
|
476
|
+
* two resolvers agreeing.
|
|
477
|
+
*
|
|
478
|
+
* The module row follows the same throw rule as the system key (task 1.3).
|
|
479
|
+
* `undefined` is the everyday case — no row, follow the system — and a row
|
|
480
|
+
* keyed to a module that was removed is impossible by schema: the FK
|
|
481
|
+
* cascades the delete.
|
|
482
|
+
*/
|
|
483
|
+
export function resolveJailPolicy(
|
|
484
|
+
envValue: string | undefined,
|
|
485
|
+
moduleValue: string | undefined,
|
|
486
|
+
configValue: string | undefined,
|
|
487
|
+
): { policy: JailPolicy; source: JailPolicySource } {
|
|
488
|
+
if (envValue !== undefined && envValue !== '') {
|
|
489
|
+
if (envValue === 'auto' || envValue === 'off' || envValue === 'required') {
|
|
490
|
+
return { policy: envValue, source: 'env' };
|
|
491
|
+
}
|
|
492
|
+
throw new Error(
|
|
493
|
+
`CELILO_HOOK_JAIL='${envValue}' is not a hook jail policy. Use 'auto' (jail when a backend is available), 'required' (an unavailable jail is a hard failure), or 'off'.`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (moduleValue !== undefined) {
|
|
497
|
+
if (moduleValue === 'auto' || moduleValue === 'off' || moduleValue === 'required') {
|
|
498
|
+
return { policy: moduleValue, source: 'module' };
|
|
499
|
+
}
|
|
500
|
+
throw new Error(
|
|
501
|
+
`the module's jail policy='${moduleValue}' is not a hook jail policy. Use 'auto' (jail when a backend is available), 'required' (an unavailable jail is a hard failure), or 'off'.`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
if (configValue !== undefined) {
|
|
505
|
+
if (configValue === 'auto' || configValue === 'off' || configValue === 'required') {
|
|
506
|
+
return { policy: configValue, source: 'config' };
|
|
507
|
+
}
|
|
508
|
+
throw new Error(
|
|
509
|
+
`hooks.jail_policy='${configValue}' is not a hook jail policy. Use 'auto' (jail when a backend is available), 'required' (an unavailable jail is a hard failure), or 'off'.`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return { policy: 'off', source: 'default' };
|
|
513
|
+
}
|
|
514
|
+
|
|
431
515
|
/**
|
|
432
516
|
* Resolve every path in a mount-set request through the filesystem (task 4.2k).
|
|
433
517
|
*
|
|
@@ -568,9 +652,14 @@ function realpathOrSelf(path: string): string {
|
|
|
568
652
|
* Why an `off` policy runs unjailed, shared with `celilo system doctor` (task
|
|
569
653
|
* 4.6) so the sentence the operator reads there is the sentence the record
|
|
570
654
|
* carries.
|
|
655
|
+
*
|
|
656
|
+
* It names no source. Since hook-jail-config-surface there are three (the
|
|
657
|
+
* environment variable, the stored `hooks.jail_policy` row, and the `off`
|
|
658
|
+
* default), and this sentence is rendered for all of them. The doctor prints
|
|
659
|
+
* the source on its own line above this one; the record carries it as state.
|
|
571
660
|
*/
|
|
572
661
|
export const JAIL_OFF_REASON =
|
|
573
|
-
|
|
662
|
+
"The hook jail policy is 'off', so hooks run unjailed on this host. Set hooks.jail_policy to 'auto' or 'required' to jail them.";
|
|
574
663
|
|
|
575
664
|
export function planJailedSpawn(
|
|
576
665
|
cmd: readonly string[],
|
|
@@ -595,7 +684,7 @@ export function planJailedSpawn(
|
|
|
595
684
|
: 'This invocation has no module tree to jail, so there is no mount set to enforce.';
|
|
596
685
|
if (policy === 'required') {
|
|
597
686
|
throw new Error(
|
|
598
|
-
`
|
|
687
|
+
`The hook jail policy is 'required' and no hook jail is available. ${reason ?? ''}`.trim(),
|
|
599
688
|
);
|
|
600
689
|
}
|
|
601
690
|
return { cmd, mode: 'unjailed', backend: availability.backend, reason, skipped: [] };
|
|
@@ -173,6 +173,19 @@ export const V1_HOOKS: ContractHooks = {
|
|
|
173
173
|
* mount-set derivation walking the declared inputs could not see it.
|
|
174
174
|
*/
|
|
175
175
|
system_state_root: { required: false, path: { access: 'read' } },
|
|
176
|
+
/**
|
|
177
|
+
* The management-host inventory as a JSON array, staged by
|
|
178
|
+
* `backup-create.ts` from `listMachines()` under the same
|
|
179
|
+
* cross_module_read privilege as `system_state_root`. Not a path: the
|
|
180
|
+
* hook reads the value, not a file, so it carries no mount-set entry.
|
|
181
|
+
*
|
|
182
|
+
* It replaces on_backup's `celilo machine list --json` spawn, which the
|
|
183
|
+
* hook jail kills (no celilo binary, celilo#1225) and which never
|
|
184
|
+
* actually produced JSON before that — the CLI printed its human report,
|
|
185
|
+
* the hook's JSON.parse failed, and the catch wrote [] into every
|
|
186
|
+
* envelope (ce-y0we).
|
|
187
|
+
*/
|
|
188
|
+
machine_pool: { required: false },
|
|
176
189
|
},
|
|
177
190
|
outputs: {
|
|
178
191
|
artifact_count: { required: true },
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
} from '../../services/module-instances';
|
|
14
14
|
import { computeFileChecksum } from './checksum';
|
|
15
15
|
import type { IntegrityViolation } from './extract';
|
|
16
|
-
import { compareVerbatimRoleAssets, readVerbatimRoleAssets } from './generated-plane';
|
|
17
16
|
import { type HostPlaneResult, verifyModuleOnHosts } from './host-plane';
|
|
18
17
|
import { classifyModulePath } from './package-rules';
|
|
19
18
|
|
|
@@ -204,30 +203,11 @@ export async function auditModule(
|
|
|
204
203
|
}
|
|
205
204
|
}
|
|
206
205
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
const generatedDir = join(moduleDir, 'generated');
|
|
213
|
-
if (existsSync(generatedDir)) {
|
|
214
|
-
const differences = compareVerbatimRoleAssets(
|
|
215
|
-
await readVerbatimRoleAssets(moduleDir),
|
|
216
|
-
await readVerbatimRoleAssets(generatedDir),
|
|
217
|
-
);
|
|
218
|
-
for (const difference of differences) {
|
|
219
|
-
violations.push({
|
|
220
|
-
type: 'stale-generated',
|
|
221
|
-
path: difference.relPath,
|
|
222
|
-
expectedDigest: difference.installedDigest,
|
|
223
|
-
actualDigest: difference.generatedDigest,
|
|
224
|
-
message:
|
|
225
|
-
difference.reason === 'missing'
|
|
226
|
-
? `Generated project is missing ${difference.relPath} — the next deploy would ship nothing for it. Run 'celilo module generate ${moduleId}'.`
|
|
227
|
-
: `Generated project holds different bytes for ${difference.relPath} (generated ${difference.generatedDigest}, installed ${difference.installedDigest}) — the next deploy would ship the wrong ones. Run 'celilo module generate ${moduleId}'.`,
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
206
|
+
// The second plane (is what we would deploy built from what we installed?)
|
|
207
|
+
// retired here: the generated tree is ephemeral now (D4 of
|
|
208
|
+
// control-plane-stops-building-modules) — rendered for a deploy and
|
|
209
|
+
// deleted on success — so a persistent generated copy that verbatim role
|
|
210
|
+
// assets could be compared against no longer exists to compare.
|
|
231
211
|
|
|
232
212
|
// Check for extra files (not in checksums)
|
|
233
213
|
const actualFiles = await scanDirectory(moduleDir, moduleDir);
|
|
@@ -244,7 +224,10 @@ export async function auditModule(
|
|
|
244
224
|
}
|
|
245
225
|
|
|
246
226
|
// Plane three: is what is running what we generated? One SSH per system,
|
|
247
|
-
// so it is asked only when the caller says so.
|
|
227
|
+
// so it is asked only when the caller says so. With the generated tree
|
|
228
|
+
// ephemeral (D4), a deep audit after a successful deploy finds no playbook
|
|
229
|
+
// and says so — the remedy is generate, which is cheap and never builds.
|
|
230
|
+
const generatedDir = join(moduleDir, 'generated');
|
|
248
231
|
let hostPlane: HostPlaneResult | undefined;
|
|
249
232
|
if (options.deep) {
|
|
250
233
|
hostPlane = await verifyModuleOnHosts({
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { parse as parseYaml } from 'yaml';
|
|
6
|
+
import { resolveBuildCommandPaths } from './build-paths';
|
|
7
|
+
|
|
8
|
+
/** Repo root: this file sits at apps/celilo/src/module/packaging/. */
|
|
9
|
+
const REPO_ROOT = join(import.meta.dir, '..', '..', '..', '..', '..');
|
|
10
|
+
|
|
11
|
+
/** Every module in the repo that declares a `build:` block (design, Context). */
|
|
12
|
+
const BUILD_MODULES = [
|
|
13
|
+
'celilo-registry',
|
|
14
|
+
'celilo-web-console',
|
|
15
|
+
'celilo-website',
|
|
16
|
+
'forgejo',
|
|
17
|
+
'npm-cache-node',
|
|
18
|
+
'wireguard-manager',
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
function buildCommand(moduleId: string): string {
|
|
22
|
+
const manifestPath = join(REPO_ROOT, 'modules', moduleId, 'manifest.yml');
|
|
23
|
+
if (!existsSync(manifestPath)) throw new Error(`fixture missing: ${manifestPath}`);
|
|
24
|
+
const manifest = parseYaml(readFileSync(manifestPath, 'utf8')) as {
|
|
25
|
+
build?: { command?: string };
|
|
26
|
+
};
|
|
27
|
+
const command = manifest.build?.command;
|
|
28
|
+
if (!command) throw new Error(`${moduleId} declares no build command`);
|
|
29
|
+
return command;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('publish-time build path gate', () => {
|
|
33
|
+
test('flags celilo-registry when packaged outside the monorepo (the control-plane shape)', () => {
|
|
34
|
+
// The known-bad form, per celilo#1307: the script reaches a sibling of the
|
|
35
|
+
// module source (`../../packages/registry-server`). Inside the monorepo
|
|
36
|
+
// that resolves; from a tree where the module stands alone — a control
|
|
37
|
+
// plane, or any packaging that is not the source checkout — it does not.
|
|
38
|
+
// The spec scenario pins this: "packaged from a tree where that directory
|
|
39
|
+
// does not exist".
|
|
40
|
+
const scratch = mkdtempSync(join(tmpdir(), 'celilo-build-paths-'));
|
|
41
|
+
const moduleSourceDir = join(scratch, 'celilo-registry');
|
|
42
|
+
cpSync(join(REPO_ROOT, 'modules', 'celilo-registry'), moduleSourceDir, {
|
|
43
|
+
recursive: true,
|
|
44
|
+
});
|
|
45
|
+
const violations = resolveBuildCommandPaths(buildCommand('celilo-registry'), {
|
|
46
|
+
moduleSourceDir,
|
|
47
|
+
buildDir: moduleSourceDir,
|
|
48
|
+
});
|
|
49
|
+
expect(violations).toHaveLength(1);
|
|
50
|
+
expect(violations[0].rawPath).toContain('../../packages/registry-server');
|
|
51
|
+
expect(violations[0].resolvedPath).not.toContain(REPO_ROOT);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('flags a plain relative escape that does not resolve (spec scenario)', () => {
|
|
55
|
+
const scratch = mkdtempSync(join(tmpdir(), 'celilo-build-paths-'));
|
|
56
|
+
const violations = resolveBuildCommandPaths(
|
|
57
|
+
'cd ../../packages/registry-server && bun install',
|
|
58
|
+
{ moduleSourceDir: scratch, buildDir: scratch },
|
|
59
|
+
);
|
|
60
|
+
expect(violations).toHaveLength(1);
|
|
61
|
+
expect(violations[0].rawPath).toBe('../../packages/registry-server');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('accepts $CELILO_MODULE_SOURCE_DIR/server when it exists (npm-cache-node shape)', () => {
|
|
65
|
+
const scratch = mkdtempSync(join(tmpdir(), 'celilo-build-paths-'));
|
|
66
|
+
const moduleSourceDir = join(scratch, 'npm-cache-node');
|
|
67
|
+
cpSync(
|
|
68
|
+
join(REPO_ROOT, 'modules', 'npm-cache-node', 'server'),
|
|
69
|
+
join(moduleSourceDir, 'server'),
|
|
70
|
+
{
|
|
71
|
+
recursive: true,
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
const violations = resolveBuildCommandPaths(buildCommand('npm-cache-node'), {
|
|
75
|
+
moduleSourceDir,
|
|
76
|
+
buildDir: moduleSourceDir,
|
|
77
|
+
});
|
|
78
|
+
expect(violations).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('accepts every module build command at publish time in the monorepo', () => {
|
|
82
|
+
// The regression surface for the five healthy modules. celilo-registry
|
|
83
|
+
// passes here too, and that is correct: at publish, inside the monorepo,
|
|
84
|
+
// its `cd` target exists (the binaries shipped on 2026-08-20 prove the
|
|
85
|
+
// build ran). The gate refuses the script only where it genuinely cannot
|
|
86
|
+
// resolve — the first test above.
|
|
87
|
+
for (const moduleId of BUILD_MODULES) {
|
|
88
|
+
const moduleSourceDir = join(REPO_ROOT, 'modules', moduleId);
|
|
89
|
+
const violations = resolveBuildCommandPaths(buildCommand(moduleId), {
|
|
90
|
+
moduleSourceDir,
|
|
91
|
+
buildDir: moduleSourceDir,
|
|
92
|
+
});
|
|
93
|
+
expect(violations, moduleId).toEqual([]);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('skips a cd target it cannot resolve rather than guessing (forgejo shape)', () => {
|
|
98
|
+
// `cd "$D"` where D was assigned earlier in the same command: the gate
|
|
99
|
+
// does not interpret shell, so an unresolvable target is skipped, not
|
|
100
|
+
// failed. A guard that rejects working modules is worse than none.
|
|
101
|
+
const scratch = mkdtempSync(join(tmpdir(), 'celilo-build-paths-'));
|
|
102
|
+
const violations = resolveBuildCommandPaths('D=files && mkdir -p "$D" && cd "$D" && ls', {
|
|
103
|
+
moduleSourceDir: scratch,
|
|
104
|
+
buildDir: scratch,
|
|
105
|
+
});
|
|
106
|
+
expect(violations).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('tracks the working directory across chained cds (celilo-web-console shape)', () => {
|
|
110
|
+
// `cd $SOURCE/../../apps/console` then `cd ../console-server`: the second
|
|
111
|
+
// target resolves against the FIRST cd's destination, not the build dir.
|
|
112
|
+
const scratch = mkdtempSync(join(tmpdir(), 'celilo-build-paths-'));
|
|
113
|
+
const moduleSourceDir = join(scratch, 'web');
|
|
114
|
+
const monorepo = join(scratch, 'mono');
|
|
115
|
+
cpSync(join(REPO_ROOT, 'modules', 'celilo-web-console'), moduleSourceDir, { recursive: true });
|
|
116
|
+
// A monorepo-shaped sibling tree, minimal but real.
|
|
117
|
+
const { mkdirSync } = require('node:fs') as typeof import('node:fs');
|
|
118
|
+
mkdirSync(join(monorepo, 'apps', 'console'), { recursive: true });
|
|
119
|
+
mkdirSync(join(monorepo, 'apps', 'console-server'), { recursive: true });
|
|
120
|
+
cpSync(moduleSourceDir, join(monorepo, 'modules', 'celilo-web-console'), { recursive: true });
|
|
121
|
+
const violations = resolveBuildCommandPaths(buildCommand('celilo-web-console'), {
|
|
122
|
+
moduleSourceDir: join(monorepo, 'modules', 'celilo-web-console'),
|
|
123
|
+
buildDir: join(monorepo, 'modules', 'celilo-web-console'),
|
|
124
|
+
});
|
|
125
|
+
expect(violations).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
});
|