noqt commited on
Commit
7ea15a2
·
1 Parent(s): 015acba

Repair Eggcracker fail-closed lifecycle for 0.1.1

Browse files
.github/pull_request_template.md CHANGED
@@ -1,6 +1,6 @@
1
  ## Summary
2
 
3
- Describe the user-visible change and why it belongs in Lumi Nutcracker's narrow kill-switch scope.
4
 
5
  ## Verification
6
 
 
1
  ## Summary
2
 
3
+ Describe the user-visible change and why it belongs in Lumi Eggcracker's narrow kill-switch scope.
4
 
5
  ## Verification
6
 
.github/workflows/ci.yml CHANGED
@@ -13,11 +13,11 @@ jobs:
13
  test-and-package:
14
  runs-on: ubuntu-24.04
15
  steps:
16
- - uses: actions/checkout@v4
17
  with:
18
  fetch-depth: 0
19
 
20
- - uses: actions/setup-python@v5
21
  with:
22
  python-version: "3.11"
23
 
@@ -38,19 +38,19 @@ jobs:
38
  - name: Verify public release
39
  run: >-
40
  python scripts/verify_release.py
41
- --artifact dist/github/lumi-nutcracker-0.1.0.pyz
42
- --source-archive dist/github/lumi-nutcracker-0.1.0-source.zip
43
- --release-bundle dist/github/lumi-nutcracker-0.1.0-linux.zip
44
 
45
  - name: Upload tag artifacts
46
  if: startsWith(github.ref, 'refs/tags/v')
47
- uses: actions/upload-artifact@v4
48
  with:
49
- name: lumi-nutcracker-${{ github.ref_name }}
50
  path: |
51
- dist/github/lumi-nutcracker-0.1.0.pyz
52
- dist/github/lumi-nutcracker-0.1.0-source.zip
53
- dist/github/lumi-nutcracker-0.1.0-linux.zip
54
  dist/github/release-manifest.json
55
  dist/github/SHA256SUMS
56
  if-no-files-found: error
 
13
  test-and-package:
14
  runs-on: ubuntu-24.04
15
  steps:
16
+ - uses: actions/checkout@v6
17
  with:
18
  fetch-depth: 0
19
 
20
+ - uses: actions/setup-python@v6
21
  with:
22
  python-version: "3.11"
23
 
 
38
  - name: Verify public release
39
  run: >-
40
  python scripts/verify_release.py
41
+ --artifact dist/github/lumi-eggcracker-0.1.1.pyz
42
+ --source-archive dist/github/lumi-eggcracker-0.1.1-source.zip
43
+ --release-bundle dist/github/lumi-eggcracker-0.1.1-linux.zip
44
 
45
  - name: Upload tag artifacts
46
  if: startsWith(github.ref, 'refs/tags/v')
47
+ uses: actions/upload-artifact@v6
48
  with:
49
+ name: lumi-eggcracker-${{ github.ref_name }}
50
  path: |
51
+ dist/github/lumi-eggcracker-0.1.1.pyz
52
+ dist/github/lumi-eggcracker-0.1.1-source.zip
53
+ dist/github/lumi-eggcracker-0.1.1-linux.zip
54
  dist/github/release-manifest.json
55
  dist/github/SHA256SUMS
56
  if-no-files-found: error
AGENTS.md DELETED
@@ -1,10 +0,0 @@
1
- # Lumi Nutcracker repository rules
2
-
3
- - This product supports only explicitly launched workloads on Linux with systemd and unified cgroup v2.
4
- - Tests may affect only fresh Nutcracker-owned units and fixture processes they create. Never signal PID 1, the test runner, its ancestors, a pre-existing process or an unrelated cgroup.
5
- - The root supervisor is the sole enforcement component. The workload identity must never gain control-socket access or root authority.
6
- - Direct `cgroup.kill` is authoritative. Do not replace it with process ancestry, compatibility backends or service-stop-only proof.
7
- - Keep a trigger-side path free of durable state, evidence, hash, model and networking operations until direct containment has been attempted.
8
- - Do not add network isolation, behavioural models, AI identification, BPF, cloud services or unsupported-platform claims without a separately approved evidence boundary.
9
- - Preserve unrelated user changes. Keep generated artifacts, models, VM images, keys and local evidence out of source control.
10
- - A partial or ambiguous result must never be reported as a successful termination.
 
 
 
 
 
 
 
 
 
 
 
CHANGELOG.md CHANGED
@@ -1,13 +1,10 @@
1
  # Changelog
2
 
3
- All notable public changes are recorded here.
 
 
 
4
 
5
  ## 0.1.0
6
 
7
- - Initial Lumi Nutcracker engineering preview.
8
- - Root-supervised launch of explicitly selected workloads in owned cgroup-v2 units.
9
- - Operator-triggered complete workload-tree termination using `cgroup.kill`.
10
- - Automatic PID-limit tripwire containment.
11
- - Dedicated workload/operator identity separation and control-socket authentication.
12
- - Post-containment receipts, status, list, doctor and version commands.
13
- - Clean installer, uninstaller, native qualification matrix and local AI smoke demonstration.
 
1
  # Changelog
2
 
3
+ ## 0.1.1
4
+
5
+ - Renamed the public product to Lumi Eggcracker.
6
+ - Repaired launch, watcher, lifecycle, receipt, identity, and release hygiene paths.
7
 
8
  ## 0.1.0
9
 
10
+ - Retained public engineering-preview predecessor; not rewritten by this release.
 
 
 
 
 
 
CONTRIBUTING.md CHANGED
@@ -1,19 +1,5 @@
1
  # Contributing
2
 
3
- Lumi Nutcracker is intentionally narrow. Changes should preserve the supported release claim and keep direct cgroup containment as the first trigger-side effect.
4
 
5
- ## Local checks
6
-
7
- ```bash
8
- PYTHONPATH=src python3 -m unittest discover -s tests -v
9
- ruff check src tests scripts
10
- python3 scripts/build_release.py --output dist
11
- python3 scripts/verify_release.py \
12
- --artifact dist/lumi-nutcracker-0.1.0.pyz \
13
- --source-archive dist/lumi-nutcracker-0.1.0-source.zip \
14
- --release-bundle dist/lumi-nutcracker-0.1.0-linux.zip
15
- ```
16
-
17
- Native tests create and terminate root-owned systemd cgroups. Run them only inside a disposable Linux VM you own, and never modify them to target pre-existing processes or cgroups.
18
-
19
- Open an issue before adding a new containment backend, network isolation, AI identification, behavioural models or a broader security claim.
 
1
  # Contributing
2
 
3
+ Keep the product claim narrow and mechanically testable. Direct `cgroup.kill` plus exact empty proof remains authoritative; do not add unsupported backends, automatic AI recognition, behavioural models, networking, or host-wide claims without a separately qualified release boundary.
4
 
5
+ Run unit tests, lint, release construction, and public-artifact verification before proposing a change.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
LIMITATIONS.md CHANGED
@@ -1,9 +1,7 @@
1
  # Limitations
2
 
3
- Lumi Nutcracker protects only commands explicitly launched through its local root supervisor. It does not inspect the host for AI, agent, malware or intrusion activity, and it cannot attach an existing process to its protected boundary.
4
 
5
- The product terminates the exact cgroup it created. It does not provide network isolation, credential isolation, filesystem isolation, container isolation, virtual-machine isolation or host isolation. It is not an EDR, antivirus or general malware-prevention product.
6
 
7
- The PID tripwire is a cgroup process-count ceiling. It is not behavioural detection, a resource-governance system or AI attribution. A supervisor restart intentionally fail-closed terminates active owned workloads rather than continuing them without a live watcher.
8
-
9
- Qualification is limited to the published tested native Linux environment. Passing local tests does not establish universal Linux compatibility or production readiness for workloads outside that environment.
 
1
  # Limitations
2
 
3
+ Lumi Eggcracker protects only commands explicitly launched through its local root supervisor on the qualified Linux/systemd/cgroup-v2 environment.
4
 
5
+ It does not discover AI processes, attach existing processes, identify malware, detect unknown intrusions, isolate networks, credentials or filesystems, provide general malware prevention, or replace EDR.
6
 
7
+ The `start` command is non-interactive in 0.1.1: there is no terminal attachment, current-working-directory forwarding, timeout, memory/CPU limit UX, or output streaming. The supervisor supports one active protected workload globally. Terminal records are retained locally up to 128 entries.
 
 
QUALIFICATION.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Qualification
2
+
3
+ The 0.1.1 candidate is releaseable only when its exact commit passes the native Ubuntu matrix and produces the minimal evidence pack. Required gates include 100/100 fork-race workload-tree kills, 100/100 unrelated-canary survivals, zero workload control-socket accesses or replacement launches, PID-tripwire containment, benign near-limit completion, fail-closed supervisor restart recovery, and p95 trigger-to-empty latency below 500 ms.
4
+
5
+ Run the matrix only on a disposable or dedicated Linux test host:
6
+
7
+ ```sh
8
+ sudo python3 scripts/run_native_matrix.py --fork-race-repetitions 100 --benign-repetitions 50 --restart-repetitions 20 --socket-attempts 100 --output ./native-matrix.json
9
+ ```
10
+
11
+ The evidence establishes behavior only for the tested environment and commit. It does not establish universal Linux compatibility or any unsupported security claim.
README.md CHANGED
@@ -1,125 +1,51 @@
1
- # Lumi Nutcracker
2
 
3
- **The local Linux kill switch for AI and agent workloads.**
4
 
5
- Lumi Nutcracker launches a command you explicitly select under a dedicated no-login identity and inside a root-controlled cgroup-v2 boundary. The operator can terminate the complete workload tree, or configure a PID ceiling that triggers automatic containment when the workload starts creating too many processes.
6
 
7
- > Lumi Nutcracker launches an explicitly selected AI or agent workload inside a root-controlled Linux cgroup and terminates the complete workload tree on operator command or when its configured PID-runaway tripwire fires.
8
 
9
- This is a Linux engineering preview. Read [Limitations](LIMITATIONS.md) before relying on it.
10
 
11
- ## What it does
 
 
 
 
12
 
13
- - launches one selected command in a Nutcracker-owned cgroup;
14
- - runs that command as a dedicated unprivileged, no-login user;
15
- - kills the complete owned cgroup with the kernel's `cgroup.kill` primitive;
16
- - verifies that the cgroup and all descendants are empty before issuing a success receipt;
17
- - automatically contains workloads that hit their configured PID limit;
18
- - fails closed for active workloads when the supervisor restarts.
19
 
20
- ## What it does not do
21
-
22
- Lumi Nutcracker does not discover AI processes, attach to existing processes, identify malware, detect unknown intrusions, isolate networks or credentials, prevent general malware, or replace endpoint security.
23
 
24
  ## Requirements
25
 
26
- - Ubuntu 24.04 or a comparable Linux distribution using systemd;
27
- - unified cgroup v2 with `cgroup.kill` and the PID controller;
28
- - Python 3.11 or later;
29
- - root access for installation;
30
- - a non-root local operator account.
31
-
32
- ## Install a GitHub release
33
-
34
- Download and extract `lumi-nutcracker-0.1.0-linux.zip`, then run:
35
-
36
- ```bash
37
- cd lumi-nutcracker-0.1.0
38
- sha256sum -c SHA256SUMS
39
- sudo python3 scripts/install.py \
40
- --operator "$(id -un)" \
41
- --artifact ./lumi-nutcracker-0.1.0.pyz
42
- nutcracker doctor
43
- ```
44
-
45
- The installer creates the dedicated `lumi-nutcracker-workload` identity dynamically. It does not assume a particular operator UID.
46
-
47
- ## Build from source
48
-
49
- ```bash
50
- python3 scripts/build_release.py --output dist
51
- python3 scripts/verify_release.py \
52
- --artifact dist/lumi-nutcracker-0.1.0.pyz \
53
- --source-archive dist/lumi-nutcracker-0.1.0-source.zip \
54
- --release-bundle dist/lumi-nutcracker-0.1.0-linux.zip
55
- sudo python3 scripts/install.py \
56
- --operator "$(id -un)" \
57
- --artifact dist/lumi-nutcracker-0.1.0.pyz
58
- ```
59
-
60
- Release builds refuse a dirty Git tree so the embedded source commit remains meaningful.
61
 
62
  ## Quick start
63
 
64
- ```bash
65
- nutcracker start --name local-agent --max-pids 64 -- /usr/bin/python3 agent.py
66
- nutcracker status --name local-agent
67
- nutcracker kill --name local-agent --receipt ./kill-receipt.json
68
- ```
69
-
70
- The receipt is written only after direct cgroup kill and empty-hierarchy verification succeed.
71
 
72
- ## Commands
73
-
74
- ```text
75
- nutcracker start --name NAME --max-pids LIMIT -- COMMAND [ARGS...]
76
- nutcracker kill --name NAME --receipt FILE
77
- nutcracker status --name NAME
78
- nutcracker list
79
- nutcracker doctor
80
- nutcracker version
81
- ```
82
-
83
- ## Local AI smoke demonstration
84
-
85
- With a locally installed `llama-cli` and a GGUF model readable by the workload identity:
86
-
87
- ```bash
88
- python3 scripts/smoke_local_ai.py \
89
- --llama-cli /opt/llama/llama-cli \
90
- --model /opt/models/local.gguf \
91
- --output ai-smoke.json
92
- ```
93
-
94
- The script waits for visible model output, kills that selected inference workload through Nutcracker, and verifies that an unrelated canary survives.
95
-
96
- ## Uninstall
97
-
98
- Terminate active protected workloads first, then run from the extracted release or source tree:
99
-
100
- ```bash
101
  sudo python3 scripts/uninstall.py
102
- sudo python3 scripts/verify_uninstalled.py
103
  ```
104
 
105
- ## Qualification
106
-
107
- The 0.1.0 native Ubuntu qualification completed:
108
 
109
- - 100/100 complete fork-race workload-tree kills;
110
- - 100/100 unrelated-canary survivals;
111
- - zero successful control-socket accesses from the workload identity;
112
- - zero successful replacement launches;
113
- - automatic PID-tripwire containment;
114
- - 50/50 bounded benign completions without a false kill;
115
- - 20/20 fail-closed supervisor restart recoveries;
116
- - measured p95 trigger-to-empty latency below 500 ms;
117
- - a real local llama.cpp workload smoke test;
118
- - clean installation and complete uninstallation;
119
- - no firewall-rule objects or dependencies.
120
 
121
- See [Security](SECURITY.md), [Limitations](LIMITATIONS.md), and [Release notes](RELEASE_NOTES.md).
122
 
123
- ## Licence
 
 
124
 
125
- Apache-2.0. See [LICENSE](LICENSE).
 
1
+ # Lumi Eggcracker
2
 
3
+ Lumi Eggcracker is a small, local Linux kill switch for an AI or agent workload you explicitly select.
4
 
5
+ > Lumi Eggcracker launches one explicitly selected AI or agent workload inside a root-controlled Linux cgroup and terminates the complete workload tree on operator command or when its configured PID-runaway tripwire fires.
6
 
7
+ It uses one root-owned supervisor, direct cgroup-v2 `cgroup.kill`, and an exact empty-cgroup proof. The workload runs under a dedicated no-login account that cannot access the control socket.
8
 
9
+ ## It does
10
 
11
+ - launch one selected command in an exact owned cgroup;
12
+ - kill its complete cgroup tree on `eggcracker kill`;
13
+ - kill it automatically when its PID limit trips;
14
+ - write a post-containment receipt only after an exact empty proof;
15
+ - provide `start`, `kill`, `status`, `list`, `doctor`, and `version`.
16
 
17
+ ## It does not do
 
 
 
 
 
18
 
19
+ It does not identify AI automatically, find unknown processes, detect intrusions or malware, isolate networks or credentials, prevent general malware, replace EDR, or attach an existing process to its boundary.
 
 
20
 
21
  ## Requirements
22
 
23
+ - Linux with systemd and unified cgroup v2;
24
+ - `pids` controller and `cgroup.kill` on transient service cgroups;
25
+ - root for install/uninstall; a non-root local operator account;
26
+ - Python 3.11 or newer.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  ## Quick start
29
 
30
+ Download and extract `lumi-eggcracker-0.1.1-linux.zip`, then run as root:
 
 
 
 
 
 
31
 
32
+ ```sh
33
+ cd lumi-eggcracker-0.1.1
34
+ sudo python3 scripts/install.py --operator "$USER" --artifact ./lumi-eggcracker-0.1.1.pyz
35
+ eggcracker doctor
36
+ eggcracker start --name demo --max-pids 8 -- /bin/sleep 60
37
+ eggcracker kill --name demo --receipt ./demo-receipt.json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  sudo python3 scripts/uninstall.py
 
39
  ```
40
 
41
+ The selected command is launched through a systemd transient unit. It does not attach an interactive terminal, preserve your current working directory, or stream output to the CLI in this preview. Use absolute paths and redirect output in a wrapper script when needed.
 
 
42
 
43
+ ## Real local AI smoke
 
 
 
 
 
 
 
 
 
 
44
 
45
+ With a local `llama-cli` and GGUF model available, the release bundle includes a smoke demonstration that waits for visible model output, kills the explicitly selected inference workload, and verifies an unrelated canary survives:
46
 
47
+ ```sh
48
+ sudo python3 scripts/smoke_local_ai.py --llama-cli /path/to/llama-cli --model /path/to/model.gguf --output ./ai-smoke.json
49
+ ```
50
 
51
+ See [SECURITY_MODEL.md](SECURITY_MODEL.md), [LIMITATIONS.md](LIMITATIONS.md), and [QUALIFICATION.md](QUALIFICATION.md) before using the preview.
RELEASE_NOTES.md CHANGED
@@ -1,23 +1,5 @@
1
- # Lumi Nutcracker 0.1.0
2
 
3
- Lumi Nutcracker 0.1.0 is a Linux engineering preview of a deliberately small AI and agent workload kill switch.
4
 
5
- ## Supported claim
6
-
7
- > Lumi Nutcracker launches an explicitly selected AI or agent workload inside a root-controlled Linux cgroup and terminates the complete workload tree on operator command or when its configured PID-runaway tripwire fires.
8
-
9
- ## Highlights
10
-
11
- - One supported root-supervisor backend on systemd and unified cgroup v2.
12
- - Dedicated no-login workload identity separated from the human operator.
13
- - Direct `cgroup.kill` enforcement followed by exact empty-hierarchy verification.
14
- - Operator kill, automatic PID tripwire, post-containment receipts and restart fail-closed recovery.
15
- - Public commands: `start`, `kill`, `status`, `list`, `doctor` and `version`.
16
-
17
- ## Qualification
18
-
19
- The release passed 100/100 fork-race kills, 100/100 unrelated-canary survivals, 100 denied workload socket attempts, zero replacement launches, five automatic PID-tripwire trials, 50 benign completions, 20 supervisor restart recoveries, clean install/uninstall and a real local AI smoke demonstration. Native p95 trigger-to-empty latency was below the precommitted 500 ms ceiling.
20
-
21
- ## Important limits
22
-
23
- This release does not identify AI, discover unknown processes, attach to existing workloads, isolate network or credentials, detect malware, or replace EDR. See [LIMITATIONS.md](LIMITATIONS.md).
 
1
+ # Lumi Eggcracker 0.1.1
2
 
3
+ 0.1.1 is a corrective engineering-preview candidate. It introduces a fail-closed launch gate, cgroup-event lifecycle authority, direct cgroup-kill-first enforcement, receipt-before-cleanup ordering, bounded run records, stricter workload-account isolation, and a consistent Lumi Eggcracker public surface.
4
 
