Run an approved command
Build a one-program catalogue for your own tool, feed it stdin, and see both refusal paths.
Install Cuprum, run one approved command, and read the result it hands back.
01 — Install
Cuprum needs Python 3.12 or later and nothing else at run time. The pure-Python wheel needs no Rust toolchain.
Everything on these pages describes Cuprum 0.2.0-beta2,
a beta release on PyPI. Most of what is shown here is new since
0.1.0, including RunOutputOptions,
ScopeConfig, and
ProgramCatalogue.from_programs(). Install it by its
exact version:
pip install cuprum==0.2.0b2
uv add cuprum==0.2.0b2
02 — First command
A builder comes from the catalogue, a call makes an exact argv, and running it returns a CommandResult.
from cuprum import ECHO, sh
cmd = sh.make(ECHO)("-n", "hello, cuprum!")
print(cmd.argv_with_program)
result = cmd.run_sync()
print(result.ok, result.exit_code, repr(result.stdout))
('echo', '-n', 'hello, cuprum!')
True 0 'hello, cuprum!'
ECHO is one of the programs in Cuprum's default catalogue,
alongside LS, GIT, RSYNC,
TAR, and DOC_TOOL. Positional arguments are
stringified in order; keyword arguments become
--flag=value; None is rejected. With no
scope established, every catalogued program may run — the permissive
default eases adoption, and the policy section
shows how to narrow it.
03 — Async
The same command awaits in async code. RunOutputOptions decides whether output is captured, echoed, or both.
import asyncio
from cuprum import ECHO, RunOutputOptions, sh
async def main() -> None:
# Pick a known fitting from the catalogue.
echo = sh.make(ECHO)
cmd = echo("-n", "hello, cuprum!")
result = await cmd.run(output=RunOutputOptions(echo=True))
if result.ok:
print()
print(f"exit {result.exit_code}, captured {result.stdout!r}")
asyncio.run(main())
hello, cuprum! exit 0, captured 'hello, cuprum!'
The first line is the echo: echo=True tees the child's
output to your terminal as it arrives. The second is the capture,
which still happens. capture=True, echo=False is the
default; echo_stdout and echo_stderr
choose per stream.
Cancelling the awaiting task sends SIGTERM, waits
half a second, then sends SIGKILL and reaps the child.
04 — Policy
A scope says which catalogued programs may run inside a block of code. Anything else is refused before it starts.
from cuprum import ECHO, LS, ForbiddenProgramError, ScopeConfig, scoped, sh
with scoped(ScopeConfig(allowlist=frozenset([ECHO]))):
print(sh.make(ECHO)("-n", "allowed").run_sync().stdout)
try:
sh.make(LS)().run_sync()
except ForbiddenProgramError as exc:
print(f"refused: {exc}")
allowed refused: Program 'ls' is not allowed in the current context
Program ls denied by context allowlist restricted_state=True
Scopes nest, and a nested scope can only narrow its parent: its
allowlist is intersected with the one it inherits. The scope lives in
a ContextVar, so it follows the current thread and async
task rather than the whole process, and it is restored when the block
exits, even on an exception.
A program that is not in the catalogue at all is refused earlier, when the builder is made:
from cuprum import Program, UnknownProgramError, sh
try:
sh.make(Program("rm"))
except UnknownProgramError as exc:
print(f"refused: {exc}")
refused: Program 'rm' is not in the catalogue allowlist
05 — Next
Build a one-program catalogue for your own tool, feed it stdin, and see both refusal paths.
Compose stages with |, tee the output, and read which stage failed when fail-fast stops the rest.
The lifecycle of one command, the pipeline plumbing, and the invariants that hold throughout.
The complete reference, on GitHub, including telemetry adapters, timeouts, and concurrency.