Budgets + Sandboxes (slice 7)¶
Module: sdust_runtime::budget
Spec: §16 Budgets and Sandboxing
Budget shape¶
pub struct Budget {
pub cpu: Option<Duration>, // spec: cpu 150ms
pub wall: Option<Duration>, // spec: wall 2s
pub mem_bytes: Option<u64>, // spec: mem 128MiB / memory = ...
pub mailbox: Option<u64>, // spec: mb 1k / mailbox = 1024
pub spawned: Option<u64>, // spec: spawned tasks
pub hosts: Option<Vec<String>>, // spec: net = ["api.example.com:443"]
pub read_paths: Option<Vec<String>>, // spec: fs.read = ["/models", ...]
pub write_paths: Option<Vec<String>>, // spec: fs.write = ["/tmp/out"]
}
A None field means unlimited (or no allowlist). Defaults are
all None so an unconfigured runtime never gates anything.
BudgetTracker¶
BudgetTracker wraps a Budget with atomic counters:
cpu_ns: AtomicU64— accumulated per-turn CPU durationmem: AtomicU64— synthetic byte cost (A37: 1 B per primitive, 24 B header per allocation, etc.; real arena allocator lands in slice 8)spawned: AtomicU64— child agent / task countstart: Instant— measured wall clock baseline
Methods¶
| Method | Returns | Notes |
|---|---|---|
record_cpu(d) |
() | adds to cpu_ns |
record_mem(b) |
() | adds to mem |
record_spawn() |
Result<(), BudgetBreach> |
bumps + checks |
check() |
Result<(), BudgetBreach> |
checks CPU/wall/mem |
check_mailbox_depth(d) |
Result<(), BudgetBreach> |
check before send |
check_host(h) |
Result<(), BudgetBreach> |
allowlist check |
check_read_path(p) |
Result<(), BudgetBreach> |
prefix-match allowlist |
check_write_path(p) |
Result<(), BudgetBreach> |
prefix-match allowlist |
Breach → RuntimeError¶
BudgetBreach::Cpu(_) → RuntimeError::BudgetExceeded("cpu ...") → MT5009
BudgetBreach::Wall(_) → RuntimeError::BudgetExceeded("wall ...") → MT5009
BudgetBreach::Mem(_) → RuntimeError::BudgetExceeded("mem ...") → MT5009
BudgetBreach::Mailbox → RuntimeError::BudgetExceeded("mailbox") → MT5009
BudgetBreach::Spawned → RuntimeError::BudgetExceeded("spawned") → MT5009
BudgetBreach::Host → RuntimeError::CapabilityOutsideSandbox → MT5015
BudgetBreach::Path → RuntimeError::CapabilityOutsideSandbox → MT5015
A40 retains spec §16.2's "Budget violations return typed errors or
trigger supervisor policy": the supervisor's From<RuntimeError> for
ChildFailure routes budget breaches to ChildFailure::Budget so
the supervisor's policy (e.g. escalate) applies.
Sandbox enforcement¶
Slice 5 lowered sandbox Name with { entries } { body } to a
HirItem::Sandbox that the slice-6 interpreter treated as metadata.
Slice 7 ships runtime enforcement at the BudgetTracker layer:
fs.read = [...]populatesread_paths.fs.write = [...]populateswrite_paths.net = [...]populateshosts.cpu = ...,wall = ...,memory = ...,mailbox = ...populate the corresponding scalar fields.
Per A43, top-level sandbox items construct a child runtime with the declared budget caps; capability calls inside the body are gated. Nested sandboxes intersect allowlists.
Slice-7 memory budget approximation¶
Per A37: without a real arena allocator (slice 8 work), mem_bytes
is approximate. Slice 7 records bytes on explicit record_mem(n)
calls only; the MtyIR interpreter does not auto-count allocations. This
makes the check pessimistic — programs may breach in production that
slice-7 tests miss. Slice 8 wires the real allocator.
Tests¶
tests/budget_enforcement.rs— CPU/wall/mem/mailbox/spawn + host/path allowlists.tests/sandbox_enforcement.rs— host + read/write path independence.
See also¶
docs/internals/runtime.mddocs/internals/supervisors.mddocs/spec/v0.1-amendments.md— A37, A43