feat(cli): trust gating with --untrusted / --trust-level#26
Merged
Conversation
The pytex CLI executes .py inputs, evaluates .tex pytex(...) replacements and Markdown eval comments, and enables shell-escape by default. That is remote-code-execution by design and is safe only for first-party documents (Red-Team finding #9). The trust model previously existed only in the pytex_api layer. Make the CLI a TRUSTED context explicitly, and add a --untrusted flag (shorthand for --trust-level untrusted) plus --trust-level {trusted, sandboxed,untrusted}. Non-trusted levels route the build through the existing pytex_api trust policy via render_blob rather than duplicating the gating: no Python exec, inert .tex/Markdown code surfaces, shell-escape forced off, package allowlist, resource limits, and (sandboxed) the Podman sandbox. The default stays trusted, so existing invocations are unchanged. TrustLevel is imported from pytex_api._models (not the package root) to avoid a circular import: pytex_api/__init__ imports pytex_builder.console. Document the trust model in the CLI help text, README, and wiki, and add tests covering the blocked .py-exec and shell-escape-package vectors, the inert .tex path, the unchanged trusted default, and the new flags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Red-Team finding #9: the
pytexCLI has no trust gating. By default it executes.pyinputs, evaluates.texpytex(...)replacements and Markdownevalcomments, and runs tectonic with shell-escape on. That is RCE-by-design — fine for your own documents, dangerous on foreign/untrusted source. The trust model previously lived only in thepytex_apilayer.What
--untrusted(shorthand for--trust-level untrusted) and--trust-level {trusted,sandboxed,untrusted}(mutually exclusive group). Default staystrusted→ fully backwards-compatible.pytex_apitrust policy viarender_blob, not duplicated logic:.py/.tex.pyrefused (no exec),.texpytex(...)markers + Markdownevalcomments stay inert,mintedrejected),sandboxedadditionally requires the Podman sandbox for PDFs.How it docks onto
pytex_api_rundispatches oncfg.trust:TRUSTEDkeeps the in-process pipeline unchanged; any other level calls_run_untrusted, which reads the source bytes, maps the suffix to anInputKind(.py→TEX_PY,.tex→TEX,.md→MARKDOWN), builds aBuildRequestwith the chosenTrustLevel, and callsrender_blob.ApiErroris mapped toBuildErrorsomainreports it like any other failure.TrustLevelis imported frompytex_api._models(not the package root) to avoid a circular import —pytex_api/__init__importspytex_builder.console.Tested vectors
--untrustedon a.pythat writes a file on import → blocked at the trust gate (exit 1, output absent, side effect never runs, error names TRUSTED).--untrustedon.texrequestingminted(the shell-escape package) → rejected.--untrustedon benign.tex→ renders, but thepytex(...)marker survives verbatim (inert)..py(regression guard).trusted,--untrusted,--trust-level sandboxed, and mutual exclusion.Status
basedpyright src(configured root): 0 errors, 0 warnings.ruff format --check+ruff check: green.🤖 Generated with Claude Code