5
+ It is not published automatically by this repository. Native Ubuntu qualification and explicit release approval are required before tag or promotion.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
SECURITY.md CHANGED
@@ -1,5 +1,7 @@
1
  # Security policy
2
 
3
- Report security issues privately through GitHub's **Security → Report a vulnerability** flow when it is enabled for the repository. Otherwise, contact the project maintainer privately. Do not open a public issue containing exploit details, live credentials, private model files or VM keys.
4
 
5
- Lumi Nutcracker is a local privileged process-containment tool. Test only workloads and machines you own or administer. Do not use it against third-party processes or systems.
 
 
 
1
  # Security policy
2
 
3
+ Lumi Eggcracker is a privileged local process-containment preview. Test only workloads and machines you own or administer.
4
 
5
+ Report vulnerabilities privately through the repository security-advisory channel. Do not include exploit code, machine-specific paths, credentials, or live workload details in public issues.
6
+
7
+ The security boundary and non-claims are documented in [SECURITY_MODEL.md](SECURITY_MODEL.md).
SECURITY_MODEL.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Security model
2
+
3
+ The root-owned `lumi-eggcracker.service` is the only enforcement component. It accepts requests only from root or the configured operator UID over a root-owned Unix socket. A selected workload runs as a dedicated no-login unprivileged user with no supplementary groups; that identity cannot read, connect to, or invoke the socket.
4
+
5
+ Each workload is placed in an exactly identified systemd transient cgroup. On an operator request, PID-limit tripwire, watcher failure, or supervisor restart recovery, the supervisor validates that exact cgroup identity and writes `1` to its `cgroup.kill` control file. It then proves that the complete cgroup hierarchy is empty. Systemd stop is cleanup only.
6
+
7
+ This is not a general sandbox, host intrusion detector, malware detector, network isolator, credential isolator, or EDR replacement. The selected workload may still access everything normally available to its unprivileged account.
pyproject.toml CHANGED
@@ -3,8 +3,8 @@ requires = ["setuptools>=77"]
3
  build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
- name = "lumi-nutcracker"
7
- version = "0.1.0"
8
  description = "A local Linux kill switch for explicitly selected AI and agent workloads"
9
  readme = "README.md"
10
  requires-python = ">=3.11"
@@ -22,7 +22,7 @@ classifiers = [
22
  ]
23
 
24
  [project.scripts]
25
- nutcracker = "lumi_nutcracker.cli:main"
26
 
27
  [tool.setuptools]
28
  package-dir = {"" = "src"}
 
3
  build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
+ name = "lumi-eggcracker"
7
+ version = "0.1.1"
8
  description = "A local Linux kill switch for explicitly selected AI and agent workloads"
9
  readme = "README.md"
10
  requires-python = ">=3.11"
 
22
  ]
23
 
24
  [project.scripts]
25
+ eggcracker = "lumi_eggcracker.cli:main"
26
 
27
  [tool.setuptools]
28
  package-dir = {"" = "src"}
scripts/build_release.py CHANGED
@@ -1,4 +1,4 @@
1
- """Build a clean Lumi Nutcracker zipapp, source archive, and checksums."""
2
 
3
  from __future__ import annotations
4
 
@@ -24,7 +24,7 @@ def digest(path: Path) -> str:
24
 
25
 
26
  def version() -> str:
27
- value = (ROOT / "src" / "lumi_nutcracker" / "__init__.py").read_text(encoding="utf-8")
28
  match = re.search(r'__version__ = "([0-9]+\.[0-9]+\.[0-9]+)"', value)
29
  if not match:
30
  raise RuntimeError("cannot determine public version")
@@ -48,20 +48,20 @@ def main() -> int:
48
  release_version = version()
49
  commit = command(["git", "-C", str(ROOT), "rev-parse", "HEAD"])
50
  args.output.mkdir(parents=True, exist_ok=True)
51
- artifact = args.output / f"lumi-nutcracker-{release_version}.pyz"
52
- source = args.output / f"lumi-nutcracker-{release_version}-source.zip"
53
- bundle = args.output / f"lumi-nutcracker-{release_version}-linux.zip"
54
- with tempfile.TemporaryDirectory(prefix="lumi-nutcracker-build-") as raw:
55
  stage = Path(raw) / "src"
