Guard direct tool calls
Use st.guard() when your code dispatches a tool directly instead of receiving a proposal from a patched provider response.
import staso as st
tool_input = {"record_id": "r_42"}
decision = st.guard("delete_record", tool_input)
if decision.action == "block":
raise RuntimeError(decision.reason)
delete_record(**tool_input)@st.tool records the function execution. It does not run this check automatically.
Launch parameters
st.guard(
tool_name: str,
tool_input: dict[str, Any],
*,
context: dict[str, Any] | None = None,
decision_id: str | UUID | None = None,
timeout: float = 10.0,
fail_closed: bool | None = None,
) -> GuardResult| Parameter | Default | Description |
|---|---|---|
tool_name | required | Tool name evaluated by configured policies. |
tool_input | required | Proposed arguments. |
context | None | Explicit trace, span, session, agent, environment, or workspace identifiers. |
decision_id | generated UUID | Reuse an explicit UUID only when retrying the same logical decision. |
timeout | 10.0 | Guard request timeout in seconds. |
fail_closed | None | Uses STASO_GUARD_FAIL_CLOSED; false when unset. |
Result handling
For the launch path, handle two wire outcomes:
allow: no configured rule blocked the proposal. Continue.block: stop the action and usereasonplusrules_triggeredfor recovery or logging.
Audit findings keep the top-level result at allow and appear in the per-rule results list as failed audit checks.
Every result also reports whether Guard made a policy decision:
| Field | Meaning |
|---|---|
decision_id | Unique decision identity echoed by Guard and safe to use for correlation. |
decision_source | Auditable origin label returned by Guard, such as manual, a native provider, or the generic sdk path. |
decision_available | True for a policy decision; False for explicit degradation. |
evaluation_status | evaluated, skipped, abstained, or unavailable. Per-rule results also carry this field. |
failure_kind | Stable failure category, or None when a decision was available. |
failure_mode | fail_open or fail_closed, the policy applied to degradation. |
retryable | Whether retrying later can reasonably succeed. |
Use each rule result's is_finding property to select evaluated policy matches. Skipped, abstained, and unavailable results are not findings. Non-finding rule results keep action="allow"; an aggregate unavailable decision can still block under fail-closed.
Stable failure kinds cover missing or rejected keys, inactive plans, forbidden access, rate limits, malformed responses, deadlines, backend or network outages, and unsupported server actions. Do not parse reason to classify a failure.
capacity_exhausted means the organization lacks enough remaining allowance for the request. If Python-run capacity is insufficient, Guard rejects the whole request before any rules execute and returns an unavailable decision. The SDK applies the configured fail-open or fail-closed behavior. Check the reset date and remaining capacity on Usage. See pricing and limits.
Each call generates a new decision_id, including identical checks in the same
trace. If your application retries one logical check after an uncertain
transport outcome, pass the same explicit UUID so backend persistence remains
idempotent. Provider and CLI integrations reuse the decision identity when the
same genuine tool invocation is observed again in the same trace. Distinct tool
calls keep distinct identities even when their arguments are identical. Calls
without a genuine invocation identifier receive fresh identities.
Automatic trace linkage
A check inside @st.agent, @st.tool, or a manual span automatically carries the active trace and span identifiers. You can add non-empty context such as environment or workspace fields:
@st.agent(name="cleanup-agent")
def cleanup(record_id: str) -> None:
decision = st.guard(
"delete_record",
{"record_id": record_id},
context={"environment": "production"},
)
if decision.action == "block":
raise RuntimeError(decision.reason)
delete_record(record_id)Active trace, span, and session identifiers remain authoritative if a stale explicit value is supplied. Empty context values do not erase client or active defaults.
Evidence and capture
Guard requests carry payload_version=1. The context_availability map inside context describes available tool input, tool schema, conversation history, and tool output. The current tool's output is unavailable because Guard runs before execution. Available history is bounded and explicitly partial.
Supported provider adapters attach context.tool_call_evidence with the argument parsing status and the exact current-request schema, when available. Static checks use this evidence. Manual calls do not infer parsing failures from payload keys or Python annotations.
Capture settings apply before Guard transmission. Disabling tool-argument capture sends no proposed arguments to Guard; disabling schema capture sends no schema. History respects request, system-prompt, argument, and output controls. Rules that need missing evidence skip or abstain.
Fail-open and fail-closed
An unavailable decision returns allow by default so authentication, service, response, or deadline failures do not stop the host application.
Use fail-closed for an action that must stop when Guard cannot return a decision:
decision = st.guard(
"delete_user",
{"user_id": "u_42"},
fail_closed=True,
)Or set it for the process:
export STASO_GUARD_FAIL_CLOSED=trueWith fail-closed enabled, an unavailable decision returns block. Check decision_available to distinguish that degradation from a policy block. Patched provider integrations surface either effective block as st.GuardBlocked.
Provider blocks
try:
response = client.chat.completions.create(...)
except st.GuardBlocked as blocked:
for item in blocked.blocked_tools:
print(item.tool_name, item.result.reason)Provider tool proposals are evaluated concurrently under one batch deadline, with four evaluations active by default. STASO_GUARD_BATCH_TIMEOUT and STASO_GUARD_MAX_CONCURRENCY configure the integration defaults. Results and blocked_tools stay in provider input order.