Example: redaction transform¶
A transform rewrites the target before the action runs. The tool receives the redacted value; the audit record proves the rewrite happened without containing the secret. Runnable as written.
"""Transform: redact a field before the tool runs; the record carries
the path, never the value."""
import asyncio
import json
from agent_hooks import (
AgentContextBuilder,
Decision,
InterceptionEmitter,
Transform,
Verdict,
)
class Redactor:
def intercept(self, context) -> Verdict:
if context["interception_point"] != "pre_tool_call":
return Verdict(decision=Decision.ALLOW)
if "ssn" in context["tool_call"]["args"]:
return Verdict(
decision=Decision.TRANSFORM,
transform=Transform(path="$target.ssn", value="[REDACTED]"),
reason="pii_redacted",
)
return Verdict(decision=Decision.ALLOW)
async def main() -> None:
emitter = InterceptionEmitter()
emitter.register(Redactor(), name="redactor")
build = AgentContextBuilder(
agent_id="records-agent", framework="quickstart", session_id="s-003"
)
ctx = build.pre_tool_call(
name="lookup", args={"name": "J. Doe", "ssn": "078-05-1120"}, call_id="t-3"
)
outcome = await emitter.emit(ctx)
# The action consumes the effective target, not the original.
print("tool receives:", outcome.target)
r = outcome.record
print(json.dumps({
"decision": r.verdict.decision.value,
"transform_in_record": r.verdict.transform.to_wire() if r.verdict.transform else None,
"identities_differ": r.input_identity != r.enforced_identity,
}, indent=2))
asyncio.run(main())
Output:
tool receives: {'name': 'J. Doe', 'ssn': '[REDACTED]'}
{
"decision": "transform",
"transform_in_record": {
"path": "$target.ssn"
},
"identities_differ": true
}
Three properties on display:
- The transform path is rooted at
$targetand uses a small, normatively-defined JSONPath subset: dot members, bracket members, bracket indexes. A path that does not resolve fails closed ashost_error:transform_invalid. - The record's projection keeps
pathand dropsvalue: the audit trail shows that$target.ssnwas rewritten, not what it was rewritten to, and certainly not what it was before. input_identity != enforced_identityis the tamper-evident proof that content changed between dispatch and action, without storing either version. Undersequential/*profiles, a second interceptor registered after the redactor would have seen the redacted context, which is exactly how redact-then-scan pipelines compose.