56
- shutil.copytree(ROOT / "src", stage, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
57
- (stage / "__main__.py").write_text("from lumi_nutcracker.cli import main\nraise SystemExit(main())\n", encoding="utf-8")
58
- (stage / "lumi_nutcracker" / "build_info.py").write_text(f'SOURCE_COMMIT = "{commit}"\n', encoding="utf-8")
59
  zipapp.create_archive(stage, target=artifact, interpreter="/usr/bin/env python3")
60
- command(["git", "-C", str(ROOT), "archive", "--format=zip", f"--prefix=lumi-nutcracker-{release_version}/", "-o", str(source), "HEAD"])
61
  manifest = {"artifact": artifact.name, "sha256": digest(artifact), "source_archive": source.name, "source_archive_sha256": digest(source), "source_commit": commit, "version": release_version}
62
  (args.output / "release-manifest.json").write_text(__import__("json").dumps(manifest, sort_keys=True) + "\n", encoding="utf-8")
63
  (args.output / "SHA256SUMS").write_text(f"{manifest['sha256']} {artifact.name}\n{manifest['source_archive_sha256']} {source.name}\n", encoding="utf-8")
64
- prefix = f"lumi-nutcracker-{release_version}"
65
  release_files = {
66
  artifact: artifact.name,
67
  source: source.name,
@@ -71,6 +71,8 @@ def main() -> int:
71
  ROOT / "LICENSE": "LICENSE",
72
  ROOT / "LIMITATIONS.md": "LIMITATIONS.md",
73
  ROOT / "SECURITY.md": "SECURITY.md",
 
 
74
  ROOT / "RELEASE_NOTES.md": "RELEASE_NOTES.md",
75
  ROOT / "scripts" / "install.py": "scripts/install.py",
76
  ROOT / "scripts" / "uninstall.py": "scripts/uninstall.py",
@@ -80,6 +82,12 @@ def main() -> int:
80
  with zipfile.ZipFile(bundle, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
81
  for path, name in release_files.items():
82
  archive.write(path, f"{prefix}/{name}")
 
 
 
 
 
 
83
  print(bundle)
84
  return 0
85
 
 
1
+ """Build a clean Lumi Eggcracker zipapp, source archive, and checksums."""
2
 
3
  from __future__ import annotations
4
 
 
24
 
25
 
26
  def version() -> str:
27
+ value = (ROOT / "src" / "lumi_eggcracker" / "__init__.py").read_text(encoding="utf-8")
28
  match = re.search(r'__version__ = "([0-9]+\.[0-9]+\.[0-9]+)"', value)
29
  if not match:
30
  raise RuntimeError("cannot determine public version")
 
48
  release_version = version()
49
  commit = command(["git", "-C", str(ROOT), "rev-parse", "HEAD"])
50
  args.output.mkdir(parents=True, exist_ok=True)
51
+ artifact = args.output / f"lumi-eggcracker-{release_version}.pyz"
52
+ source = args.output / f"lumi-eggcracker-{release_version}-source.zip"
53
+ bundle = args.output / f"lumi-eggcracker-{release_version}-linux.zip"
54
+ with tempfile.TemporaryDirectory(prefix="lumi-eggcracker-build-") as raw:
55
  stage = Path(raw) / "src"
56
+ shutil.copytree(ROOT / "src", stage, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.egg-info"))
57
+ (stage / "__main__.py").write_text("from lumi_eggcracker.cli import main\nraise SystemExit(main())\n", encoding="utf-8")
58
+ (stage / "lumi_eggcracker" / "build_info.py").write_text(f'SOURCE_COMMIT = "{commit}"\n', encoding="utf-8")
59
  zipapp.create_archive(stage, target=artifact, interpreter="/usr/bin/env python3")
60
+ command(["git", "-C", str(ROOT), "archive", "--format=zip", f"--prefix=lumi-eggcracker-{release_version}/", "-o", str(source), "HEAD"])
61
  manifest = {"artifact": artifact.name, "sha256": digest(artifact), "source_archive": source.name, "source_archive_sha256": digest(source), "source_commit": commit, "version": release_version}
62
  (args.output / "release-manifest.json").write_text(__import__("json").dumps(manifest, sort_keys=True) + "\n", encoding="utf-8")
63
  (args.output / "SHA256SUMS").write_text(f"{manifest['sha256']} {artifact.name}\n{manifest['source_archive_sha256']} {source.name}\n", encoding="utf-8")
64
+ prefix = f"lumi-eggcracker-{release_version}"
65
  release_files = {
66
  artifact: artifact.name,
67
  source: source.name,
 
71
  ROOT / "LICENSE": "LICENSE",
72
  ROOT / "LIMITATIONS.md": "LIMITATIONS.md",
73
  ROOT / "SECURITY.md": "SECURITY.md",
74
+ ROOT / "SECURITY_MODEL.md": "SECURITY_MODEL.md",
75
+ ROOT / "QUALIFICATION.md": "QUALIFICATION.md",
76
  ROOT / "RELEASE_NOTES.md": "RELEASE_NOTES.md",
77
  ROOT / "scripts" / "install.py": "scripts/install.py",
78
  ROOT / "scripts" / "uninstall.py": "scripts/uninstall.py",
 
82
  with zipfile.ZipFile(bundle, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
83
  for path, name in release_files.items():
84
  archive.write(path, f"{prefix}/{name}")
85
+ # The bundle cannot contain a checksum of itself without recursion. The
86
+ # detached checksum file intentionally covers all published binary assets.
87
+ (args.output / "SHA256SUMS").write_text(
88
+ f"{manifest['sha256']} {artifact.name}\n{manifest['source_archive_sha256']} {source.name}\n{digest(bundle)} {bundle.name}\n",
89
+ encoding="utf-8",
90
+ )
91
  print(bundle)
92
  return 0
93
 
scripts/install.py CHANGED
@@ -1,4 +1,4 @@
1
- """Manifest-bound installer for one local root supervisor and workload account."""
2
 
3
  from __future__ import annotations
4
 
@@ -16,14 +16,15 @@ import time
16
  from pathlib import Path
17
  from typing import Any
18
 
19
- LIB = Path("/usr/local/lib/lumi-nutcracker")
20
- BIN = Path("/usr/local/bin/nutcracker")
21
- ETC = Path("/etc/lumi-nutcracker")
22
- UNIT = Path("/etc/systemd/system/lumi-nutcracker.service")
23
- STATE = Path("/var/lib/lumi-nutcracker")
24
- RUNTIME = Path("/run/lumi-nutcracker")
 
25
  SOCKET = RUNTIME / "control.sock"
26
- WORKLOAD_NAME = "lumi-nutcracker-workload"
27
  TARGETS = (LIB, BIN, ETC, UNIT, STATE, RUNTIME)
28
 
29
 
@@ -54,7 +55,7 @@ def manifest_for(artifact: Path) -> dict[str, Any]:
54
  expected = {"artifact", "sha256", "source_archive", "source_archive_sha256", "source_commit", "version"}
55
  if set(value) != expected or value["artifact"] != artifact.name or value["sha256"] != digest(artifact):
56
  raise RuntimeError("release artifact identity does not match manifest")
57
- if value["version"] != "0.1.0" or len(value["source_commit"]) != 40:
58
  raise RuntimeError("release manifest version or source identity is invalid")
59
  return value
60
 
@@ -63,7 +64,7 @@ def workload_account() -> tuple[pwd.struct_passwd, bool]:
63
  try:
64
  account = pwd.getpwnam(WORKLOAD_NAME)
65
  if account.pw_uid == 0 or account.pw_shell not in {"/usr/sbin/nologin", "/sbin/nologin"} or account.pw_dir != "/nonexistent":
66
- raise RuntimeError("existing workload account does not meet Nutcracker contract")
67
  return account, False
68
  except KeyError:
69
  nologin = "/usr/sbin/nologin" if Path("/usr/sbin/nologin").is_file() else "/sbin/nologin"
@@ -74,12 +75,11 @@ def workload_account() -> tuple[pwd.struct_passwd, bool]:
74
 
75
 
76
  def service() -> bytes:
77
- return b"""[Unit]\nDescription=Lumi Nutcracker protected workload supervisor\nAfter=multi-user.target\nStartLimitIntervalSec=60\nStartLimitBurst=30\n\n[Service]\nType=simple\nExecStart=/usr/bin/python3 /usr/local/lib/lumi-nutcracker/lumi-nutcracker.pyz _supervisor --policy /etc/lumi-nutcracker/policy.json\nRestart=always\nRestartSec=0.1\nRuntimeDirectory=lumi-nutcracker\nRuntimeDirectoryMode=0710\nUMask=0077\nNoNewPrivileges=yes\n\n[Install]\nWantedBy=multi-user.target\n"""
78
 
79
 
80
  def cgroup_kill_available() -> bool:
81
- """Probe a disposable child cgroup; cgroup.kill is intentionally absent at root."""
82
- unit = f"lumi-nutcracker-preflight-{secrets.token_hex(8)}.service"
83
  started = run(["/usr/bin/systemd-run", f"--unit={unit}", "--collect", "--property=Type=exec", "--", "/bin/sleep", "5"])
84
  if started.returncode:
85
  return False
@@ -87,14 +87,13 @@ def cgroup_kill_available() -> bool:
87
  shown = run(["/usr/bin/systemctl", "show", unit, "--property=ControlGroup"])
88
  values = dict(line.split("=", 1) for line in shown.stdout.splitlines() if "=" in line)
89
  cgroup = values.get("ControlGroup", "")
90
- parts = cgroup.lstrip("/").split("/")
91
- return bool(cgroup.startswith("/system.slice/")) and (Path("/sys/fs/cgroup").joinpath(*parts) / "cgroup.kill").is_file()
92
  finally:
93
  run(["/usr/bin/systemctl", "stop", unit])
94
 
95
 
96
- def cleanup(created: list[Path], created_user: bool, created_group: bool = False) -> None:
97
- run(["/usr/bin/systemctl", "disable", "--now", "lumi-nutcracker.service"])
98
  for path in reversed(created):
99
  if path.is_dir() and not path.is_symlink():
100
  shutil.rmtree(path)
@@ -121,39 +120,32 @@ def main() -> int:
121
  if operator.pw_uid == 0:
122
  raise SystemExit("operator must be non-root")
123
  if any(path.exists() or path.is_symlink() for path in TARGETS):
124
- raise SystemExit("refusing pre-existing Nutcracker installation target")
125
- if not Path("/sys/fs/cgroup/cgroup.controllers").is_file():
126
- raise SystemExit("unified cgroup v2 is required")
127
- if "pids" not in Path("/sys/fs/cgroup/cgroup.controllers").read_text(encoding="ascii").split():
128
- raise SystemExit("cgroup PID controller is required")
129
- if not cgroup_kill_available():
130
- raise SystemExit("a transient system workload cgroup with cgroup.kill is required")
131
  account, created_user = workload_account()
132
  group = grp.getgrgid(account.pw_gid)
133
  created_group = created_user and group.gr_name == WORKLOAD_NAME
134
- if account.pw_uid in {0, operator.pw_uid} or account.pw_gid == operator.pw_gid:
 
135
  cleanup([], created_user, created_group)
136
- raise SystemExit("workload identity must be separate from operator")
137
  created: list[Path] = []
138
  try:
139
- LIB.mkdir(mode=0o755)
140
- created.append(LIB)
141
- ETC.mkdir(mode=0o700)
142
- created.append(ETC)
143
- STATE.mkdir(mode=0o700)
144
- created.append(STATE)
145
- shutil.copyfile(args.artifact, LIB / "lumi-nutcracker.pyz")
146
- os.chmod(LIB / "lumi-nutcracker.pyz", 0o755)
147
- write_new(BIN, b"#!/bin/sh\nexec /usr/bin/python3 /usr/local/lib/lumi-nutcracker/lumi-nutcracker.pyz \"$@\"\n", 0o755)
148
- created.append(BIN)
149
- policy = {"operator_gid": operator.pw_gid, "operator_uid": operator.pw_uid, "schema_version": "lumi-nutcracker.policy.v1", "socket_path": str(SOCKET), "source_commit": release["source_commit"], "state_dir": str(STATE), "unit_prefix": "lumi-nutcracker-workload-", "version": release["version"], "workload_gid": account.pw_gid, "workload_uid": account.pw_uid}
150
  write_new(ETC / "policy.json", (json.dumps(policy, sort_keys=True) + "\n").encode(), 0o600)
151
- write_new(UNIT, service(), 0o644)
152
- created.append(UNIT)
153
- manifest = {"created_workload_group": created_group, "created_workload_user": created_user, "files": {str(BIN): digest(BIN), str(ETC / "policy.json"): digest(ETC / "policy.json"), str(LIB / "lumi-nutcracker.pyz"): digest(LIB / "lumi-nutcracker.pyz"), str(UNIT): digest(UNIT)}, "operator": operator.pw_name, "operator_uid": operator.pw_uid, "schema_version": "lumi-nutcracker.install.v1", "targets": [str(path) for path in TARGETS], "workload_group": group.gr_name, "workload_uid": account.pw_uid, "workload_user": account.pw_name}
154
  write_new(STATE / "install-manifest.json", (json.dumps(manifest, sort_keys=True) + "\n").encode(), 0o600)
155
  checked = run(["/usr/bin/systemctl", "daemon-reload"])
156
- started = run(["/usr/bin/systemctl", "enable", "--now", "lumi-nutcracker.service"])
157
  if checked.returncode or started.returncode:
158
  raise RuntimeError((checked.stderr + started.stderr).strip() or "cannot start supervisor")
159
  deadline = time.monotonic() + 15
@@ -162,7 +154,7 @@ def main() -> int:
162
  metadata = SOCKET.stat()
163
  if not SOCKET.exists() or stat.S_IMODE(metadata.st_mode) != 0o660 or metadata.st_uid != 0 or metadata.st_gid != operator.pw_gid:
164
  raise RuntimeError("control socket ownership contract failed")
165
- print(json.dumps({"result": "INSTALLED", "service": "lumi-nutcracker.service", "workload_uid": account.pw_uid}, sort_keys=True))
166
  return 0
167
  except Exception:
168
  cleanup(created, created_user, created_group)
 
1
+ """Manifest-bound installer for Lumi Eggcracker's root supervisor."""
2
 
3
  from __future__ import annotations
4
 
 
16
  from pathlib import Path
17
  from typing import Any
18
 
19
+ VERSION = "0.1.1"
20
+ LIB = Path("/usr/local/lib/lumi-eggcracker")
21
+ BIN = Path("/usr/local/bin/eggcracker")
22
+ ETC = Path("/etc/lumi-eggcracker")
23
+ UNIT = Path("/etc/systemd/system/lumi-eggcracker.service")
24
+ STATE = Path("/var/lib/lumi-eggcracker")
25
+ RUNTIME = Path("/run/lumi-eggcracker")
26
  SOCKET = RUNTIME / "control.sock"
27
+ WORKLOAD_NAME = "lumi-eggcracker-workload"
28
  TARGETS = (LIB, BIN, ETC, UNIT, STATE, RUNTIME)
29
 
30
 
 
55
  expected = {"artifact", "sha256", "source_archive", "source_archive_sha256", "source_commit", "version"}
56
  if set(value) != expected or value["artifact"] != artifact.name or value["sha256"] != digest(artifact):
57
  raise RuntimeError("release artifact identity does not match manifest")
58
+ if value["version"] != VERSION or len(value["source_commit"]) != 40:
59
  raise RuntimeError("release manifest version or source identity is invalid")
60
  return value
61
 
 
64
  try:
65
  account = pwd.getpwnam(WORKLOAD_NAME)
66
  if account.pw_uid == 0 or account.pw_shell not in {"/usr/sbin/nologin", "/sbin/nologin"} or account.pw_dir != "/nonexistent":
67
+ raise RuntimeError("existing workload account does not meet Eggcracker contract")
68
  return account, False
69
  except KeyError:
70
  nologin = "/usr/sbin/nologin" if Path("/usr/sbin/nologin").is_file() else "/sbin/nologin"
 
75
 
76
 
77
  def service() -> bytes:
78
+ return b"""[Unit]\nDescription=Lumi Eggcracker protected workload supervisor\nAfter=multi-user.target\nStartLimitIntervalSec=60\nStartLimitBurst=60\n\n[Service]\nType=simple\nExecStart=/usr/bin/python3 /usr/local/lib/lumi-eggcracker/lumi-eggcracker.pyz _supervisor --policy /etc/lumi-eggcracker/policy.json\nRestart=always\nRestartSec=0.1\nRuntimeDirectory=lumi-eggcracker\nRuntimeDirectoryMode=0710\nUMask=0077\nNoNewPrivileges=yes\n\n[Install]\nWantedBy=multi-user.target\n"""
79
 
80
 
81
  def cgroup_kill_available() -> bool:
82
+ unit = f"lumi-eggcracker-preflight-{secrets.token_hex(8)}.service"
 
83
  started = run(["/usr/bin/systemd-run", f"--unit={unit}", "--collect", "--property=Type=exec", "--", "/bin/sleep", "5"])
84
  if started.returncode:
85
  return False
 
87
  shown = run(["/usr/bin/systemctl", "show", unit, "--property=ControlGroup"])
88
  values = dict(line.split("=", 1) for line in shown.stdout.splitlines() if "=" in line)
89
  cgroup = values.get("ControlGroup", "")
90
+ return bool(cgroup.startswith("/system.slice/")) and (Path("/sys/fs/cgroup").joinpath(*cgroup.lstrip("/").split("/")) / "cgroup.kill").is_file()
 
91
  finally:
92
  run(["/usr/bin/systemctl", "stop", unit])
93
 
94
 
95
+ def cleanup(created: list[Path], created_user: bool, created_group: bool) -> None:
96
+ run(["/usr/bin/systemctl", "disable", "--now", "lumi-eggcracker.service"])
97
  for path in reversed(created):
98
  if path.is_dir() and not path.is_symlink():
99
  shutil.rmtree(path)
 
120
  if operator.pw_uid == 0:
121
  raise SystemExit("operator must be non-root")
122
  if any(path.exists() or path.is_symlink() for path in TARGETS):
123
+ raise SystemExit("refusing pre-existing Eggcracker installation target")
124
+ controllers = Path("/sys/fs/cgroup/cgroup.controllers")
125
+ if not controllers.is_file() or "pids" not in controllers.read_text(encoding="ascii").split() or not cgroup_kill_available():
126
+ raise SystemExit("unified cgroup v2 with cgroup.kill and PID controller is required")
 
 
 
127
  account, created_user = workload_account()
128
  group = grp.getgrgid(account.pw_gid)
129
  created_group = created_user and group.gr_name == WORKLOAD_NAME
130
+ supplementary = set(os.getgrouplist(account.pw_name, account.pw_gid))
131
+ if account.pw_uid in {0, operator.pw_uid} or account.pw_gid == operator.pw_gid or supplementary != {account.pw_gid}:
132
  cleanup([], created_user, created_group)
133
+ raise SystemExit("workload identity must be isolated from the operator and have no supplementary groups")
134
  created: list[Path] = []
135
  try:
136
+ LIB.mkdir(mode=0o755); created.append(LIB)
137
+ ETC.mkdir(mode=0o700); created.append(ETC)
138
+ STATE.mkdir(mode=0o700); created.append(STATE)
139
+ shutil.copyfile(args.artifact, LIB / "lumi-eggcracker.pyz")
140
+ os.chmod(LIB / "lumi-eggcracker.pyz", 0o755)
141
+ write_new(BIN, b"#!/bin/sh\nexec /usr/bin/python3 /usr/local/lib/lumi-eggcracker/lumi-eggcracker.pyz \"$@\"\n", 0o755); created.append(BIN)
142
+ policy = {"operator_gid": operator.pw_gid, "operator_uid": operator.pw_uid, "schema_version": "lumi-eggcracker.policy.v2", "socket_path": str(SOCKET), "source_commit": release["source_commit"], "state_dir": str(STATE), "unit_prefix": "lumi-eggcracker-workload-", "version": release["version"], "workload_gid": account.pw_gid, "workload_uid": account.pw_uid}
 
 
 
 
143
  write_new(ETC / "policy.json", (json.dumps(policy, sort_keys=True) + "\n").encode(), 0o600)
144
+ write_new(UNIT, service(), 0o644); created.append(UNIT)
145
+ manifest = {"created_workload_group": created_group, "created_workload_user": created_user, "files": {str(BIN): digest(BIN), str(ETC / "policy.json"): digest(ETC / "policy.json"), str(LIB / "lumi-eggcracker.pyz"): digest(LIB / "lumi-eggcracker.pyz"), str(UNIT): digest(UNIT)}, "operator": operator.pw_name, "operator_uid": operator.pw_uid, "schema_version": "lumi-eggcracker.install.v2", "targets": [str(path) for path in TARGETS], "workload_group": group.gr_name, "workload_uid": account.pw_uid, "workload_user": account.pw_name}
 
146
  write_new(STATE / "install-manifest.json", (json.dumps(manifest, sort_keys=True) + "\n").encode(), 0o600)
147
  checked = run(["/usr/bin/systemctl", "daemon-reload"])
148
+ started = run(["/usr/bin/systemctl", "enable", "--now", "lumi-eggcracker.service"])
149
  if checked.returncode or started.returncode:
150
  raise RuntimeError((checked.stderr + started.stderr).strip() or "cannot start supervisor")
151
  deadline = time.monotonic() + 15
 
154
  metadata = SOCKET.stat()
155
  if not SOCKET.exists() or stat.S_IMODE(metadata.st_mode) != 0o660 or metadata.st_uid != 0 or metadata.st_gid != operator.pw_gid:
156
  raise RuntimeError("control socket ownership contract failed")
157
+ print(json.dumps({"result": "INSTALLED", "service": "lumi-eggcracker.service", "workload_uid": account.pw_uid}, sort_keys=True))
158
  return 0
159
  except Exception:
160
  cleanup(created, created_user, created_group)
scripts/run_native_matrix.py CHANGED
@@ -1,4 +1,4 @@
1
- """Root-only native qualification for fresh Nutcracker-owned fixture workloads."""
2
 
3
  from __future__ import annotations
4
 
@@ -14,7 +14,7 @@ from pathlib import Path
14
  from typing import Any
15
 
16
  ROOT = Path(__file__).resolve().parents[1]
17
- CLI = "/usr/local/bin/nutcracker"
18
 
19
 
20
  def run(argv: list[str], *, check: bool = True, timeout: int = 30) -> subprocess.CompletedProcess[str]:
@@ -77,7 +77,7 @@ def main() -> int:
77
  raise SystemExit("output must be a new file under an existing directory")
78
  if args.fork_race_repetitions != 100 or args.benign_repetitions < 50 or args.restart_repetitions < 20 or args.socket_attempts < 100:
79
  raise SystemExit("qualification counts are below the precommitted gates")
80
- install = json.loads(Path("/var/lib/lumi-nutcracker/install-manifest.json").read_text(encoding="utf-8"))
81
  operator = str(install["operator"])
82
  workload = str(install["workload_user"])
83
  workload_uid = pwd.getpwnam(workload).pw_uid
@@ -97,7 +97,7 @@ def main() -> int:
97
  name = f"race-{token}-{index}"
98
  call(operator, ["start", "--name", name, "--max-pids", "4096", "--", "/usr/bin/python3", str(ROOT / "tests/fixtures/fork_race.py"), modes[index % len(modes)]])
99
  time.sleep(0.12)
100
- receipt_path = Path("/tmp") / f"lumi-nutcracker-receipt-{token}-{index}.json"
101
  receipt = call(operator, ["kill", "--name", name, "--receipt", str(receipt_path)])
102
  if receipt.get("result") != "TERMINATED" or receipt["trigger"]["kind"] != "OPERATOR" or receipt["containment"]["surviving_pids"]:
103
  raise RuntimeError("fork race did not produce exact termination receipt")
@@ -113,7 +113,7 @@ def main() -> int:
113
  name = f"pressure-{token}-{index}"
114
  call(operator, ["start", "--name", name, "--max-pids", "4", "--", "/usr/bin/python3", str(ROOT / "tests/fixtures/pid_pressure.py")])
115
  state = wait_state(operator, name, "TERMINATED")
116
- receipt_files = sorted(Path("/var/lib/lumi-nutcracker/receipts").glob("*.json"), key=lambda path: path.stat().st_mtime_ns)
117
  receipt = json.loads(receipt_files[-1].read_text(encoding="utf-8"))
118
  if receipt["trigger"]["kind"] != "PID_LIMIT":
119
  raise RuntimeError(f"PID tripwire did not create PID receipt: {state}")
@@ -126,7 +126,7 @@ def main() -> int:
126
  if state.get("state") != "COMPLETED_ALLOWED":
127
  raise RuntimeError("benign near-limit workload was not allowed")
128
  results["benign"].append(state["state"])
129
- hostile_path = Path("/tmp") / f"lumi-nutcracker-hostile-{token}.json"
130
  hostile = f"hostile-{token}"
131
  call(operator, ["start", "--name", hostile, "--max-pids", "8", "--", "/usr/bin/python3", str(ROOT / "tests/fixtures/hostile_client.py"), str(hostile_path), str(args.socket_attempts)])
132
  wait_state(operator, hostile, "COMPLETED_ALLOWED", timeout=30)
@@ -141,9 +141,9 @@ def main() -> int:
141
  for index in range(args.restart_repetitions):
142
  name = f"restart-{token}-{index}"
143
  call(operator, ["start", "--name", name, "--max-pids", "8", "--", "/bin/sleep", "30"])
144
- run(["/usr/bin/systemctl", "kill", "--kill-who=main", "-s", "SIGKILL", "lumi-nutcracker.service"])
145
  state = wait_state(operator, name, "TERMINATED", timeout=12)
146
- receipt_files = sorted(Path("/var/lib/lumi-nutcracker/receipts").glob("*.json"), key=lambda path: path.stat().st_mtime_ns)
147
  receipt = json.loads(receipt_files[-1].read_text(encoding="utf-8"))
148
  if receipt["trigger"]["kind"] != "SUPERVISOR_RESTART_FAIL_CLOSED":
149
  raise RuntimeError(f"restart did not fail closed: {state}")
@@ -162,9 +162,9 @@ def main() -> int:
162
  args.output.write_text(json.dumps(results, sort_keys=True) + "\n", encoding="utf-8")
163
  raise
164
  finally:
165
- active = run(["/usr/bin/systemctl", "list-units", "lumi-nutcracker-workload-*", "--type=service", "--state=active", "--plain", "--no-legend"], check=False)
166
  for line in active.stdout.splitlines():
167
- if line.split() and line.split()[0].startswith("lumi-nutcracker-workload-"):
168
  run(["/usr/bin/systemctl", "stop", line.split()[0]], check=False)
169
 
170
 
 
1
+ """Root-only native qualification for fresh Eggcracker-owned fixture workloads."""
2
 
3
  from __future__ import annotations
4
 
 
14
  from typing import Any
15
 
16
  ROOT = Path(__file__).resolve().parents[1]
17
+ CLI = "/usr/local/bin/eggcracker"
18
 
19
 
20
  def run(argv: list[str], *, check: bool = True, timeout: int = 30) -> subprocess.CompletedProcess[str]:
 
77
  raise SystemExit("output must be a new file under an existing directory")
78
  if args.fork_race_repetitions != 100 or args.benign_repetitions < 50 or args.restart_repetitions < 20 or args.socket_attempts < 100:
79
  raise SystemExit("qualification counts are below the precommitted gates")
80
+ install = json.loads(Path("/var/lib/lumi-eggcracker/install-manifest.json").read_text(encoding="utf-8"))
81
  operator = str(install["operator"])
82
  workload = str(install["workload_user"])
83
  workload_uid = pwd.getpwnam(workload).pw_uid
 
97
  name = f"race-{token}-{index}"
98
  call(operator, ["start", "--name", name, "--max-pids", "4096", "--", "/usr/bin/python3", str(ROOT / "tests/fixtures/fork_race.py"), modes[index % len(modes)]])
99
  time.sleep(0.12)
100
+ receipt_path = Path("/tmp") / f"lumi-eggcracker-receipt-{token}-{index}.json"
101
  receipt = call(operator, ["kill", "--name", name, "--receipt", str(receipt_path)])
102
  if receipt.get("result") != "TERMINATED" or receipt["trigger"]["kind"] != "OPERATOR" or receipt["containment"]["surviving_pids"]:
103
  raise RuntimeError("fork race did not produce exact termination receipt")
 
113
  name = f"pressure-{token}-{index}"
114
  call(operator, ["start", "--name", name, "--max-pids", "4", "--", "/usr/bin/python3", str(ROOT / "tests/fixtures/pid_pressure.py")])
115
  state = wait_state(operator, name, "TERMINATED")
116
+ receipt_files = sorted(Path("/var/lib/lumi-eggcracker/receipts").glob("*.json"), key=lambda path: path.stat().st_mtime_ns)
117
  receipt = json.loads(receipt_files[-1].read_text(encoding="utf-8"))
118
  if receipt["trigger"]["kind"] != "PID_LIMIT":
119
  raise RuntimeError(f"PID tripwire did not create PID receipt: {state}")
 
126
  if state.get("state") != "COMPLETED_ALLOWED":
127
  raise RuntimeError("benign near-limit workload was not allowed")
128
  results["benign"].append(state["state"])
129
+ hostile_path = Path("/tmp") / f"lumi-eggcracker-hostile-{token}.json"
130
  hostile = f"hostile-{token}"
131
  call(operator, ["start", "--name", hostile, "--max-pids", "8", "--", "/usr/bin/python3", str(ROOT / "tests/fixtures/hostile_client.py"), str(hostile_path), str(args.socket_attempts)])
132
  wait_state(operator, hostile, "COMPLETED_ALLOWED", timeout=30)
 
141
  for index in range(args.restart_repetitions):
142
  name = f"restart-{token}-{index}"
143
  call(operator, ["start", "--name", name, "--max-pids", "8", "--", "/bin/sleep", "30"])
144
+ run(["/usr/bin/systemctl", "kill", "--kill-who=main", "-s", "SIGKILL", "lumi-eggcracker.service"])
145
  state = wait_state(operator, name, "TERMINATED", timeout=12)
146
+ receipt_files = sorted(Path("/var/lib/lumi-eggcracker/receipts").glob("*.json"), key=lambda path: path.stat().st_mtime_ns)
147
  receipt = json.loads(receipt_files[-1].read_text(encoding="utf-8"))
148
  if receipt["trigger"]["kind"] != "SUPERVISOR_RESTART_FAIL_CLOSED":
149
  raise RuntimeError(f"restart did not fail closed: {state}")
 
162
  args.output.write_text(json.dumps(results, sort_keys=True) + "\n", encoding="utf-8")
163
  raise
164
  finally:
165
+ active = run(["/usr/bin/systemctl", "list-units", "lumi-eggcracker-workload-*", "--type=service", "--state=active", "--plain", "--no-legend"], check=False)
166
  for line in active.stdout.splitlines():
167
+ if line.split() and line.split()[0].startswith("lumi-eggcracker-workload-"):
168
  run(["/usr/bin/systemctl", "stop", line.split()[0]], check=False)
169
 
170
 
scripts/smoke_local_ai.py CHANGED
@@ -23,9 +23,9 @@ def digest(path: Path) -> str:
23
 
24
 
25
  def call(args: list[str]) -> dict[str, object]:
26
- result = subprocess.run(["/usr/local/bin/nutcracker", *args], capture_output=True, text=True, check=False, timeout=30)
27
  if result.returncode:
28
- raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "Nutcracker command failed")
29
  return json.loads(result.stdout)
30
 
31
 
@@ -39,7 +39,7 @@ def main() -> int:
39
  raise SystemExit("output must be a new file under an existing directory")
40
  if not args.llama_cli.is_file() or not os.access(args.llama_cli, os.X_OK) or not args.model.is_file():
41
  raise SystemExit("llama-cli executable and GGUF model are required")
42
- with tempfile.TemporaryDirectory(prefix="lumi-nutcracker-ai-smoke-", dir="/tmp") as raw:
43
  root = Path(raw)
44
  # The operator owns this short-lived directory; the dedicated workload
45
  # identity needs a writable output location for the visible demo.
 
23
 
24
 
25
  def call(args: list[str]) -> dict[str, object]:
26
+ result = subprocess.run(["/usr/local/bin/eggcracker", *args], capture_output=True, text=True, check=False, timeout=30)
27
  if result.returncode:
28
+ raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "Eggcracker command failed")
29
  return json.loads(result.stdout)
30
 
31
 
 
39
  raise SystemExit("output must be a new file under an existing directory")
40
  if not args.llama_cli.is_file() or not os.access(args.llama_cli, os.X_OK) or not args.model.is_file():
41
  raise SystemExit("llama-cli executable and GGUF model are required")
42
+ with tempfile.TemporaryDirectory(prefix="lumi-eggcracker-ai-smoke-", dir="/tmp") as raw:
43
  root = Path(raw)
44
  # The operator owns this short-lived directory; the dedicated workload
45
  # identity needs a writable output location for the visible demo.
scripts/uninstall.py CHANGED
@@ -1,4 +1,4 @@
1
- """Exact manifest-bound uninstaller for Lumi Nutcracker."""
2
 
3
  from __future__ import annotations
4
 
@@ -11,12 +11,13 @@ import shutil
11
  import subprocess
12
  from pathlib import Path
13
 
14
- LIB = Path("/usr/local/lib/lumi-nutcracker")
15
- BIN = Path("/usr/local/bin/nutcracker")
16
- ETC = Path("/etc/lumi-nutcracker")
17
- UNIT = Path("/etc/systemd/system/lumi-nutcracker.service")
18
- STATE = Path("/var/lib/lumi-nutcracker")
19
- RUNTIME = Path("/run/lumi-nutcracker")
 
20
 
21
 
22
  def digest(path: Path) -> str:
@@ -31,6 +32,23 @@ def run(argv: list[str]) -> subprocess.CompletedProcess[str]:
31
  return subprocess.run(argv, capture_output=True, text=True, check=False, timeout=60)
32
 
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  def main() -> int:
35
  if os.geteuid() != 0:
36
  raise SystemExit("uninstaller must run as root")
@@ -38,23 +56,19 @@ def main() -> int:
38
  if manifest_path.is_symlink() or not manifest_path.is_file():
39
  raise SystemExit("installed manifest is missing")
40
  manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
41
- if manifest.get("schema_version") != "lumi-nutcracker.install.v1":
42
  raise SystemExit("installed manifest schema is invalid")
43
  for name, expected in manifest.get("files", {}).items():
44
  path = Path(name)
45
  if path.is_symlink() or not path.is_file() or digest(path) != expected:
46
  raise SystemExit(f"refusing uninstall because installed file drifted: {path}")
47
- active = run(["/usr/bin/systemctl", "list-units", "lumi-nutcracker-workload-*", "--type=service", "--state=active", "--no-legend", "--no-pager"])
48
- if active.stdout.strip():
49
- raise SystemExit("refusing uninstall with active Nutcracker workloads")
50
- run(["/usr/bin/systemctl", "disable", "--now", "lumi-nutcracker.service"])
51
- run(["/usr/bin/systemctl", "reset-failed", "lumi-nutcracker.service"])
52
  for path in (UNIT, BIN, LIB, ETC, STATE, RUNTIME):
53
  if path.exists() and not path.is_symlink():
54
- if path.is_dir():
55
- shutil.rmtree(path)
56
- else:
57
- path.unlink()
58
  if manifest.get("created_workload_user"):
59
  try:
60
  account = pwd.getpwnam(str(manifest["workload_user"]))
@@ -65,9 +79,8 @@ def main() -> int:
65
  if result.returncode:
66
  raise SystemExit(result.stderr.strip() or "cannot remove created workload account")
67
  if manifest.get("created_workload_group"):
68
- group_name = str(manifest.get("workload_group", ""))
69
  try:
70
- group = grp.getgrnam(group_name)
71
  except KeyError:
72
  group = None
73
  if group:
 
1
+ """Exact manifest-bound uninstaller for Lumi Eggcracker."""
2
 
3
  from __future__ import annotations
4
 
 
11
  import subprocess
12
  from pathlib import Path
13
 
14
+ LIB = Path("/usr/local/lib/lumi-eggcracker")
15
+ BIN = Path("/usr/local/bin/eggcracker")
16
+ ETC = Path("/etc/lumi-eggcracker")
17
+ UNIT = Path("/etc/systemd/system/lumi-eggcracker.service")
18
+ STATE = Path("/var/lib/lumi-eggcracker")
19
+ RUNTIME = Path("/run/lumi-eggcracker")
20
+ PREFIX = "lumi-eggcracker-workload-"
21
 
22
 
23
  def digest(path: Path) -> str:
 
32
  return subprocess.run(argv, capture_output=True, text=True, check=False, timeout=60)
33
 
34
 
35
+ def owned_cgroups_empty() -> bool:
36
+ root = Path("/sys/fs/cgroup/system.slice")
37
+ if not root.is_dir():
38
+ return False
39
+ for path in root.glob(f"{PREFIX}*.service"):
40
+ if not path.is_dir() or path.is_symlink():
41
+ return False
42
+ events = path / "cgroup.events"
43
+ try:
44
+ populated = dict(line.split(" ", 1) for line in events.read_text(encoding="ascii").splitlines()).get("populated")
45
+ except OSError:
46
+ return False
47
+ if populated != "0":
48
+ return False
49
+ return True
50
+
51
+
52
  def main() -> int:
53
  if os.geteuid() != 0:
54
  raise SystemExit("uninstaller must run as root")
 
56
  if manifest_path.is_symlink() or not manifest_path.is_file():
57
  raise SystemExit("installed manifest is missing")
58
  manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
59
+ if manifest.get("schema_version") != "lumi-eggcracker.install.v2":
60
  raise SystemExit("installed manifest schema is invalid")
61
  for name, expected in manifest.get("files", {}).items():
62
  path = Path(name)
63
  if path.is_symlink() or not path.is_file() or digest(path) != expected:
64
  raise SystemExit(f"refusing uninstall because installed file drifted: {path}")
65
+ if not owned_cgroups_empty():
66
+ raise SystemExit("refusing uninstall with populated or uncertain Eggcracker cgroups")
67
+ run(["/usr/bin/systemctl", "disable", "--now", "lumi-eggcracker.service"])
68
+ run(["/usr/bin/systemctl", "reset-failed", "lumi-eggcracker.service"])
 
69
  for path in (UNIT, BIN, LIB, ETC, STATE, RUNTIME):
70
  if path.exists() and not path.is_symlink():
71
+ shutil.rmtree(path) if path.is_dir() else path.unlink()
 
 
 
72
  if manifest.get("created_workload_user"):
73
  try:
74
  account = pwd.getpwnam(str(manifest["workload_user"]))
 
79
  if result.returncode:
80
  raise SystemExit(result.stderr.strip() or "cannot remove created workload account")
81
  if manifest.get("created_workload_group"):
 
82
  try:
83
+ group = grp.getgrnam(str(manifest.get("workload_group", "")))
84
  except KeyError:
85
  group = None
86
  if group:
scripts/verify_release.py CHANGED
@@ -10,18 +10,19 @@ import sys
10
  import zipfile
11
  from pathlib import Path
12
 
13
- FORBIDDEN = ("/mnt/f/", "f:\\", "network-deny", "network_rule", "nftables", "/usr/sbin/nft", "b20", "brief 1", "brief-", "rootless", "skylark sentinel", "skylark-sentinel", "/usr/local/bin/lumi-nutcracker")
 
 
 
 
 
 
 
14
 
15
 
16
  def text_from_zip(path: Path) -> str:
17
  with zipfile.ZipFile(path) as archive:
18
- values: list[str] = []
19
- for name in archive.namelist():
20
- if name.endswith("scripts/verify_release.py"):
21
- continue
22
- if name.endswith((".py", ".md", ".toml", ".txt")):
23
- values.append(archive.read(name).decode("utf-8", errors="ignore").lower())
24
- return "\n".join(values)
25
 
26
 
27
  def digest_bytes(value: bytes) -> str:
@@ -34,43 +35,28 @@ def main() -> int:
34
  parser.add_argument("--source-archive", required=True, type=Path)
35
  parser.add_argument("--release-bundle", required=True, type=Path)
36
  args = parser.parse_args()
37
- if not args.artifact.is_file() or not args.source_archive.is_file() or not args.release_bundle.is_file():
38
  raise SystemExit("release artifacts are missing")
39
  for path in (args.artifact, args.source_archive, args.release_bundle):
40
- value = text_from_zip(path)
41
- leaks = [item for item in FORBIDDEN if item in value]
42
  if leaks:
43
  raise SystemExit(f"forbidden public artifact content in {path.name}: {leaks}")
44
  result = subprocess.run([sys.executable, str(args.artifact), "version"], capture_output=True, text=True, check=False)
45
- if result.returncode or result.stdout.strip() != "0.1.0":
46
  raise SystemExit("artifact version is inconsistent")
47
  manifest = json.loads((args.artifact.parent / "release-manifest.json").read_text(encoding="utf-8"))
48
- if manifest.get("version") != "0.1.0" or manifest.get("artifact") != args.artifact.name:
49
  raise SystemExit("release manifest is inconsistent")
50
- prefix = "lumi-nutcracker-0.1.0/"
51
- expected = {
52
- prefix + args.artifact.name,
53
- prefix + args.source_archive.name,
54
- prefix + "release-manifest.json",
55
- prefix + "SHA256SUMS",
56
- prefix + "README.md",
57
- prefix + "LICENSE",
58
- prefix + "LIMITATIONS.md",
59
- prefix + "SECURITY.md",
60
- prefix + "RELEASE_NOTES.md",
61
- prefix + "scripts/install.py",
62
- prefix + "scripts/uninstall.py",
63
- prefix + "scripts/verify_uninstalled.py",
64
- prefix + "scripts/smoke_local_ai.py",
65
- }
66
  with zipfile.ZipFile(args.release_bundle) as archive:
67
  if set(archive.namelist()) != expected:
68
  raise SystemExit("release bundle contents are inconsistent")
69
- if digest_bytes(archive.read(prefix + args.artifact.name)) != manifest["sha256"]:
70
  raise SystemExit("bundled artifact digest is inconsistent")
71
- if digest_bytes(archive.read(prefix + args.source_archive.name)) != manifest["source_archive_sha256"]:
72
- raise SystemExit("bundled source digest is inconsistent")
73
- print(json.dumps({"result": "PASS", "version": "0.1.0"}, sort_keys=True))
 
74
  return 0
75
 
76
 
 
10
  import zipfile
11
  from pathlib import Path
12
 
13
+ VERSION = "0.1.1"
14
+ PREFIX = f"lumi-eggcracker-{VERSION}/"
15
+ FORBIDDEN = (
16
+ "/mnt/" + "f/", "f" + ":\\", "network" + "-deny", "network" + "_rule",
17
+ "nft" + "ables", "/usr/sbin/" + "nft", "b" + "20", "brief" + " 1",
18
+ "brief" + "-", "root" + "less", "skylark" + " sentinel",
19
+ "skylark" + "-sentinel", "nut" + "cracker",
20
+ )
21
 
22
 
23
  def text_from_zip(path: Path) -> str:
24
  with zipfile.ZipFile(path) as archive:
25
+ return "\n".join(archive.read(name).decode("utf-8", errors="ignore").lower() for name in archive.namelist() if name.endswith((".py", ".md", ".toml", ".txt")))
 
 
 
 
 
 
26
 
27
 
28
  def digest_bytes(value: bytes) -> str:
 
35
  parser.add_argument("--source-archive", required=True, type=Path)
36
  parser.add_argument("--release-bundle", required=True, type=Path)
37
  args = parser.parse_args()
38
+ if not all(path.is_file() for path in (args.artifact, args.source_archive, args.release_bundle)):
39
  raise SystemExit("release artifacts are missing")
40
  for path in (args.artifact, args.source_archive, args.release_bundle):
41
+ leaks = [item for item in FORBIDDEN if item in text_from_zip(path)]
 
42
  if leaks:
43
  raise SystemExit(f"forbidden public artifact content in {path.name}: {leaks}")
44
  result = subprocess.run([sys.executable, str(args.artifact), "version"], capture_output=True, text=True, check=False)
45
+ if result.returncode or result.stdout.strip() != VERSION:
46
  raise SystemExit("artifact version is inconsistent")
47
  manifest = json.loads((args.artifact.parent / "release-manifest.json").read_text(encoding="utf-8"))
48
+ if manifest.get("version") != VERSION or manifest.get("artifact") != args.artifact.name:
49
  raise SystemExit("release manifest is inconsistent")
50
+ expected = {PREFIX + name for name in (args.artifact.name, args.source_archive.name, "release-manifest.json", "SHA256SUMS", "README.md", "LICENSE", "LIMITATIONS.md", "SECURITY.md", "SECURITY_MODEL.md", "QUALIFICATION.md", "RELEASE_NOTES.md", "scripts/install.py", "scripts/uninstall.py", "scripts/verify_uninstalled.py", "scripts/smoke_local_ai.py")}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  with zipfile.ZipFile(args.release_bundle) as archive:
52
  if set(archive.namelist()) != expected:
53
  raise SystemExit("release bundle contents are inconsistent")
54
+ if digest_bytes(archive.read(PREFIX + args.artifact.name)) != manifest["sha256"] or digest_bytes(archive.read(PREFIX + args.source_archive.name)) != manifest["source_archive_sha256"]:
55
  raise SystemExit("bundled artifact digest is inconsistent")
56
+ sums = (args.artifact.parent / "SHA256SUMS").read_text(encoding="utf-8")
57
+ if args.artifact.name not in sums or args.source_archive.name not in sums or args.release_bundle.name not in sums:
58
+ raise SystemExit("checksums do not cover every release asset")
59
+ print(json.dumps({"result": "PASS", "version": VERSION}, sort_keys=True))
60
  return 0
61
 
62
 
scripts/verify_uninstalled.py CHANGED
@@ -1,4 +1,4 @@
1
- """Verify that a clean-install test left no Nutcracker service or paths behind."""
2
 
3
  from __future__ import annotations
4
 
@@ -8,25 +8,25 @@ import pwd
8
  import subprocess
9
  from pathlib import Path
10
 
11
- PATHS = (Path("/usr/local/lib/lumi-nutcracker"), Path("/usr/local/bin/nutcracker"), Path("/etc/lumi-nutcracker"), Path("/etc/systemd/system/lumi-nutcracker.service"), Path("/var/lib/lumi-nutcracker"), Path("/run/lumi-nutcracker"))
12
 
13
 
14
  def main() -> int:
15
  if os.geteuid() != 0:
16
  raise SystemExit("uninstall verifier must run as root")
17
  if any(path.exists() or path.is_symlink() for path in PATHS):
18
- raise SystemExit("Nutcracker installation path remains")
19
- units = subprocess.run(["/usr/bin/systemctl", "list-units", "lumi-nutcracker*", "--all", "--plain", "--no-legend"], capture_output=True, text=True, check=False)
20
  if units.stdout.strip():
21
- raise SystemExit("Nutcracker unit remains")
22
  try:
23
- pwd.getpwnam("lumi-nutcracker-workload")
24
  except KeyError:
25
  pass
26
  else:
27
  raise SystemExit("created workload account remains")
28
  try:
29
- grp.getgrnam("lumi-nutcracker-workload")
30
  except KeyError:
31
  print('{"result":"PASS"}')
32
  return 0
 
1
+ """Verify that a clean-install test left no Eggcracker paths behind."""
2
 
3
  from __future__ import annotations
4
 
 
8
  import subprocess
9
  from pathlib import Path
10
 
11
+ PATHS = (Path("/usr/local/lib/lumi-eggcracker"), Path("/usr/local/bin/eggcracker"), Path("/etc/lumi-eggcracker"), Path("/etc/systemd/system/lumi-eggcracker.service"), Path("/var/lib/lumi-eggcracker"), Path("/run/lumi-eggcracker"))
12
 
13
 
14
  def main() -> int:
15
  if os.geteuid() != 0:
16
  raise SystemExit("uninstall verifier must run as root")
17
  if any(path.exists() or path.is_symlink() for path in PATHS):
18
+ raise SystemExit("Eggcracker installation path remains")
19
+ units = subprocess.run(["/usr/bin/systemctl", "list-units", "lumi-eggcracker*", "--all", "--plain", "--no-legend"], capture_output=True, text=True, check=False)
20
  if units.stdout.strip():
21
+ raise SystemExit("Eggcracker unit remains")
22
  try:
23
+ pwd.getpwnam("lumi-eggcracker-workload")
24
  except KeyError:
25
  pass
26
  else:
27
  raise SystemExit("created workload account remains")
28
  try:
29
+ grp.getgrnam("lumi-eggcracker-workload")
30
  except KeyError:
31
  print('{"result":"PASS"}')
32
  return 0
src/lumi_eggcracker/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """Lumi Eggcracker: explicit protected Linux workload termination."""
2
+
3
+ __version__ = "0.1.1"
src/{lumi_nutcracker → lumi_eggcracker}/__main__.py RENAMED
File without changes
src/{lumi_nutcracker → lumi_eggcracker}/cli.py RENAMED
@@ -9,12 +9,13 @@ from pathlib import Path
9
 
10
  from . import __version__
11
  from .client import request
 
12
  from .jsonio import JsonInputError, write_new_json
13
  from .supervisor import main as supervisor_main
14
 
15
 
16
  def _parser() -> argparse.ArgumentParser:
17
- parser = argparse.ArgumentParser(prog="nutcracker")
18
  commands = parser.add_subparsers(dest="command", required=True)
19
  start = commands.add_parser("start", help="launch an explicitly selected protected workload")
20
  start.add_argument("--name", required=True)
@@ -39,6 +40,8 @@ def main(argv: list[str] | None = None) -> int:
39
  values = sys.argv[1:] if argv is None else argv
40
  if values[:1] == ["_supervisor"]:
41
  return supervisor_main(values[1:])
 
 
42
  args = _parser().parse_args(values)
43
  if args.command == "version":
44
  print(__version__)
@@ -63,5 +66,5 @@ def main(argv: list[str] | None = None) -> int:
63
  print(json.dumps(value, sort_keys=True))
64
  return 0
65
  except (JsonInputError, OSError) as error:
66
- print(f"nutcracker: {error}", file=sys.stderr)
67
  return 4
 
9
 
10
  from . import __version__
11
  from .client import request
12
+ from .gate import main as gate_main
13
  from .jsonio import JsonInputError, write_new_json
14
  from .supervisor import main as supervisor_main
15
 
16
 
17
  def _parser() -> argparse.ArgumentParser:
18
+ parser = argparse.ArgumentParser(prog="eggcracker")
19
  commands = parser.add_subparsers(dest="command", required=True)
20
  start = commands.add_parser("start", help="launch an explicitly selected protected workload")
21
  start.add_argument("--name", required=True)
 
40
  values = sys.argv[1:] if argv is None else argv
41
  if values[:1] == ["_supervisor"]:
42
  return supervisor_main(values[1:])
43
+ if values[:1] == ["_gate"]:
44
+ return gate_main(values[1:])
45
  args = _parser().parse_args(values)
46
  if args.command == "version":
47
  print(__version__)
 
66
  print(json.dumps(value, sort_keys=True))
67
  return 0
68
  except (JsonInputError, OSError) as error:
69
+ print(f"eggcracker: {error}", file=sys.stderr)
70
  return 4
src/{lumi_nutcracker → lumi_eggcracker}/client.py RENAMED
@@ -10,7 +10,7 @@ from typing import Any
10
  from .jsonio import JsonInputError
11
 
12
  MAX_FRAME = 32 * 1024
13
- SOCKET_PATH = "/run/lumi-nutcracker/control.sock"
14
 
15
 
16
  def _receive(connection: socket.socket) -> dict[str, Any]:
 
10
  from .jsonio import JsonInputError
11
 
12
  MAX_FRAME = 32 * 1024
13
+ SOCKET_PATH = "/run/lumi-eggcracker/control.sock"
14
 
15
 
16
  def _receive(connection: socket.socket) -> dict[str, Any]:
src/{lumi_nutcracker → lumi_eggcracker}/containment.py RENAMED
@@ -12,7 +12,7 @@ from pathlib import Path
12
  from .jsonio import JsonInputError
13
 
14
  CGROUP_ROOT = Path("/sys/fs/cgroup")
15
- UNIT_RE = re.compile(r"^/system\.slice/lumi-nutcracker-workload-([0-9a-f]{24})\.service$")
16
 
17
 
18
  @dataclass(frozen=True)
@@ -24,16 +24,6 @@ class CgroupIdentity:
24
  run_id: str
25
  unit: str
26
 
27
- def as_dict(self) -> dict[str, object]:
28
- return {
29
- "boot_id": self.boot_id,
30
- "cgroup": self.cgroup,
31
- "cgroup_device": self.device,
32
- "cgroup_inode": self.inode,
33
- "run_id": self.run_id,
34
- "unit": self.unit,
35
- }
36
-
37
 
38
  @dataclass(frozen=True)
39
  class EmptyProof:
@@ -52,35 +42,40 @@ def boot_id() -> str:
52
 
53
  def _path(cgroup: str, *, root: Path = CGROUP_ROOT) -> Path:
54
  if not UNIT_RE.fullmatch(cgroup):
55
- raise JsonInputError("cgroup is outside the Nutcracker unit namespace")
56
  candidate = root.joinpath(*cgroup.lstrip("/").split("/"))
57
  if candidate.is_symlink() or not candidate.is_dir():
58
  raise JsonInputError("owned cgroup is unavailable")
59
  return candidate
60
 
61
 
 
 
 
 
 
 
 
 
 
 
62
  def _events(path: Path) -> dict[str, int]:
63
  try:
64
- pairs = [line.split(" ", 1) for line in path.read_text(encoding="ascii").splitlines()]
 
65
  except OSError as error:
66
  raise JsonInputError(f"cannot read {path.name}: {error}") from error
67
- value = {key: raw for key, raw in pairs if key and raw}
68
- if any(not raw.isdigit() for raw in value.values()):
69
- raise JsonInputError(f"{path.name} has invalid values")
70
- return {key: int(raw) for key, raw in value.items()}
71
 
72
 
73
  def capture_identity(cgroup: str, run_id: str, unit: str, *, root: Path = CGROUP_ROOT) -> CgroupIdentity:
74
- if unit != f"lumi-nutcracker-workload-{run_id}.service":
75
  raise JsonInputError("unit and run identity disagree")
76
  path = _path(cgroup, root=root)
77
  required = ("cgroup.kill", "cgroup.events", "cgroup.procs", "pids.events", "pids.max")
78
  if not all((path / name).is_file() for name in required):
79
  raise JsonInputError("owned cgroup lacks required v2 control files")
80
- if "populated" not in _events(path / "cgroup.events"):
81
- raise JsonInputError("owned cgroup does not expose populated state")
82
- if "max" not in _events(path / "pids.events"):
83
- raise JsonInputError("owned cgroup does not expose PID events")
84
  metadata = path.stat(follow_symlinks=False)
85
  return CgroupIdentity(boot_id(), cgroup, metadata.st_dev, metadata.st_ino, run_id, unit)
86
 
@@ -89,21 +84,13 @@ def validate_identity(identity: CgroupIdentity, *, root: Path = CGROUP_ROOT) ->
89
  if identity.boot_id != boot_id():
90
  raise JsonInputError("workload boot identity changed")
91
  path = _path(identity.cgroup, root=root)
92
- try:
93
- metadata = path.stat(follow_symlinks=False)
94
- except OSError as error:
95
- raise JsonInputError("owned cgroup is unavailable") from error
96
  if metadata.st_dev != identity.device or metadata.st_ino != identity.inode:
97
  raise JsonInputError("owned cgroup identity drifted")
98
  return path
99
 
100
 
101
- def pids_max_event(identity: CgroupIdentity, *, root: Path = CGROUP_ROOT) -> int:
102
- return _events(validate_identity(identity, root=root) / "pids.events")["max"]
103
-
104
-
105
  def kill_path(path: Path) -> tuple[int, int]:
106
- """Write a previously validated exact cgroup.kill file directly."""
107
  control = path / "cgroup.kill"
108
  started = time.monotonic_ns()
109
  descriptor = os.open(control, os.O_WRONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0))
@@ -116,55 +103,41 @@ def kill_path(path: Path) -> tuple[int, int]:
116
 
117
 
118
  def kill(identity: CgroupIdentity, *, root: Path = CGROUP_ROOT) -> tuple[int, int]:
119
- """Validate then write cgroup.kill; use kill_path when trigger order matters."""
120
  return kill_path(validate_identity(identity, root=root))
121
 
122
 
123
  def _survivors(path: Path) -> tuple[list[int], int]:
124
- directories = [path]
125
- directories.extend(item for item in path.rglob("*") if item.is_dir() and not item.is_symlink())
126
  values: list[int] = []
127
  for directory in directories:
128
- control = directory / "cgroup.procs"
129
- if control.is_file():
130
- for line in control.read_text(encoding="ascii").splitlines():
131
- if not line.isdigit():
132
- raise JsonInputError("cgroup.procs contains invalid PID")
133
- values.append(int(line))
134
  return sorted(set(values)), len(directories)
135
 
136
 
137
- def _collected(error: Exception) -> bool:
138
  return isinstance(error, OSError) and error.errno in {errno.ENOENT, errno.ENODEV}
139
 
140
 
141
  def verify_empty(identity: CgroupIdentity, *, deadline_seconds: float = 0.9, root: Path = CGROUP_ROOT) -> tuple[int, EmptyProof]:
142
- """Prove an empty hierarchy after direct kill, re-applying it across fork races."""
143
  deadline = time.monotonic() + deadline_seconds
144
  last = EmptyProof(False, 0, -1, [])
145
  while time.monotonic() <= deadline:
146
  try:
147
  path = validate_identity(identity, root=root)
148
- except JsonInputError as error:
149
- if "unavailable" in str(error):
150
- return time.monotonic_ns(), EmptyProof(True, 0, 0, [])
151
- raise
152
- try:
153
  events = _events(path / "cgroup.events")
154
  survivors, descendants = _survivors(path)
155
  except (JsonInputError, OSError) as error:
156
- # A transient service may be collected immediately after cgroup.kill.
157
- # ENODEV/ENOENT from cgroupfs is proof that no live hierarchy remains.
158
- if _collected(error) or "No such device" in str(error) or "No such file" in str(error):
159
  return time.monotonic_ns(), EmptyProof(True, 0, 0, [])
160
  raise
161
  populated = events.get("populated", -1)
162
  last = EmptyProof(populated == 0 and not survivors, descendants, populated, survivors)
163
  if last.complete:
164
  return time.monotonic_ns(), last
165
- # A process can fork while the kernel is walking a busy hierarchy for a
166
- # prior cgroup.kill. Re-applying the same direct primitive closes that
167
- # narrow race; nothing is persisted or explained before enforcement.
168
  try:
169
  kill_path(path)
170
  except OSError as error:
 
12
  from .jsonio import JsonInputError
13
 
14
  CGROUP_ROOT = Path("/sys/fs/cgroup")
15
+ UNIT_RE = re.compile(r"^/system\.slice/lumi-eggcracker-workload-([0-9a-f]{24})\.service$")
16
 
17
 
18
  @dataclass(frozen=True)
 
24
  run_id: str
25
  unit: str
26
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  @dataclass(frozen=True)
29
  class EmptyProof:
 
42
 
43
  def _path(cgroup: str, *, root: Path = CGROUP_ROOT) -> Path:
44
  if not UNIT_RE.fullmatch(cgroup):
45
+ raise JsonInputError("cgroup is outside the Eggcracker unit namespace")
46
  candidate = root.joinpath(*cgroup.lstrip("/").split("/"))
47
  if candidate.is_symlink() or not candidate.is_dir():
48
  raise JsonInputError("owned cgroup is unavailable")
49
  return candidate
50
 
51
 
52
+ def events_from_fd(descriptor: int) -> dict[str, int]:
53
+ os.lseek(descriptor, 0, os.SEEK_SET)
54
+ raw = os.read(descriptor, 4096).decode("ascii")
55
+ pairs = [line.split(" ", 1) for line in raw.splitlines()]
56
+ value = {key: item for key, item in pairs if key and item}
57
+ if not value or any(not item.isdigit() for item in value.values()):
58
+ raise JsonInputError("cgroup event file has invalid values")
59
+ return {key: int(item) for key, item in value.items()}
60
+
61
+
62
  def _events(path: Path) -> dict[str, int]:
63
  try:
64
+ with path.open("rb", buffering=0) as handle:
65
+ return events_from_fd(handle.fileno())
66
  except OSError as error:
67
  raise JsonInputError(f"cannot read {path.name}: {error}") from error
 
 
 
 
68
 
69
 
70
  def capture_identity(cgroup: str, run_id: str, unit: str, *, root: Path = CGROUP_ROOT) -> CgroupIdentity:
71
+ if unit != f"lumi-eggcracker-workload-{run_id}.service":
72
  raise JsonInputError("unit and run identity disagree")
73
  path = _path(cgroup, root=root)
74
  required = ("cgroup.kill", "cgroup.events", "cgroup.procs", "pids.events", "pids.max")
75
  if not all((path / name).is_file() for name in required):
76
  raise JsonInputError("owned cgroup lacks required v2 control files")
77
+ if "populated" not in _events(path / "cgroup.events") or "max" not in _events(path / "pids.events"):
78
+ raise JsonInputError("owned cgroup does not expose required events")
 
 
79
  metadata = path.stat(follow_symlinks=False)
80
  return CgroupIdentity(boot_id(), cgroup, metadata.st_dev, metadata.st_ino, run_id, unit)
81
 
 
84
  if identity.boot_id != boot_id():
85
  raise JsonInputError("workload boot identity changed")
86
  path = _path(identity.cgroup, root=root)
87
+ metadata = path.stat(follow_symlinks=False)
 
 
 
88
  if metadata.st_dev != identity.device or metadata.st_ino != identity.inode:
89
  raise JsonInputError("owned cgroup identity drifted")
90
  return path
91
 
92
 
 
 
 
 
93
  def kill_path(path: Path) -> tuple[int, int]:
 
94
  control = path / "cgroup.kill"
95
  started = time.monotonic_ns()
96
  descriptor = os.open(control, os.O_WRONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0))
 
103
 
104
 
105
  def kill(identity: CgroupIdentity, *, root: Path = CGROUP_ROOT) -> tuple[int, int]:
106
+ """Validate the exact identity, then apply the authoritative primitive."""
107
  return kill_path(validate_identity(identity, root=root))
108
 
109
 
110
  def _survivors(path: Path) -> tuple[list[int], int]:
111
+ directories = [path, *(item for item in path.rglob("*") if item.is_dir() and not item.is_symlink())]
 
112
  values: list[int] = []
113
  for directory in directories:
114
+ for line in (directory / "cgroup.procs").read_text(encoding="ascii").splitlines():
115
+ if not line.isdigit():
116
+ raise JsonInputError("cgroup.procs contains invalid PID")
117
+ values.append(int(line))
 
 
118
  return sorted(set(values)), len(directories)
119
 
120
 
121
+ def _collected(error: BaseException) -> bool:
122
  return isinstance(error, OSError) and error.errno in {errno.ENOENT, errno.ENODEV}
123
 
124
 
125
  def verify_empty(identity: CgroupIdentity, *, deadline_seconds: float = 0.9, root: Path = CGROUP_ROOT) -> tuple[int, EmptyProof]:
 
126
  deadline = time.monotonic() + deadline_seconds
127
  last = EmptyProof(False, 0, -1, [])
128
  while time.monotonic() <= deadline:
129
  try:
130
  path = validate_identity(identity, root=root)
 
 
 
 
 
131
  events = _events(path / "cgroup.events")
132
  survivors, descendants = _survivors(path)
133
  except (JsonInputError, OSError) as error:
134
+ if _collected(error) or "unavailable" in str(error) or "No such file" in str(error) or "No such device" in str(error):
 
 
135
  return time.monotonic_ns(), EmptyProof(True, 0, 0, [])
136
  raise
137
  populated = events.get("populated", -1)
138
  last = EmptyProof(populated == 0 and not survivors, descendants, populated, survivors)
139
  if last.complete:
140
  return time.monotonic_ns(), last
 
 
 
141
  try:
142
  kill_path(path)
143
  except OSError as error:
src/lumi_eggcracker/gate.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tiny workload-side pre-exec gate; it has no supervisor privileges."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ from pathlib import Path
8
+
9
+
10
+ def main(argv: list[str] | None = None) -> int:
11
+ parser = argparse.ArgumentParser(prog="eggcracker internal-gate")
12
+ parser.add_argument("--fifo", required=True, type=Path)
13
+ parser.add_argument("command", nargs=argparse.REMAINDER)
14
+ args = parser.parse_args(argv)
15
+ command = args.command[1:] if args.command[:1] == ["--"] else args.command
16
+ if not command:
17
+ raise SystemExit("missing gated command")
18
+ with args.fifo.open("rb", buffering=0) as gate:
19
+ if gate.read(3) != b"GO\n":
20
+ raise SystemExit("invalid launch-gate release")
21
+ os.execvp(command[0], command)
22
+ return 127
src/{lumi_nutcracker → lumi_eggcracker}/jsonio.py RENAMED
@@ -54,14 +54,17 @@ def write_new_json(destination: Path, value: dict[str, Any], *, mode: int = 0o60
54
  descriptor, raw = tempfile.mkstemp(prefix=f".{destination.name}.", dir=destination.parent)
55
  temporary = Path(raw)
56
  try:
57
- os.fchmod(descriptor, mode)
 
58
  os.write(descriptor, canonical_bytes(value))
59
  os.fsync(descriptor)
60
  finally:
61
  os.close(descriptor)
 
 
62
  try:
63
  os.link(temporary, destination)
64
- directory = os.open(destination.parent, os.O_RDONLY)
65
  try:
66
  os.fsync(directory)
67
  finally:
 
54
  descriptor, raw = tempfile.mkstemp(prefix=f".{destination.name}.", dir=destination.parent)
55
  temporary = Path(raw)
56
  try:
57
+ if hasattr(os, "fchmod"):
58
+ os.fchmod(descriptor, mode)
59
  os.write(descriptor, canonical_bytes(value))
60
  os.fsync(descriptor)
61
  finally:
62
  os.close(descriptor)
63
+ if not hasattr(os, "fchmod"):
64
+ os.chmod(temporary, mode)
65
  try:
66
  os.link(temporary, destination)
67
+ directory = os.open(destination.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
68
  try:
69
  os.fsync(directory)
70
  finally:
src/{lumi_nutcracker → lumi_eggcracker}/records.py RENAMED
@@ -2,6 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  import os
6
  import re
7
  import tempfile
@@ -11,54 +12,80 @@ from typing import Any
11
  from .containment import CgroupIdentity, EmptyProof
12
  from .jsonio import JsonInputError, canonical_bytes, load_regular_json
13
 
14
- RUN_SCHEMA = "lumi-nutcracker.run.v1"
15
- RECEIPT_SCHEMA = "lumi-nutcracker.receipt.v1"
16
  NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}\Z")
 
 
 
17
 
18
 
19
  def write_atomic(path: Path, value: dict[str, Any]) -> None:
 
20
  descriptor, raw = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
21
  temporary = Path(raw)
22
  try:
23
- os.fchmod(descriptor, 0o600)
 
24
  os.write(descriptor, canonical_bytes(value))
25
  os.fsync(descriptor)
26
  finally:
27
  os.close(descriptor)
 
 
28
  os.replace(temporary, path)
29
- directory = os.open(path.parent, os.O_RDONLY)
 
 
 
30
  try:
31
  os.fsync(directory)
 
 
32
  finally:
33
  os.close(directory)
34
 
35
 
36
- def record_path(runs: Path, name: str) -> Path:
37
- if not NAME.fullmatch(name):
 
 
 
 
 
 
38
  raise JsonInputError("workload name is invalid")
39
- return runs / f"{name}.json"
 
 
 
 
 
 
40
 
41
 
42
  def validate_run(value: dict[str, Any]) -> dict[str, Any]:
43
  expected = {
44
- "argv", "boot_id", "cgroup", "cgroup_device", "cgroup_inode", "created_monotonic_ns",
45
- "max_pids", "name", "operator_uid", "run_id", "schema_version", "state", "unit",
46
- "workload_gid", "workload_uid",
47
  }
48
  if set(value) != expected or value.get("schema_version") != RUN_SCHEMA:
49
  raise JsonInputError("run record schema is invalid")
50
  if not isinstance(value["name"], str) or not NAME.fullmatch(value["name"]):
51
  raise JsonInputError("run record name is invalid")
52
- if not isinstance(value["run_id"], str) or not re.fullmatch(r"[0-9a-f]{24}", value["run_id"]):
53
  raise JsonInputError("run record identity is invalid")
54
- if value["unit"] != f"lumi-nutcracker-workload-{value['run_id']}.service":
55
  raise JsonInputError("run record unit is invalid")
56
- if not isinstance(value["argv"], list) or not value["argv"] or not all(isinstance(item, str) and item for item in value["argv"]):
57
- raise JsonInputError("run record argv is invalid")
58
- keys = ("cgroup_device", "cgroup_inode", "created_monotonic_ns", "operator_uid", "workload_gid", "workload_uid", "max_pids")
 
 
59
  if any(isinstance(value[key], bool) or not isinstance(value[key], int) or value[key] < 0 for key in keys):
60
  raise JsonInputError("run record integer field is invalid")
61
- if value["state"] not in {"RUNNING", "COMPLETED_ALLOWED", "TERMINATED", "CONTAINMENT_FAILED", "CONTAINED_RECEIPT_FAILED"}:
62
  raise JsonInputError("run record state is invalid")
63
  return value
64
 
@@ -68,20 +95,20 @@ def identity_from_run(value: dict[str, Any]) -> CgroupIdentity:
68
  return CgroupIdentity(record["boot_id"], record["cgroup"], record["cgroup_device"], record["cgroup_inode"], record["run_id"], record["unit"])
69
 
70
 
71
- def load_run(runs: Path, name: str) -> dict[str, Any]:
72
- return validate_run(load_regular_json(record_path(runs, name)))
73
 
74
 
75
  def make_receipt(
76
  *, record: dict[str, Any], trigger: str, trigger_ns: int, kill_started_ns: int, kill_complete_ns: int,
77
- empty_ns: int, proof: EmptyProof, version: str, source_commit: str, cleanup: dict[str, Any], event_id: str,
78
  ) -> dict[str, Any]:
79
- if trigger not in {"OPERATOR", "PID_LIMIT", "SUPERVISOR_RESTART_FAIL_CLOSED"} or not re.fullmatch(r"[0-9a-f]{24}", event_id):
80
  raise JsonInputError("receipt trigger or event identity is invalid")
81
  if not proof.complete or proof.root_populated != 0 or proof.surviving_pids:
82
  raise JsonInputError("cannot issue a success receipt before exact emptiness proof")
83
  return {
84
- "cleanup": cleanup,
85
  "containment": {
86
  "cgroup_kill_written": True,
87
  "descendant_cgroups_checked": proof.descendant_cgroups_checked,
 
2
 
3
  from __future__ import annotations
4
 
5
+ import hashlib
6
  import os
7
  import re
8
  import tempfile
 
12
  from .containment import CgroupIdentity, EmptyProof
13
  from .jsonio import JsonInputError, canonical_bytes, load_regular_json
14
 
15
+ RUN_SCHEMA = "lumi-eggcracker.run.v2"
16
+ RECEIPT_SCHEMA = "lumi-eggcracker.receipt.v2"
17
  NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}\Z")
18
+ RUN_ID = re.compile(r"[0-9a-f]{24}\Z")
19
+ ACTIVE_STATES = {"STARTING", "RUNNING"}
20
+ TERMINAL_STATES = {"COMPLETED_ALLOWED", "TERMINATED", "CONTAINMENT_FAILED", "CONTAINED_RECEIPT_FAILED"}
21
 
22
 
23
  def write_atomic(path: Path, value: dict[str, Any]) -> None:
24
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
25
  descriptor, raw = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
26
  temporary = Path(raw)
27
  try:
28
+ if hasattr(os, "fchmod"):
29
+ os.fchmod(descriptor, 0o600)
30
  os.write(descriptor, canonical_bytes(value))
31
  os.fsync(descriptor)
32
  finally:
33
  os.close(descriptor)
34
+ if not hasattr(os, "fchmod"):
35
+ os.chmod(temporary, 0o600)
36
  os.replace(temporary, path)
37
+ try:
38
+ directory = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
39
+ except OSError:
40
+ return # Directory fsync is unavailable on Windows test hosts.
41
  try:
42
  os.fsync(directory)
43
+ except OSError:
44
+ return
45
  finally:
46
  os.close(directory)
47
 
48
 
49
+ def run_path(runs: Path, run_id: str) -> Path:
50
+ if not isinstance(run_id, str) or not RUN_ID.fullmatch(run_id):
51
+ raise JsonInputError("workload run identity is invalid")
52
+ return runs / f"{run_id}.json"
53
+
54
+
55
+ def name_path(names: Path, name: str) -> Path:
56
+ if not isinstance(name, str) or not NAME.fullmatch(name):
57
  raise JsonInputError("workload name is invalid")
58
+ return names / f"{name}.json"
59
+
60
+
61
+ def command_summary(argv: list[str]) -> dict[str, Any]:
62
+ if not isinstance(argv, list) or not argv or not all(isinstance(item, str) and item for item in argv):
63
+ raise JsonInputError("workload argv is invalid")
64
+ return {"argv_count": len(argv), "argv_sha256": hashlib.sha256("\0".join(argv).encode("utf-8")).hexdigest(), "executable": argv[0]}
65
 
66
 
67
  def validate_run(value: dict[str, Any]) -> dict[str, Any]:
68
  expected = {
69
+ "argv_count", "argv_sha256", "boot_id", "cgroup", "cgroup_device", "cgroup_inode",
70
+ "created_monotonic_ns", "executable", "max_pids", "name", "operator_uid", "run_id",
71
+ "schema_version", "state", "unit", "workload_gid", "workload_uid",
72
  }
73
  if set(value) != expected or value.get("schema_version") != RUN_SCHEMA:
74
  raise JsonInputError("run record schema is invalid")
75
  if not isinstance(value["name"], str) or not NAME.fullmatch(value["name"]):
76
  raise JsonInputError("run record name is invalid")
77
+ if not isinstance(value["run_id"], str) or not RUN_ID.fullmatch(value["run_id"]):
78
  raise JsonInputError("run record identity is invalid")
79
+ if value["unit"] != f"lumi-eggcracker-workload-{value['run_id']}.service":
80
  raise JsonInputError("run record unit is invalid")
81
+ if not isinstance(value["executable"], str) or not value["executable"]:
82
+ raise JsonInputError("run record executable is invalid")
83
+ if not isinstance(value["argv_sha256"], str) or not re.fullmatch(r"[0-9a-f]{64}", value["argv_sha256"]):
84
+ raise JsonInputError("run record command hash is invalid")
85
+ keys = ("cgroup_device", "cgroup_inode", "created_monotonic_ns", "operator_uid", "workload_gid", "workload_uid", "max_pids", "argv_count")
86
  if any(isinstance(value[key], bool) or not isinstance(value[key], int) or value[key] < 0 for key in keys):
87
  raise JsonInputError("run record integer field is invalid")
88
+ if value["state"] not in ACTIVE_STATES | TERMINAL_STATES:
89
  raise JsonInputError("run record state is invalid")
90
  return value
91
 
 
95
  return CgroupIdentity(record["boot_id"], record["cgroup"], record["cgroup_device"], record["cgroup_inode"], record["run_id"], record["unit"])
96
 
97
 
98
+ def load_run(runs: Path, run_id: str) -> dict[str, Any]:
99
+ return validate_run(load_regular_json(run_path(runs, run_id)))
100
 
101
 
102
  def make_receipt(
103
  *, record: dict[str, Any], trigger: str, trigger_ns: int, kill_started_ns: int, kill_complete_ns: int,
104
+ empty_ns: int, proof: EmptyProof, version: str, source_commit: str, event_id: str,
105
  ) -> dict[str, Any]:
106
+ if trigger not in {"OPERATOR", "PID_LIMIT", "SUPERVISOR_FAILURE", "SUPERVISOR_RESTART_FAIL_CLOSED"} or not RUN_ID.fullmatch(event_id):
107
  raise JsonInputError("receipt trigger or event identity is invalid")
108
  if not proof.complete or proof.root_populated != 0 or proof.surviving_pids:
109
  raise JsonInputError("cannot issue a success receipt before exact emptiness proof")
110
  return {
111
+ "cleanup": {"attempted": False},
112
  "containment": {
113
  "cgroup_kill_written": True,
114
  "descendant_cgroups_checked": proof.descendant_cgroups_checked,
src/{lumi_nutcracker → lumi_eggcracker}/supervisor.py RENAMED
@@ -1,9 +1,10 @@
1
- """Root-owned single-backend supervisor for explicit protected workloads."""
2
 
3
  from __future__ import annotations
4
 
5
  import argparse
6
  import datetime as dt
 
7
  import json
8
  import os
9
  import re
@@ -12,6 +13,7 @@ import signal
12
  import socket
13
  import struct
14
  import subprocess
 
15
  import threading
16
  import time
17
  from pathlib import Path
@@ -20,29 +22,34 @@ from typing import Any
20
  from . import __version__
21
  from .containment import (
22
  capture_identity,
 
23
  kill_path,
24
- pids_max_event,
25
  validate_identity,
26
  verify_empty,
27
  )
28
  from .jsonio import JsonInputError, load_regular_json
29
  from .records import (
 
 
30
  RUN_SCHEMA,
 
31
  identity_from_run,
32
  load_run,
33
  make_receipt,
34
- record_path,
 
35
  validate_run,
36
  write_atomic,
37
  )
38
 
39
  MAX_FRAME = 32 * 1024
40
  NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}\Z")
41
- RUN_ID = re.compile(r"[0-9a-f]{24}\Z")
42
- POLICY_SCHEMA = "lumi-nutcracker.policy.v1"
43
- SOCKET_PATH = Path("/run/lumi-nutcracker/control.sock")
44
- STATE_DIR = Path("/var/lib/lumi-nutcracker")
45
- UNIT_PREFIX = "lumi-nutcracker-workload-"
 
46
 
47
 
48
  def _pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
@@ -103,11 +110,14 @@ class Supervisor:
103
  raise JsonInputError("supervisor build identity is invalid")
104
  self.policy = policy
105
  self.runs = STATE_DIR / "runs"
 
106
  self.receipts = STATE_DIR / "receipts"
107
  self.stop_event = threading.Event()
108
  self.locks: dict[str, threading.Lock] = {}
 
 
109
  self.completed: dict[str, dict[str, Any]] = {}
110
- self.operations: list[str] = [] # Test-visible in-memory ordering only.
111
 
112
  @property
113
  def operator_uid(self) -> int:
@@ -118,24 +128,75 @@ class Supervisor:
118
 
119
  def _show(self, unit: str) -> dict[str, str]:
120
  if not unit.startswith(UNIT_PREFIX) or not unit.endswith(".service"):
121
- raise JsonInputError("unit is outside Nutcracker namespace")
122
  result = self._run(["/usr/bin/systemctl", "show", unit, "--property=ActiveState", "--property=ControlGroup", "--property=TasksMax"])
123
  if result.returncode:
124
- return {"ActiveState": "inactive", "ControlGroup": "", "TasksMax": ""}
125
  return {key: value for key, value in (line.split("=", 1) for line in result.stdout.splitlines() if "=" in line)}
126
 
 
 
 
 
127
  def _store(self, record: dict[str, Any]) -> None:
 
128
  self.operations.append("durable-state")
129
- write_atomic(record_path(self.runs, record["name"]), validate_run(record))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  def _load(self, name: str) -> dict[str, Any]:
132
- return load_run(self.runs, name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  def _receipt_path(self, event_id: str) -> Path:
135
  return self.receipts / f"{event_id}.json"
136
 
137
  def _prepare(self) -> None:
138
- for path, mode, gid in ((SOCKET_PATH.parent, 0o710, self.policy["operator_gid"]), (STATE_DIR, 0o700, 0), (self.runs, 0o700, 0), (self.receipts, 0o700, 0)):
 
 
 
139
  path.mkdir(mode=mode, parents=True, exist_ok=True)
140
  os.chown(path, 0, gid)
141
  os.chmod(path, mode)
@@ -143,38 +204,25 @@ class Supervisor:
143
  raise JsonInputError("control socket already exists")
144
  self._recover()
145
 
146
- def _new_lock(self, name: str) -> threading.Lock:
147
- if name not in self.locks:
148
- self.locks[name] = threading.Lock()
149
- return self.locks[name]
150
-
151
  def _cleanup(self, unit: str) -> dict[str, Any]:
152
  result = self._run(["/usr/bin/systemctl", "stop", unit])
153
- return {"systemctl_stop_attempted": True, "systemctl_stop_returncode": result.returncode, "systemctl_stop_stderr": result.stderr.strip()}
154
 
155
- def _complete_allowed(self, record: dict[str, Any]) -> bool:
156
- """Persist normal completion only after systemd reports the unit inactive."""
157
- lock = self._new_lock(record["name"])
158
- with lock:
159
- if record["run_id"] in self.completed or record["state"] != "RUNNING":
160
- return False
161
- if self._show(record["unit"])["ActiveState"] == "active":
162
- return False
163
- record["state"] = "COMPLETED_ALLOWED"
164
- self._store(record)
165
- return True
166
 
167
  def _contain(self, record: dict[str, Any], trigger: str, trigger_ns: int | None = None) -> dict[str, Any]:
168
- """Contain exactly one verified cgroup. First trigger-side effect is cgroup.kill."""
169
- lock = self._new_lock(record["name"])
170
  with lock:
171
  if record["run_id"] in self.completed:
172
  return self.completed[record["run_id"]]
173
  identity = identity_from_run(record)
174
- # Identity validation completes before the trigger is declared.
175
- path = validate_identity(identity)
176
  observed = trigger_ns if trigger_ns is not None else time.monotonic_ns()
177
  try:
 
178
  self.operations.append("cgroup.kill")
179
  kill_started, kill_completed = kill_path(path)
180
  empty_ns, proof = verify_empty(identity)
@@ -184,112 +232,205 @@ class Supervisor:
184
  record["state"] = "CONTAINMENT_FAILED"
185
  self._store(record)
186
  raise JsonInputError(f"containment failed: {error}") from error
187
- cleanup = self._cleanup(record["unit"])
188
  event_id = os.urandom(12).hex()
189
- receipt = make_receipt(record=record, trigger=trigger, trigger_ns=observed, kill_started_ns=kill_started, kill_complete_ns=kill_completed, empty_ns=empty_ns, proof=proof, version=__version__, source_commit=self.policy["source_commit"], cleanup=cleanup, event_id=event_id)
190
- receipt["receipt_written_utc"] = dt.datetime.now(dt.UTC).isoformat().replace("+00:00", "Z")
191
  try:
192
- self.operations.append("durable-receipt")
193
- write_atomic(self._receipt_path(event_id), receipt)
194
  record["state"] = "TERMINATED"
195
  self._store(record)
196
  except Exception as error:
197
  record["state"] = "CONTAINED_RECEIPT_FAILED"
198
  self._store(record)
199
  raise JsonInputError(f"contained but receipt persistence failed: {error}") from error
 
 
 
 
 
 
 
200
  receipt["receipt_path"] = str(self._receipt_path(event_id))
201
  self.completed[record["run_id"]] = receipt
202
  return receipt
203
 
204
- def _watch(self, record: dict[str, Any]) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  identity = identity_from_run(record)
 
 
 
206
  try:
207
- baseline = pids_max_event(identity)
208
- descriptor = os.open(validate_identity(identity) / "pids.events", os.O_RDONLY | os.O_CLOEXEC)
 
209
  poller = select.poll()
210
- poller.register(descriptor, select.POLLPRI | select.POLLERR | select.POLLIN)
 
 
 
211
  while not self.stop_event.is_set():
212
- if not poller.poll(250):
213
- if self._complete_allowed(record):
214
- return
215
- continue
216
- # --collect may remove the cgroup between poll wake-up and the
217
- # pids.events read. An already inactive unit completed normally;
218
- # do not turn that lifecycle race into a false containment failure.
219
- if self._complete_allowed(record):
220
- return
221
- os.lseek(descriptor, 0, os.SEEK_SET)
222
- if pids_max_event(identity) > baseline:
223
  self._contain(record, "PID_LIMIT", time.monotonic_ns())
224
  return
225
- except Exception: # noqa: BLE001 - any watcher failure must fail closed
226
- # A lost watcher must not leave an active owned workload running.
227
- try:
228
- self._contain(record, "SUPERVISOR_RESTART_FAIL_CLOSED", time.monotonic_ns())
229
- except JsonInputError:
230
- pass
231
  finally:
232
- if "descriptor" in locals():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  os.close(descriptor)
 
 
 
 
 
 
 
 
 
 
 
234
 
235
  def _start(self, args: dict[str, Any]) -> dict[str, Any]:
236
  if set(args) != {"argv", "max_pids", "name"}:
237
  raise JsonInputError("start arguments are invalid")
238
  name, argv, maximum = args["name"], args["argv"], args["max_pids"]
239
- if not isinstance(name, str) or not NAME.fullmatch(name) or record_path(self.runs, name).exists():
240
- raise JsonInputError("workload name is unavailable")
241
- if not isinstance(argv, list) or not argv or not all(isinstance(item, str) and item for item in argv):
242
- raise JsonInputError("workload argv is invalid")
243
  if isinstance(maximum, bool) or not isinstance(maximum, int) or not 4 <= maximum <= 4096:
244
  raise JsonInputError("max_pids must be from 4 to 4096")
245
- run_id = os.urandom(12).hex()
246
- unit = f"{UNIT_PREFIX}{run_id}.service"
247
- result = self._run(["/usr/bin/systemd-run", f"--unit={unit}", "--collect", f"--uid={self.policy['workload_uid']}", f"--gid={self.policy['workload_gid']}", "--property=Type=exec", "--property=ExitType=cgroup", "--property=KillMode=control-group", "--property=NoNewPrivileges=yes", "--property=UMask=0077", f"--property=TasksMax={maximum}", "--", *argv])
248
- if result.returncode:
249
- raise JsonInputError(result.stderr.strip() or "system workload launch failed")
250
- deadline = time.monotonic() + 2.0
251
- props: dict[str, str] = {}
252
- while time.monotonic() < deadline:
253
- props = self._show(unit)
254
- if props["ActiveState"] == "active" and props["ControlGroup"]:
255
- break
256
- time.sleep(0.01)
257
- if props.get("ActiveState") != "active" or not props.get("ControlGroup"):
258
- self._run(["/usr/bin/systemctl", "stop", unit])
259
- raise JsonInputError("workload did not become active")
260
- identity = capture_identity(props["ControlGroup"], run_id, unit)
261
- record = {"argv": argv, "boot_id": identity.boot_id, "cgroup": identity.cgroup, "cgroup_device": identity.device, "cgroup_inode": identity.inode, "created_monotonic_ns": time.monotonic_ns(), "max_pids": maximum, "name": name, "operator_uid": self.operator_uid, "run_id": run_id, "schema_version": RUN_SCHEMA, "state": "RUNNING", "unit": unit, "workload_gid": self.policy["workload_gid"], "workload_uid": self.policy["workload_uid"]}
262
- self._store(record)
263
- threading.Thread(target=self._watch, args=(record,), daemon=True).start()
264
- return {"name": name, "state": "RUNNING", "unit": unit, "workload_uid": record["workload_uid"]}
265
-
266
- def _orphan_record(self, unit: str, cgroup: str) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  run_id = unit.removeprefix(UNIT_PREFIX).removesuffix(".service")
268
  if not RUN_ID.fullmatch(run_id):
269
- raise JsonInputError("orphan unit identity is invalid")
270
  identity = capture_identity(cgroup, run_id, unit)
271
- return {"argv": ["<orphaned-owned-unit>"], "boot_id": identity.boot_id, "cgroup": identity.cgroup, "cgroup_device": identity.device, "cgroup_inode": identity.inode, "created_monotonic_ns": time.monotonic_ns(), "max_pids": 0, "name": f"orphan-{run_id}", "operator_uid": self.operator_uid, "run_id": run_id, "schema_version": RUN_SCHEMA, "state": "RUNNING", "unit": unit, "workload_gid": self.policy["workload_gid"], "workload_uid": self.policy["workload_uid"]}
272
-
273
- def _active_units(self) -> list[str]:
274
- result = self._run(["/usr/bin/systemctl", "list-units", f"{UNIT_PREFIX}*", "--type=service", "--all", "--plain", "--no-legend"])
275
- if result.returncode:
276
- raise JsonInputError("cannot enumerate owned units")
277
- return [line.split()[0] for line in result.stdout.splitlines() if line.split() and line.split()[0].startswith(UNIT_PREFIX)]
278
 
279
  def _recover(self) -> None:
280
- records: dict[str, dict[str, Any]] = {}
281
  for path in self.runs.glob("*.json"):
282
  try:
283
  record = load_run(self.runs, path.stem)
284
- records[record["unit"]] = record
285
  except JsonInputError:
286
  continue
287
- for unit in self._active_units():
288
- props = self._show(unit)
289
- if props["ActiveState"] != "active" or not props["ControlGroup"]:
 
 
 
 
 
290
  continue
291
- record = records.get(unit) or self._orphan_record(unit, props["ControlGroup"])
292
- self._contain(record, "SUPERVISOR_RESTART_FAIL_CLOSED")
 
293
 
294
  def handle(self, value: dict[str, Any]) -> dict[str, Any]:
295
  if set(value) != {"action", "args"} or not isinstance(value["action"], str) or not isinstance(value["args"], dict):
@@ -301,13 +442,14 @@ class Supervisor:
301
  if action == "start":
302
  return self._start(args)
303
  if action == "status" and set(args) == {"name"}:
304
- record = self._load(args["name"])
305
- if record["state"] == "RUNNING":
306
- self._complete_allowed(record)
307
- props = self._show(record["unit"])
308
- return {"active_state": props["ActiveState"], "name": record["name"], "state": record["state"], "unit": record["unit"], "workload_uid": record["workload_uid"]}
309
  if action == "list" and not args:
310
- return {"runs": [self.handle({"action": "status", "args": {"name": path.stem}}) for path in sorted(self.runs.glob("*.json"))]}
 
311
  if action == "kill" and set(args) == {"name"}:
312
  return self._contain(self._load(args["name"]), "OPERATOR")
313
  raise JsonInputError("unsupported supervisor action")
@@ -341,11 +483,15 @@ class Supervisor:
341
 
342
 
343
  def main(argv: list[str] | None = None) -> int:
344
- parser = argparse.ArgumentParser(prog="nutcracker internal-supervisor")
345
  parser.add_argument("--policy", required=True, type=Path)
346
  args = parser.parse_args(argv)
347
  supervisor = Supervisor(args.policy)
348
- signal.signal(signal.SIGTERM, lambda *_: supervisor.stop_event.set())
 
 
 
 
349
  return supervisor.serve()
350
 
351
 
 
1
+ """Root-owned, fail-closed supervisor for one explicit protected workload."""
2
 
3
  from __future__ import annotations
4
 
5
  import argparse
6
  import datetime as dt
7
+ import errno
8
  import json
9
  import os
10
  import re
 
13
  import socket
14
  import struct
15
  import subprocess
16
+ import sys
17
  import threading
18
  import time
19
  from pathlib import Path
 
22
  from . import __version__
23
  from .containment import (
24
  capture_identity,
25
+ events_from_fd,
26
  kill_path,
 
27
  validate_identity,
28
  verify_empty,
29
  )
30
  from .jsonio import JsonInputError, load_regular_json
31
  from .records import (
32
+ ACTIVE_STATES,
33
+ RUN_ID,
34
  RUN_SCHEMA,
35
+ command_summary,
36
  identity_from_run,
37
  load_run,
38
  make_receipt,
39
+ name_path,
40
+ run_path,
41
  validate_run,
42
  write_atomic,
43
  )
44
 
45
  MAX_FRAME = 32 * 1024
46
  NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}\Z")
47
+ POLICY_SCHEMA = "lumi-eggcracker.policy.v2"
48
+ SOCKET_PATH = Path("/run/lumi-eggcracker/control.sock")
49
+ STATE_DIR = Path("/var/lib/lumi-eggcracker")
50
+ UNIT_PREFIX = "lumi-eggcracker-workload-"
51
+ GATES_DIR = Path("/run/lumi-eggcracker/gates")
52
+ MAX_TERMINAL_RECORDS = 128
53
 
54
 
55
  def _pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
 
110
  raise JsonInputError("supervisor build identity is invalid")
111
  self.policy = policy
112
  self.runs = STATE_DIR / "runs"
113
+ self.names = STATE_DIR / "names"
114
  self.receipts = STATE_DIR / "receipts"
115
  self.stop_event = threading.Event()
116
  self.locks: dict[str, threading.Lock] = {}
117
+ self.lock_guard = threading.Lock()
118
+ self.start_lock = threading.Lock()
119
  self.completed: dict[str, dict[str, Any]] = {}
120
+ self.operations: list[str] = [] # Test-visible ordering only.
121
 
122
  @property
123
  def operator_uid(self) -> int:
 
128
 
129
  def _show(self, unit: str) -> dict[str, str]:
130
  if not unit.startswith(UNIT_PREFIX) or not unit.endswith(".service"):
131
+ raise JsonInputError("unit is outside Eggcracker namespace")
132
  result = self._run(["/usr/bin/systemctl", "show", unit, "--property=ActiveState", "--property=ControlGroup", "--property=TasksMax"])
133
  if result.returncode:
134
+ return {"ActiveState": "unknown", "ControlGroup": "", "TasksMax": ""}
135
  return {key: value for key, value in (line.split("=", 1) for line in result.stdout.splitlines() if "=" in line)}
136
 
137
+ def _new_lock(self, name: str) -> threading.Lock:
138
+ with self.lock_guard:
139
+ return self.locks.setdefault(name, threading.Lock())
140
+
141
  def _store(self, record: dict[str, Any]) -> None:
142
+ record = validate_run(record)
143
  self.operations.append("durable-state")
144
+ write_atomic(run_path(self.runs, record["run_id"]), record)
145
+ pointer = name_path(self.names, record["name"])
146
+ if record["state"] in ACTIVE_STATES:
147
+ write_atomic(pointer, {"run_id": record["run_id"]})
148
+ elif pointer.exists():
149
+ try:
150
+ current = load_regular_json(pointer)
151
+ if current == {"run_id": record["run_id"]}:
152
+ pointer.unlink()
153
+ except JsonInputError:
154
+ raise JsonInputError("workload name index is invalid")
155
+ self._prune_terminal_records()
156
+
157
+ def _prune_terminal_records(self) -> None:
158
+ records: list[tuple[int, Path]] = []
159
+ for path in self.runs.glob("*.json"):
160
+ try:
161
+ value = load_run(self.runs, path.stem)
162
+ except JsonInputError:
163
+ continue
164
+ if value["state"] not in ACTIVE_STATES:
165
+ records.append((int(value["created_monotonic_ns"]), path))
166
+ for _, path in sorted(records, reverse=True)[MAX_TERMINAL_RECORDS:]:
167
+ path.unlink(missing_ok=True)
168
 
169
  def _load(self, name: str) -> dict[str, Any]:
170
+ pointer = load_regular_json(name_path(self.names, name))
171
+ if set(pointer) != {"run_id"}:
172
+ raise JsonInputError("workload name index is invalid")
173
+ record = load_run(self.runs, pointer["run_id"])
174
+ if record["name"] != name or record["state"] not in ACTIVE_STATES:
175
+ raise JsonInputError("workload name is unavailable")
176
+ return record
177
+
178
+ def _latest_by_name(self, name: str) -> dict[str, Any]:
179
+ """Resolve a completed run only when its name is no longer active."""
180
+ candidates: list[dict[str, Any]] = []
181
+ for path in self.runs.glob("*.json"):
182
+ try:
183
+ record = load_run(self.runs, path.stem)
184
+ except JsonInputError:
185
+ continue
186
+ if record["name"] == name:
187
+ candidates.append(record)
188
+ if not candidates:
189
+ raise JsonInputError("workload name is unavailable")
190
+ return max(candidates, key=lambda item: int(item["created_monotonic_ns"]))
191
 
192
  def _receipt_path(self, event_id: str) -> Path:
193
  return self.receipts / f"{event_id}.json"
194
 
195
  def _prepare(self) -> None:
196
+ # Workloads need traversal only to their group-readable gate. The
197
+ # control socket remains a root/operator 0660 inode, so traversal does
198
+ # not grant control-socket access or directory listing.
199
+ for path, mode, gid in ((SOCKET_PATH.parent, 0o711, self.policy["operator_gid"]), (GATES_DIR, 0o710, self.policy["workload_gid"]), (STATE_DIR, 0o700, 0), (self.runs, 0o700, 0), (self.names, 0o700, 0), (self.receipts, 0o700, 0)):
200
  path.mkdir(mode=mode, parents=True, exist_ok=True)
201
  os.chown(path, 0, gid)
202
  os.chmod(path, mode)
 
204
  raise JsonInputError("control socket already exists")
205
  self._recover()
206
 
 
 
 
 
 
207
  def _cleanup(self, unit: str) -> dict[str, Any]:
208
  result = self._run(["/usr/bin/systemctl", "stop", unit])
209
+ return {"attempted": True, "systemctl_stop_returncode": result.returncode, "systemctl_stop_stderr": result.stderr.strip()}
210
 
211
+ def _write_receipt(self, receipt: dict[str, Any], event_id: str) -> None:
212
+ receipt["receipt_written_utc"] = dt.datetime.now(dt.UTC).isoformat().replace("+00:00", "Z")
213
+ self.operations.append("durable-receipt")
214
+ write_atomic(self._receipt_path(event_id), receipt)
 
 
 
 
 
 
 
215
 
216
  def _contain(self, record: dict[str, Any], trigger: str, trigger_ns: int | None = None) -> dict[str, Any]:
217
+ """Direct cgroup.kill is the first trigger-side effect; cleanup is post-proof only."""
218
+ lock = self._new_lock(record["run_id"])
219
  with lock:
220
  if record["run_id"] in self.completed:
221
  return self.completed[record["run_id"]]
222
  identity = identity_from_run(record)
 
 
223
  observed = trigger_ns if trigger_ns is not None else time.monotonic_ns()
224
  try:
225
+ path = validate_identity(identity)
226
  self.operations.append("cgroup.kill")
227
  kill_started, kill_completed = kill_path(path)
228
  empty_ns, proof = verify_empty(identity)
 
232
  record["state"] = "CONTAINMENT_FAILED"
233
  self._store(record)
234
  raise JsonInputError(f"containment failed: {error}") from error
 
235
  event_id = os.urandom(12).hex()
236
+ receipt = make_receipt(record=record, trigger=trigger, trigger_ns=observed, kill_started_ns=kill_started, kill_complete_ns=kill_completed, empty_ns=empty_ns, proof=proof, version=__version__, source_commit=self.policy["source_commit"], event_id=event_id)
 
237
  try:
238
+ self._write_receipt(receipt, event_id)
 
239
  record["state"] = "TERMINATED"
240
  self._store(record)
241
  except Exception as error:
242
  record["state"] = "CONTAINED_RECEIPT_FAILED"
243
  self._store(record)
244
  raise JsonInputError(f"contained but receipt persistence failed: {error}") from error
245
+ # Cleanup is deliberately not in the enforcement or proof path.
246
+ cleanup = self._cleanup(record["unit"])
247
+ receipt["cleanup"] = cleanup
248
+ try:
249
+ write_atomic(self._receipt_path(event_id), receipt)
250
+ except (OSError, JsonInputError):
251
+ receipt["cleanup_update_error"] = True
252
  receipt["receipt_path"] = str(self._receipt_path(event_id))
253
  self.completed[record["run_id"]] = receipt
254
  return receipt
255
 
256
+ def _complete_allowed(self, record: dict[str, Any], cgroup_events_fd: int) -> bool:
257
+ """Only exact cgroup population, not systemd state, permits completion."""
258
+ events = events_from_fd(cgroup_events_fd)
259
+ if events.get("populated") != 0:
260
+ return False
261
+ lock = self._new_lock(record["run_id"])
262
+ with lock:
263
+ if record["state"] not in ACTIVE_STATES:
264
+ return False
265
+ record["state"] = "COMPLETED_ALLOWED"
266
+ self._store(record)
267
+ return True
268
+
269
+ def _watch_once(self, record: dict[str, Any], ready: threading.Event | None = None) -> None:
270
  identity = identity_from_run(record)
271
+ path = validate_identity(identity)
272
+ pids_fd = os.open(path / "pids.events", os.O_RDONLY | os.O_CLOEXEC)
273
+ cgroup_fd = os.open(path / "cgroup.events", os.O_RDONLY | os.O_CLOEXEC)
274
  try:
275
+ baseline = events_from_fd(pids_fd).get("max")
276
+ if baseline is None:
277
+ raise JsonInputError("pids.events lacks max counter")
278
  poller = select.poll()
279
+ poller.register(pids_fd, select.POLLPRI)
280
+ poller.register(cgroup_fd, select.POLLPRI)
281
+ if ready is not None:
282
+ ready.set()
283
  while not self.stop_event.is_set():
284
+ poller.poll(250)
285
+ current = events_from_fd(pids_fd).get("max")
286
+ if current is None:
287
+ raise JsonInputError("pids.events lacks max counter")
288
+ if current > baseline:
 
 
 
 
 
 
289
  self._contain(record, "PID_LIMIT", time.monotonic_ns())
290
  return
291
+ if self._complete_allowed(record, cgroup_fd):
292
+ return
 
 
 
 
293
  finally:
294
+ os.close(pids_fd)
295
+ os.close(cgroup_fd)
296
+
297
+ def _watch(self, record: dict[str, Any], ready: threading.Event | None = None) -> None:
298
+ failure: BaseException | None = None
299
+ for attempt in range(2):
300
+ try:
301
+ self._watch_once(record, ready)
302
+ return
303
+ except (JsonInputError, OSError, RuntimeError) as error:
304
+ failure = error
305
+ if attempt == 0:
306
+ continue
307
+ try:
308
+ self._contain(record, "SUPERVISOR_FAILURE", time.monotonic_ns())
309
+ except JsonInputError:
310
+ # A failed containment is intentionally left as a durable failure state.
311
+ pass
312
+ if failure is not None:
313
+ print(f"eggcracker watcher failed closed: {failure}", file=sys.stderr, flush=True)
314
+
315
+ def _make_gate(self, run_id: str) -> Path:
316
+ gate = GATES_DIR / run_id
317
+ if gate.exists() or gate.is_symlink():
318
+ raise JsonInputError("launch gate already exists")
319
+ os.mkfifo(gate, 0o640)
320
+ os.chown(gate, 0, self.policy["workload_gid"])
321
+ os.chmod(gate, 0o640)
322
+ return gate
323
+
324
+ def _release_gate(self, gate: Path) -> None:
325
+ deadline = time.monotonic() + 1.5
326
+ try:
327
+ while True:
328
+ try:
329
+ descriptor = os.open(gate, os.O_WRONLY | os.O_NONBLOCK | os.O_CLOEXEC)
330
+ break
331
+ except OSError as error:
332
+ if error.errno != errno.ENXIO or time.monotonic() >= deadline:
333
+ raise JsonInputError("workload gate did not attach") from error
334
+ time.sleep(0.01)
335
+ try:
336
+ if os.write(descriptor, b"GO\n") != 3:
337
+ raise JsonInputError("short launch-gate release")
338
+ finally:
339
  os.close(descriptor)
340
+ finally:
341
+ gate.unlink(missing_ok=True)
342
+
343
+ def _active_exists(self) -> bool:
344
+ for path in self.runs.glob("*.json"):
345
+ try:
346
+ if load_run(self.runs, path.stem)["state"] in ACTIVE_STATES:
347
+ return True
348
+ except JsonInputError:
349
+ raise JsonInputError("run record is invalid")
350
+ return False
351
 
352
  def _start(self, args: dict[str, Any]) -> dict[str, Any]:
353
  if set(args) != {"argv", "max_pids", "name"}:
354
  raise JsonInputError("start arguments are invalid")
355
  name, argv, maximum = args["name"], args["argv"], args["max_pids"]
356
+ if not isinstance(name, str) or not NAME.fullmatch(name):
357
+ raise JsonInputError("workload name is invalid")
358
+ summary = command_summary(argv)
 
359
  if isinstance(maximum, bool) or not isinstance(maximum, int) or not 4 <= maximum <= 4096:
360
  raise JsonInputError("max_pids must be from 4 to 4096")
361
+ with self.start_lock:
362
+ if name_path(self.names, name).exists() or self._active_exists():
363
+ raise JsonInputError("one protected workload is already active or name is unavailable")
364
+ run_id = os.urandom(12).hex()
365
+ unit = f"{UNIT_PREFIX}{run_id}.service"
366
+ gate = self._make_gate(run_id)
367
+ result = self._run(["/usr/bin/systemd-run", "--no-block", f"--unit={unit}", "--collect", f"--uid={self.policy['workload_uid']}", f"--gid={self.policy['workload_gid']}", "--property=Type=exec", "--property=ExitType=cgroup", "--property=KillMode=control-group", "--property=NoNewPrivileges=yes", "--property=UMask=0077", f"--property=TasksMax={maximum}", "--", "/usr/bin/python3", "/usr/local/lib/lumi-eggcracker/lumi-eggcracker.pyz", "_gate", "--fifo", str(gate), "--", *argv])
368
+ if result.returncode:
369
+ gate.unlink(missing_ok=True)
370
+ raise JsonInputError(result.stderr.strip() or "system workload launch failed")
371
+ try:
372
+ deadline = time.monotonic() + 2.0
373
+ props: dict[str, str] = {}
374
+ while time.monotonic() < deadline:
375
+ props = self._show(unit)
376
+ if props["ActiveState"] == "active" and props["ControlGroup"]:
377
+ break
378
+ time.sleep(0.01)
379
+ if props.get("ActiveState") != "active" or not props.get("ControlGroup"):
380
+ raise JsonInputError("gated workload did not become active")
381
+ identity = capture_identity(props["ControlGroup"], run_id, unit)
382
+ record = {**summary, "boot_id": identity.boot_id, "cgroup": identity.cgroup, "cgroup_device": identity.device, "cgroup_inode": identity.inode, "created_monotonic_ns": time.monotonic_ns(), "max_pids": maximum, "name": name, "operator_uid": self.operator_uid, "run_id": run_id, "schema_version": RUN_SCHEMA, "state": "STARTING", "unit": unit, "workload_gid": self.policy["workload_gid"], "workload_uid": self.policy["workload_uid"]}
383
+ self._store(record)
384
+ ready = threading.Event()
385
+ watcher = threading.Thread(target=self._watch, args=(record, ready), daemon=True)
386
+ watcher.start()
387
+ if not ready.wait(2.0):
388
+ raise JsonInputError("watcher did not become ready before target release")
389
+ # The gated target has not executed yet. The watcher has opened
390
+ # both event descriptors and captured the PID baseline.
391
+ self._release_gate(gate)
392
+ if record["state"] in ACTIVE_STATES:
393
+ record["state"] = "RUNNING"
394
+ self._store(record)
395
+ return {"name": name, "run_id": run_id, "state": record["state"], "unit": unit, "workload_uid": record["workload_uid"]}
396
+ except Exception:
397
+ gate.unlink(missing_ok=True)
398
+ try:
399
+ # Best effort rollback after a launch failure; direct kill is still authoritative.
400
+ identity = capture_identity(props["ControlGroup"], run_id, unit)
401
+ kill_path(validate_identity(identity))
402
+ verify_empty(identity)
403
+ except (JsonInputError, OSError, RuntimeError):
404
+ self._run(["/usr/bin/systemctl", "stop", unit])
405
+ raise
406
+
407
+ def _orphan_record(self, cgroup: str) -> dict[str, Any]:
408
+ unit = Path(cgroup).name
409
  run_id = unit.removeprefix(UNIT_PREFIX).removesuffix(".service")
410
  if not RUN_ID.fullmatch(run_id):
411
+ raise JsonInputError("orphan cgroup identity is invalid")
412
  identity = capture_identity(cgroup, run_id, unit)
413
+ return {"argv_count": 0, "argv_sha256": "0" * 64, "boot_id": identity.boot_id, "cgroup": identity.cgroup, "cgroup_device": identity.device, "cgroup_inode": identity.inode, "created_monotonic_ns": time.monotonic_ns(), "executable": "<orphaned-owned-cgroup>", "max_pids": 0, "name": f"orphan-{run_id}", "operator_uid": self.operator_uid, "run_id": run_id, "schema_version": RUN_SCHEMA, "state": "RUNNING", "unit": unit, "workload_gid": self.policy["workload_gid"], "workload_uid": self.policy["workload_uid"]}
 
 
 
 
 
 
414
 
415
  def _recover(self) -> None:
416
+ recorded_ids: set[str] = set()
417
  for path in self.runs.glob("*.json"):
418
  try:
419
  record = load_run(self.runs, path.stem)
 
420
  except JsonInputError:
421
  continue
422
+ recorded_ids.add(record["run_id"])
423
+ if record["state"] in ACTIVE_STATES:
424
+ self._contain(record, "SUPERVISOR_RESTART_FAIL_CLOSED")
425
+ root = Path("/sys/fs/cgroup/system.slice")
426
+ if not root.is_dir():
427
+ return
428
+ for path in root.glob(f"{UNIT_PREFIX}*.service"):
429
+ if not path.is_dir() or path.is_symlink():
430
  continue
431
+ run_id = path.name.removeprefix(UNIT_PREFIX).removesuffix(".service")
432
+ if RUN_ID.fullmatch(run_id) and run_id not in recorded_ids:
433
+ self._contain(self._orphan_record("/system.slice/" + path.name), "SUPERVISOR_RESTART_FAIL_CLOSED")
434
 
435
  def handle(self, value: dict[str, Any]) -> dict[str, Any]:
436
  if set(value) != {"action", "args"} or not isinstance(value["action"], str) or not isinstance(value["args"], dict):
 
442
  if action == "start":
443
  return self._start(args)
444
  if action == "status" and set(args) == {"name"}:
445
+ try:
446
+ record = self._load(args["name"])
447
+ except JsonInputError:
448
+ record = self._latest_by_name(args["name"])
449
+ return {"name": record["name"], "run_id": record["run_id"], "state": record["state"], "unit": record["unit"], "workload_uid": record["workload_uid"]}
450
  if action == "list" and not args:
451
+ runs = [self.handle({"action": "status", "args": {"name": path.stem}}) for path in sorted(self.names.glob("*.json"))]
452
+ return {"runs": runs}
453
  if action == "kill" and set(args) == {"name"}:
454
  return self._contain(self._load(args["name"]), "OPERATOR")
455
  raise JsonInputError("unsupported supervisor action")
 
483
 
484
 
485
  def main(argv: list[str] | None = None) -> int:
486
+ parser = argparse.ArgumentParser(prog="eggcracker internal-supervisor")
487
  parser.add_argument("--policy", required=True, type=Path)
488
  args = parser.parse_args(argv)
489
  supervisor = Supervisor(args.policy)
490
+ def stop(*_: object) -> None:
491
+ supervisor.stop_event.set()
492
+ raise SystemExit(0)
493
+
494
+ signal.signal(signal.SIGTERM, stop)
495
  return supervisor.serve()
496
 
497
 
src/lumi_nutcracker/__init__.py DELETED
@@ -1,3 +0,0 @@
1
- """Lumi Nutcracker: explicit protected Linux workload termination."""
2
-
3
- __version__ = "0.1.0"
 
 
 
 
tests/fixtures/hostile_client.py CHANGED
@@ -11,7 +11,7 @@ from pathlib import Path
11
 
12
  output = Path(sys.argv[1])
13
  attempts = int(sys.argv[2])
14
- socket_path = "/run/lumi-nutcracker/control.sock"
15
  successes = 0
16
  replacement_successes = 0
17
  for index in range(attempts):
@@ -22,7 +22,7 @@ for index in range(attempts):
22
  successes += 1
23
  except OSError:
24
  pass
25
- result = subprocess.run(["/usr/local/bin/nutcracker", "start", "--name", "replacement-hostile", "--max-pids", "4", "--", "/bin/sleep", "2"], capture_output=True, text=True, check=False)
26
  if result.returncode == 0:
27
  replacement_successes += 1
28
  output.write_text(json.dumps({"connection_successes": successes, "replacement_successes": replacement_successes, "uid": os.getuid()}) + "\n", encoding="utf-8")
 
11
 
12
  output = Path(sys.argv[1])
13
  attempts = int(sys.argv[2])
14
+ socket_path = "/run/lumi-eggcracker/control.sock"
15
  successes = 0
16
  replacement_successes = 0
17
  for index in range(attempts):
 
22
  successes += 1
23
  except OSError:
24
  pass
25
+ result = subprocess.run(["/usr/local/bin/eggcracker", "start", "--name", "replacement-hostile", "--max-pids", "4", "--", "/bin/sleep", "2"], capture_output=True, text=True, check=False)
26
  if result.returncode == 0:
27
  replacement_successes += 1
28
  output.write_text(json.dumps({"connection_successes": successes, "replacement_successes": replacement_successes, "uid": os.getuid()}) + "\n", encoding="utf-8")
tests/test_cli.py CHANGED
@@ -5,7 +5,7 @@ import unittest
5
  from contextlib import redirect_stdout
6
  from unittest.mock import patch
7
 
8
- from lumi_nutcracker.cli import main
9
 
10
 
11
  class CliTests(unittest.TestCase):
@@ -13,19 +13,17 @@ class CliTests(unittest.TestCase):
13
  output = io.StringIO()
14
  with redirect_stdout(output):
15
  self.assertEqual(0, main(["version"]))
16
- self.assertEqual("0.1.0", output.getvalue().strip())
17
 
18
  def test_public_help_has_only_supported_commands(self) -> None:
19
- from lumi_nutcracker.cli import _parser
20
-
21
  help_text = _parser().format_help()
22
  for command in ("start", "kill", "status", "list", "doctor", "version"):
23
  self.assertIn(command, help_text)
24
  self.assertNotIn("_supervisor", help_text)
25
  self.assertNotIn("network" + "-deny", help_text)
26
- self.assertNotIn("root" + "less", help_text)
27
 
28
  def test_internal_supervisor_dispatch_remains_available(self) -> None:
29
- with patch("lumi_nutcracker.cli.supervisor_main", return_value=7) as supervisor:
30
  self.assertEqual(main(["_supervisor", "--policy", "/tmp/policy.json"]), 7)
31
  supervisor.assert_called_once_with(["--policy", "/tmp/policy.json"])
 
5
  from contextlib import redirect_stdout
6
  from unittest.mock import patch
7
 
8
+ from lumi_eggcracker.cli import main
9
 
10
 
11
  class CliTests(unittest.TestCase):
 
13
  output = io.StringIO()
14
  with redirect_stdout(output):
15
  self.assertEqual(0, main(["version"]))
16
+ self.assertEqual("0.1.1", output.getvalue().strip())
17
 
18
  def test_public_help_has_only_supported_commands(self) -> None:
19
+ from lumi_eggcracker.cli import _parser
 
20
  help_text = _parser().format_help()
21
  for command in ("start", "kill", "status", "list", "doctor", "version"):
22
  self.assertIn(command, help_text)
23
  self.assertNotIn("_supervisor", help_text)
24
  self.assertNotIn("network" + "-deny", help_text)
 
25
 
26
  def test_internal_supervisor_dispatch_remains_available(self) -> None:
27
+ with patch("lumi_eggcracker.cli.supervisor_main", return_value=7) as supervisor:
28
  self.assertEqual(main(["_supervisor", "--policy", "/tmp/policy.json"]), 7)
29
  supervisor.assert_called_once_with(["--policy", "/tmp/policy.json"])
tests/test_containment.py CHANGED
@@ -5,14 +5,14 @@ import unittest
5
  from pathlib import Path
6
  from unittest.mock import patch
7
 
8
- from lumi_nutcracker import containment
9
- from lumi_nutcracker.jsonio import JsonInputError
10
 
11
 
12
  class ContainmentTests(unittest.TestCase):
13
  def _root(self, temporary: Path) -> tuple[Path, str, str, str]:
14
  run_id = "a" * 24
15
- unit = f"lumi-nutcracker-workload-{run_id}.service"
16
  cgroup = f"/system.slice/{unit}"
17
  path = temporary / "system.slice" / unit
18
  path.mkdir(parents=True)
@@ -51,11 +51,10 @@ class ContainmentTests(unittest.TestCase):
51
  self.assertTrue(direct.called)
52
  self.assertTrue(proof.complete)
53
 
54
- def test_collected_cgroup_after_direct_kill_is_empty(self) -> None:
55
  with tempfile.TemporaryDirectory() as raw, patch.object(containment, "boot_id", return_value="a" * 36):
56
- path, cgroup, run_id, unit = self._root(Path(raw))
57
  identity = containment.capture_identity(cgroup, run_id, unit, root=Path(raw))
58
- (path / "cgroup.events").write_text("populated 1\n", encoding="ascii")
59
- with patch.object(containment, "kill_path", side_effect=FileNotFoundError(2, "No such file")):
60
  _, proof = containment.verify_empty(identity, root=Path(raw))
61
  self.assertTrue(proof.complete)
 
5
  from pathlib import Path
6
  from unittest.mock import patch
7
 
8
+ from lumi_eggcracker import containment
9
+ from lumi_eggcracker.jsonio import JsonInputError
10
 
11
 
12
  class ContainmentTests(unittest.TestCase):
13
  def _root(self, temporary: Path) -> tuple[Path, str, str, str]:
14
  run_id = "a" * 24
15
+ unit = f"lumi-eggcracker-workload-{run_id}.service"
16
  cgroup = f"/system.slice/{unit}"
17
  path = temporary / "system.slice" / unit
18
  path.mkdir(parents=True)
 
51
  self.assertTrue(direct.called)
52
  self.assertTrue(proof.complete)
53
 
54
+ def test_collected_cgroup_wrapped_as_json_error_is_still_an_empty_proof(self) -> None:
55
  with tempfile.TemporaryDirectory() as raw, patch.object(containment, "boot_id", return_value="a" * 36):
56
+ _, cgroup, run_id, unit = self._root(Path(raw))
57
  identity = containment.capture_identity(cgroup, run_id, unit, root=Path(raw))
58
+ with patch.object(containment, "_events", side_effect=JsonInputError("cannot read cgroup.events: No such file")):
 
59
  _, proof = containment.verify_empty(identity, root=Path(raw))
60
  self.assertTrue(proof.complete)
tests/test_records.py CHANGED
@@ -2,25 +2,29 @@ from __future__ import annotations
2
 
3
  import unittest
4
 
5
- from lumi_nutcracker.containment import EmptyProof
6
- from lumi_nutcracker.jsonio import JsonInputError
7
- from lumi_nutcracker.records import RUN_SCHEMA, make_receipt, validate_run
8
 
9
 
10
  def record() -> dict[str, object]:
11
  run_id = "a" * 24
12
- return {"argv": ["/bin/true"], "boot_id": "b" * 36, "cgroup": f"/system.slice/lumi-nutcracker-workload-{run_id}.service", "cgroup_device": 1, "cgroup_inode": 2, "created_monotonic_ns": 3, "max_pids": 8, "name": "demo", "operator_uid": 1001, "run_id": run_id, "schema_version": RUN_SCHEMA, "state": "RUNNING", "unit": f"lumi-nutcracker-workload-{run_id}.service", "workload_gid": 2001, "workload_uid": 2001}
13
 
14
 
15
  class RecordTests(unittest.TestCase):
16
  def test_receipt_requires_exact_empty_proof(self) -> None:
17
  value = record()
18
- receipt = make_receipt(record=value, trigger="OPERATOR", trigger_ns=10, kill_started_ns=11, kill_complete_ns=12, empty_ns=13, proof=EmptyProof(True, 1, 0, []), version="0.1.0", source_commit="c" * 40, cleanup={}, event_id="d" * 24)
19
  self.assertEqual("TERMINATED", receipt["result"])
20
  self.assertEqual("cgroup.kill", receipt["containment"]["primitive"])
21
 
22
- def test_schema_rejects_unknown_field(self) -> None:
23
  value = record()
24
- value["extra"] = True
 
 
 
 
25
  with self.assertRaises(JsonInputError):
26
  validate_run(value)
 
2
 
3
  import unittest
4
 
5
+ from lumi_eggcracker.containment import EmptyProof
6
+ from lumi_eggcracker.jsonio import JsonInputError
7
+ from lumi_eggcracker.records import RUN_SCHEMA, command_summary, make_receipt, validate_run
8
 
9
 
10
  def record() -> dict[str, object]:
11
  run_id = "a" * 24
12
+ return {**command_summary(["/bin/true", "--safe"]), "boot_id": "b" * 36, "cgroup": f"/system.slice/lumi-eggcracker-workload-{run_id}.service", "cgroup_device": 1, "cgroup_inode": 2, "created_monotonic_ns": 3, "max_pids": 8, "name": "demo", "operator_uid": 1001, "run_id": run_id, "schema_version": RUN_SCHEMA, "state": "RUNNING", "unit": f"lumi-eggcracker-workload-{run_id}.service", "workload_gid": 2001, "workload_uid": 2001}
13
 
14
 
15
  class RecordTests(unittest.TestCase):
16
  def test_receipt_requires_exact_empty_proof(self) -> None:
17
  value = record()
18
+ receipt = make_receipt(record=value, trigger="OPERATOR", trigger_ns=10, kill_started_ns=11, kill_complete_ns=12, empty_ns=13, proof=EmptyProof(True, 1, 0, []), version="0.1.1", source_commit="c" * 40, event_id="d" * 24)
19
  self.assertEqual("TERMINATED", receipt["result"])
20
  self.assertEqual("cgroup.kill", receipt["containment"]["primitive"])
21
 
22
+ def test_durable_record_redacts_command_arguments(self) -> None:
23
  value = record()
24
+ self.assertNotIn("argv", value)
25
+ self.assertNotIn("--safe", str(value))
26
+
27
+ def test_schema_rejects_unknown_field(self) -> None:
28
+ value = record(); value["extra"] = True
29
  with self.assertRaises(JsonInputError):
30
  validate_run(value)
tests/test_supervisor.py CHANGED
@@ -1,45 +1,78 @@
1
  from __future__ import annotations
2
 
 
 
3
  import unittest
4
  from pathlib import Path
5
  from unittest.mock import patch
6
 
7
- from lumi_nutcracker.containment import EmptyProof
8
- from lumi_nutcracker.records import RUN_SCHEMA
9
- from lumi_nutcracker.supervisor import Supervisor
 
10
 
11
 
12
  def record() -> dict[str, object]:
13
  run_id = "a" * 24
14
- return {"argv": ["/bin/true"], "boot_id": "b" * 36, "cgroup": f"/system.slice/lumi-nutcracker-workload-{run_id}.service", "cgroup_device": 1, "cgroup_inode": 2, "created_monotonic_ns": 3, "max_pids": 8, "name": "demo", "operator_uid": 1001, "run_id": run_id, "schema_version": RUN_SCHEMA, "state": "RUNNING", "unit": f"lumi-nutcracker-workload-{run_id}.service", "workload_gid": 2001, "workload_uid": 2001}
15
 
16
 
17
  class SupervisorTests(unittest.TestCase):
18
  def _instance(self) -> Supervisor:
19
  value = object.__new__(Supervisor)
20
- value.policy = {"source_commit": "c" * 40}
21
  value.runs = Path(".")
 
22
  value.receipts = Path(".")
23
  value.locks = {}
 
 
24
  value.completed = {}
25
  value.operations = []
26
  return value
27
 
28
- def test_containment_orders_direct_kill_before_durable_writes(self) -> None:
29
  supervisor = self._instance()
30
- saved: list[str] = []
31
- response = {"result": "TERMINATED"}
32
- with patch("lumi_nutcracker.supervisor.validate_identity", return_value=Path("/owned")), patch("lumi_nutcracker.supervisor.kill_path", return_value=(11, 12)), patch("lumi_nutcracker.supervisor.verify_empty", return_value=(13, EmptyProof(True, 1, 0, []))), patch.object(supervisor, "_cleanup", return_value={}), patch("lumi_nutcracker.supervisor.make_receipt", return_value=response), patch("lumi_nutcracker.supervisor.write_atomic", side_effect=lambda *_: saved.append("write")), patch.object(supervisor, "_store", side_effect=lambda *_: supervisor.operations.append("durable-state")):
33
  result = supervisor._contain(record(), "OPERATOR", 10)
34
  self.assertEqual("TERMINATED", result["result"])
35
  self.assertEqual("cgroup.kill", supervisor.operations[0])
36
  self.assertLess(supervisor.operations.index("cgroup.kill"), supervisor.operations.index("durable-receipt"))
37
- self.assertLess(supervisor.operations.index("cgroup.kill"), supervisor.operations.index("durable-state"))
38
- self.assertEqual(["write"], saved)
39
 
40
- def test_normal_completion_is_reconciled_when_cgroup_is_collected(self) -> None:
41
  supervisor = self._instance()
42
  saved: list[dict[str, object]] = []
43
- with patch.object(supervisor, "_show", return_value={"ActiveState": "inactive"}), patch.object(supervisor, "_store", side_effect=lambda value: saved.append(value.copy())):
44
- self.assertTrue(supervisor._complete_allowed(record()))
45
  self.assertEqual("COMPLETED_ALLOWED", saved[0]["state"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ import tempfile
4
+ import threading
5
  import unittest
6
  from pathlib import Path
7
  from unittest.mock import patch
8
 
9
+ from lumi_eggcracker.containment import EmptyProof
10
+ from lumi_eggcracker.jsonio import JsonInputError
11
+ from lumi_eggcracker.records import RUN_SCHEMA, command_summary
12
+ from lumi_eggcracker.supervisor import Supervisor
13
 
14
 
15
  def record() -> dict[str, object]:
16
  run_id = "a" * 24
17
+ return {**command_summary(["/bin/true"]), "boot_id": "b" * 36, "cgroup": f"/system.slice/lumi-eggcracker-workload-{run_id}.service", "cgroup_device": 1, "cgroup_inode": 2, "created_monotonic_ns": 3, "max_pids": 8, "name": "demo", "operator_uid": 1001, "run_id": run_id, "schema_version": RUN_SCHEMA, "state": "RUNNING", "unit": f"lumi-eggcracker-workload-{run_id}.service", "workload_gid": 2001, "workload_uid": 2001}
18
 
19
 
20
  class SupervisorTests(unittest.TestCase):
21
  def _instance(self) -> Supervisor:
22
  value = object.__new__(Supervisor)
23
+ value.policy = {"source_commit": "c" * 40, "workload_uid": 2001}
24
  value.runs = Path(".")
25
+ value.names = Path(".")
26
  value.receipts = Path(".")
27
  value.locks = {}
28
+ value.lock_guard = threading.Lock()
29
+ value.start_lock = threading.Lock()
30
  value.completed = {}
31
  value.operations = []
32
  return value
33
 
34
+ def test_containment_orders_direct_kill_before_receipt_state_and_cleanup(self) -> None:
35
  supervisor = self._instance()
36
+ response = {"result": "TERMINATED", "cleanup": {"attempted": False}}
37
+ with patch("lumi_eggcracker.supervisor.validate_identity", return_value=Path("/owned")), patch("lumi_eggcracker.supervisor.kill_path", return_value=(11, 12)), patch("lumi_eggcracker.supervisor.verify_empty", return_value=(13, EmptyProof(True, 1, 0, []))), patch.object(supervisor, "_cleanup", side_effect=lambda _: supervisor.operations.append("cleanup") or {}), patch("lumi_eggcracker.supervisor.make_receipt", return_value=response), patch("lumi_eggcracker.supervisor.write_atomic"), patch.object(supervisor, "_store", side_effect=lambda *_: supervisor.operations.append("durable-state")):
 
38
  result = supervisor._contain(record(), "OPERATOR", 10)
39
  self.assertEqual("TERMINATED", result["result"])
40
  self.assertEqual("cgroup.kill", supervisor.operations[0])
41
  self.assertLess(supervisor.operations.index("cgroup.kill"), supervisor.operations.index("durable-receipt"))
42
+ self.assertLess(supervisor.operations.index("durable-receipt"), supervisor.operations.index("cleanup"))
 
43
 
44
+ def test_exact_empty_cgroup_allows_normal_completion_without_systemctl(self) -> None:
45
  supervisor = self._instance()
46
  saved: list[dict[str, object]] = []
47
+ with patch("lumi_eggcracker.supervisor.events_from_fd", return_value={"populated": 0}), patch.object(supervisor, "_store", side_effect=lambda value: saved.append(value.copy())):
48
+ self.assertTrue(supervisor._complete_allowed(record(), 42))
49
  self.assertEqual("COMPLETED_ALLOWED", saved[0]["state"])
50
+
51
+ def test_status_is_read_only(self) -> None:
52
+ supervisor = self._instance()
53
+ item = record()
54
+ with patch.object(supervisor, "_load", return_value=item), patch.object(supervisor, "_store") as stored:
55
+ value = supervisor.handle({"action": "status", "args": {"name": "demo"}})
56
+ self.assertEqual("RUNNING", value["state"])
57
+ stored.assert_not_called()
58
+
59
+ def test_status_resolves_the_latest_terminal_run_after_name_reuse_is_enabled(self) -> None:
60
+ supervisor = self._instance()
61
+ item = record(); item["state"] = "COMPLETED_ALLOWED"
62
+ with patch.object(supervisor, "_load", side_effect=JsonInputError("gone")), patch.object(supervisor, "_latest_by_name", return_value=item), patch.object(supervisor, "_store") as stored:
63
+ value = supervisor.handle({"action": "status", "args": {"name": "demo"}})
64
+ self.assertEqual("COMPLETED_ALLOWED", value["state"])
65
+ stored.assert_not_called()
66
+
67
+ def test_run_records_are_keyed_by_run_id_and_name_pointer_is_removed_when_terminal(self) -> None:
68
+ supervisor = self._instance()
69
+ with tempfile.TemporaryDirectory() as raw:
70
+ root = Path(raw)
71
+ supervisor.runs = root / "runs"; supervisor.names = root / "names"
72
+ item = record()
73
+ supervisor._store(item)
74
+ self.assertTrue((supervisor.runs / ("a" * 24 + ".json")).is_file())
75
+ self.assertTrue((supervisor.names / "demo.json").is_file())
76
+ item["state"] = "TERMINATED"
77
+ supervisor._store(item)
78
+ self.assertFalse((supervisor.names / "demo.json").exists())