Staso Docs
Guard

Write a Python rule

Use a Python rule when your application has a precise condition that Guard should check before a tool executes. Open Rules, create a custom rule, and select Python rule. Python execution requires the isolated runner to be enabled for your workspace.

Run allowance

All workspaces in the organization share its Python-run allowance. Traces, Python runs, and LLM-judge evaluations have independent limits and counters. See current pricing and allowances for your plan's included usage and saved-rule limits.

One execution of one Python rule uses one run. Three rules executed for one tool call use three runs. Test code and Test connection also consume a run when they execute Python. A timeout or runtime error after dispatch counts. Rejected code and requests rejected before execution use no runs.

Creating or saving rules does not consume runs. Your plan's saved-rule count limits still apply. Home and Usage show saved rules and run usage separately. See pricing and limits for billing-period resets and capacity changes.

Start with a local condition

MAX_RECORDS = 100

def evaluate(payload):
    args = payload["tool_input"]
    limit = args.get("limit")
    if payload["tool_name"] == "export_records" and isinstance(limit, int):
        if limit > MAX_RECORDS:
            return {"verdict": "block", "reason": "Request at most 100 records."}
    return {"verdict": "allow", "reason": ""}

Return {"verdict": "allow" | "block", "reason": "..."}. Existing boolean returns remain compatible: True matches the rule and False allows. Prefer an explicit verdict to make the intent clear. An audit policy records a block verdict as a would-block finding and lets the tool proceed. An enforce policy can stop it.

Functions, constants, loops, comprehensions, and basic data manipulation are supported. Filesystem access, imports, subprocesses, dynamic code execution, raw networking, and access to credentials are unavailable.

Input reference

evaluate(payload) receives a JSON object:

FieldMeaning
payload_versionVersion of the input contract, currently 1.
tool_nameName of the current proposed tool.
tool_inputCaptured current arguments; check availability before relying on missing fields.
agent_id, agent_name, environmentAvailable agent and environment identifiers.
trace_id, session_idAvailable trace and conversation identifiers.
tool_call_evidenceExplicit argument parsing status and current tool schema availability.
context_availabilityAvailability of tool input, schema, history, and output.

Choose Current tool call when your rule only needs this proposal. Choose Available history for bounded conversation_history and prior_spans. History is incomplete; absence of an earlier approval does not prove that no approval exists. The current tool's output is unavailable because it has not executed.

Call your decision service

Create a workspace connection with its HTTPS endpoint, method, and authentication. Credentials are write-only. Select that connection in the rule editor and use its alias:

def evaluate(payload):
    decision = http.post(
        "approval_service",
        json={
            "tool_name": payload["tool_name"],
            "tool_input": payload["tool_input"],
        },
    ).json()

    if not isinstance(decision.get("allowed"), bool):
        raise ValueError("Expected an 'allowed' boolean.")

    return {
        "verdict": "allow" if decision["allowed"] else "block",
        "reason": decision.get("reason", ""),
    }

Replace approval_service with the selected alias. The connection supplies the endpoint and authentication. Only the payload fields selected in the request reach your service. Keep URLs, authorization headers, and secrets out of rule code.

GET and POST connections are supported. The response must be a JSON object. Private-network destinations and redirects are rejected. Use a service you trust with the connection credential and the sample or tool data you send it.

Test the complete callback path

Use Staso's decision fixture to exercise the same HTTPS, authentication, sandbox, policy, and Codex hook path as your own service.

Create a Guard connection with these values:

FieldValue
NameStaso decision test
Aliasstaso_test_api
HTTPS endpointhttps://api.staso.ai/v1/guard/test-decision
MethodPOST
AuthenticationBearer token
CredentialA current Staso access token from your account

Staso access tokens expire after 15 minutes. Replace the saved credential if the test starts returning 401, then save the rule again because credential rotation invalidates its approval.

Select staso_test_api in a new Current tool call Python rule, keep the timeout at 1 second, and paste this code:

