Tutorial 55: Agent Control Specification¶
Time: 20 minutes · Level: Intermediate · Prerequisites: Python 3.11+, a repository checkout, and
opaonPATH
What you will build¶
Build an ACS policy enforcement point for an email tool. The host sends ACS a complete snapshot before and after the tool call, then enforces the returned verdict.
Prefer a runnable repository example?
Start with examples/acs-email-tool in a repository checkout. It demonstrates the canonical Python host path through AgentControl, HostSession, and SnapshotBuilder without OPA. This tutorial adds a Rego policy to expose the native manifest and policy-input contract.
You will create:
- a flat ACS manifest
- a Rego policy bundle
- a Python host that calls
AgentControl.run_tool() - three outcomes:
allow,transform, anddeny
Engine dependency
AGT uses the published agent-control-spec engine. This tutorial uses AGT's compatibility host SDK in policy-engine/, not the standalone upstream Python API.
How ACS fits in AGT¶
ACS makes the policy decision. Your application or adapter enforces it.
Each evaluation includes its full context: the intervention point, tool call, tool result, labels, and metadata. ACS retains no session state between calls.
Step 1: Install the Python SDK from the repo¶
From the repository root:
The agent-control-specification distribution builds AGT's native host binding with maturin. Its Rust dependencies include the published ACS engine. It includes AgentControl, HostSession, and SnapshotBuilder for Python hosts. The upstream agent-control-spec Python package has a different API and cannot be substituted in this tutorial.
OPA-backed Rego examples require the opa CLI on PATH.
Step 2: Create the tutorial workspace¶
Step 3: Write the ACS manifest¶
Create manifest.yaml:
agent_control_specification_version: "0.4.0-alpha.1"
metadata:
name: acs-email-tutorial
policies:
email_policy:
type: rego
bundle: ./policy
query: data.agent_control_specification.email_policy.verdict
intervention_points:
pre_tool_call:
policy_target: $.tool_call.args
policy_target_kind: tool_args
tool_name_from: $.tool_call.name
policy:
id: email_policy
post_tool_call:
policy_target: $.tool_result.value
policy_target_kind: tool_result
tool_name_from: $.tool_call.name
policy:
id: email_policy
tools:
send_email:
type: Tool
id: send_email
clearance: internal
The manifest binds the same Rego policy at two intervention points:
| Intervention point | What ACS evaluates |
|---|---|
pre_tool_call | The outbound tool arguments before the email tool runs |
post_tool_call | The tool result before it returns to the caller |
Step 4: Write the Rego policy¶
Create policy/email_policy.rego:
package agent_control_specification.email_policy
import rego.v1
default verdict := {"decision": "allow"}
verdict := {
"decision": "deny",
"reason": "external_recipient_blocked",
"message": "Messages to external recipients are blocked."
} if {
input.intervention_point == "pre_tool_call"
input.tool.name == "send_email"
endswith(input.policy_target.value.to, "@example.net")
}
verdict := {
"decision": "transform",
"reason": "redact_tracking_token",
"message": "Tracking token redacted before tool execution.",
"transform": {
"path": "$target.body",
"value": "Your case is ready. Tracking token: [REDACTED]"
}
} if {
input.intervention_point == "pre_tool_call"
input.tool.name == "send_email"
contains(input.policy_target.value.body, "TRACK-")
}
The policy returns:
| Input | Verdict |
|---|---|
| Normal internal email | allow |
| Internal email with a tracking token | transform |
External @example.net recipient | deny |
Step 5: Write the host¶
Create run.py:
import asyncio
from pathlib import Path
from agent_control_specification import AgentControl, AgentControlBlocked
ROOT = Path(__file__).parent
async def send_email(args):
return {"sent": True, "to": args["to"], "body": args["body"]}
async def main():
control = AgentControl.from_path(str(ROOT / "manifest.yaml"))
allowed = await control.run_tool(
"send_email",
{"to": "customer@example.com", "body": "Your case is ready."},
send_email,
tool_call_id="email-1",
)
print(allowed.value)
transformed = await control.run_tool(
"send_email",
{
"to": "customer@example.com",
"body": "Your case is ready. Tracking token: TRACK-123",
},
send_email,
tool_call_id="email-2",
)
print(transformed.value)
try:
await control.run_tool(
"send_email",
{"to": "partner@example.net", "body": "Hello."},
send_email,
tool_call_id="email-3",
)
except AgentControlBlocked as exc:
print(exc.result.verdict.reason)
asyncio.run(main())
run_tool() evaluates pre_tool_call before execution and post_tool_call after the tool returns. The manifest must bind both points. An unbound point denies with runtime_error:intervention_point_unknown, and if post_tool_call is missing, run_tool() raises that block before it calls the tool, so no side effect happens for a call it would report as blocked.
Step 6: Run it¶
Expected output:
{'sent': True, 'to': 'customer@example.com', 'body': 'Your case is ready.'}
{'sent': True, 'to': 'customer@example.com', 'body': 'Your case is ready. Tracking token: [REDACTED]'}
external_recipient_blocked
The second call applies the transform verdict before execution. The third blocks the tool after a deny verdict.
Step 7: Inspect the policy input shape¶
ACS policies evaluate a canonical policy input. For pre_tool_call, the Rego policy receives fields like:
{
"intervention_point": "pre_tool_call",
"policy_target": {
"path": "$.tool_call.args",
"kind": "tool_args",
"value": {
"to": "customer@example.com",
"body": "Your case is ready."
}
},
"tool": {
"name": "send_email",
"id": "send_email",
"clearance": "internal"
}
}
The input may include more snapshot, annotation, and manifest-derived fields. Read only the canonical fields the policy needs; do not depend on host-local state.
Step 8: Try a fail-closed case¶
Change the manifest so tool_name_from points at a missing path:
Run the host again. ACS fails closed with a runtime error verdict instead of allowing the call.
Malformed manifests, missing paths, policy dispatcher failures, and invalid transform targets produce deny verdicts with reserved runtime-error reasons.
Next steps¶
- Run the framework-neutral AGT host example at
examples/acs-email-tool. - Inspect the ACS plus ATR annotator example.
- Read the Agent Control Specification package page.
- Compare Rego and Cedar in OPA / Rego / Cedar Policies.
- Add human review with Approval Workflows.
- Review policy composition with Policy Composition.