@celilo/cli 1.13.0 → 2.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 +1 -1
- package/CELILO_SUBSYSTEMS.md +31 -5
- package/README.md +0 -2
- package/drizzle/0030_drop_module_builds_environment.sql +8 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +3 -3
- package/src/capabilities/public-web-helpers.test.ts +12 -6
- package/src/capabilities/public-web-publish.test.ts +42 -13
- package/src/capabilities/validation.test.ts +31 -0
- package/src/cli/commands/alerts-sweep.ts +3 -0
- package/src/cli/commands/console-get-chain.test.ts +96 -0
- package/src/cli/commands/console.ts +13 -5
- package/src/cli/commands/monitor.ts +15 -2
- package/src/cli/commands/notify-config.test.ts +79 -0
- package/src/cli/commands/notify-config.ts +13 -2
- package/src/cli/commands/system-doctor.test.ts +121 -1
- package/src/cli/commands/system-doctor.ts +151 -1
- package/src/cli/commands/system-ensure-fleet-key.ts +52 -0
- package/src/cli/completion.ts +10 -2
- package/src/cli/index.ts +7 -1
- package/src/console/closure.test.ts +76 -0
- package/src/console/closure.ts +87 -1
- package/src/console/control-plane-boundary.test.ts +82 -4
- package/src/console/projection.test.ts +63 -1
- package/src/console/projection.ts +39 -2
- package/src/db/schema.ts +0 -1
- package/src/hooks/capability-loader-control-plane-api.test.ts +124 -0
- package/src/hooks/capability-loader.ts +81 -10
- package/src/hooks/executor.ts +110 -17
- package/src/hooks/hook-jail-toolchain-reach.test.ts +224 -0
- package/src/hooks/hook-jail-unreachability.test.ts +28 -2
- package/src/hooks/hook-protocol.ts +44 -0
- package/src/hooks/hook-runner-entry.ts +23 -0
- package/src/hooks/hook-runner.ts +10 -0
- package/src/hooks/hook-trespass.test.ts +9 -3
- package/src/hooks/jail-browser-launch-flags.test.ts +34 -0
- package/src/hooks/jail.test.ts +92 -0
- package/src/hooks/jail.ts +128 -11
- package/src/hooks/mount-set.test.ts +28 -6
- package/src/hooks/mount-set.ts +34 -20
- package/src/hooks/remote-broker.test.ts +350 -0
- package/src/hooks/remote-broker.ts +404 -0
- package/src/hooks/run-named-hook.ts +2 -0
- package/src/hooks/test-fixtures/jail-probe-hook.ts +14 -1
- package/src/hooks/test-fixtures/jail-toolchain-hook.ts +227 -0
- package/src/hooks/test-fixtures/remote-bridge-probe.ts +82 -0
- package/src/hooks/unjailed-lint.test.ts +251 -0
- package/src/hooks/unjailed-lint.ts +395 -0
- package/src/manifest/contracts/v1.ts +22 -1
- package/src/manifest/validate.ts +25 -4
- package/src/module/web-root.ts +35 -0
- package/src/policy/module-business-baseline.ts +27 -3
- package/src/policy/module-script-scan.test.ts +22 -0
- package/src/policy/module-script-scan.ts +92 -1
- package/src/policy/no-hand-built-ssh.test.ts +39 -1
- package/src/policy/no-module-business-in-core.test.ts +1 -1
- package/src/services/alerting/hook-jail.test.ts +66 -0
- package/src/services/alerting/hook-jail.ts +70 -0
- package/src/services/alerting/run-monitor.test.ts +62 -0
- package/src/services/alerting/run-monitor.ts +12 -0
- package/src/services/alerting/sweep-runner.test.ts +1 -0
- package/src/services/api-principal-enrolment.test.ts +73 -0
- package/src/services/api-principal-enrolment.ts +55 -0
- package/src/services/backup-create.ts +36 -7
- package/src/services/backup-restore.ts +2 -0
- package/src/services/celilo-mgmt-hooks.test.ts +38 -79
- package/src/services/deploy-ansible.ts +9 -1
- package/src/services/fleet-key.test.ts +47 -0
- package/src/services/fleet-key.ts +75 -0
- package/src/services/health-runner.ts +2 -0
- package/src/services/module-build.test.ts +1 -64
- package/src/services/module-build.ts +10 -86
- package/src/services/module-deploy.ts +20 -0
- package/src/services/remote-access.test.ts +139 -0
- package/src/services/remote-access.ts +98 -0
- package/src/services/restore-from-file.ts +12 -6
- package/src/services/static-content-converge.test.ts +338 -0
- package/src/services/static-content-converge.ts +299 -0
- package/src/services/system-state-stage.test.ts +165 -0
- package/src/services/system-state-stage.ts +196 -0
package/src/hooks/jail.ts
CHANGED
|
@@ -37,9 +37,57 @@ import { hostname } from 'node:os';
|
|
|
37
37
|
import { dirname, join } from 'node:path';
|
|
38
38
|
import { getDataDir } from '../config/paths';
|
|
39
39
|
import { type MountSet, type MountSetRequest, toBwrapArgs } from './mount-set';
|
|
40
|
+
import { JAIL_PROVIDED_PATHS } from './unjailed-lint';
|
|
40
41
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Which jail celilo can build here. `none` means the hook runs unjailed.
|
|
44
|
+
*
|
|
45
|
+
* `sandbox-exec` is in the union ahead of its backend (task 4.8) because the
|
|
46
|
+
* POLICY for it is decided and landed first (ce-29z): `auto` defers on it
|
|
47
|
+
* until D14's declared-path mounts exist, and the planner below enforces
|
|
48
|
+
* that whether or not a platform reports the backend yet.
|
|
49
|
+
*/
|
|
50
|
+
export type JailBackend = 'bubblewrap' | 'sandbox-exec' | 'none';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether `auto` may jail through `sandbox-exec`.
|
|
54
|
+
*
|
|
55
|
+
* False on purpose, and the load-bearing fact of ce-29z. Measured there,
|
|
56
|
+
* same hardware, one variable (`CELILO_HOOK_JAIL=off` equals main's
|
|
57
|
+
* behaviour): under bubblewrap a fixture hook's write to an undeclared host
|
|
58
|
+
* path lands in the private tmpfs (D13b) and succeeds silently, while under
|
|
59
|
+
* `sandbox-exec` the same write is `EPERM` — the hook exits non-zero and the
|
|
60
|
+
* full `apps/celilo` suite goes red on macOS where it is green on Linux CI.
|
|
61
|
+
* Sandbox-exec DENIES rather than masks, so the macOS jail is stricter than
|
|
62
|
+
* the Linux one for exactly the paths nobody declared.
|
|
63
|
+
*
|
|
64
|
+
* The mechanism that declares them is D14 (`type: path` on a manifest
|
|
65
|
+
* variable), and D14 is not built speculatively — task 4.2a measured the
|
|
66
|
+
* population and found no consumer that survives D9b. So `auto` defers on
|
|
67
|
+
* this backend until that mechanism lands; flip this to `true` as part of
|
|
68
|
+
* that landing, never before. `CELILO_HOOK_JAIL=required` bypasses the
|
|
69
|
+
* deferral: it is the operator's explicit act (D8), and an operator who sets
|
|
70
|
+
* it on macOS has opted into the strictness.
|
|
71
|
+
*/
|
|
72
|
+
const SANDBOX_EXEC_AUTO_JAIL_ENABLED = false;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Why an `auto` sandbox-exec spawn was deferred. Operator-facing: it names
|
|
76
|
+
* the decision and the one way past it. Exported because `system doctor`
|
|
77
|
+
* renders the same deferral from (policy, availability) alone, where no plan
|
|
78
|
+
* — and so no plan reason — exists yet.
|
|
79
|
+
*/
|
|
80
|
+
export const SANDBOX_EXEC_AUTO_DEFERRED_REASON =
|
|
81
|
+
'macOS hooks run unjailed: sandbox-exec denies (EPERM) a hook write to any host path the mount set does not declare, where bubblewrap masks it into its private tmpfs — undeclared config paths would break module hooks on this host. The declaration mechanism (D14, manifest `type: path`) is not built, so auto defers on this backend (ce-29z). Set CELILO_HOOK_JAIL=required to jail anyway.';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Does `auto` defer on this backend? Shared by the planner and by
|
|
85
|
+
* `system doctor`, so the doctor cannot report a jail the executor will not
|
|
86
|
+
* build.
|
|
87
|
+
*/
|
|
88
|
+
export function autoJailDefers(availability: JailAvailability): boolean {
|
|
89
|
+
return availability.backend === 'sandbox-exec' && !SANDBOX_EXEC_AUTO_JAIL_ENABLED;
|
|
90
|
+
}
|
|
43
91
|
|
|
44
92
|
/**
|
|
45
93
|
* What the operator asked for. `CELILO_HOOK_JAIL`, and the default is `auto`.
|
|
@@ -75,8 +123,8 @@ export interface JailPlan {
|
|
|
75
123
|
*
|
|
76
124
|
* bubblewrap fails the whole jail on a bind whose SOURCE is missing, and
|
|
77
125
|
* several rows are legitimately absent: `/lib64` does not exist on arm64,
|
|
78
|
-
* `<module>/generated` only appears once celilo has generated
|
|
79
|
-
*
|
|
126
|
+
* and `<module>/generated` only appears once celilo has generated
|
|
127
|
+
* something. Dropping them is the caller's job rather
|
|
80
128
|
* than the derivation's, which is why they are reported rather than silently
|
|
81
129
|
* filtered — a contract input landing here is a real defect and this is
|
|
82
130
|
* where it becomes visible.
|
|
@@ -104,7 +152,17 @@ export interface JailPlan {
|
|
|
104
152
|
* `--new-session` is NOT here. It defends against TIOCSTI injection into a
|
|
105
153
|
* controlling terminal, and the child is spawned with `stdout: 'pipe'` and no
|
|
106
154
|
* tty, so there is nothing to inject into.
|
|
155
|
+
*
|
|
156
|
+
* The `--proc`/`--dev` pairs are BUILT from `JAIL_PROVIDED_PATHS`
|
|
157
|
+
* (unjailed-lint.ts), the same list the advisory lint reads, so the spawn and
|
|
158
|
+
* the lint cannot disagree about what the namespace itself provides. Adding a
|
|
159
|
+
* path to that constant without a flag here is a compile error.
|
|
107
160
|
*/
|
|
161
|
+
const JAIL_PROVIDED_FLAGS: Record<(typeof JAIL_PROVIDED_PATHS)[number], string> = {
|
|
162
|
+
'/proc': '--proc',
|
|
163
|
+
'/dev': '--dev',
|
|
164
|
+
};
|
|
165
|
+
|
|
108
166
|
const JAIL_NAMESPACE_ARGS = [
|
|
109
167
|
'--unshare-user',
|
|
110
168
|
'--unshare-ipc',
|
|
@@ -113,10 +171,7 @@ const JAIL_NAMESPACE_ARGS = [
|
|
|
113
171
|
// The one unshare that legitimately may be unavailable on an older kernel.
|
|
114
172
|
'--unshare-cgroup-try',
|
|
115
173
|
'--die-with-parent',
|
|
116
|
-
|
|
117
|
-
'/proc',
|
|
118
|
-
'--dev',
|
|
119
|
-
'/dev',
|
|
174
|
+
...JAIL_PROVIDED_PATHS.flatMap((path) => [JAIL_PROVIDED_FLAGS[path], path]),
|
|
120
175
|
] as const;
|
|
121
176
|
|
|
122
177
|
/**
|
|
@@ -258,7 +313,6 @@ export function realpathRequest(request: MountSetRequest): MountSetRequest {
|
|
|
258
313
|
...input,
|
|
259
314
|
value: realpathOrSelf(input.value),
|
|
260
315
|
})),
|
|
261
|
-
sshDir: request.sshDir ? realpathOrSelf(request.sshDir) : undefined,
|
|
262
316
|
};
|
|
263
317
|
}
|
|
264
318
|
|
|
@@ -362,6 +416,14 @@ function realpathOrSelf(path: string): string {
|
|
|
362
416
|
* @param set - The derived mount set, or `undefined` when the caller has no
|
|
363
417
|
* module tree to jail (the executor's own fixtures, and the bus handler path).
|
|
364
418
|
*/
|
|
419
|
+
/**
|
|
420
|
+
* Why an `off` policy runs unjailed, shared with `celilo system doctor` (task
|
|
421
|
+
* 4.6) so the sentence the operator reads there is the sentence the record
|
|
422
|
+
* carries.
|
|
423
|
+
*/
|
|
424
|
+
export const JAIL_OFF_REASON =
|
|
425
|
+
'CELILO_HOOK_JAIL=off: the operator switched the hook jail off on this host.';
|
|
426
|
+
|
|
365
427
|
export function planJailedSpawn(
|
|
366
428
|
cmd: readonly string[],
|
|
367
429
|
set: MountSet | undefined,
|
|
@@ -374,7 +436,7 @@ export function planJailedSpawn(
|
|
|
374
436
|
cmd,
|
|
375
437
|
mode: 'unjailed',
|
|
376
438
|
backend: availability.backend,
|
|
377
|
-
reason:
|
|
439
|
+
reason: JAIL_OFF_REASON,
|
|
378
440
|
skipped: [],
|
|
379
441
|
};
|
|
380
442
|
}
|
|
@@ -391,7 +453,24 @@ export function planJailedSpawn(
|
|
|
391
453
|
return { cmd, mode: 'unjailed', backend: availability.backend, reason, skipped: [] };
|
|
392
454
|
}
|
|
393
455
|
|
|
456
|
+
// ce-29z: auto defers on sandbox-exec until D14 exists. Not an availability
|
|
457
|
+
// question — the backend is there; it is a policy one, which is why this
|
|
458
|
+
// sits in the planner rather than the probe.
|
|
459
|
+
if (policy === 'auto' && autoJailDefers(availability)) {
|
|
460
|
+
return {
|
|
461
|
+
cmd,
|
|
462
|
+
mode: 'unjailed',
|
|
463
|
+
backend: availability.backend,
|
|
464
|
+
reason: SANDBOX_EXEC_AUTO_DEFERRED_REASON,
|
|
465
|
+
skipped: [],
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
394
469
|
// A tmpfs needs no source — bubblewrap creates it — so it is never dropped.
|
|
470
|
+
// @psbanka-agent note (ce-29z, 2026-09): with SANDBOX_EXEC_AUTO_JAIL_ENABLED
|
|
471
|
+
// still false, the only way to reach this line with a sandbox-exec
|
|
472
|
+
// availability is policy 'required', and task 4.8 owns the spawn arm that
|
|
473
|
+
// makes that real. Until it lands, do not flip the flag.
|
|
395
474
|
const present = set.entries.filter((e) => e.mode === 'tmpfs' || exists(e.path));
|
|
396
475
|
const skipped = set.entries.filter((e) => !present.includes(e)).map((e) => e.path);
|
|
397
476
|
|
|
@@ -423,6 +502,20 @@ export interface JailModeRecord {
|
|
|
423
502
|
*/
|
|
424
503
|
readonly host: string;
|
|
425
504
|
readonly recordedAt: string;
|
|
505
|
+
/**
|
|
506
|
+
* The jailed record this host most recently held, present only on an
|
|
507
|
+
* `unjailed` record that replaced one on the SAME host.
|
|
508
|
+
*
|
|
509
|
+
* This is what makes the jailed-to-unjailed transition durable. The write
|
|
510
|
+
* below overwrites the previous record, so without this field the file for
|
|
511
|
+
* a host that used to jail and has stopped reads identically to a Mac that
|
|
512
|
+
* never jailed — and the self-monitor (task 4.4) runs on a sweep, long
|
|
513
|
+
* after `recordJailMode`'s return value is gone.
|
|
514
|
+
*/
|
|
515
|
+
readonly lastJailed?: {
|
|
516
|
+
readonly backend: JailBackend;
|
|
517
|
+
readonly recordedAt: string;
|
|
518
|
+
};
|
|
426
519
|
}
|
|
427
520
|
|
|
428
521
|
/**
|
|
@@ -457,12 +550,14 @@ export function readJailMode(): JailModeRecord | undefined {
|
|
|
457
550
|
*/
|
|
458
551
|
export function recordJailMode(plan: JailPlan): { previous?: JailModeRecord } {
|
|
459
552
|
const previous = readJailMode();
|
|
553
|
+
const host = hostname();
|
|
460
554
|
const record: JailModeRecord = {
|
|
461
555
|
mode: plan.mode,
|
|
462
556
|
backend: plan.backend,
|
|
463
557
|
...(plan.reason ? { reason: plan.reason } : {}),
|
|
464
|
-
host
|
|
558
|
+
host,
|
|
465
559
|
recordedAt: new Date().toISOString(),
|
|
560
|
+
...(lastJailedFor(plan.mode, previous, host) ?? {}),
|
|
466
561
|
};
|
|
467
562
|
|
|
468
563
|
if (
|
|
@@ -489,3 +584,25 @@ export function recordJailMode(plan: JailPlan): { previous?: JailModeRecord } {
|
|
|
489
584
|
}
|
|
490
585
|
return { previous };
|
|
491
586
|
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* What an unjailed record remembers about the last time this host jailed.
|
|
590
|
+
*
|
|
591
|
+
* A record from a DIFFERENT host contributes nothing: that is the
|
|
592
|
+
* copied-between-boxes case the `host` field exists for, and reading it as a
|
|
593
|
+
* transition would alert on a move that regressed nothing. A jailed record
|
|
594
|
+
* remembers nothing either — re-jailing is the recovery, and a later
|
|
595
|
+
* regression should date from the NEW jailed record, not the first ever.
|
|
596
|
+
*/
|
|
597
|
+
function lastJailedFor(
|
|
598
|
+
mode: JailMode,
|
|
599
|
+
previous: JailModeRecord | undefined,
|
|
600
|
+
host: string,
|
|
601
|
+
): Pick<JailModeRecord, 'lastJailed'> | undefined {
|
|
602
|
+
if (mode !== 'unjailed' || !previous || previous.host !== host) return undefined;
|
|
603
|
+
if (previous.mode === 'jailed') {
|
|
604
|
+
return { lastJailed: { backend: previous.backend, recordedAt: previous.recordedAt } };
|
|
605
|
+
}
|
|
606
|
+
// unjailed → unjailed (a backend or reason change): the memory rides along.
|
|
607
|
+
return previous.lastJailed ? { lastJailed: previous.lastJailed } : undefined;
|
|
608
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, expect, test } from 'bun:test';
|
|
10
|
+
import { BROWSER_ROOT } from '@celilo/capabilities';
|
|
10
11
|
import { deriveMountSet, forbiddenPaths, isForbidden, toBwrapArgs } from './mount-set';
|
|
11
12
|
|
|
12
13
|
const BASE = {
|
|
@@ -136,13 +137,34 @@ describe('paths are identical inside and outside', () => {
|
|
|
136
137
|
});
|
|
137
138
|
});
|
|
138
139
|
|
|
139
|
-
describe('~/.ssh is stage
|
|
140
|
-
test('
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
describe('~/.ssh is never in the mount set (stage 3, D12)', () => {
|
|
141
|
+
test('no derivation produces an .ssh row', () => {
|
|
142
|
+
// Withholding the key is what turns the remote-ops broker's target check
|
|
143
|
+
// from a convention into a boundary: with no credential in the jail, a
|
|
144
|
+
// hand-built `ssh` cannot authenticate anywhere.
|
|
145
|
+
expect(pathsOf(BASE).some((p) => p.endsWith('/.ssh'))).toBe(false);
|
|
143
146
|
});
|
|
147
|
+
});
|
|
144
148
|
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
describe('the fleet browser is reachable, and only read-only (task 4.10)', () => {
|
|
150
|
+
test('BROWSER_ROOT is bound', () => {
|
|
151
|
+
// Task 4.10 names `~/.cache/ms-playwright`. That path is stale:
|
|
152
|
+
// `managed-browser-runtime` moved the browser into a celilo-owned tree,
|
|
153
|
+
// and `resolveBrowser()` — the thing a hook actually asks — returns
|
|
154
|
+
// `BROWSER_EXECUTABLE_PATH` under this root. Asserted against the exported
|
|
155
|
+
// constant rather than a literal, so a future move cannot leave this test
|
|
156
|
+
// passing about a directory nothing launches from.
|
|
157
|
+
expect(pathsOf(BASE)).toContain(BROWSER_ROOT);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('read-only, because a hook has no business writing the shared install', () => {
|
|
161
|
+
expect(deriveMountSet(BASE).entries.find((e) => e.path === BROWSER_ROOT)?.mode).toBe('ro');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('the data directory beside it stays out', () => {
|
|
165
|
+
// The row is one named subdirectory, not `/var/lib/celilo`. Binding the
|
|
166
|
+
// parent would put `celilo.db` and `master.key` back inside the jail —
|
|
167
|
+
// the exact acceptance criterion D9 satisfies by absence.
|
|
168
|
+
expect(pathsOf(BASE)).not.toContain('/var/lib/celilo');
|
|
147
169
|
});
|
|
148
170
|
});
|
package/src/hooks/mount-set.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
20
|
-
import type
|
|
20
|
+
import { BROWSER_ROOT, type PathAccess } from '@celilo/capabilities';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* `tmpfs` is not an access level, it is "put a fresh empty filesystem here".
|
|
@@ -105,15 +105,6 @@ export interface MountSetRequest {
|
|
|
105
105
|
readonly runtimeModulePaths?: readonly string[];
|
|
106
106
|
/** Contract-declared path inputs, already resolved to values. */
|
|
107
107
|
readonly pathInputs: readonly DeclaredPathInput[];
|
|
108
|
-
/**
|
|
109
|
-
* The operator's `~/.ssh`, read-only, STAGE 2 ONLY.
|
|
110
|
-
*
|
|
111
|
-
* `remote.ts` still runs inside the hook and needs the key. Stage 3 brokers
|
|
112
|
-
* those calls and drops this row, which is what turns D12's target check
|
|
113
|
-
* from a convention into a boundary. Dropping it before stage 3 lands
|
|
114
|
-
* hardens nothing — it just stops every hook reaching its own systems.
|
|
115
|
-
*/
|
|
116
|
-
readonly sshDir?: string;
|
|
117
108
|
}
|
|
118
109
|
|
|
119
110
|
/** Directories whose contents the runtime needs in order to start at all. */
|
|
@@ -181,6 +172,34 @@ export function deriveMountSet(request: MountSetRequest): MountSet {
|
|
|
181
172
|
for (const dir of RUNTIME_SUPPORT_DIRS) {
|
|
182
173
|
entries.push(entry(dir, 'ro', 'shared libraries and trust store'));
|
|
183
174
|
}
|
|
175
|
+
// The fleet browser, read-only (task 4.10).
|
|
176
|
+
//
|
|
177
|
+
// `BROWSER_ROOT` rather than `~/.cache/ms-playwright`, which is what task 4.10
|
|
178
|
+
// says to bind and is now the wrong path: `managed-browser-runtime` moved the
|
|
179
|
+
// browser into a celilo-owned tree and `resolveBrowser()` is what a hook asks
|
|
180
|
+
// for it. Binding the cache would bind a directory nothing launches from.
|
|
181
|
+
//
|
|
182
|
+
// Read-only because a hook has no business writing to the shared browser
|
|
183
|
+
// install, and unconditional because it costs nothing when absent —
|
|
184
|
+
// `planJailedSpawn` drops a row whose source does not exist, so a host with no
|
|
185
|
+
// provisioned browser gets no row and no error. Gating it on some
|
|
186
|
+
// "this module uses a browser" signal would need a declaration that does not
|
|
187
|
+
// exist, and would fail closed in the one case that matters.
|
|
188
|
+
//
|
|
189
|
+
// Note what this does NOT bind: `/var/lib/celilo` itself stays out, so the
|
|
190
|
+
// data directory beside it is as absent as it was before. This is one
|
|
191
|
+
// subdirectory, named explicitly.
|
|
192
|
+
//
|
|
193
|
+
// ⚠️ The bind is necessary and not sufficient. Measured 2026-08-28 under a
|
|
194
|
+
// real bubblewrap jail (`hook-jail-toolchain-reach.test.ts`): Chromium with
|
|
195
|
+
// its own sandbox does NOT start inside the jail, and the same binary with
|
|
196
|
+
// `--no-sandbox` renders. Chromium's sandbox forks a helper into a new user
|
|
197
|
+
// namespace and nesting that inside bubblewrap's unprivileged one fails. So a
|
|
198
|
+
// browser hook also needs `--no-sandbox`, which belongs to whatever launches
|
|
199
|
+
// the browser rather than here. That interim landed 2026-08-31 (celilo#1215):
|
|
200
|
+
// the launch path in `test-fixtures/jail-toolchain-hook.ts` carries the flag,
|
|
201
|
+
// and `jail-browser-launch-flags.test.ts` pins it there.
|
|
202
|
+
entries.push(entry(BROWSER_ROOT, 'ro', 'the fleet browser, when one is provisioned'));
|
|
184
203
|
|
|
185
204
|
// 3. The module's own tree, read-only, then its writable directories carved
|
|
186
205
|
// on top. bubblewrap resolves that in the right order, which is why the
|
|
@@ -215,16 +234,11 @@ export function deriveMountSet(request: MountSetRequest): MountSet {
|
|
|
215
234
|
);
|
|
216
235
|
}
|
|
217
236
|
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
'ro',
|
|
224
|
-
'remote.ts needs the key until the broker holds it (D12)',
|
|
225
|
-
),
|
|
226
|
-
);
|
|
227
|
-
}
|
|
237
|
+
// There is deliberately NO `~/.ssh` row (stage 3, design D12). With no key
|
|
238
|
+
// in the jail, a hand-built `ssh` cannot authenticate anywhere; the remote
|
|
239
|
+
// primitives cross to the remote-ops broker instead, and withholding the
|
|
240
|
+
// key is what makes that broker's target check a boundary rather than a
|
|
241
|
+
// convention.
|
|
228
242
|
|
|
229
243
|
return {
|
|
230
244
|
entries: entries.filter((e) => !isForbidden(e.path)),
|