@celilo/cli 2.0.0 → 2.2.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 -1
- package/CELILO_SUBSYSTEMS.md +1 -0
- package/drizzle/0031_module_config_source.sql +20 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +2 -2
- package/schemas/system_config.json +2 -1
- package/src/capabilities/public-web-publish.test.ts +61 -0
- package/src/cli/commands/firewall-interface-list.test.ts +156 -7
- package/src/cli/commands/firewall-interface-list.ts +73 -7
- package/src/cli/commands/machine-add.ts +12 -55
- package/src/cli/commands/module-config.test.ts +20 -1
- package/src/cli/commands/module-import.ts +1 -1
- package/src/cli/commands/module-update.test.ts +82 -0
- package/src/cli/commands/module-update.ts +14 -4
- package/src/cli/commands/monitor.ts +2 -10
- package/src/cli/commands/restore.ts +16 -6
- package/src/cli/generate-zsh-completion.ts +1 -1
- package/src/cli/index.ts +4 -3
- package/src/cli/restore-migration-failure.test.ts +159 -0
- package/src/db/client.ts +5 -0
- package/src/db/migrate.test.ts +61 -135
- package/src/db/migrate.ts +7 -2
- package/src/db/schema.ts +10 -0
- package/src/hooks/broker.test.ts +106 -2
- package/src/hooks/broker.ts +91 -1
- package/src/hooks/capability-loader-firewall.test.ts +37 -0
- package/src/hooks/capability-loader.ts +16 -3
- package/src/hooks/define-hook.test.ts +4 -3
- package/src/hooks/executor.test.ts +19 -18
- package/src/hooks/executor.ts +88 -11
- package/src/hooks/hook-jail-toolchain-reach.test.ts +79 -29
- package/src/hooks/hook-jail-unreachability.test.ts +55 -29
- package/src/hooks/hook-protocol.ts +46 -1
- package/src/hooks/hook-runner.ts +36 -0
- package/src/hooks/hook-store-proxy.test.ts +109 -0
- package/src/hooks/hook-store-proxy.ts +85 -0
- package/src/hooks/hook-store.test.ts +162 -0
- package/src/hooks/hook-store.ts +290 -0
- package/src/hooks/hook-timeout.test.ts +3 -2
- package/src/hooks/hook-trespass.test.ts +94 -14
- package/src/hooks/jail.test.ts +1 -1
- package/src/hooks/jail.ts +194 -32
- package/src/hooks/mount-set.test.ts +296 -1
- package/src/hooks/mount-set.ts +216 -13
- package/src/hooks/run-named-hook.ts +2 -0
- package/src/hooks/test-fixtures/jail-probe-hook.ts +1 -1
- package/src/hooks/test-fixtures/on-restore-staging-hook.ts +26 -0
- package/src/hooks/test-fixtures/store-backed.ts +47 -0
- package/src/hooks/test-fixtures/store-writing-hook.ts +63 -0
- package/src/hooks/unjailed-lint.test.ts +27 -8
- package/src/manifest/schema.ts +1 -0
- package/src/module/packaging/build.ts +70 -2
- package/src/module/web-root.ts +17 -1
- package/src/policy/fixture-capability-coverage.test.ts +322 -0
- package/src/policy/module-script-scan.test.ts +42 -1
- package/src/policy/module-script-scan.ts +275 -5
- package/src/policy/no-hand-built-ssh.test.ts +34 -1
- package/src/policy/no-swallowed-refusal.test.ts +265 -0
- package/src/policy/no-tar-shell-out-in-services.test.ts +43 -0
- package/src/registry/client.test.ts +149 -0
- package/src/registry/client.ts +203 -11
- package/src/services/alerting/coverage-source.test.ts +86 -0
- package/src/services/alerting/coverage-source.ts +11 -1
- package/src/services/alerting/format.test.ts +57 -0
- package/src/services/alerting/format.ts +24 -0
- package/src/services/alerting/run-monitor.ts +2 -2
- package/src/services/backup-create.ts +7 -7
- package/src/services/backup-envelope-roundtrip.test.ts +45 -2
- package/src/services/backup-restore.ts +8 -4
- package/src/services/bus-interview.ts +37 -14
- package/src/services/config-provenance.ts +4 -0
- package/src/services/control-plane-bootstrap.test.ts +297 -0
- package/src/services/control-plane-bootstrap.ts +223 -0
- package/src/services/control-plane-health.test.ts +66 -0
- package/src/services/control-plane-health.ts +67 -0
- package/src/services/deploy-preflight.ts +8 -2
- package/src/services/deploy-validation.test.ts +22 -0
- package/src/services/deploy-validation.ts +8 -0
- package/src/services/deployed-systems.ts +12 -0
- package/src/services/dns-discovery.test.ts +147 -0
- package/src/services/dns-discovery.ts +134 -0
- package/src/services/fleet-checks.ts +6 -2
- package/src/services/fleet-key.test.ts +66 -2
- package/src/services/fleet-key.ts +54 -0
- package/src/services/health-runner.ts +36 -3
- package/src/services/module-config.ts +20 -2
- package/src/services/module-deploy.dns-repoint.test.ts +187 -0
- package/src/services/module-deploy.ts +214 -1
- package/src/services/module-validator/git-hygiene.test.ts +122 -3
- package/src/services/module-validator/git-hygiene.ts +83 -14
- package/src/services/remote-access.test.ts +88 -4
- package/src/services/remote-access.ts +52 -1
- package/src/services/restore-from-file.test.ts +20 -0
- package/src/services/restore-from-file.ts +21 -6
- package/src/services/static-content-converge.test.ts +140 -2
- package/src/services/static-content-converge.ts +55 -8
- package/src/services/system-config-schema-types.ts +1 -1
- package/src/services/system-config-validator.test.ts +36 -0
- package/src/services/system-config-validator.ts +11 -0
- package/src/services/trusted-sources.test.ts +30 -0
- package/src/services/trusted-sources.ts +47 -10
- package/src/templates/generator.ts +9 -2
- package/src/variables/context.ts +16 -5
|
@@ -12,7 +12,11 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { buildModule } from '../module/packaging/build';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
JAILED_CLI_SPAWN_RULE,
|
|
17
|
+
scanModuleDirectory,
|
|
18
|
+
scanModuleScriptSource,
|
|
19
|
+
} from './module-script-scan';
|
|
16
20
|
|
|
17
21
|
const rules = (src: string) => scanModuleScriptSource('f.ts', src).map((v) => v.rule);
|
|
18
22
|
|
|
@@ -60,6 +64,43 @@ describe('module script scan — namespace tools (hygiene, not a boundary)', ()
|
|
|
60
64
|
});
|
|
61
65
|
});
|
|
62
66
|
|
|
67
|
+
describe('module script scan — jailed CLI spawn rule', () => {
|
|
68
|
+
// Each spawn spelling asserted separately. An alternation that loses a leg
|
|
69
|
+
// still passes a test that only exercises the first.
|
|
70
|
+
it.each([
|
|
71
|
+
[
|
|
72
|
+
"execFileSync('celilo', argv)",
|
|
73
|
+
"execFileSync('celilo', ['module', 'config', 'set', 'x', 'y']);",
|
|
74
|
+
],
|
|
75
|
+
['execFileSync("celilo", argv)', 'execFileSync("celilo", args);'],
|
|
76
|
+
['execSync(`celilo …`)', 'execSync(`celilo module secret set x y`);'],
|
|
77
|
+
[
|
|
78
|
+
'exec(`celilo …`) after a shell prefix',
|
|
79
|
+
'exec(`cd /opt && sudo celilo system config get k`);',
|
|
80
|
+
],
|
|
81
|
+
["spawn('celilo', argv)", "const child = spawn('celilo', ['events', 'status']);"],
|
|
82
|
+
])('catches %s', (_spelling, src) => {
|
|
83
|
+
expect(rules(src)).toContain(JAILED_CLI_SPAWN_RULE);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('does not fire on operator prose that merely names the CLI', () => {
|
|
87
|
+
expect(rules('throw new Error("re-run `celilo module deploy x` to regenerate it");')).toEqual(
|
|
88
|
+
[],
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// The `\w` after `celilo ` is what separates a CLI invocation from an
|
|
93
|
+
// argument that merely begins with the string, so the negative case has to
|
|
94
|
+
// be a hyphenated name, which would match a bare `\bcelilo\b`.
|
|
95
|
+
it('does not fire on a container argument named celilo-*', () => {
|
|
96
|
+
expect(rules('execSync(`docker exec celilo-mgr status`);')).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('does not fire on the celilo.db file name', () => {
|
|
100
|
+
expect(rules('execSync(`cp /var/lib/celilo/celilo.db /tmp/x`);')).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
63
104
|
describe('module script scan — raw-exec escape hatch', () => {
|
|
64
105
|
it('flags a runAppCommand call with no justification', () => {
|
|
65
106
|
expect(rules('const r = runAppCommand(system, "rm -f /tmp/x", run);')).toContain(
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
29
|
-
import { join, relative } from 'node:path';
|
|
29
|
+
import { basename, join, relative } from 'node:path';
|
|
30
30
|
|
|
31
31
|
export interface ScanViolation {
|
|
32
32
|
/** Path as the operator should see it — relative to the scanned root. */
|
|
@@ -52,7 +52,58 @@ export interface ScanViolation {
|
|
|
52
52
|
*/
|
|
53
53
|
const ESCAPE_HATCH_LOOKBACK_LINES = 8;
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The rule name for a module script that reaches the `celilo` CLI as a child
|
|
57
|
+
* process. Exported because the debt list below and both test files key on it;
|
|
58
|
+
* a literal repeated in three places is the duplicate that drifts.
|
|
59
|
+
*/
|
|
60
|
+
export const JAILED_CLI_SPAWN_RULE = 'jailed hook spawns the celilo CLI';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The rule name for a module script that catches a refused framework call and
|
|
64
|
+
* carries on with a value it invented (e2e-suite-recovery §4, spec delta
|
|
65
|
+
* `specs/module-lifecycle/spec.md`). Exported for the same reason as the spawn
|
|
66
|
+
* rule: the debt list below and the gate's test file key on it.
|
|
67
|
+
*/
|
|
68
|
+
export const SWALLOWED_REFUSAL_RULE = 'swallowed framework refusal';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A framework-reaching call, spelled as this repo spells them today: one of
|
|
72
|
+
* the spawn-family calls naming the `celilo` CLI (the same shape the spawn
|
|
73
|
+
* rule above keys on — extracted here so the two rules cannot disagree about
|
|
74
|
+
* what reaches the framework), or a call through `runCelilo`, knot-unbound-
|
|
75
|
+
* internal's injected CLI runner (wired to `execFileSync('celilo', …)` where
|
|
76
|
+
* the hook is constructed; the debt list's own reason text names the
|
|
77
|
+
* "runCelilo wrapper").
|
|
78
|
+
*
|
|
79
|
+
* What it deliberately cannot see, same honesty class as every rule in this
|
|
80
|
+
* file: a wrapper under any other name, a binary path resolved into a
|
|
81
|
+
* variable, or a spawn reached through an indirection. A lint, not a boundary.
|
|
82
|
+
*/
|
|
83
|
+
const CLI_SPAWN_RE =
|
|
84
|
+
/\b(?:execSync|execFileSync|execFile|exec|spawnSync|spawn)\s*\(\s*(?:['"`]\s*celilo(?=\s|['"`])|`[^`]*\bcelilo(?=\s|`))/;
|
|
85
|
+
const FRAMEWORK_CALL_RE = new RegExp(`${CLI_SPAWN_RE.source}|\\brunCelilo\\s*\\(`);
|
|
86
|
+
|
|
55
87
|
const PATTERN_RULES: Array<{ rule: string; re: RegExp; hint: string }> = [
|
|
88
|
+
{
|
|
89
|
+
rule: JAILED_CLI_SPAWN_RULE,
|
|
90
|
+
// Same regex as CLI_SPAWN_RE below — one definition of "reaches the
|
|
91
|
+
// framework", used by both rules that key on it. Matches the CLI as
|
|
92
|
+
// argv[0] of a spawn-family call ('celilo' / "celilo"), or inside a shell
|
|
93
|
+
// string passed to one, where celilo is followed by a word (a subcommand)
|
|
94
|
+
// or the closing quote — so `docker exec celilo-mgr …`, a `celilo.db` file
|
|
95
|
+
// name, and prose that merely names the CLI do not fire. What it
|
|
96
|
+
// deliberately cannot see: a binary path resolved into a variable, or a
|
|
97
|
+
// spawn reached through an indirection. Same honesty class as the
|
|
98
|
+
// namespace rule — a lint, not a boundary.
|
|
99
|
+
re: CLI_SPAWN_RE,
|
|
100
|
+
hint:
|
|
101
|
+
'A jailed hook has no celilo binary and no shell, so this dies with ' +
|
|
102
|
+
'"Executable not found in $PATH" (celilo#1225). Replace the spawn with an ' +
|
|
103
|
+
'injected capability — context.secrets/context.config for self-config and ' +
|
|
104
|
+
'self-secrets (openspec/changes/hook-owned-state) — or move the operation ' +
|
|
105
|
+
'into the framework the way celilo-mgmt on_install did (5e0e624d).',
|
|
106
|
+
},
|
|
56
107
|
{
|
|
57
108
|
rule: 'raw ssh invocation (StrictHostKeyChecking)',
|
|
58
109
|
re: /StrictHostKeyChecking/,
|
|
@@ -97,6 +148,45 @@ const PATTERN_RULES: Array<{ rule: string; re: RegExp; hint: string }> = [
|
|
|
97
148
|
},
|
|
98
149
|
];
|
|
99
150
|
|
|
151
|
+
/**
|
|
152
|
+
* How far below a framework-reaching call the `catch` clause may sit for the
|
|
153
|
+
* call to count as caught. Calibrated against the three real sites rather than
|
|
154
|
+
* guessed: the widest call-to-catch gap among them is four lines (on_backup's
|
|
155
|
+
* machine-pool snapshot parses between the call and the catch). Six covers
|
|
156
|
+
* those with headroom and cannot reach the next statement's catch in any
|
|
157
|
+
* module script in the tree.
|
|
158
|
+
*/
|
|
159
|
+
const SWALLOW_LOOKAHEAD_LINES = 6;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* How deep into a catch body the scan looks for a `throw` before concluding
|
|
163
|
+
* the catch swallows. A refusal that propagates (rethrow, wrapped throw) is
|
|
164
|
+
* correct handling and must not fire; a catch that returns a literal, logs and
|
|
165
|
+
* continues, or only comments must. Eight lines is beyond any catch body in
|
|
166
|
+
* the tree; a longer one hides from this scan the same way a renamed wrapper
|
|
167
|
+
* does.
|
|
168
|
+
*/
|
|
169
|
+
const SWALLOW_CATCH_BODY_LINES = 8;
|
|
170
|
+
|
|
171
|
+
const CATCH_CLAUSE_RE = /\bcatch\b/;
|
|
172
|
+
const THROW_RE = /\bthrow\b/;
|
|
173
|
+
const CATCH_CLOSE_RE = /^\s*}\s*(?:catch|finally|else)?\s*;?\s*$/;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The catch clause a framework-reaching call landed in swallows when its body
|
|
177
|
+
* contains no `throw` within the window: the refusal is absorbed and the hook
|
|
178
|
+
* carries on. Returns false (handled correctly) when the catch propagates the
|
|
179
|
+
* refusal, and true only for the swallowed shape.
|
|
180
|
+
*/
|
|
181
|
+
function catchSwallows(lines: string[], catchIdx: number): boolean {
|
|
182
|
+
const end = Math.min(catchIdx + SWALLOW_CATCH_BODY_LINES, lines.length - 1);
|
|
183
|
+
for (let k = catchIdx + 1; k <= end; k++) {
|
|
184
|
+
if (THROW_RE.test(lines[k])) return false;
|
|
185
|
+
if (k > catchIdx + 1 && CATCH_CLOSE_RE.test(lines[k])) break;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
100
190
|
/** A `runAppCommand(` / `runAppCommandWithSecret(` CALL — not the import. */
|
|
101
191
|
const RAW_EXEC_CALL = /\brunAppCommand(?:WithSecret)?\s*\(/;
|
|
102
192
|
|
|
@@ -131,6 +221,35 @@ export function scanModuleScriptSource(file: string, source: string): ScanViolat
|
|
|
131
221
|
}
|
|
132
222
|
});
|
|
133
223
|
|
|
224
|
+
// The swallowed-refusal pass: windowed, like the escape-hatch check above —
|
|
225
|
+
// a catch clause this far below a framework-reaching call is the call's
|
|
226
|
+
// catch. Reported on the CALL line, because that is the site an author fixes.
|
|
227
|
+
lines.forEach((text, i) => {
|
|
228
|
+
if (!FRAMEWORK_CALL_RE.test(text)) return;
|
|
229
|
+
const last = Math.min(i + SWALLOW_LOOKAHEAD_LINES, lines.length - 1);
|
|
230
|
+
for (let j = i + 1; j <= last; j++) {
|
|
231
|
+
if (CATCH_CLAUSE_RE.test(lines[j])) {
|
|
232
|
+
if (catchSwallows(lines, j)) {
|
|
233
|
+
violations.push({
|
|
234
|
+
file,
|
|
235
|
+
line: i + 1,
|
|
236
|
+
rule: SWALLOWED_REFUSAL_RULE,
|
|
237
|
+
hint:
|
|
238
|
+
'A refused framework call must fail, not continue with an answer it invented ' +
|
|
239
|
+
'(e2e-suite-recovery §4): a refusal and an absence are different facts. Let the ' +
|
|
240
|
+
'error propagate, or read through a surface that distinguishes them — an injected ' +
|
|
241
|
+
'typed-absent reader (openspec/changes/hook-owned-state) — rather than catching ' +
|
|
242
|
+
'and substituting.',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
// A blank line or a closing brace at statement level ends the call's
|
|
248
|
+
// statement; a catch further down belongs to something else.
|
|
249
|
+
if (lines[j].trim() === '' || /^[)}]/.test(lines[j])) break;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
134
253
|
return violations;
|
|
135
254
|
}
|
|
136
255
|
|
|
@@ -164,12 +283,25 @@ export function moduleScriptFiles(scriptsDir: string): string[] {
|
|
|
164
283
|
|
|
165
284
|
/**
|
|
166
285
|
* Scan a module directory (the one holding `manifest.yml`). Returns every
|
|
167
|
-
* violation in its `scripts/`, with paths relative to `moduleDir
|
|
286
|
+
* violation in its `scripts/`, with paths relative to `moduleDir`, minus the
|
|
287
|
+
* jailed-CLI-spawn sites on the debt list. The module id is the directory's
|
|
288
|
+
* basename — the same assumption `packaging/build.ts`'s refusal message makes.
|
|
168
289
|
*/
|
|
169
290
|
export function scanModuleDirectory(moduleDir: string): ScanViolation[] {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
291
|
+
const moduleId = basename(moduleDir);
|
|
292
|
+
// One exemption set per rule, keyed the same way: a debt entry silences its
|
|
293
|
+
// own rule on its own file and nothing else.
|
|
294
|
+
const debtByRule: Record<string, Set<string>> = {
|
|
295
|
+
[JAILED_CLI_SPAWN_RULE]: new Set(
|
|
296
|
+
JAILED_CLI_SPAWN_DEBT.filter((e) => e.module === moduleId).map((e) => e.file),
|
|
297
|
+
),
|
|
298
|
+
[SWALLOWED_REFUSAL_RULE]: new Set(
|
|
299
|
+
SWALLOWED_REFUSAL_DEBT.filter((e) => e.module === moduleId).map((e) => e.file),
|
|
300
|
+
),
|
|
301
|
+
};
|
|
302
|
+
return moduleScriptFiles(join(moduleDir, 'scripts'))
|
|
303
|
+
.flatMap((f) => scanModuleScriptSource(relative(moduleDir, f), readFileSync(f, 'utf-8')))
|
|
304
|
+
.filter((v) => !(debtByRule[v.rule]?.has(v.file) ?? false));
|
|
173
305
|
}
|
|
174
306
|
|
|
175
307
|
/** Render violations for a test failure message or a refused publish. */
|
|
@@ -177,6 +309,144 @@ export function formatViolations(violations: ScanViolation[]): string {
|
|
|
177
309
|
return violations.map((v) => ` ${v.file}:${v.line}\n → ${v.rule}. ${v.hint}`).join('\n');
|
|
178
310
|
}
|
|
179
311
|
|
|
312
|
+
export interface PolicyDebtEntry {
|
|
313
|
+
/** Module directory name under `modules/`. */
|
|
314
|
+
module: string;
|
|
315
|
+
/** Script path relative to the module directory, e.g. `scripts/setup.ts`. */
|
|
316
|
+
file: string;
|
|
317
|
+
/** How many jailed-CLI spawns the file carries today. Pinned so a NEW spawn in an exempted file cannot hide behind the entry. */
|
|
318
|
+
matches: number;
|
|
319
|
+
/** Who converts this site, and into what. */
|
|
320
|
+
reason: string;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The known jailed-CLI spawns, exempted from the rule above until their
|
|
325
|
+
* conversion lands (hook-owned-state task 5.5; e2e-suite-recovery §3).
|
|
326
|
+
*
|
|
327
|
+
* These sites are tracked defects, not sanctioned forms: the jail kills every
|
|
328
|
+
* one of them the same way it killed celilo-mgmt's on_install (5e0e624d). The
|
|
329
|
+
* list exists so the rule can land RED-proof while the conversion — which
|
|
330
|
+
* needs the HookStore from hook-owned-state tasks 2 and 3 — is still at 0%.
|
|
331
|
+
* It is narrow on purpose (celilo#1014's lesson): one entry per file, each
|
|
332
|
+
* naming its owner, each pinning the exact number of spawns so a new call
|
|
333
|
+
* site cannot hide behind an existing entry.
|
|
334
|
+
*
|
|
335
|
+
* The debt test in `no-hand-built-ssh.test.ts` asserts every entry still
|
|
336
|
+
* matches its recorded count. When a conversion lands the count drops to zero,
|
|
337
|
+
* the test goes red, and the entry must be deleted in the same change.
|
|
338
|
+
*/
|
|
339
|
+
export const JAILED_CLI_SPAWN_DEBT: readonly PolicyDebtEntry[] = [
|
|
340
|
+
{
|
|
341
|
+
module: 'celilo-mgmt',
|
|
342
|
+
file: 'scripts/on_backup.ts',
|
|
343
|
+
matches: 1,
|
|
344
|
+
reason:
|
|
345
|
+
'hook-owned-state 5.5 / survey class G: machine list becomes a machine-pool read capability',
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
module: 'caddy-internal',
|
|
349
|
+
file: 'scripts/private-web-functions.ts',
|
|
350
|
+
matches: 1,
|
|
351
|
+
reason: 'hook-owned-state 5.5 (routes, capability-owned-tables Group B1)',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
module: 'dnsmasq-dhcp',
|
|
355
|
+
file: 'scripts/dhcp-server-functions.ts',
|
|
356
|
+
matches: 1,
|
|
357
|
+
reason:
|
|
358
|
+
'hook-owned-state 5.5 shape (self-config write); not in its 10-site table — reconcile there',
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
module: 'forgejo',
|
|
362
|
+
file: 'scripts/on-consumer-removed.ts',
|
|
363
|
+
matches: 1,
|
|
364
|
+
reason: 'hook-owned-state 5.5 (runner_registrations, Group B3)',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
module: 'forgejo',
|
|
368
|
+
file: 'scripts/setup.ts',
|
|
369
|
+
matches: 1,
|
|
370
|
+
reason: 'hook-owned-state 5.5 (secret set)',
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
module: 'forgejo',
|
|
374
|
+
file: 'scripts/source-forge-functions.ts',
|
|
375
|
+
matches: 1,
|
|
376
|
+
reason:
|
|
377
|
+
'hook-owned-state 5.5 shape (self-config write); not in its 10-site table — reconcile there',
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
module: 'generic-cpanel-hosting-provider',
|
|
381
|
+
file: 'scripts/on-consumer-removed.ts',
|
|
382
|
+
matches: 1,
|
|
383
|
+
reason: 'hook-owned-state 5.5 (publications, Group B4)',
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
module: 'generic-cpanel-hosting-provider',
|
|
387
|
+
file: 'scripts/publish-functions.ts',
|
|
388
|
+
matches: 1,
|
|
389
|
+
reason:
|
|
390
|
+
'hook-owned-state 5.5 shape (self-config write); not in its 10-site table — reconcile there',
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
module: 'wireguard',
|
|
394
|
+
file: 'scripts/control-plane-vpn-functions.ts',
|
|
395
|
+
matches: 1,
|
|
396
|
+
reason:
|
|
397
|
+
'hook-owned-state 5.5 shape (self-config write); not in its 10-site table — reconcile there',
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
module: 'wireguard',
|
|
401
|
+
file: 'scripts/health-check.ts',
|
|
402
|
+
matches: 1,
|
|
403
|
+
reason:
|
|
404
|
+
'survey class C: system config get becomes an injected read; not in hook-owned-state 5.5 — reconcile there',
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
module: 'wireguard',
|
|
408
|
+
file: 'scripts/on_install.ts',
|
|
409
|
+
matches: 1,
|
|
410
|
+
reason:
|
|
411
|
+
'hook-owned-state 5.5 (registered_peers, Group B2); THE five-suite blocker per the wave-0 census',
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
module: 'wireguard-manager',
|
|
415
|
+
file: 'scripts/setup.ts',
|
|
416
|
+
matches: 3,
|
|
417
|
+
reason:
|
|
418
|
+
'hook-owned-state 5.5 (vpn_endpoint/vpn_server_public_key/client_pool, category 3; secret set)',
|
|
419
|
+
},
|
|
420
|
+
];
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The known swallowed refusals, exempted from the rule above until their
|
|
424
|
+
* conversion lands (hook-owned-state 5.5; e2e-suite-recovery §4). Same
|
|
425
|
+
* mechanics and same narrowness as the spawn debt above — one entry per file,
|
|
426
|
+
* each naming its owner, each pinning the exact number of swallows — with the
|
|
427
|
+
* same guarded rot: when a site converts, the count drops, the debt test goes
|
|
428
|
+
* red, and the entry must be deleted in the same change.
|
|
429
|
+
*
|
|
430
|
+
* Converted on 2026-09-07 by ce-pvii: knot-unbound-internal/scripts/on-install.ts
|
|
431
|
+
* dropped its last swallow (the runCelilo wrapper), so the rule fires on
|
|
432
|
+
* exactly these two sites and nothing else in the tree.
|
|
433
|
+
*/
|
|
434
|
+
export const SWALLOWED_REFUSAL_DEBT: readonly PolicyDebtEntry[] = [
|
|
435
|
+
{
|
|
436
|
+
module: 'celilo-mgmt',
|
|
437
|
+
file: 'scripts/on_backup.ts',
|
|
438
|
+
matches: 1,
|
|
439
|
+
reason:
|
|
440
|
+
'hook-owned-state 5.5 / survey class G: machine list becomes a typed-absent machine-pool read',
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
module: 'wireguard',
|
|
444
|
+
file: 'scripts/health-check.ts',
|
|
445
|
+
matches: 1,
|
|
446
|
+
reason: 'survey class C: system config get becomes an injected typed-absent read',
|
|
447
|
+
},
|
|
448
|
+
];
|
|
449
|
+
|
|
180
450
|
/**
|
|
181
451
|
* The files inside `@celilo/capabilities` that implement the remote-exec
|
|
182
452
|
* primitives themselves. `remote.ts` is the ONE sanctioned place the
|
|
@@ -12,15 +12,18 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { describe, expect, test } from 'bun:test';
|
|
15
|
-
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
16
16
|
import { join, resolve } from 'node:path';
|
|
17
17
|
import {
|
|
18
|
+
JAILED_CLI_SPAWN_DEBT,
|
|
19
|
+
JAILED_CLI_SPAWN_RULE,
|
|
18
20
|
REMOTE_PRIMITIVE_FILES,
|
|
19
21
|
capabilityPackageSourceFiles,
|
|
20
22
|
formatViolations,
|
|
21
23
|
moduleScriptFiles,
|
|
22
24
|
scanCapabilityPackageSource,
|
|
23
25
|
scanModuleDirectory,
|
|
26
|
+
scanModuleScriptSource,
|
|
24
27
|
} from './module-script-scan';
|
|
25
28
|
|
|
26
29
|
/** Walk up from this test to the repo root (the dir holding both modules/ and apps/). */
|
|
@@ -56,6 +59,36 @@ describe('recurrence gate: modules never hand-build SSH', () => {
|
|
|
56
59
|
});
|
|
57
60
|
});
|
|
58
61
|
|
|
62
|
+
describe('recurrence gate: the jailed-CLI-spawn debt list is honest', () => {
|
|
63
|
+
// The debt list in module-script-scan exempts real, tracked defects from the
|
|
64
|
+
// jailed-CLI-spawn rule so the rule can land green before hook-owned-state
|
|
65
|
+
// converts them. Three ways this list could silently rot, and this is the
|
|
66
|
+
// guard for each:
|
|
67
|
+
// a site is converted -> the file stops matching, the entry must go
|
|
68
|
+
// a NEW site appears -> the pinned count moves, the entry must be revisited
|
|
69
|
+
// a file is deleted -> the entry names a ghost and must go
|
|
70
|
+
test('every entry names an existing file carrying exactly its pinned spawn count', () => {
|
|
71
|
+
for (const entry of JAILED_CLI_SPAWN_DEBT) {
|
|
72
|
+
const f = join(repoRoot(), 'modules', entry.module, entry.file);
|
|
73
|
+
expect(
|
|
74
|
+
existsSync(f),
|
|
75
|
+
`${entry.module}/${entry.file} no longer exists — delete its debt entry`,
|
|
76
|
+
).toBe(true);
|
|
77
|
+
const hits = scanModuleScriptSource(entry.file, readFileSync(f, 'utf-8')).filter(
|
|
78
|
+
(v) => v.rule === JAILED_CLI_SPAWN_RULE,
|
|
79
|
+
);
|
|
80
|
+
expect(
|
|
81
|
+
hits.length,
|
|
82
|
+
`${entry.module}/${entry.file} carries ${hits.length} jailed-CLI spawns, the debt entry pins ${entry.matches} (${entry.reason}) — convert or update the entry`,
|
|
83
|
+
).toBe(entry.matches);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('the debt list is non-trivial (the guard actually reached the entries)', () => {
|
|
88
|
+
expect(JAILED_CLI_SPAWN_DEBT.length).toBeGreaterThan(5);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
59
92
|
describe('recurrence gate: @celilo/capabilities itself never hand-builds SSH', () => {
|
|
60
93
|
// The workspace source — where the package is authored, and the only copy a
|
|
61
94
|
// commit in this repo can fix. Each module's bundled copy is an npm snapshot
|