Run an approved command
Build a one-program catalogue for your own tool, feed it stdin, read the result, and meet both ways Cuprum refuses a program.
01 — Problem
The job
A script needs one external tool — here, wc, to count the words
in a piece of text. With subprocess that is a string or a list
assembled wherever it is needed, run with whatever is on the machine. With
Cuprum the program is declared once in a catalogue, the command is built
from typed arguments, and the script says exactly which programs it may run.
This job builds that catalogue, feeds the text on standard input rather than as an argument, and turns the captured output into an integer.
02 — Before you start
Before you start
- Level
- Beginner
- Needs
- Python 3.12 or later, and wc on PATH (any POSIX system)
- Install
- Cuprum 0.2.0b2 from PyPI; see Getting started
- Tested
- 2026-10-19 against Cuprum 0.2.0b2 on Linux, Python 3.12
- Uses
- ProgramCatalogue.from_programs, sh.make, StdinInput, scoped(catalogue=…), DEFAULT_CATALOGUE
03 — Code
The code
from cuprum import (
Program,
ProgramCatalogue,
StdinInput,
scoped,
sh,
)
WC = Program("wc")
CATALOGUE = ProgramCatalogue.from_programs(WC, name="word-count")
count_words = sh.make(WC, catalogue=CATALOGUE)
def word_count(text: str) -> int:
cmd = count_words("-w")
with scoped(catalogue=CATALOGUE):
result = cmd.run_sync(stdin=StdinInput(text=text))
if not result.ok:
msg = f"wc failed with exit code {result.exit_code}: {result.stderr}"
raise RuntimeError(msg)
return int((result.stdout or "").strip())
print(count_words("-w").argv_with_program)
print(count_words("-w").project.name)
print(word_count("Every command gets the right fitting."))
Reading it
-
ProgramCatalogue.from_programs(WC, name="word-count")builds a single-project catalogue whose allowlist is exactlywc. Withoutname=, the project would be named after the programs' base names. -
sh.make(WC, catalogue=CATALOGUE)returns a builder. It has already checked thatwcis catalogued, so every command it builds carries the project's metadata. -
StdinInput(text=...)is encoded with the context's encoding (UTF-8 by default) and written to the child's standard input, so the text never appears in argv. -
strip()is there becausewcpads its count differently on different systems. Capture keeps the child's bytes as they were.
04 — Output
Expected output
('wc', '-w')
word-count
6
The first line is the exact argv Cuprum will pass — program first, no shell. The second is the project the command belongs to, which is what downstream tooling reads to find noise rules and documentation links. The third is the count.
05 — Failure path
When it fails
Cuprum refuses a program at two different moments, for two different reasons. This script triggers both.
from cuprum import (
DEFAULT_CATALOGUE,
ECHO,
ForbiddenProgramError,
Program,
ProgramCatalogue,
UnknownProgramError,
scoped,
sh,
)
WC = Program("wc")
CATALOGUE = ProgramCatalogue.from_programs(WC, name="word-count")
# Not in the catalogue: refused when the builder is made.
try:
sh.make(Program("curl"), catalogue=CATALOGUE)
except UnknownProgramError as exc:
print(f"UnknownProgramError: {exc}")
# In a catalogue, but outside the active scope: refused when it runs.
with scoped(catalogue=CATALOGUE):
try:
sh.make(ECHO, catalogue=DEFAULT_CATALOGUE)("hello").run_sync()
except ForbiddenProgramError as exc:
print(f"ForbiddenProgramError: {exc}")
UnknownProgramError: Program 'curl' is not in the catalogue allowlist ForbiddenProgramError: Program 'echo' is not allowed in the current context
Program echo denied by context allowlist restricted_state=True
UnknownProgramError is a construction failure: the catalogue
you passed does not know the program, so no command exists to run.
ForbiddenProgramError is an authorization failure:
ECHO is in Cuprum's default catalogue, so the command was
built, but the scope around it allows only wc. Nothing is
spawned in either case.
The second refusal names its catalogue on purpose. Inside
scoped(catalogue=...), sh.make resolves a
program from the scope's catalogue, so a bare sh.make(ECHO)
there would be the first refusal again: echo is not in the
word-count catalogue, and the builder is never made. Passing
catalogue=DEFAULT_CATALOGUE builds the command from the
catalogue that does list echo, which is what lets the
scope's allowlist be the check that fails.
A non-zero exit is not an exception. If wc itself failed,
result.ok would be false and the script above raises its own
RuntimeError with the exit code and stderr.
06 — Why it is allowed
Why the policy permits it
Two independent checks let this run. The builder was made from a catalogue
that lists wc, and the scoped() block takes its
allowlist from that same catalogue, so wc is the one program
the block may run. The block also makes that catalogue the one
sh.make consults, so a builder made inside it could drop the
catalogue= argument; this script names it anyway, because the
builder is made before the block.
The scope is what a reviewer reads. Remove the with block and
the command still runs, because an unscoped root context permits every
catalogued program; that default exists to ease adoption, and narrowing it
is how a script states its policy.