Budgets, tenants, egress & sandbox
Budgets, tenants, egress, and sandbox configuration.
Beyond rules: and tests:, security.yaml supports 24 more optional top-level blocks. This
page covers the ones most deployments reach for. Every key here is real and schema-backed —
nothing on this page is aspirational.
budgets: — call, token, and dollar caps
budgets:
session_calls: 50
daily_calls: 500
on_exceed: defer # "" | block | defer
token_session: 100000
cost:
session_max: 5.00
daily_max: 50.00
rates:
default: 0.002
anomaly:
action: defer # "" | fact | defer | deny
z_threshold: 3.0on_exceed picks whether a breached call/daily budget denies (deny.budget_exceeded, default)
or defers (defer.budget_exceeded). budgets.cost is a separate dollar cap estimated from
token rates — note cost_cap_usd is a rejected, not silently ignored, deprecated field:
setting it at all is a load error telling you to move to budgets.cost.session_max /
daily_max. budgets.anomaly runs a per-principal Z-score spend-anomaly check
(min_samples: 10, window: 1000 by default) that can fact (record only), defer, or deny
on an outlier spend sample.
egress: — host allow/block lists
egress:
allow: [api.stripe.com, hooks.slack.com]
block: [internal-metadata.local]Every entry must be a bare hostname — no scheme, no path:
security.yaml: egress.allow[0] "https://x" must be a bare hostname (no scheme or path).
sandbox: — OS-level filesystem containment
sandbox:
fs_read: ["./data"]
fs_write: ["./out"]
enforce: trueenforce is a secure-default *bool: unset or true means fail-closed — the child process
refuses to spawn if this host can't enforce a populated dimension. Setting enforce: false
explicitly is the documented, loudly-audited escape hatch, not a silent degrade.
Egress containment inside the OS sandbox is Unenforceable on both Linux and macOS — Landlock
v1 has no network primitive, and macOS's Seatbelt (SBPL) can only express allow-all/localhost,
not a per-host list. enforce: true with a populated egress profile refuses to spawn rather
than run unconfined or emit a broken profile. Egress control at the network layer is what
egress: (above) and denyWhenTainted (see Rules & effects) are
for — the sandbox block only governs the filesystem.
taint_denial: — the deny-on-taint posture
taint_denial:
posture: redact_permit # redact_permit (default) | strict
selected_by: prompt # prompt | flag | default_yes | default_non_interactive | default_no_answerraxit init writes this block on every run — it's the recorded, self-explanatory answer to
"does a tainted session's egress call actually get denied, or only redacted?" See
Taint-denial posture for the concept and
raxit init for exactly how init decides posture/selected_by (prompt text,
flags, and the full non-interactive decision table).
Both fields are a closed enum, validated at load:
posture— anything other thanredact_permit/strictfails to load:security.yaml: taint_denial.posture "x" is unsupported; use one of redact_permit, strict.selected_by— anything other than the five listed values fails the same way.posture: strictis fail-closed at the document level: it requires at least one rule elsewhere in the document carrying adenyWhenTaintedguard (see Rules & effects), or the whole file refuses to load:security.yaml: taint_denial: strict declared but no denyWhenTainted rule present (fail-closed). An inertstrictmarker with nothing that actually denies on taint is rejected rather than accepted as a no-op.
raxit init --taint-denial-posture strict satisfies that requirement itself by generating one
denyWhenTainted rule per discovered egress-capable tool — see
raxit init for the exact generated shape. Hand-authoring taint_denial: {posture: strict, ...} without a matching denyWhenTainted rule is the one way to trip this validation
yourself.
tenants: / tenants_enforce: — cross-tenant isolation
tenants:
acme: ["*.acme.example.com"]
globex: ["*.globex.example.com"]
tenants_enforce: trueMaps a tenant id to the host patterns it owns. No two patterns from different tenants may
overlap — a fail-closed ambiguity guard, since a host that could belong to either tenant can't
be safely routed. tenants_enforce: true (or leaving it unset, since nil also means
fail-closed) requires a discoverable cross-tenant forbid in the compiled policy bundle, or the
bundle refuses to load — a tenants: block with no enforcement path behind it is a load error,
not a silent no-op. tenants_enforce: false is the explicit, loud record-only opt-out.
classification: / classify: — taint labeling
classify:
"read_customer_*": [PII]
"stripe/*": [FINANCIAL]
classification:
provider: local
on_classifier_unavailable: regex_degrade # or fail_closedclassify: declares which resources carry which taint labels by glob. classification:
configures the classifier backend: provider: local (the sealed regex/Luhn floor) is the only
value fully implemented today — onnx-local and raxit-cloud are registered names that return
"not yet available (Phase E)" if selected. See
Taint & the lethal trifecta for what the local classifier actually
detects.
classification.default_when_unknown is the fail-safe for the gap between those two: a non-
empty tool result that neither classify: nor the classifier scan actually labels stays
untainted by default (today's behavior, and an explicit opt-out if left unset). Setting it names
one or more taint labels (from the closed PII/SECRET/FINANCIAL vocabulary) applied to that
result instead, so nothing unclassified slips through as clean:
classification:
default_when_unknown: [SECRET]An empty tool result is never tainted by this, regardless of the setting — there's nothing in it to classify.
credentials: — per-agent credential scoping
credentials:
- agent: default-agent
operation: read
resources: ["s3://reports/*"]operation is one of read, write, invoke. Once this block is present at all, access is
deny-by-default per (agent, operation) pair — an agent with no matching grant gets nothing.
At runtime, a permit on a call this block covers mints a short-lived (~30s default, broker-
configurable) credential scoped to that grant, hands it to the call through the request's own
scope (never a shared/global one), and releases it once the call returns. The credential value
itself is never logged, written to disk, or included in the audit record — audit only ever
records that a credential was brokered and its source/scope, not the secret.
warrants: — standing-grant envelopes
warrants:
templates:
- name: refund-approval
tool: "stripe/refund"
max_ttl: 1h
max_uses: 5This block only declares the shape a warrant may take — it is never an issuance path. An
attacker who can write security.yaml still cannot self-issue a warrant this way; warrants are
minted separately, through approval. See
Approvals & warrants, including the caveat that warrants are
in-memory only today (no persistence across a daemon restart).
limits: — rate limiting, loop detection, circuit breaking
limits:
rate_max: 20
rate_window: 1m
loop_max: 5
breaker:
failure_threshold: 5
recovery: 30s
on_open: deny # deny | deferlimits.breaker trips on repeated tool-execution failures, not policy denies — a founder-
ratified invariant: a raxit policy deny never counts toward tripping the breaker.
phases: — workflow-scoped tool gating
phases:
initial: research
list:
- id: research
allowed_tools: [search_docs, read_file]
allowed_transitions: [execute]
- id: execute
allowed_tools: [send_email]
max_duration: 10mRestricts which tools are callable in each named phase of a session, with an optional
max_duration per phase. The full transition graph is validated at load time (unreferenced
phase ids, unreachable transitions all fail to load).
import: — versioned policy imports
import:
- "policies/baseline-pii@1.2.0"Every entry must be <kind>/<name>@<version>, where kind is policies or frameworks —
@latest is explicitly rejected, and a missing pin fails load:
security.yaml: import[0] "policies/baseline-pii": missing version pin (use @<version>).
audit: — signing requirement
audit:
require_signed: trueStrengthens (never weakens) the CLI's own --require-signed flag. See
Audit & the DPR chain for what "signed" does and doesn't mean today.
Everything else, briefly
The remaining blocks exist and are schema-validated but are more specialized — consult
raxit schema for the exact live shape when you need one:
compensation:— declares an "undo" tool for a permitted-then-output-blocked call.context_guard:— required context keys per resource, clock-skew and replay-sequence checks.delegation:— max chain depth/TTL and allowed issuers for delegated calls.drift:/session_drift:/intent:— semantic drift and intent-baseline scoring; all observe-first by default (enforce: falserecords and scores but never denies on its own).input_scan:— fact-only prompt-injection signal (context.injection_suspected); never autonomously denies, an operator-authoredforbidhas to key off it.mcp:— MCP tool fingerprint pinning/TOFU and description-scan-for-hidden-instructions.multiagent:— pool budgets shared across a set of principals (each principal in at most one pool).otlp:— OpenTelemetry export config (endpoint/protocol/service_name/insecure) with two independent, authoritative enables:enabledturns on decision-trace export, andmetricsturns on decision-metrics export (per-decision counters + a latency histogram). Both reuse the same block's endpoint/protocol, and both are reviewablesecurity.yamlkeys that show up inraxit plan. TheRAXIT_OTEL_METRICS_ENDPOINT/_PROTOCOL/_INSECUREenv vars only override where metrics point — theRAXIT_OTEL_METRICSenv var can never enable export (a truthy value withotlp.metricsoff is ignored and logged loudly at startup). See Observability: OTel decision metrics.postcondition:— output-scanning rules (this is the subsystem wheretruncatedoes exist, as anaction, distinct fromrules[].modify).redact:— audited-args redaction only; never affects the decision itself.webhooks:— outbound notifications on decision/approval events (genericorslackkind).

