fix(cli): give a guard refusal its own code instead of LAUNCH_FAILED - #24
Merged
Conversation
`prk run` refusing a loader-controlled secret reported
`{"code":"LAUNCH_FAILED"}` and exit 1, where the docs promise
`UNSAFE_ENVIRONMENT` and exit 11. A pipeline doing `case $? in 11)` never
matched, and exit 1 is the generic bucket, so a refused unsafe environment
was indistinguishable from any other unspecified failure -- for a security
control whose whole point is that a caller can see it fire.
The guard is raised in `with_secrets`, so it arrives as
`prick_exec::LaunchError::Guard`, and thiserror's derived `From` filed every
variant of that type under `CliError::Launch`. `CliError::Guard` mapped to
`UNSAFE_ENVIRONMENT` and 11 exactly as documented; nothing on a real path
ever constructed it.
Hand-write the conversion so `Guard` comes back out. The binary is where
this belongs: `prick-exec` documents its codes as the shell's 126 and 127
rather than the API taxonomy, and this crate's module doc already says the
binary is where library errors are collected and classified.
`prick_exec::LaunchError::Guard` keeps its own exit code of 1. The CLI no
longer reads it -- `CliError::Launch` cannot hold a `Guard` now -- and giving
a taxonomy value to a type that disclaims the taxonomy would be the same
confusion in the other direction.
Also fix the `LAUNCH_FAILED` row, which was wrong beyond the guard:
`InvalidKey`, `CommandLine` and `Io` all exit 1, so `126, 127` was never the
whole set.
The regression test builds the refusal the way `run` does -- real
`LaunchSpec`, strict guard, same `?` -- rather than naming `CliError::Guard`.
Naming the variant is what hid this: the existing test asserted the mapping
in isolation and passed throughout.
Co-Authored-By: Claude Opus 5 <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.
What and why
Found in a live smoke test with prk 2026.819.1.
docs/reference/cli/errors.mddocuments the guard refusal asUNSAFE_ENVIRONMENTat exit 11. Whatprk runactually emitted when an environment holds a secret namedNODE_OPTIONS:A three-way mismatch: the code is wrong, and exit 1 matches neither the
UNSAFE_ENVIRONMENTrow nor theLAUNCH_FAILEDrow (126, 127).This matters because the guard is a security control and the docs tell scripts to branch on its code. A pipeline doing
case $? in 11)never matched, and exit 1 is the generic bucket, so a refused unsafe environment was indistinguishable from any other unspecified failure. Under--json, a consumer keying onUNSAFE_ENVIRONMENTnever saw it.Root cause
The guard is raised in
LaunchSpec::with_secrets, so it arrives asprick_exec::LaunchError::Guard, and thiserror's derivedFromfiled every variant of that type underCliError::Launch.CliError::Guardmapped toUNSAFE_ENVIRONMENTand exit 11 exactly as documented — nothing on a real path ever constructed it. The hint text is identical in both variants, which is what made this easy to miss by eye: only the code and the exit were wrong.The fix
Hand-write
From<LaunchError> for CliErrorsoGuardcomes back out, and drop#[from]from theLaunchvariant so the derive cannot reinstate the bug.The binary is the right place for this.
prick-execdocuments its codes as the shell's 126 and 127 rather than the API taxonomy, and this crate's module doc already states that the binary is where library errors are collected and given an exit code, a machine code and a hint.prick_exec::LaunchError::Guardkeeps its own exit code of 1. The CLI no longer reads it —CliError::Launchcannot hold aGuardnow — and giving a taxonomy value to a type that explicitly disclaims the taxonomy would be the same confusion pointing the other way. Happy to change that instead if you read the comment atcrates/prick-exec/src/error.rs:94-96as intending 11.Docs
The
LAUNCH_FAILEDrow was wrong beyond the guard case:InvalidKey,CommandLineandIoall exit 1, so126, 127was never the whole set. It now reads1, 126, 127with the breakdown spelled out.run.mdandusing-secrets/index.mdalready documented 11 /UNSAFE_ENVIRONMENTand are correct as of this change.How to verify
No Cloudflare account needed.
cargo test -p prk --libThe regression test builds the refusal the way
rundoes — a realLaunchSpec, the strict guard, and the same?conversion — rather than by namingCliError::Guarddirectly. Naming the variant is precisely what hid this: the pre-existinga_guard_refusal_points_at_the_overrideasserted the mapping in isolation and passed throughout.To confirm the test earns its place, delete the
Guardarm from the newFromimpl incrates/prk/src/error.rsand re-run:That is the reported symptom, reproduced.
a_guard_refusal_points_at_the_overridestill passes with the bug present — the blind spot, confirmed.Checklist
mise run cipasses locally. Full run, exit 0 — includinge2e(101 passed),test(623 Rust / 1152 JS),miri,deny,audit,openapi:checkandversion:check.GuardError::LoaderControlledcarries only the variable name, never its value, and this change moves that error between variants without touching itsDisplay. Variable names are already treated as safe to show — see theLaunchSpec::env_namesdoc comment.If this touches encryption or the database
Not applicable.
If this touches authorization
Not applicable. The env guard is a local policy on what may be injected into a child process, not a grant check.
🤖 Generated with Claude Code