Sectioned profile schema and Sandbox API consolidation#42
Merged
Conversation
Signed-off-by: Cong Wang <cwang@multikernel.io>
Phase 2 of the sectioned-policy-schema effort. After Phase 1 introduced
the sectioned profile parser (translating into the legacy field names),
this phase aligns the rest of the project — Policy struct, CLI flags,
FFI, and Python bindings — with the schema's intent-shaped vocabulary,
and reshapes the Policy/PolicyBuilder/CLI relationship so a single
canonical input definition feeds both TOML profiles and clap argv
parsing.
Field and flag renames:
- Policy::https_ca → Policy::http_ca (CLI: --https-ca → --http-ca)
- Policy::https_key → Policy::http_key (CLI: --https-key → --http-key)
- Policy::block_syscalls → Policy::extra_deny_syscalls
(CLI: --block-syscall → --extra-deny-syscall)
- Policy::allow_sysv_ipc removed; folded into
Policy::extra_allow_syscalls = ["sysv_ipc"]
(CLI: --allow-sysv-ipc → --extra-allow-syscall)
Signed-off-by: Cong Wang <cwang@multikernel.io>
Two related restructures landed together. The first renames the data
struct that describes one sandboxed run from `Policy` to `Sandbox`; the
second merges the formerly-separate runtime type (`SandboxProcess`)
into `Sandbox` so there is exactly one sandbox concept in both Python
and Rust.
The trigger for the rename was that "Policy" had become overloaded —
the struct mixed policy-shaped fields (filesystem allows, network
rules, syscall allow/deny) with config-shaped fields (TLS keys, GPU
IDs, host paths, determinism knobs). "Sandbox" describes the contents
honestly and matches the schema's role as a complete spec for one
sandboxed run. The schema's TOML format never said "Policy" so it does
not change; CLI flag names also do not change.
Type renames:
- Policy → Sandbox
- PolicyBuilder → SandboxBuilder
- ConfinePolicy → Confinement
- ConfinePolicyBuilder → ConfinementBuilder
- PolicyError → SandboxError
(config-validation errors)
- pre-existing runtime SandboxError → SandboxProcessError
(process-runtime errors; later subsumed — see below)
Sandbox merge — Python:
The Python `SandboxProcess` class is deleted entirely. All of its
methods (run, run_interactive, dry_run, cmd, reduce, start, pause,
resume, kill, wait, the context-manager protocol, the `pid` /
`is_running` properties) move onto `Sandbox`. The four runtime kwargs
(`name`, `policy_fn`, `init_fn`, `work_fn`) join the constructor
alongside config kwargs. `Sandbox` is no longer `frozen=True` because
it now carries optional runtime state initialized in `__post_init__`.
The natural Python idiom is now one-class:
sandbox = Sandbox(fs_readable=["/usr"], extra_allow_syscalls=["sysv_ipc"])
result = sandbox.run(["python3", "task.py"])
Sandbox merge — Rust:
The runtime struct that was `SandboxProcess` is collapsed into
`Sandbox`. Runtime state lives in a private `Runtime` struct held as
`Option<Box<Runtime>>` on `Sandbox` — heap-allocated only when a
lifecycle method is called, freed on completion. The `crate::process`
module is reduced to just the nesting-detection globals (`CONFINED`,
`is_nested`); the `SandboxProcess` type is gone from the public API.
Construction is `Sandbox::builder()...build()?` — the `Sandbox::new`
family of constructors that took `&Sandbox` and cloned-into-runtime is
deleted; that pattern recreated the two-class shape the merge was
meant to eliminate. Lifecycle methods (run, spawn, start, pause,
resume, kill, wait, dry_run, fork, reduce, checkpoint) take `&mut
self` and lazily initialize the `Runtime` via a private
`ensure_runtime()` helper. Validation that previously lived on
`SandboxProcess::new` (name format, length) moves to `set_name` /
`with_name` (fail-fast) and `ensure_runtime` (final guard).
Builder gains methods for the runtime kwargs:
SandboxBuilder::name(s)
SandboxBuilder::init_fn(f)
SandboxBuilder::work_fn(f)
SandboxBuilder::policy_fn(f)
`Sandbox::with_name(self, name) -> Self` is added for pipeline-style
fan-out where one config feeds many runs:
let template = Sandbox::builder()...build()?;
let mut s1 = template.clone().with_name("worker-1");
let mut s2 = template.clone().with_name("worker-2");
`Clone` clones config + name + work_fn + policy_fn (Arc-wrapped); it
drops `init_fn` (FnOnce can't clone) and always sets `runtime: None`
on the clone so multiple Sandboxes never share runtime state.
Signed-off-by: Cong Wang <cwang@multikernel.io>
After SandboxProcess was merged into Sandbox, the runtime error type and its SandlockError variant still bore the deleted struct's name. Rename for accuracy: - SandboxProcessError → SandboxRuntimeError - SandlockError::Process → SandlockError::Runtime The split between config-validation errors (SandboxError) and process-runtime errors (SandboxRuntimeError) is preserved — the two have different audiences (user-actionable vs environmental) and recovery paths. The rename only fixes the name's reference to a deleted struct. Signed-off-by: Cong Wang <cwang@multikernel.io>
Three correctness fixes that belong together:
1. --no-supervisor base block now transfers extra_deny_syscalls,
clean_env, env, no_coredump, no_huge_pages, no_randomize_memory,
uid, cwd, max_memory, max_processes, max_open_files, random_seed
(every field enforceable without the supervisor).
2. supervisor base block now transfers extra_deny_syscalls,
max_open_files, max_disk, env, http_ca, http_key, chroot,
fs_storage, fs_isolation, fs_mount, on_exit, on_error,
deterministic_dirs, no_randomize_memory, no_huge_pages,
no_coredump, time_start, port_remap, uid, gpu_devices, cpu_cores.
3. sandlock.h declarations of sandlock_run / sandlock_run_interactive
gain the `name` parameter to match the Rust FFI signatures
(NULL auto-generates "sandbox-{pid}").
Without (1) and (2), profile-supplied policy was silently dropped
between TOML parse and Sandbox build. Without (3), C consumers
hit a link/ABI mismatch.
Signed-off-by: Cong Wang <cwang@multikernel.io>
The Python profile loader and the Rust profile parser had diverged: Python accepted a flat-keyed TOML format (`fs_readable = [...]` at the top level) while Rust required the sectioned schema (`[program]`, `[filesystem]`, `[network]`, …). The same `.toml` file would load in one and fail in the other. Signed-off-by: Cong Wang <cwang@multikernel.io>
Signed-off-by: Cong Wang <cwang@multikernel.io>
Signed-off-by: Cong Wang <cwang@multikernel.io>
Signed-off-by: Cong Wang <cwang@multikernel.io>
Signed-off-by: Cong Wang <cwang@multikernel.io>
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.
Summary
Replaces sandlock's flat-keyed TOML profile format with a sectioned schema, then realigns the rest of the project — Rust types, CLI flags, FFI accessors, Python bindings, and READMEs — so the API has one type per concept and the vocabulary matches the schema. Pre-1.0 hard break: existing user profiles and downstream callers using the old field/flag/type names need to be rewritten.
Four commits, one per phase:
ab4b0c3Phase 1 — sectioned profile parser. NewProfileInput+ 8 section structs ([config],[determinism],[program],[filesystem],[network],[http],[syscalls],[limits]). Newparse_inputtranslator producing(Sandbox, ProgramSpec).parse_profile/load_profilerewritten as thin wrappers; legacy flat-keyed parser deleted. CLI gains--profile-fileand honors[program].exec/argsfrom a profile when no trailing positional command is given (trailing command overrides).10b1508Phase 2 — vocabulary alignment + canonical CLI input. Renameshttps_ca/https_key/block_syscalls→http_ca/http_key/extra_deny_syscalls; dropsallow_sysv_ipc(folded into a newextra_allow_syscalls: Vec<String>plus anallows_sysv_ipc()helper). CLI flags renamed to match (--http-ca,--http-key,--extra-deny-syscall, new--extra-allow-syscall). Newvalidate()owns cross-section invariants;build_unchecked()runs per-field validation andbuild()collapses tobuild_unchecked()? + validate()?. The builder carries a feature-gatedclap::Argsderive; the CLI'sRunArgsflattens it via#[clap(flatten)], eliminating the duplicated parallel args struct.637a1dePhase 3 — renamePolicy→Sandboxand mergeSandboxProcessintoSandbox. The data struct was overloaded — it mixed policy-shaped fields with config-shaped ones (TLS keys, GPU IDs, host paths, determinism knobs);Sandboxdescribes the contents honestly and matches the schema's role as a complete spec for one sandboxed run. The runtime type (SandboxProcess) is folded intoSandboxitself: runtime state lives in a privateRuntimestruct held asOption<Box<Runtime>>onSandbox, heap-allocated only when a lifecycle method runs.crate::processis reduced to nesting-detection globals;SandboxProcessis gone from the public API in both Rust and Python. Construction isSandbox::builder()…build()?; lifecycle methods (run,spawn,start,pause,resume,kill,wait,dry_run,fork,reduce,checkpoint) take&mut selfand lazily initialize the runtime viaensure_runtime().Sandbox::with_name(self, name) -> Selfsupports pipeline-style fan-out (template.clone().with_name("worker-1")). PythonSandboxabsorbs all the runtime methods so user code issandbox.run(["python3", "task.py"]).ff30e3fPhase 3 follow-up — error rename.SandboxProcessError→SandboxRuntimeError;SandlockError::Process→SandlockError::Runtime. The split between config-validation and process-runtime errors is preserved (different audiences, different recovery paths); the rename only fixes the name's reference to a deleted struct.The TOML schema format itself is unchanged across all four commits; section names and field names were chosen at Phase 1 and stay stable. Public CLI flag names are unchanged from Phase 2 onward.
Test Plan
cargo test --workspacepasses — 514 tests total (278 core unit + 212 integration + 17 CLI + 5 profile integration + 2 sandbox-validate).pip install -e python/ && pytest python/tests/passes — 228 Python tests.cargo check --workspace --all-targetsis clean.sandlock run --profile-file <profile>.tomlexits 0 with a representative profile.sandlock run --profile-file <profile>.toml -- /bin/echo overrideruns the trailing command, not the profile's[program].exec.--http-ca,--http-key,--extra-deny-syscall,--extra-allow-syscall sysv_ipc.--https-ca,--https-key,--block-syscall,--allow-sysv-ipcproduce clap "unexpected argument" errors.[filesystem].isolation = "overlayfs"without[config].workdirreturns the documented error mentioningworkdir.from sandlock import Sandbox; sandbox.run(["echo", "hi"])works without ever referencingSandboxProcess. In Rust,Sandbox::builder()...name("x").build()?.run(&cmd)?works without referencingSandbox::newor any second type.nm -D --defined-only target/release/libsandlock_ffi.so | grep ' T sandlock_'declares onlysandlock_sandbox_*(nosandlock_policy_*, nosandlock_process_*); the public C header agrees.🤖 Generated with Claude Code