@celilo/cli 2.2.1 → 3.0.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 +2 -1
- 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/src/__integration__/container-services-cli.integration.test.ts +1 -1
- package/src/api/serve.ts +13 -1
- package/src/api/sessions.test.ts +3 -3
- package/src/api-clients/proxmox.ts +10 -7
- package/src/cli/backup-rename.test.ts +3 -3
- package/src/cli/cli.test.ts +1 -1
- package/src/cli/commands/events.test.ts +2 -2
- package/src/cli/commands/firewall-interface-list.test.ts +2 -2
- package/src/cli/commands/machine-list.test.ts +57 -0
- package/src/cli/commands/machine-list.ts +35 -4
- package/src/cli/commands/module-health.test.ts +35 -0
- package/src/cli/commands/module-health.ts +12 -3
- package/src/cli/commands/module-import-registry.test.ts +1 -1
- 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-publish.test.ts +28 -6
- package/src/cli/commands/module-publish.ts +14 -12
- package/src/cli/commands/module-update.test.ts +22 -7
- package/src/cli/commands/module-update.ts +73 -18
- package/src/cli/commands/module-upgrade-gate.test.ts +154 -0
- package/src/cli/commands/module-upgrade.test.ts +115 -1
- package/src/cli/commands/module-upgrade.ts +66 -4
- package/src/cli/commands/module-verify.test.ts +1 -1
- package/src/cli/commands/publish/execute.ts +4 -1
- package/src/cli/commands/publish/helpers.ts +4 -3
- package/src/cli/commands/publish/index.ts +23 -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.test.ts +64 -0
- package/src/cli/commands/publish/plan.ts +52 -19
- package/src/cli/commands/publish/types.ts +20 -0
- package/src/cli/commands/registry-owner.test.ts +1 -1
- package/src/cli/commands/registry-token.test.ts +1 -1
- package/src/cli/commands/subscribers-install-daemon.test.ts +44 -0
- package/src/cli/commands/subscribers-install-daemon.ts +107 -0
- package/src/cli/commands/subscribers-serve.test.ts +22 -0
- package/src/cli/commands/subscribers-serve.ts +22 -4
- package/src/cli/commands/system-audit.ts +34 -11
- package/src/cli/commands/system-doctor-remediation-gate.test.ts +295 -0
- package/src/cli/commands/system-doctor.test.ts +76 -10
- package/src/cli/commands/system-doctor.ts +34 -3
- package/src/cli/commands/system-init-deprecation.test.ts +1 -1
- package/src/cli/commands/system-update.ts +16 -6
- package/src/cli/completion.ts +18 -3
- package/src/cli/flag-surface-gate.test.ts +279 -0
- package/src/cli/fuel-gauge.ts +12 -4
- package/src/cli/index.ts +80 -2
- package/src/cli/json-output.test.ts +81 -0
- package/src/cli/parser.test.ts +37 -1
- package/src/cli/restore-command.test.ts +3 -3
- package/src/cli/restore-migration-failure.test.ts +2 -2
- package/src/cli/tui/audit-state.ts +2 -0
- package/src/cli/types.ts +9 -0
- package/src/config/paths.test.ts +19 -19
- package/src/db/client.test.ts +46 -1
- package/src/db/client.ts +26 -0
- package/src/db/schema.ts +63 -0
- package/src/hooks/broker.test.ts +9 -3
- package/src/hooks/capability-loader-firewall.test.ts +9 -1
- package/src/hooks/capability-loader.ts +8 -0
- package/src/hooks/executor.test.ts +91 -14
- package/src/hooks/executor.ts +31 -1
- package/src/hooks/hook-jail-toolchain-reach.test.ts +8 -3
- package/src/hooks/hook-jail-unreachability.test.ts +27 -20
- package/src/hooks/hook-trespass.test.ts +22 -6
- package/src/hooks/jail.test.ts +105 -17
- package/src/hooks/jail.ts +47 -10
- package/src/hooks/run-named-hook.ts +19 -16
- package/src/hooks/test-fixtures/artifact-writing-hook.ts +0 -1
- package/src/hooks/test-fixtures/capability-calling-hook.ts +20 -13
- package/src/hooks/test-fixtures/jail-probe-hook.ts +9 -1
- package/src/hooks/test-fixtures/jail-toolchain-hook.ts +7 -2
- package/src/hooks/test-fixtures/runaway-hook.ts +0 -1
- package/src/hooks/test-fixtures/sigterm-ignoring-hook.ts +0 -1
- package/src/hooks/test-fixtures/silent-hook.ts +0 -1
- package/src/hooks/test-fixtures/store-writing-hook.ts +20 -11
- package/src/hooks/test-fixtures/success-hook.ts +4 -4
- package/src/manifest/contracts/v1.ts +32 -14
- package/src/manifest/json-schema-roundtrip.test.ts +1 -1
- package/src/manifest/schema.ts +66 -16
- package/src/manifest/validate.test.ts +47 -0
- package/src/policy/capability-shape-baseline.ts +63 -21
- package/src/policy/capability-shape-drift.test.ts +53 -1
- package/src/policy/capability-shape.test.ts +105 -0
- package/src/policy/capability-shape.ts +283 -2
- package/src/policy/module-script-scan.ts +52 -68
- package/src/policy/no-hand-built-ssh.test.ts +5 -1
- package/src/policy/no-swallowed-refusal.test.ts +14 -14
- package/src/registry/client.test.ts +67 -2
- package/src/registry/client.ts +7 -7
- package/src/secrets/storage.test.ts +70 -4
- package/src/secrets/storage.ts +71 -1
- package/src/services/alerting/keys.test.ts +4 -0
- package/src/services/alerting/keys.ts +12 -2
- package/src/services/audit/health.test.ts +155 -2
- package/src/services/audit/health.ts +76 -2
- package/src/services/audit/index.test.ts +23 -1
- package/src/services/audit/index.ts +8 -2
- package/src/services/audit/interface-classification.test.ts +16 -5
- package/src/services/audit/interface-classification.ts +25 -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 -1
- package/src/services/audit/module-integrity.ts +7 -2
- package/src/services/audit/module-versions.ts +5 -1
- package/src/services/audit/public-dns.test.ts +20 -0
- package/src/services/audit/public-dns.ts +7 -2
- package/src/services/audit/recurrence-gate.test.ts +225 -0
- package/src/services/audit/trusted-sources.test.ts +17 -0
- package/src/services/audit/trusted-sources.ts +21 -0
- package/src/services/audit/types.ts +2 -1
- package/src/services/backup-create.ts +9 -0
- package/src/services/backup-envelope-roundtrip.test.ts +1 -1
- package/src/services/backup-in-flight-refusal.test.ts +1 -1
- package/src/services/build-bus/hook-dispatch-executor.test.ts +269 -0
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +105 -105
- package/src/services/build-bus/hook-dispatch-path.test.ts +95 -0
- package/src/services/build-bus/hook-dispatch.test.ts +86 -116
- package/src/services/build-bus/hook-dispatch.ts +99 -121
- package/src/services/build-bus/hook-dispatcher.ts +143 -17
- package/src/services/build-bus/receiver-daemon.test.ts +189 -0
- package/src/services/build-bus/receiver-daemon.ts +355 -0
- package/src/services/build-bus/self-update.ts +156 -0
- package/src/services/bus-ensure-flow.test.ts +1 -1
- package/src/services/bus-interview-park.test.ts +2 -2
- package/src/services/bus-interview.test.ts +2 -2
- package/src/services/bus-secret-flow.test.ts +1 -1
- package/src/services/capability-compat.test.ts +90 -0
- package/src/services/capability-compat.ts +128 -0
- package/src/services/celilo-events.test.ts +1 -1
- package/src/services/celilo-mgmt-hooks.test.ts +23 -5
- package/src/services/container-service.test.ts +1 -1
- package/src/services/cross-module-read.test.ts +2 -2
- package/src/services/deploy-preflight.ts +7 -0
- package/src/services/deploy-terraform.ts +38 -1
- package/src/services/deployed-systems.ts +1 -1
- package/src/services/events-daemon.test.ts +57 -0
- package/src/services/events-daemon.ts +76 -0
- package/src/services/firewall-reach.ts +21 -8
- package/src/services/fleet-checks.test.ts +159 -4
- package/src/services/fleet-checks.ts +206 -3
- package/src/services/health-runner.test.ts +87 -2
- package/src/services/health-runner.ts +83 -16
- package/src/services/infrastructure-selector.test.ts +1 -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 +1 -1
- package/src/services/module-deploy-prune.test.ts +89 -0
- package/src/services/module-deploy.ts +84 -129
- package/src/services/module-subscriptions.test.ts +2 -2
- package/src/services/module-types-drift.test.ts +1 -1
- package/src/services/module-validator/capability-versions.test.ts +13 -2
- package/src/services/network-discovery.test.ts +64 -1
- package/src/services/network-discovery.ts +30 -5
- package/src/services/responder-probe.test.ts +1 -1
- package/src/services/restore-from-file.test.ts +4 -4
- package/src/services/restore-preflight.test.ts +1 -1
- package/src/services/ssh-key-manager.test.ts +2 -2
- package/src/services/system-state-stage.test.ts +2 -2
- package/src/services/terraform-safety.test.ts +83 -0
- package/src/services/terraform-safety.ts +53 -0
- package/src/services/update/orchestrator.test.ts +3 -1
- package/src/test-utils/bus-responder.ts +1 -1
- package/tsconfig.json +2 -13
package/src/hooks/jail.test.ts
CHANGED
|
@@ -209,9 +209,13 @@ describe('the three policies', () => {
|
|
|
209
209
|
});
|
|
210
210
|
|
|
211
211
|
test("'required' turns an unavailable jail into a hard failure (task 4.5)", () => {
|
|
212
|
-
expect(
|
|
213
|
-
planJailedSpawn(CMD, deriveMountSet(REQUEST), UNAVAILABLE, 'required', allPresent),
|
|
214
|
-
|
|
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'/);
|
|
215
219
|
});
|
|
216
220
|
|
|
217
221
|
test("'required' names the reason, so the operator can act on it", () => {
|
|
@@ -291,30 +295,42 @@ describe('jailPolicy reads the operator’s switch', () => {
|
|
|
291
295
|
|
|
292
296
|
describe('resolveJailPolicy applies D2 precedence (hook-jail-config-surface)', () => {
|
|
293
297
|
test('an env value set and non-empty wins over stored config', () => {
|
|
294
|
-
expect(resolveJailPolicy('required', 'off')).toEqual({
|
|
298
|
+
expect(resolveJailPolicy('required', undefined, 'off')).toEqual({
|
|
299
|
+
policy: 'required',
|
|
300
|
+
source: 'env',
|
|
301
|
+
});
|
|
295
302
|
});
|
|
296
303
|
|
|
297
304
|
test('an EMPTY STRING env value counts as unset and falls through to config', () => {
|
|
298
305
|
// jailPolicy() already treats '' as unset, so the resolver agrees rather
|
|
299
306
|
// than inventing a second rule for what counts as set.
|
|
300
|
-
expect(resolveJailPolicy('', 'off')).toEqual({ policy: 'off', source: 'config' });
|
|
307
|
+
expect(resolveJailPolicy('', undefined, 'off')).toEqual({ policy: 'off', source: 'config' });
|
|
301
308
|
});
|
|
302
309
|
|
|
303
310
|
test('stored config wins when the env var is absent', () => {
|
|
304
|
-
expect(resolveJailPolicy(undefined, 'off')).toEqual({
|
|
305
|
-
|
|
311
|
+
expect(resolveJailPolicy(undefined, undefined, 'off')).toEqual({
|
|
312
|
+
policy: 'off',
|
|
313
|
+
source: 'config',
|
|
314
|
+
});
|
|
315
|
+
expect(resolveJailPolicy(undefined, undefined, 'required')).toEqual({
|
|
306
316
|
policy: 'required',
|
|
307
317
|
source: 'config',
|
|
308
318
|
});
|
|
309
319
|
});
|
|
310
320
|
|
|
311
|
-
test('
|
|
312
|
-
expect(resolveJailPolicy(undefined, undefined)).toEqual({
|
|
313
|
-
|
|
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
|
+
});
|
|
314
330
|
});
|
|
315
331
|
|
|
316
332
|
test('an explicit env auto still wins (peba, ce-rez7)', () => {
|
|
317
|
-
expect(resolveJailPolicy('auto', 'off')).toEqual({ policy: 'auto', source: 'env' });
|
|
333
|
+
expect(resolveJailPolicy('auto', undefined, 'off')).toEqual({ policy: 'auto', source: 'env' });
|
|
318
334
|
});
|
|
319
335
|
|
|
320
336
|
test('a stored empty jail_policy THROWS, it does not resolve to auto (peba, ce-8832)', () => {
|
|
@@ -322,8 +338,8 @@ describe('resolveJailPolicy applies D2 precedence (hook-jail-config-surface)', (
|
|
|
322
338
|
// can only arrive via a restore from file or a hand edit to the DB — the
|
|
323
339
|
// untrusted paths D3 exists to catch. It is outside the enum and fails
|
|
324
340
|
// closed like any other bad stored value.
|
|
325
|
-
expect(() => resolveJailPolicy(undefined, '')).toThrow(/is not a hook jail policy/);
|
|
326
|
-
expect(() => resolveJailPolicy(undefined, '')).toThrow(/hooks\.jail_policy=''/);
|
|
341
|
+
expect(() => resolveJailPolicy(undefined, undefined, '')).toThrow(/is not a hook jail policy/);
|
|
342
|
+
expect(() => resolveJailPolicy(undefined, undefined, '')).toThrow(/hooks\.jail_policy=''/);
|
|
327
343
|
});
|
|
328
344
|
|
|
329
345
|
test('a typo in the STORED row is refused, not silently treated as auto (D3)', () => {
|
|
@@ -331,15 +347,87 @@ describe('resolveJailPolicy applies D2 precedence (hook-jail-config-surface)', (
|
|
|
331
347
|
// foreign state, and a typo that silently meant `auto` reads as an armed
|
|
332
348
|
// jail when nothing is armed. The config path is no laxer than the env
|
|
333
349
|
// path.
|
|
334
|
-
expect(() => resolveJailPolicy(undefined, 'alwayssafe')).toThrow(
|
|
335
|
-
|
|
350
|
+
expect(() => resolveJailPolicy(undefined, undefined, 'alwayssafe')).toThrow(
|
|
351
|
+
/is not a hook jail policy/,
|
|
352
|
+
);
|
|
353
|
+
expect(() => resolveJailPolicy(undefined, undefined, 'alwayssafe')).toThrow(
|
|
336
354
|
/hooks\.jail_policy='alwayssafe'/,
|
|
337
355
|
);
|
|
338
356
|
});
|
|
339
357
|
|
|
340
358
|
test('a typo in the env value is still refused (unchanged jailPolicy behaviour)', () => {
|
|
341
|
-
expect(() => resolveJailPolicy('requried', undefined)).toThrow(
|
|
342
|
-
|
|
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/);
|
|
343
431
|
});
|
|
344
432
|
});
|
|
345
433
|
|
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
|
|
@@ -433,21 +435,37 @@ export function jailPolicy(): JailPolicy {
|
|
|
433
435
|
}
|
|
434
436
|
|
|
435
437
|
/** Where the effective jail policy came from. Rendered by `system doctor` (D4). */
|
|
436
|
-
export type JailPolicySource = 'env' | 'config' | 'default';
|
|
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
|
+
}
|
|
437
454
|
|
|
438
455
|
/**
|
|
439
|
-
* Apply
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
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).
|
|
443
461
|
*
|
|
444
462
|
* Pure, so both consumers (executor.ts and system-doctor.ts) can fetch the
|
|
445
|
-
* stored
|
|
463
|
+
* stored rows themselves and stay testable without a database.
|
|
446
464
|
*
|
|
447
465
|
* The empty-string env value is treated as unset, matching `jailPolicy()`'s
|
|
448
466
|
* own handling — one rule for "what counts as set", not a second one here.
|
|
449
467
|
*
|
|
450
|
-
* The
|
|
468
|
+
* The stored paths are deliberately stricter than the env path: a stored
|
|
451
469
|
* empty string THROWS (peba's ruling on ce-8832). The set-time pattern
|
|
452
470
|
* `^(auto|off|required)$` rejects `''`, so a stored one can only arrive via
|
|
453
471
|
* a restore from file or a hand edit to the DB — exactly the untrusted
|
|
@@ -456,9 +474,15 @@ export type JailPolicySource = 'env' | 'config' | 'default';
|
|
|
456
474
|
* closed is the whole point. An empty CELILO_HOOK_JAIL stays "not set",
|
|
457
475
|
* because `jailPolicy()` relies on that and slice 1's safety rests on the
|
|
458
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.
|
|
459
482
|
*/
|
|
460
483
|
export function resolveJailPolicy(
|
|
461
484
|
envValue: string | undefined,
|
|
485
|
+
moduleValue: string | undefined,
|
|
462
486
|
configValue: string | undefined,
|
|
463
487
|
): { policy: JailPolicy; source: JailPolicySource } {
|
|
464
488
|
if (envValue !== undefined && envValue !== '') {
|
|
@@ -469,6 +493,14 @@ export function resolveJailPolicy(
|
|
|
469
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'.`,
|
|
470
494
|
);
|
|
471
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
|
+
}
|
|
472
504
|
if (configValue !== undefined) {
|
|
473
505
|
if (configValue === 'auto' || configValue === 'off' || configValue === 'required') {
|
|
474
506
|
return { policy: configValue, source: 'config' };
|
|
@@ -620,9 +652,14 @@ function realpathOrSelf(path: string): string {
|
|
|
620
652
|
* Why an `off` policy runs unjailed, shared with `celilo system doctor` (task
|
|
621
653
|
* 4.6) so the sentence the operator reads there is the sentence the record
|
|
622
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.
|
|
623
660
|
*/
|
|
624
661
|
export const JAIL_OFF_REASON =
|
|
625
|
-
|
|
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.";
|
|
626
663
|
|
|
627
664
|
export function planJailedSpawn(
|
|
628
665
|
cmd: readonly string[],
|
|
@@ -647,7 +684,7 @@ export function planJailedSpawn(
|
|
|
647
684
|
: 'This invocation has no module tree to jail, so there is no mount set to enforce.';
|
|
648
685
|
if (policy === 'required') {
|
|
649
686
|
throw new Error(
|
|
650
|
-
`
|
|
687
|
+
`The hook jail policy is 'required' and no hook jail is available. ${reason ?? ''}`.trim(),
|
|
651
688
|
);
|
|
652
689
|
}
|
|
653
690
|
return { cmd, mode: 'unjailed', backend: availability.backend, reason, skipped: [] };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HookDefinition } from '@celilo/capabilities';
|
|
1
2
|
/**
|
|
2
3
|
* Helper to load + invoke a single named hook on a module.
|
|
3
4
|
*
|
|
@@ -41,6 +42,16 @@ export interface RunNamedHookOptions {
|
|
|
41
42
|
* hook's own `timeout` or the executor default applies.
|
|
42
43
|
*/
|
|
43
44
|
timeoutMs?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Use this hook definition instead of the manifest's own entry for the
|
|
47
|
+
* name. Only `on_upstream_publish` needs it: its manifest value is an
|
|
48
|
+
* ARRAY of match-rule entries and the build-bus dispatcher has already
|
|
49
|
+
* chosen the matching one, so the definition it passes is the matched
|
|
50
|
+
* entry (module-orchestrator-primitives slice 7). Every other hook keeps
|
|
51
|
+
* reading the manifest, and `celilo module run-hook` — which has no
|
|
52
|
+
* matched entry to supply — still gets `notDefined` for the array form.
|
|
53
|
+
*/
|
|
54
|
+
hookDefinition?: HookDefinition;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
export interface RunNamedHookResult extends HookResult {
|
|
@@ -113,22 +124,14 @@ export async function runNamedHook(
|
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
const manifest = module.manifestData as ModuleManifest;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
// Defensive narrow: `on_upstream_publish` is also under
|
|
126
|
-
// `manifest.hooks` (build-bus, Phase 4) but is an ARRAY of
|
|
127
|
-
// match-rule hooks dispatched by the receiver daemon — NOT
|
|
128
|
-
// runnable via `celilo module run-hook`. The HookName type
|
|
129
|
-
// excludes it, but the indexed-access return type can't prove
|
|
130
|
-
// that, so narrow at the value level.
|
|
131
|
-
if (Array.isArray(hookDef)) {
|
|
127
|
+
// `on_upstream_publish` is under `manifest.hooks` as an ARRAY of
|
|
128
|
+
// match-rule entries; the dispatcher supplies the matched entry via
|
|
129
|
+
// `options.hookDefinition`. Without one (e.g. `celilo module run-hook`)
|
|
130
|
+
// there is no single definition to run, so the hook reports notDefined
|
|
131
|
+
// rather than guessing an entry.
|
|
132
|
+
const hookDef =
|
|
133
|
+
options.hookDefinition ?? manifest.hooks?.[hookName as keyof typeof manifest.hooks];
|
|
134
|
+
if (!hookDef || Array.isArray(hookDef)) {
|
|
132
135
|
return {
|
|
133
136
|
success: true,
|
|
134
137
|
outputs: {},
|
|
@@ -20,6 +20,5 @@ export default defineHook({
|
|
|
20
20
|
writeFileSync(join(dir, 'spa-failure.png'), 'not-really-a-png');
|
|
21
21
|
writeFileSync(join(dir, 'spa-failure.html'), '<html></html>');
|
|
22
22
|
writeFileSync(join(dir, 'spa-failure.requests.txt'), 'GET /api/getActiveMonth 200');
|
|
23
|
-
return { api_key: 'wrote-artifacts' };
|
|
24
23
|
},
|
|
25
24
|
});
|
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
* One test per SHAPE rather than per method: the broker is generic, so a
|
|
6
6
|
* per-method suite would prove the same thing thirty-seven times and drift the
|
|
7
7
|
* moment a capability gained a method.
|
|
8
|
+
*
|
|
9
|
+
* Reports through a JSON file in its state directory: hook return values are
|
|
10
|
+
* no longer carried anywhere (hook-owned-state D5).
|
|
8
11
|
*/
|
|
9
12
|
|
|
13
|
+
import { writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
10
15
|
import { defineHook, isMissingProviderInputError } from '@celilo/capabilities';
|
|
11
16
|
|
|
12
17
|
interface DemoCapability {
|
|
@@ -27,25 +32,25 @@ export default defineHook({
|
|
|
27
32
|
// `demo` is a fixture capability, not a registry entry, so the typed
|
|
28
33
|
// capability map does not know it. The broker is generic and does not care.
|
|
29
34
|
const demo = (ctx.capabilities as unknown as Record<string, DemoCapability>).demo;
|
|
30
|
-
const
|
|
35
|
+
const report: Record<string, unknown> = {};
|
|
31
36
|
|
|
32
37
|
// Non-function properties are copied verbatim, which is what keeps
|
|
33
38
|
// `providerModuleId` readable — a hook names the provider in its errors.
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
report.providerModuleId = demo.providerModuleId;
|
|
40
|
+
report.version = demo.version;
|
|
36
41
|
|
|
37
42
|
// An optional method the provider did not implement must be ABSENT, not a
|
|
38
43
|
// proxy that throws, or `if (cap.registerTrustedSource)` answers wrongly.
|
|
39
|
-
|
|
44
|
+
report.optionalMethodAbsent = demo.sometimesAbsent === undefined;
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
report.returned = await demo.echo({ x: 1, nested: { y: [2, 3] } });
|
|
47
|
+
report.undefinedBecomesNull = await demo.returnsNothing({});
|
|
43
48
|
|
|
44
49
|
try {
|
|
45
50
|
await demo.boom({});
|
|
46
|
-
|
|
51
|
+
report.plainThrow = 'did not throw';
|
|
47
52
|
} catch (error) {
|
|
48
|
-
|
|
53
|
+
report.plainThrow = {
|
|
49
54
|
isError: error instanceof Error,
|
|
50
55
|
name: (error as Error).name,
|
|
51
56
|
message: (error as Error).message,
|
|
@@ -55,10 +60,10 @@ export default defineHook({
|
|
|
55
60
|
|
|
56
61
|
try {
|
|
57
62
|
await demo.missingInput({});
|
|
58
|
-
|
|
63
|
+
report.missingProviderInput = 'did not throw';
|
|
59
64
|
} catch (error) {
|
|
60
65
|
const e = error as Record<string, unknown>;
|
|
61
|
-
|
|
66
|
+
report.missingProviderInput = {
|
|
62
67
|
recognised: isMissingProviderInputError(error),
|
|
63
68
|
providerModuleId: e.providerModuleId,
|
|
64
69
|
ensureId: e.ensureId,
|
|
@@ -69,11 +74,13 @@ export default defineHook({
|
|
|
69
74
|
|
|
70
75
|
try {
|
|
71
76
|
await (demo as unknown as { nope(): Promise<unknown> }).nope();
|
|
72
|
-
|
|
77
|
+
report.unknownMethod = 'did not throw';
|
|
73
78
|
} catch (error) {
|
|
74
|
-
|
|
79
|
+
report.unknownMethod = (error as Error).message;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
// Hand the report to the caller through the state directory, the one
|
|
83
|
+
// channel a hook still has.
|
|
84
|
+
writeFileSync(join(ctx.stateDir as string, 'report.json'), JSON.stringify(report));
|
|
78
85
|
},
|
|
79
86
|
});
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* bubblewrap removes the path and gives `ENOENT`; `sandbox-exec` denies it and
|
|
7
7
|
* gives `EPERM`. Both satisfy the requirement, so this fixture records only
|
|
8
8
|
* whether the access worked and hands the message back for the post-mortem.
|
|
9
|
+
*
|
|
10
|
+
* The report is written to a JSON file in the state directory: hook return
|
|
11
|
+
* values are no longer carried anywhere (hook-owned-state D5), and `state/`
|
|
12
|
+
* is the one writable path inside the jail.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
@@ -32,7 +36,7 @@ export default defineHook({
|
|
|
32
36
|
staged_input: string;
|
|
33
37
|
};
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
const report = {
|
|
36
40
|
// Outside the module tree entirely, and the whole acceptance criterion:
|
|
37
41
|
// celilo's data directory is not bound, so the key is not merely denied
|
|
38
42
|
// but absent.
|
|
@@ -68,5 +72,9 @@ export default defineHook({
|
|
|
68
72
|
for (const name of names) readFileSync(join(sshDir, name));
|
|
69
73
|
}),
|
|
70
74
|
};
|
|
75
|
+
|
|
76
|
+
// Hand the report to the parent through the state directory — the one
|
|
77
|
+
// channel a jailed hook still has (D5 removed the return channel).
|
|
78
|
+
writeFileSync(`${ctx.stateDir}/report.json`, JSON.stringify(report));
|
|
71
79
|
},
|
|
72
80
|
});
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { execFileSync, execSync } from 'node:child_process';
|
|
24
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
24
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
25
25
|
import { dirname } from 'node:path';
|
|
26
26
|
import { BROWSER_ROOT, defineHook } from '@celilo/capabilities';
|
|
27
27
|
|
|
@@ -81,7 +81,7 @@ export default defineHook({
|
|
|
81
81
|
// run that supplied one — a false ABSENT indistinguishable from a real one.
|
|
82
82
|
const browser = (ctx.config as { browser_executable?: string }).browser_executable ?? '';
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
const reach = {
|
|
85
85
|
// The one every other shell-out depends on. `execSync` spawns
|
|
86
86
|
// `/bin/sh -c`, so a jail without it cannot run ANY hook that uses
|
|
87
87
|
// `node:child_process`, whatever binary that hook was reaching for.
|
|
@@ -223,5 +223,10 @@ export default defineHook({
|
|
|
223
223
|
throw new Error('no bwrap on any known path');
|
|
224
224
|
}),
|
|
225
225
|
};
|
|
226
|
+
|
|
227
|
+
// Hand the report to the parent through the state directory — the one
|
|
228
|
+
// channel a jailed hook still has (hook-owned-state D5 removed the
|
|
229
|
+
// return channel).
|
|
230
|
+
writeFileSync(`${ctx.stateDir}/report.json`, JSON.stringify(reach));
|
|
226
231
|
},
|
|
227
232
|
});
|
|
@@ -3,25 +3,32 @@
|
|
|
3
3
|
* the process boundary — set/get round trip on both stores, delete, a
|
|
4
4
|
* committed transaction, a discarded transaction, and the undeclared-name
|
|
5
5
|
* throw the whole design exists to produce.
|
|
6
|
+
*
|
|
7
|
+
* Reports through a JSON file in its state directory: hook return values
|
|
8
|
+
* are no longer carried anywhere (hook-owned-state D5), and the state
|
|
9
|
+
* directory is the sanctioned place a hook hands structured data to its
|
|
10
|
+
* caller.
|
|
6
11
|
*/
|
|
7
12
|
|
|
13
|
+
import { writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
8
15
|
import { defineHook } from '@celilo/capabilities';
|
|
9
16
|
|
|
10
17
|
export default defineHook({
|
|
11
18
|
hook: 'on_install',
|
|
12
19
|
requires: [],
|
|
13
20
|
handler: async (ctx) => {
|
|
14
|
-
const
|
|
21
|
+
const report: Record<string, unknown> = {};
|
|
15
22
|
|
|
16
23
|
// Round trip on each store.
|
|
17
24
|
await ctx.secrets.set('bot_token', 'token-value');
|
|
18
|
-
|
|
25
|
+
report.secretRoundTrip = await ctx.secrets.get('bot_token');
|
|
19
26
|
|
|
20
27
|
await ctx.config.set('public_ip', '203.0.113.7');
|
|
21
|
-
|
|
28
|
+
report.configRoundTrip = await ctx.config.get('public_ip');
|
|
22
29
|
|
|
23
30
|
// The map surface still answers from the values the context carried.
|
|
24
|
-
|
|
31
|
+
report.mapRead = ctx.config.mapOnlyValue;
|
|
25
32
|
|
|
26
33
|
// Delete removes; deleting a declared-but-never-written name is a no-op.
|
|
27
34
|
// (An UNDECLARED name on delete throws — that is 3.4, tested below.
|
|
@@ -29,15 +36,15 @@ export default defineHook({
|
|
|
29
36
|
await ctx.secrets.delete('bot_token');
|
|
30
37
|
await ctx.secrets.set('api_key', 'to-delete');
|
|
31
38
|
await ctx.secrets.delete('api_key');
|
|
32
|
-
|
|
39
|
+
report.deletedIsGone = await ctx.secrets.get('api_key');
|
|
33
40
|
|
|
34
41
|
// Committed transaction: both writes land.
|
|
35
42
|
await ctx.secrets.transaction((s) => {
|
|
36
43
|
s.set('bot_token', 'committed-a');
|
|
37
44
|
s.set('api_key', 'committed-b');
|
|
38
45
|
});
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
report.transactionA = await ctx.secrets.get('bot_token');
|
|
47
|
+
report.transactionB = await ctx.secrets.get('api_key');
|
|
41
48
|
|
|
42
49
|
// Discarded transaction: nothing lands, including the overwrite of the
|
|
43
50
|
// committed value above.
|
|
@@ -47,17 +54,19 @@ export default defineHook({
|
|
|
47
54
|
throw new Error('hook failed midway');
|
|
48
55
|
});
|
|
49
56
|
} catch (error) {
|
|
50
|
-
|
|
57
|
+
report.discarded = (error as Error).message;
|
|
51
58
|
}
|
|
52
|
-
|
|
59
|
+
report.afterDiscard = await ctx.secrets.get('bot_token');
|
|
53
60
|
|
|
54
61
|
// An undeclared name throws, naming the module and the declared set.
|
|
55
62
|
try {
|
|
56
63
|
await ctx.secrets.set('not_declared', 'x');
|
|
57
64
|
} catch (error) {
|
|
58
|
-
|
|
65
|
+
report.undeclaredSecret = (error as Error).message;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
// Hand the report to the caller through the state directory, the one
|
|
69
|
+
// channel a hook still has.
|
|
70
|
+
writeFileSync(join(ctx.stateDir as string, 'report.json'), JSON.stringify(report));
|
|
62
71
|
},
|
|
63
72
|
});
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Test fixture: successful hook script. Migrated to defineHook in
|
|
3
3
|
* HOOK_API_V2 Phase 8 — the executor now requires the brand.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Declares `hook: 'container_created'` and returns void: hook return
|
|
6
|
+
* values are no longer persisted or consumed (hook-owned-state D5), so a
|
|
7
|
+
* hook reports through the logger and, where a test needs structured data
|
|
8
|
+
* back, through a file written to its state directory.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { defineHook } from '@celilo/capabilities';
|
|
@@ -15,6 +16,5 @@ export default defineHook({
|
|
|
15
16
|
handler: async (ctx) => {
|
|
16
17
|
ctx.logger.info('Starting test hook');
|
|
17
18
|
ctx.logger.success('Test hook completed');
|
|
18
|
-
return { api_key: `test-key-for-${(ctx.vps_ip as string | undefined) ?? 'unknown'}` };
|
|
19
19
|
},
|
|
20
20
|
});
|