raxit
security.yaml reference

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.0

on_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: true

enforce 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_answer

raxit 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 than redact_permit/strict fails 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: strict is fail-closed at the document level: it requires at least one rule elsewhere in the document carrying a denyWhenTainted guard (see Rules & effects), or the whole file refuses to load: security.yaml: taint_denial: strict declared but no denyWhenTainted rule present (fail-closed). An inert strict marker 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: true

Maps 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_closed

classify: 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: 5

This 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 | defer

limits.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: 10m

Restricts 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: true

Strengthens (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: false records and scores but never denies on its own).
  • input_scan: — fact-only prompt-injection signal (context.injection_suspected); never autonomously denies, an operator-authored forbid has 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: enabled turns on decision-trace export, and metrics turns on decision-metrics export (per-decision counters + a latency histogram). Both reuse the same block's endpoint/protocol, and both are reviewable security.yaml keys that show up in raxit plan. The RAXIT_OTEL_METRICS_ENDPOINT/_PROTOCOL/_INSECURE env vars only override where metrics point — the RAXIT_OTEL_METRICS env var can never enable export (a truthy value with otlp.metrics off is ignored and logged loudly at startup). See Observability: OTel decision metrics.
  • postcondition: — output-scanning rules (this is the subsystem where truncate does exist, as an action, distinct from rules[].modify).
  • redact: — audited-args redaction only; never affects the decision itself.
  • webhooks: — outbound notifications on decision/approval events (generic or slack kind).

On this page