def evaluate(payload):
    args = payload.get("tool_input", {})
    command = args.get("cmd") or args.get("command") or ""
    response_mode = "json"
    delay_ms = 0

    if "STASO_TEST_ERROR" in command:
        response_mode = "http_error"
    elif "STASO_TEST_INVALID" in command:
        response_mode = "invalid_json"
    elif "STASO_TEST_SHAPE" in command:
        response_mode = "invalid_shape"
    elif "STASO_TEST_OVERSIZED" in command:
        response_mode = "oversized_json"
    elif "STASO_TEST_TIMEOUT" in command:
        delay_ms = 2000

    decision = http.post(
        "staso_test_api",
        json={
            "decision": "block" if "STASO_TEST_BLOCK" in command else "allow",
            "response_mode": response_mode,
            "delay_ms": delay_ms,
            "data": {
                "tool_name": payload.get("tool_name"),
                "tool_input": args,
            },
        },
    ).json()

    return {
        "verdict": "allow" if decision["allowed"] else "block",
        "reason": decision["reason"],
    }

Use this sample in the editor before saving:

{
  "payload_version": 1,
  "tool_name": "exec_command",
  "tool_input": {
    "cmd": "printf 'STASO_TEST_BLOCK\\n'"
  },
  "agent_id": "codex",
  "agent_name": "codex",
  "environment": "development"
}

Choose Test connection to send this sample through the live endpoint. The result should be block. Save the rule and attach it to an audit policy first. In Codex, ask it to run one of these harmless commands:

Prompt markerExpected result
Run exactly: printf 'STASO_TEST_ALLOW\n'Callback allows the tool.
Run exactly: printf 'STASO_TEST_BLOCK\n'Audit records would-block; enforce stops the command.
Run exactly: printf 'STASO_TEST_ERROR\n'Callback returns HTTP 503; Guard applies the policy's failure mode.
Run exactly: printf 'STASO_TEST_INVALID\n'Callback returns invalid JSON; Guard records an unavailable evaluation.
Run exactly: printf 'STASO_TEST_SHAPE\n'Callback returns a JSON array; Guard rejects the response shape.
Run exactly: printf 'STASO_TEST_OVERSIZED\n'Callback exceeds 64 KiB; Guard rejects the response size.
Run exactly: printf 'STASO_TEST_TIMEOUT\n'Callback waits 2 seconds; the 1-second rule timeout wins.

The endpoint accepts decision (allow or block), response_mode (json, http_error, invalid_json, invalid_shape, or oversized_json), delay_ms (0 through 5000), reason, and an arbitrary data object. It does not store the request body.

Test before activation

  1. Choose Test code with a sample payload and mocked connection responses. This does not call your endpoint.
  2. Choose Test connection only when you want the sample data sent to the selected live endpoint.
  3. Save the rule after validation succeeds. Saving never calls your endpoint.
  4. Attach the saved rule to an audit policy under Policies and review its findings.

Code, payload, connection, or timeout edits invalidate an earlier test result. A failed test or save preserves your code. If policy attachment fails after a successful save, retry attachment using the saved rule.

The timeout defaults to 1 second and accepts 100 ms through 5 seconds. Guard's remaining request deadline can shorten it. An evaluation can make at most three HTTP calls; requests and responses are bounded. Slow services, non-success responses, invalid result shapes, and timeouts produce evaluation errors, not policy matches.

Admission and failures

Saving checks source structure and supported capabilities, then performs a safety review. Rejected code or an unavailable reviewer cannot approve a new revision. A failed edit preserves the previous approved revision.

Runtime isolation remains active independently of that review. If the runner is unavailable, Guard reports an unavailable evaluation and applies your failure policy. It does not run the rule inside the application server.

If a Guard request needs more Python runs than your organization has remaining, the whole request is rejected before any rules execute. The SDK reports an unavailable decision with failure_kind="capacity_exhausted" and applies the same failure policy: fail-open allows the action, and fail-closed blocks it. Add Team capacity or wait for the reset shown on Usage. You can keep creating or saving rules within your plan's saved-rule limits.