lightcone-cli 0.4.2__tar.gz → 0.5.0rc2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. lightcone_cli-0.5.0rc2/PKG-INFO +83 -0
  2. lightcone_cli-0.5.0rc2/README.md +54 -0
  3. lightcone_cli-0.5.0rc2/pyproject.toml +111 -0
  4. lightcone_cli-0.5.0rc2/src/lightcone/_sandbox_exec.py +328 -0
  5. {lightcone_cli-0.4.2 → lightcone_cli-0.5.0rc2}/src/lightcone/cli/__init__.py +5 -7
  6. lightcone_cli-0.5.0rc2/src/lightcone/cli/commands.py +448 -0
  7. lightcone_cli-0.5.0rc2/src/lightcone/engine/__init__.py +9 -0
  8. lightcone_cli-0.5.0rc2/src/lightcone/engine/assets.py +547 -0
  9. lightcone_cli-0.5.0rc2/src/lightcone/engine/container.py +693 -0
  10. lightcone_cli-0.5.0rc2/src/lightcone/engine/crate.py +688 -0
  11. lightcone_cli-0.5.0rc2/src/lightcone/engine/dataset.py +497 -0
  12. lightcone_cli-0.5.0rc2/src/lightcone/engine/identity.py +348 -0
  13. lightcone_cli-0.5.0rc2/src/lightcone/engine/image.py +386 -0
  14. lightcone_cli-0.5.0rc2/src/lightcone/engine/materialize.py +1097 -0
  15. lightcone_cli-0.5.0rc2/src/lightcone/engine/plan.py +349 -0
  16. lightcone_cli-0.5.0rc2/src/lightcone/engine/project.py +906 -0
  17. lightcone_cli-0.5.0rc2/src/lightcone/engine/run.py +140 -0
  18. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/__init__.py +38 -0
  19. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/boundary.py +297 -0
  20. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/denial.py +204 -0
  21. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/landlock.py +117 -0
  22. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/model.py +164 -0
  23. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/oci.py +128 -0
  24. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/policy.py +471 -0
  25. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/profiles/base.sbpl +139 -0
  26. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/profiles/network.sbpl +48 -0
  27. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/profiles/platform-defaults.sbpl +211 -0
  28. lightcone_cli-0.5.0rc2/src/lightcone/engine/sandbox/seatbelt.py +273 -0
  29. lightcone_cli-0.5.0rc2/src/lightcone/engine/templates/__init__.py +329 -0
  30. lightcone_cli-0.5.0rc2/src/lightcone/engine/templates/files/data-README.md.tmpl +10 -0
  31. lightcone_cli-0.5.0rc2/src/lightcone/engine/templates/files/datalad-config.tmpl +2 -0
  32. lightcone_cli-0.5.0rc2/src/lightcone/engine/templates/files/gitattributes.tmpl +15 -0
  33. lightcone_cli-0.5.0rc2/src/lightcone/engine/templates/files/gitignore.tmpl +8 -0
  34. lightcone_cli-0.5.0rc2/src/lightcone/engine/templates/files/index.md.tmpl +29 -0
  35. lightcone_cli-0.5.0rc2/src/lightcone/engine/templates/files/myst.yml.tmpl +12 -0
  36. lightcone_cli-0.5.0rc2/src/lightcone/engine/templates/files/pyproject.toml.tmpl +8 -0
  37. lightcone_cli-0.5.0rc2/src/lightcone/engine/templates/files/results-README.md.tmpl +9 -0
  38. lightcone_cli-0.5.0rc2/src/lightcone/engine/venue.py +326 -0
  39. lightcone_cli-0.5.0rc2/src/lightcone/engine/worker.py +470 -0
  40. lightcone_cli-0.4.2/PKG-INFO +0 -88
  41. lightcone_cli-0.4.2/README.md +0 -52
  42. lightcone_cli-0.4.2/claude/lightcone/agents/lc-extractor.md +0 -113
  43. lightcone_cli-0.4.2/claude/lightcone/hooks.json +0 -30
  44. lightcone_cli-0.4.2/claude/lightcone/scripts/activate-venv.sh +0 -23
  45. lightcone_cli-0.4.2/claude/lightcone/scripts/session-start.sh +0 -80
  46. lightcone_cli-0.4.2/claude/lightcone/scripts/validate-on-save.sh +0 -44
  47. lightcone_cli-0.4.2/claude/lightcone/skills/README.md +0 -42
  48. lightcone_cli-0.4.2/claude/lightcone/skills/astra/SKILL.md +0 -397
  49. lightcone_cli-0.4.2/claude/lightcone/skills/check-sentence-by-sentence/SKILL.md +0 -371
  50. lightcone_cli-0.4.2/claude/lightcone/skills/figure-comparison/SKILL.md +0 -591
  51. lightcone_cli-0.4.2/claude/lightcone/skills/lc-cli/SKILL.md +0 -110
  52. lightcone_cli-0.4.2/claude/lightcone/skills/lc-feedback/SKILL.md +0 -93
  53. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-code/SKILL.md +0 -132
  54. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/SKILL.md +0 -164
  55. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/references/architect.md +0 -115
  56. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/references/compare.md +0 -118
  57. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/references/implement.md +0 -109
  58. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/references/literature.md +0 -199
  59. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/references/orient.md +0 -235
  60. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/references/review.md +0 -108
  61. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/references/run.md +0 -57
  62. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/references/specify.md +0 -195
  63. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/templates/CLAUDE.md +0 -36
  64. lightcone_cli-0.4.2/claude/lightcone/skills/lc-from-paper/templates/constitution.md +0 -45
  65. lightcone_cli-0.4.2/claude/lightcone/skills/lc-new/SKILL.md +0 -179
  66. lightcone_cli-0.4.2/claude/lightcone/skills/paper-extraction/SKILL.md +0 -238
  67. lightcone_cli-0.4.2/claude/lightcone/skills/paper-extraction/references/arxiv-source.md +0 -47
  68. lightcone_cli-0.4.2/claude/lightcone/skills/paper-extraction/references/pdf-fallback.md +0 -66
  69. lightcone_cli-0.4.2/claude/lightcone/skills/paper-extraction/scripts/extract-paper-substrate.py +0 -1473
  70. lightcone_cli-0.4.2/claude/lightcone/skills/ralph/SKILL.md +0 -195
  71. lightcone_cli-0.4.2/claude/lightcone/skills/ralph/references/constitution.md +0 -133
  72. lightcone_cli-0.4.2/claude/lightcone/skills/ralph/references/crafting.md +0 -181
  73. lightcone_cli-0.4.2/claude/lightcone/skills/ralph/scripts/ralph +0 -145
  74. lightcone_cli-0.4.2/claude/lightcone/templates/CLAUDE.md +0 -22
  75. lightcone_cli-0.4.2/pyproject.toml +0 -140
  76. lightcone_cli-0.4.2/src/lightcone/cli/commands.py +0 -1254
  77. lightcone_cli-0.4.2/src/lightcone/cli/plugin.py +0 -34
  78. lightcone_cli-0.4.2/src/lightcone/engine/__init__.py +0 -6
  79. lightcone_cli-0.4.2/src/lightcone/engine/cloudbuild.py +0 -415
  80. lightcone_cli-0.4.2/src/lightcone/engine/container.py +0 -896
  81. lightcone_cli-0.4.2/src/lightcone/engine/dask_cluster.py +0 -516
  82. lightcone_cli-0.4.2/src/lightcone/engine/manifest.py +0 -216
  83. lightcone_cli-0.4.2/src/lightcone/engine/runner.py +0 -110
  84. lightcone_cli-0.4.2/src/lightcone/engine/scratch.py +0 -208
  85. lightcone_cli-0.4.2/src/lightcone/engine/site_registry.py +0 -205
  86. lightcone_cli-0.4.2/src/lightcone/engine/snakefile.py +0 -487
  87. lightcone_cli-0.4.2/src/lightcone/engine/status.py +0 -170
  88. lightcone_cli-0.4.2/src/lightcone/engine/tree.py +0 -375
  89. lightcone_cli-0.4.2/src/lightcone/engine/validation.py +0 -180
  90. lightcone_cli-0.4.2/src/lightcone/engine/verify.py +0 -140
  91. lightcone_cli-0.4.2/src/lightcone/engine/wrroc.py +0 -885
  92. lightcone_cli-0.4.2/src/lightcone/eval/__init__.py +0 -25
  93. lightcone_cli-0.4.2/src/lightcone/eval/build.py +0 -111
  94. lightcone_cli-0.4.2/src/lightcone/eval/cli.py +0 -176
  95. lightcone_cli-0.4.2/src/lightcone/eval/graders.py +0 -197
  96. lightcone_cli-0.4.2/src/lightcone/eval/harness.py +0 -285
  97. lightcone_cli-0.4.2/src/lightcone/eval/models.py +0 -117
  98. lightcone_cli-0.4.2/src/lightcone/eval/report.py +0 -214
  99. lightcone_cli-0.4.2/src/lightcone/eval/sandbox.py +0 -406
  100. lightcone_cli-0.4.2/src/snakemake_executor_plugin_dask/__init__.py +0 -29
  101. lightcone_cli-0.4.2/src/snakemake_executor_plugin_dask/executor.py +0 -229
  102. {lightcone_cli-0.4.2 → lightcone_cli-0.5.0rc2}/.gitignore +0 -0
  103. {lightcone_cli-0.4.2 → lightcone_cli-0.5.0rc2}/LICENSE +0 -0
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.5
2
+ Name: lightcone-cli
3
+ Version: 0.5.0rc2
4
+ Summary: Lightcone Research's agentic execution layer
5
+ Project-URL: Repository, https://github.com/LightconeResearch/lightcone-cli
6
+ Author: Lightcone Research
7
+ License: BSD-3-Clause
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: astra-tools==0.2.17
23
+ Requires-Dist: click>=8.0
24
+ Requires-Dist: distributed>=2026.7
25
+ Requires-Dist: git-annex>=10.2026
26
+ Requires-Dist: rich>=13.0
27
+ Requires-Dist: rocrate>=0.15
28
+ Description-Content-Type: text/markdown
29
+
30
+ # lightcone-cli
31
+
32
+ [![License](https://img.shields.io/badge/License-BSD_3--Clause-426b78.svg?style=flat)](https://opensource.org/licenses/BSD-3-Clause)
33
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-4e5a70?style=flat)](https://pypi.org/project/lightcone-cli/)
34
+ [![PyPI](https://img.shields.io/pypi/v/lightcone-cli?style=flat&color=f8f7f3)](https://pypi.org/project/lightcone-cli/)
35
+ [![Tests](https://img.shields.io/github/actions/workflow/status/LightconeResearch/lightcone-cli/tests.yml?style=flat&color=darkgreen)](https://github.com/LightconeResearch/lightcone-cli/actions/workflows/tests.yml)
36
+
37
+ **lightcone-cli** (`lc`) is the execution layer for
38
+ [ASTRA](https://astra-spec.org/latest/) (Agentic Schema for Transparent
39
+ Research Analysis). Describe your analysis in an `astra.yaml`
40
+ specification and `lc` takes care of the rest — execution, environments,
41
+ and provenance.
42
+
43
+ ## Quick Start
44
+
45
+ **lightcone-cli** only requires you to have `uv` installed on your environment, and will take care of everything else. See how to install uv here: [https://docs.astral.sh/uv/getting-started/installation](https://docs.astral.sh/uv/getting-started/installation).
46
+
47
+ Then to install **lightcone-cli**:
48
+ ```bash
49
+ uv tool install lightcone-cli
50
+ ```
51
+
52
+ Once the CLI is installed, you can use it to create an ASTRA project and generate outputs like so:
53
+
54
+ ```bash
55
+ lc init my-analysis
56
+ cd my-analysis
57
+ # describe your analysis in astra.yaml, write your scripts,
58
+ # declare what they import through normal uv interactions:
59
+ uv add numpy
60
+ # When you are done with your edits, commit:
61
+ git add -A && git commit -m "First analysis"
62
+ # Use the lightcone CLI to generate your outputs with full provenance tracking
63
+ lc materialize
64
+ ```
65
+
66
+ ASTRA specs are plain, structured YAML — they work well hand-written or
67
+ drafted with any AI coding assistant.
68
+
69
+ → [Full getting-started guide](https://docs.lightconeresearch.org/user/getting-started/)
70
+
71
+ ## Capabilities
72
+
73
+ - **Multiverse analysis** — declare methodological decisions with multiple defensible options; `lc` materializes your analysis across every universe you define
74
+ - **Provenance by construction** — every output is committed to git together with a content-addressed manifest and a re-runnable run record; git-annex carries the bytes, so results travel with the repository
75
+ - **Locked, isolated execution** — a project's environment is `pyproject.toml` + `uv.lock`; recipes run in it under a sandbox (Landlock on Linux, Seatbelt on macOS) that keeps undeclared files out and stray writes contained
76
+ - **Containers and HPC** — declare `[tool.lightcone.image]` and recipes run in a content-addressed image archived in the repository itself; a SLURM allocation is detected and used automatically, every node included
77
+ - **Publication view** — declare a license and `lc materialize` maintains an [RO-Crate](https://www.researchobject.org/ro-crate/) of the project and its provenance, ready to archive or deposit
78
+
79
+ → [Full documentation](https://docs.lightconeresearch.org)
80
+
81
+ ## License
82
+
83
+ BSD 3-Clause — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,54 @@
1
+ # lightcone-cli
2
+
3
+ [![License](https://img.shields.io/badge/License-BSD_3--Clause-426b78.svg?style=flat)](https://opensource.org/licenses/BSD-3-Clause)
4
+ [![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-4e5a70?style=flat)](https://pypi.org/project/lightcone-cli/)
5
+ [![PyPI](https://img.shields.io/pypi/v/lightcone-cli?style=flat&color=f8f7f3)](https://pypi.org/project/lightcone-cli/)
6
+ [![Tests](https://img.shields.io/github/actions/workflow/status/LightconeResearch/lightcone-cli/tests.yml?style=flat&color=darkgreen)](https://github.com/LightconeResearch/lightcone-cli/actions/workflows/tests.yml)
7
+
8
+ **lightcone-cli** (`lc`) is the execution layer for
9
+ [ASTRA](https://astra-spec.org/latest/) (Agentic Schema for Transparent
10
+ Research Analysis). Describe your analysis in an `astra.yaml`
11
+ specification and `lc` takes care of the rest — execution, environments,
12
+ and provenance.
13
+
14
+ ## Quick Start
15
+
16
+ **lightcone-cli** only requires you to have `uv` installed on your environment, and will take care of everything else. See how to install uv here: [https://docs.astral.sh/uv/getting-started/installation](https://docs.astral.sh/uv/getting-started/installation).
17
+
18
+ Then to install **lightcone-cli**:
19
+ ```bash
20
+ uv tool install lightcone-cli
21
+ ```
22
+
23
+ Once the CLI is installed, you can use it to create an ASTRA project and generate outputs like so:
24
+
25
+ ```bash
26
+ lc init my-analysis
27
+ cd my-analysis
28
+ # describe your analysis in astra.yaml, write your scripts,
29
+ # declare what they import through normal uv interactions:
30
+ uv add numpy
31
+ # When you are done with your edits, commit:
32
+ git add -A && git commit -m "First analysis"
33
+ # Use the lightcone CLI to generate your outputs with full provenance tracking
34
+ lc materialize
35
+ ```
36
+
37
+ ASTRA specs are plain, structured YAML — they work well hand-written or
38
+ drafted with any AI coding assistant.
39
+
40
+ → [Full getting-started guide](https://docs.lightconeresearch.org/user/getting-started/)
41
+
42
+ ## Capabilities
43
+
44
+ - **Multiverse analysis** — declare methodological decisions with multiple defensible options; `lc` materializes your analysis across every universe you define
45
+ - **Provenance by construction** — every output is committed to git together with a content-addressed manifest and a re-runnable run record; git-annex carries the bytes, so results travel with the repository
46
+ - **Locked, isolated execution** — a project's environment is `pyproject.toml` + `uv.lock`; recipes run in it under a sandbox (Landlock on Linux, Seatbelt on macOS) that keeps undeclared files out and stray writes contained
47
+ - **Containers and HPC** — declare `[tool.lightcone.image]` and recipes run in a content-addressed image archived in the repository itself; a SLURM allocation is detected and used automatically, every node included
48
+ - **Publication view** — declare a license and `lc materialize` maintains an [RO-Crate](https://www.researchobject.org/ro-crate/) of the project and its provenance, ready to archive or deposit
49
+
50
+ → [Full documentation](https://docs.lightconeresearch.org)
51
+
52
+ ## License
53
+
54
+ BSD 3-Clause — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,111 @@
1
+ [project]
2
+ name = "lightcone-cli"
3
+ dynamic = ["version"]
4
+ description = "Lightcone Research's agentic execution layer"
5
+ readme = "README.md"
6
+ license = { text = "BSD-3-Clause" }
7
+ requires-python = ">=3.11"
8
+ authors = [
9
+ { name = "Lightcone Research" }
10
+ ]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Environment :: Console",
14
+ "Intended Audience :: Science/Research",
15
+ "License :: OSI Approved :: BSD License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Topic :: Scientific/Engineering",
24
+ ]
25
+
26
+ dependencies = [
27
+ "astra-tools==0.2.17",
28
+ "click>=8.0",
29
+ "rich>=13.0",
30
+ "git-annex>=10.2026",
31
+ "distributed>=2026.7",
32
+ "rocrate>=0.15",
33
+ ]
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "pytest>=8.0",
38
+ "pytest-cov",
39
+ "ruff",
40
+ "mypy",
41
+ "datalad",
42
+ "roc-validator>=0.11.3",
43
+ ]
44
+ docs = [
45
+ "zensical>=0.0.33",
46
+ # squidfunk's mike fork — required by zensical's versioning provider.
47
+ # Not on PyPI; install from git.
48
+ "mike @ git+https://github.com/squidfunk/mike.git ; python_version >= '3.10'",
49
+ ]
50
+
51
+
52
+ [project.urls]
53
+ Repository = "https://github.com/LightconeResearch/lightcone-cli"
54
+
55
+ [project.scripts]
56
+ lc = "lightcone.cli:main"
57
+ # The git-annex wheel's own entry points, re-declared verbatim: an
58
+ # installer links only the requested package's executables, and a plain
59
+ # `uv tool install lightcone-cli` must put git-annex on the shell's PATH
60
+ # — `filter=annex` makes the researcher's own `git add` need it. The
61
+ # suite pins these against the wheel's, so an upstream change fails
62
+ # loudly here rather than silently at install time.
63
+ git-annex = "git_annex:cli"
64
+ git-annex-shell = "git_annex:cli"
65
+ git-remote-annex = "git_annex:cli"
66
+ git-remote-tor-annex = "git_annex:cli"
67
+
68
+ [build-system]
69
+ requires = ["hatchling", "hatch-vcs"]
70
+ build-backend = "hatchling.build"
71
+
72
+ [tool.hatch.version]
73
+ source = "vcs"
74
+
75
+ [tool.hatch.build.targets.wheel]
76
+ packages = ["src/lightcone"]
77
+
78
+ [tool.hatch.build.targets.sdist]
79
+ include = ["src/lightcone"]
80
+
81
+ [tool.ruff]
82
+ target-version = "py311"
83
+ line-length = 100
84
+
85
+ [tool.ruff.lint]
86
+ select = ["E", "F", "I", "N", "W", "UP"]
87
+
88
+ [tool.mypy]
89
+ python_version = "3.11"
90
+ strict = true
91
+ namespace_packages = true
92
+ explicit_package_bases = true
93
+ mypy_path = "src"
94
+
95
+ [[tool.mypy.overrides]]
96
+ module = ["astra.*", "rocrate.*"]
97
+ ignore_missing_imports = true
98
+ follow_untyped_imports = true
99
+
100
+ # rocrate ships no type information, and engine/crate.py is a client of
101
+ # little else — one module-scoped relaxation beats an ignore on every call.
102
+ [[tool.mypy.overrides]]
103
+ module = ["lightcone.engine.crate"]
104
+ disallow_untyped_calls = false
105
+
106
+ [tool.pytest.ini_options]
107
+ testpaths = ["tests"]
108
+ # The container smoke tests leave ~200 MB image archives in each test's
109
+ # tmp_path; keeping every passing run's directories exhausts a tmpfs.
110
+ tmp_path_retention_policy = "failed"
111
+ tmp_path_retention_count = 1
@@ -0,0 +1,328 @@
1
+ """The Landlock exec shim — ``python -m lightcone._sandbox_exec``.
2
+
3
+ Landlock is a *self*-restriction: a process narrows its own access rights
4
+ and can never widen them again, and the restriction is inherited by
5
+ everything it execs. There is no way to restrict *another* process, so
6
+ there is no ``sandbox-exec``-shaped command to wrap a recipe in. This
7
+ module is that missing command: it reads a policy, restricts itself,
8
+ and ``execvp``'s the real command. That is what lets the engine treat
9
+ Landlock and Seatbelt as the same thing — a pure argv rewrite (see
10
+ :mod:`lightcone.engine.sandbox`).
11
+
12
+ The policy arrives as **JSON on argv**, not as an inherited ruleset FD —
13
+ the shape the codex CLI uses. A Landlock FD cannot be reopened, so
14
+ whether one would survive ``uv run``'s spawn/exec chain is a question
15
+ serializing the policy never has to answer, and it is what keeps the
16
+ engine's argv rewrite a pure function.
17
+
18
+ Two properties of this module are load-bearing and pinned by tests:
19
+
20
+ - **Stdlib only, and no lightcone imports.** ``lightcone`` is a PEP 420
21
+ namespace package with no ``__init__``, so ``-m lightcone._sandbox_exec``
22
+ executes this file and nothing else. It runs on every sandboxed exec;
23
+ it must not drag the engine in.
24
+ - **It never proceeds unsandboxed.** Every setup failure exits
25
+ :data:`SETUP_FAILURE_EXIT` with a distinguishable message, so a broken
26
+ sandbox can never be mistaken for a working one (the "silent
27
+ best-effort" trap this design exists to avoid).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import ctypes
33
+ import errno
34
+ import functools
35
+ import json
36
+ import os
37
+ import stat
38
+ import sys
39
+
40
+ # --- the kernel interface --------------------------------------------------
41
+
42
+ # asm-generic syscall numbers — identical on x86_64 and aarch64.
43
+ SYS_LANDLOCK_CREATE_RULESET = 444
44
+ SYS_LANDLOCK_ADD_RULE = 445
45
+ SYS_LANDLOCK_RESTRICT_SELF = 446
46
+
47
+ _LANDLOCK_CREATE_RULESET_VERSION = 1 << 0
48
+ _RULE_PATH_BENEATH = 1
49
+ _PR_SET_NO_NEW_PRIVS = 38
50
+
51
+ # Access-right bits, grouped by the ABI that introduced them.
52
+ ACCESS_FS_EXECUTE = 1 << 0
53
+ ACCESS_FS_WRITE_FILE = 1 << 1
54
+ ACCESS_FS_READ_FILE = 1 << 2
55
+ ACCESS_FS_READ_DIR = 1 << 3
56
+ _ABI1_ALL = (1 << 13) - 1 # bits 0..12: read, write, and every make/remove
57
+ ACCESS_FS_REFER = 1 << 13 # ABI ≥ 2
58
+ ACCESS_FS_TRUNCATE = 1 << 14 # ABI ≥ 3
59
+
60
+ #: Rights the kernel accepts on a rule whose parent is a regular file.
61
+ #: Everything else is directory-only and makes ``add_rule`` fail EINVAL.
62
+ _FILE_ONLY_BITS = (
63
+ ACCESS_FS_EXECUTE | ACCESS_FS_WRITE_FILE | ACCESS_FS_READ_FILE | ACCESS_FS_TRUNCATE
64
+ )
65
+
66
+ READ_BITS = ACCESS_FS_READ_FILE | ACCESS_FS_READ_DIR
67
+ EXEC_BITS = ACCESS_FS_EXECUTE | ACCESS_FS_READ_FILE
68
+
69
+ #: Landlock's syscall numbers are asm-generic, but the ABI is not defined
70
+ #: for every architecture; refuse rather than issue syscall 444 blind.
71
+ _SUPPORTED_ARCHES = frozenset({"x86_64", "aarch64", "arm64"})
72
+
73
+ #: Reserved exit code for a failure to *set up* the sandbox — never a
74
+ #: command's own. The engine renders it as an lc problem, not a recipe's.
75
+ SETUP_FAILURE_EXIT = 97
76
+
77
+ class _RulesetAttr(ctypes.Structure):
78
+ _fields_ = [
79
+ ("handled_access_fs", ctypes.c_uint64),
80
+ ("handled_access_net", ctypes.c_uint64),
81
+ ]
82
+
83
+
84
+ class _PathBeneathAttr(ctypes.Structure):
85
+ # The kernel struct is packed (u64 followed by s32). `_layout_` is the
86
+ # 3.14 spelling and is ignored by older ctypes, which honor `_pack_`.
87
+ _pack_ = 1
88
+ _layout_ = "ms"
89
+ _fields_ = [
90
+ ("allowed_access", ctypes.c_uint64),
91
+ ("parent_fd", ctypes.c_int32),
92
+ ]
93
+
94
+
95
+ @functools.cache
96
+ def _libc() -> ctypes.CDLL:
97
+ """The already-loaded libc.
98
+
99
+ Cached, and `CDLL(None)` rather than `find_library("c")`: this is
100
+ called once per rule, and `find_library` forks `ldconfig -p` every
101
+ time — 74 subprocesses for a 71-path policy, which measured at ~740 ms
102
+ of the shim's ~780 ms. `dlopen(NULL)` reaches the libc already mapped
103
+ into this process, needs no search, and drops `ctypes.util` (and the
104
+ `tempfile`/`random` chain behind it) off the import path.
105
+ """
106
+ return ctypes.CDLL(None, use_errno=True)
107
+
108
+
109
+ def abi() -> int:
110
+ """Probe the Landlock ABI this kernel supports.
111
+
112
+ Returns:
113
+ The ABI level, or 0 if the kernel supports none.
114
+ """
115
+ if os.uname().machine not in _SUPPORTED_ARCHES:
116
+ return 0
117
+ try:
118
+ result = _libc().syscall(
119
+ SYS_LANDLOCK_CREATE_RULESET, None, ctypes.c_size_t(0), ctypes.c_uint32(
120
+ _LANDLOCK_CREATE_RULESET_VERSION
121
+ )
122
+ )
123
+ except OSError: # pragma: no cover - no libc to load
124
+ return 0
125
+ return int(result) if result > 0 else 0
126
+
127
+
128
+ def handled_access(abi_level: int) -> int:
129
+ """Build the mask of rights the ruleset declares it governs.
130
+
131
+ Args:
132
+ abi_level: The ABI reported by :func:`abi`.
133
+
134
+ Returns:
135
+ Every right this ABI knows about. A right the kernel does not
136
+ know is a ruleset it rejects outright.
137
+ """
138
+ handled = _ABI1_ALL
139
+ if abi_level >= 2:
140
+ handled |= ACCESS_FS_REFER
141
+ if abi_level >= 3:
142
+ handled |= ACCESS_FS_TRUNCATE
143
+ return handled
144
+
145
+
146
+ def write_bits(abi_level: int) -> int:
147
+ """Build the mask a writable root is granted.
148
+
149
+ Args:
150
+ abi_level: The ABI reported by :func:`abi`.
151
+
152
+ Returns:
153
+ Everything but EXECUTE, which is granted per file.
154
+ """
155
+ return (handled_access(abi_level) & ~ACCESS_FS_EXECUTE) | READ_BITS
156
+
157
+
158
+ def create_ruleset(handled_fs: int) -> int:
159
+ """Create an empty Landlock ruleset.
160
+
161
+ Args:
162
+ handled_fs: The rights the ruleset governs.
163
+
164
+ Returns:
165
+ A ruleset file descriptor.
166
+
167
+ Raises:
168
+ OSError: If the kernel refuses the ruleset.
169
+ """
170
+ attr = _RulesetAttr(handled_fs, 0)
171
+ fd = _libc().syscall(
172
+ SYS_LANDLOCK_CREATE_RULESET,
173
+ ctypes.byref(attr),
174
+ ctypes.c_size_t(ctypes.sizeof(attr)),
175
+ ctypes.c_uint32(0),
176
+ )
177
+ if fd < 0:
178
+ raise OSError(ctypes.get_errno(), "landlock_create_ruleset failed")
179
+ return int(fd)
180
+
181
+
182
+ def add_path_rule(ruleset_fd: int, path: str, access: int) -> None:
183
+ """Grant access beneath a path.
184
+
185
+ Args:
186
+ ruleset_fd: The ruleset to add to.
187
+ path: The directory or file to grant on.
188
+ access: The rights to grant, masked to what the path can bear.
189
+
190
+ Raises:
191
+ OSError: If the path cannot be opened or the rule is refused.
192
+ """
193
+ parent_fd = os.open(path, os.O_PATH | os.O_CLOEXEC)
194
+ try:
195
+ # `stat.S_ISDIR`, not a bitwise test: S_IFSOCK (0o140000) and
196
+ # S_IFBLK (0o060000) both carry the S_IFDIR bit, so a hand-rolled
197
+ # mask calls a socket a directory, leaves directory-only rights
198
+ # unmasked, and turns a declared unix-socket input into EINVAL —
199
+ # surfacing as a sandbox *setup* failure rather than a grant.
200
+ if not stat.S_ISDIR(os.fstat(parent_fd).st_mode):
201
+ access &= _FILE_ONLY_BITS
202
+ if not access:
203
+ return
204
+ attr = _PathBeneathAttr(access, parent_fd)
205
+ result = _libc().syscall(
206
+ SYS_LANDLOCK_ADD_RULE,
207
+ ctypes.c_int(ruleset_fd),
208
+ ctypes.c_uint32(_RULE_PATH_BENEATH),
209
+ ctypes.byref(attr),
210
+ ctypes.c_uint32(0),
211
+ )
212
+ if result != 0:
213
+ code = ctypes.get_errno()
214
+ raise OSError(code, f"landlock_add_rule({path}): {errno.errorcode.get(code, code)}")
215
+ finally:
216
+ os.close(parent_fd)
217
+
218
+
219
+ def build_ruleset(policy: dict[str, object], abi_level: int) -> int:
220
+ """Build a ruleset realizing a policy document.
221
+
222
+ Args:
223
+ policy: The read, write and execute path lists from argv.
224
+ abi_level: The ABI reported by :func:`abi`.
225
+
226
+ Returns:
227
+ A ruleset file descriptor.
228
+
229
+ Raises:
230
+ OSError: If the ruleset cannot be created or a rule refused.
231
+ """
232
+ fd = create_ruleset(handled_access(abi_level))
233
+ try:
234
+ for key, access in (
235
+ ("read", READ_BITS),
236
+ ("write", write_bits(abi_level)),
237
+ ("execute", EXEC_BITS),
238
+ ):
239
+ for path in _paths(policy, key):
240
+ try:
241
+ add_path_rule(fd, path, access)
242
+ except FileNotFoundError:
243
+ continue
244
+ except BaseException:
245
+ os.close(fd)
246
+ raise
247
+ return fd
248
+
249
+
250
+ def restrict_self(ruleset_fd: int) -> None:
251
+ """Apply a ruleset to this process, irreversibly.
252
+
253
+ Args:
254
+ ruleset_fd: The ruleset to enter.
255
+
256
+ Raises:
257
+ OSError: If ``no_new_privs`` or the restriction is refused.
258
+ """
259
+ libc = _libc()
260
+ if libc.prctl(_PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0:
261
+ raise OSError(ctypes.get_errno(), "prctl(PR_SET_NO_NEW_PRIVS) failed")
262
+ if libc.syscall(SYS_LANDLOCK_RESTRICT_SELF, ctypes.c_int(ruleset_fd), ctypes.c_uint32(0)) != 0:
263
+ raise OSError(ctypes.get_errno(), "landlock_restrict_self failed")
264
+
265
+
266
+ # --- the entry point -------------------------------------------------------
267
+
268
+
269
+ def _paths(policy: dict[str, object], key: str) -> list[str]:
270
+ value = policy.get(key, [])
271
+ if not isinstance(value, list) or not all(isinstance(p, str) for p in value):
272
+ raise ValueError(f"policy field {key!r} must be a list of strings")
273
+ return list(value)
274
+
275
+
276
+ def _fail(message: str) -> None:
277
+ sys.stderr.write(f"lc sandbox setup failed: {message}\n")
278
+ raise SystemExit(SETUP_FAILURE_EXIT)
279
+
280
+
281
+ def main(argv: list[str] | None = None) -> None:
282
+ """Restrict this process, then exec the command it was given.
283
+
284
+ ``--policy <json> -- <command>...``. Never falls through to running
285
+ the command unsandboxed: a setup failure exits with a reserved code
286
+ that nothing a command could return will collide with.
287
+
288
+ Args:
289
+ argv: Defaults to the process arguments.
290
+ """
291
+ args = list(sys.argv[1:] if argv is None else argv)
292
+ if len(args) < 2 or args[0] != "--policy":
293
+ _fail("usage: -m lightcone._sandbox_exec --policy <json> -- <command>...")
294
+ raw = args[1]
295
+ rest = args[2:]
296
+ if not rest or rest[0] != "--":
297
+ _fail("missing `--` before the command")
298
+ command = rest[1:]
299
+ if not command:
300
+ _fail("no command after `--`")
301
+
302
+ try:
303
+ policy = json.loads(raw)
304
+ if not isinstance(policy, dict):
305
+ raise ValueError("policy must be a JSON object")
306
+ abi_level = abi()
307
+ if abi_level == 0:
308
+ raise ValueError("landlock unavailable (kernel < 5.13, or blocked by seccomp)")
309
+ fd = build_ruleset(policy, abi_level)
310
+ restrict_self(fd)
311
+ os.close(fd)
312
+ except (ValueError, OSError) as e:
313
+ _fail(str(e))
314
+
315
+ try:
316
+ os.execvp(command[0], command)
317
+ except OSError as e:
318
+ # Not a setup failure: we are already restricted, so this is very
319
+ # likely the sandbox denying the exec. Report it the way a shell
320
+ # would — 126 "cannot execute", 127 "not found" — so the denial
321
+ # classifier sees a message and an exit code it recognizes, and
322
+ # exit 97 keeps meaning "lc's own sandbox setup broke".
323
+ sys.stderr.write(f"lc sandbox: {command[0]}: {e.strerror}\n")
324
+ raise SystemExit(127 if e.errno == errno.ENOENT else 126) from e
325
+
326
+
327
+ if __name__ == "__main__":
328
+ main()
@@ -2,15 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- try:
6
- from importlib.metadata import version
7
-
8
- __version__ = version("lightcone-cli")
9
- except Exception:
10
- __version__ = "0.0.0.dev"
11
-
12
5
 
13
6
  def main() -> None:
7
+ """Run the CLI.
8
+
9
+ The console-script entry point. Imports the command module lazily, so
10
+ the cost of click and the engine is paid only once a command runs.
11
+ """
14
12
  from lightcone.cli.commands import main as _main
15
13
 
16
14
  _main()