public enum Provenance { Trusted, Tainted }| Value | Meaning |
|---|---|
Trusted | Originated inside your boundary: system prompt, configuration, a value an authenticated user typed into a field you control, a row your own code wrote after a successful action |
Tainted | Everything else, including anything of unknown origin |
Join rule: any tainted input makes the result tainted. No operation converts tainted back to trusted. A function claiming to do so is a classifier, and chapter 4 covers why that is not a boundary.
public readonly record struct Tagged<T>(T Value, Provenance Origin);| Field | Notes |
|---|---|
Value | The value itself |
Origin | Survives storage as a persisted column (ch 12), crosses process boundaries explicitly, defaults to Tainted when unknown |
public sealed record ToolPolicy(
string ToolName,
Capability RequiredCapability,
bool Irreversible,
bool ExternallyVisible,
bool CrossesTrustBoundary,
ProvenanceRule ArgumentRule);| Field | Required | Notes |
|---|---|---|
ToolName | yes | Must match the registered function name exactly |
RequiredCapability | yes | Checked against the caller's set; see below |
Irreversible | yes | No default. Does an inverse operation exist, and can you reach it inside the window that matters? Ask your retention configuration, not your intuition |
ExternallyVisible | yes | Can anything outside the boundary observe that this happened |
CrossesTrustBoundary | yes | Does data move from private to less-private |
ArgumentRule | yes | How argument provenance affects the verdict |
A tool is consequential if any of Irreversible, ExternallyVisible or CrossesTrustBoundary is true. That is chapter 5's definition, mechanised.
Make all six fields required. An optional field acquires a default, the default is permissive, and a tool gets added on a Friday without anyone answering the question.
ProvenanceRule| Value | Effect |
|---|---|
AllowAny | Provenance is not considered. Only for tools with no consequential flag set |
DenyTainted | Any tainted argument denies the call |
EscalateTainted | Any tainted argument routes to a human (ch 14) |
DenyTaintedIn(params string[]) | Applies to named arguments only, for tools where one field matters and others do not |
The common configuration for an irreversible tool is EscalateTainted. DenyTainted where no reviewer has the context to judge.
public sealed record Capability(
string Operation,
string Resource,
DateTimeOffset Expires,
string TaskId);| Field | Notes |
|---|---|
Operation | A verb: read, send, refund, delete |
Resource | Where the reduction comes from. order:88431, not order:*. Two-stage issuance where the specific resource is not known at task start |
Expires | Set from the task, not a global default. Seconds to minutes. Anything in days is a service account |
TaskId | Makes the audit trail readable months later |
public sealed record CapabilitySet(IReadOnlyList<Capability> Items)
{
public bool Covers(Capability required) =>
Items.Any(c => c.Operation == required.Operation
&& c.Resource == required.Resource
&& c.Expires > DateTimeOffset.UtcNow);
}Capabilities are derived from the authenticated user's authority at task start, never from a service account, and never widened by anything the model says. Task intent is declared in code, not inferred.
public sealed record Proposal(
string ToolName,
IReadOnlyList<Argument> Arguments,
CapabilitySet Held);
public sealed record Argument(string Name, object? Value, Provenance Origin);
public enum Outcome { Allow, Deny, Escalate }
public sealed record Verdict(
Outcome Outcome,
string DeniedBy,
string ReasonForModel,
string ReasonForAudit);| Field | Notes |
|---|---|
DeniedBy | Names the mechanism: default-deny, capability, tainted-irreversible, egress-policy, budget-exhausted, policy-unavailable, human-rejected. Never null on a non-allow verdict |
ReasonForModel | Goes back into the conversation. Deliberately uninformative. "That action is not available." A detailed denial is a free probe of your policy |
ReasonForAudit | Goes to the log only. The sentence you want during an incident review |
Fixed, and the order matters.
public Verdict Evaluate(Proposal p)
{
if (!_policies.TryGetValue(p.ToolName, out var rule))
return Verdict.Deny("default-deny", "That action is not available.");
if (!p.Held.Covers(rule.RequiredCapability))
return Verdict.Deny("capability", "That action is not available.");
if (rule.Irreversible && p.Arguments.Any(a => a.Origin is Provenance.Tainted))
return Verdict.Escalate("tainted-irreversible", "This needs confirmation.");
return Verdict.Allow();
}Fail closed. If the policy store cannot be reached, deny. An agent that stops working is an incident; an agent that keeps working is a breach discovered later by someone else.
public bool IsAllowed(string url, Provenance origin)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
if (uri.Scheme is not ("https" or "mailto")) return false;
if (!_allowedHosts.Contains(uri.Host)) return false;
if (origin is Provenance.Tainted && uri.Query.Length > 0) return false;
return true;
}| Rule | Why |
|---|---|
| Scheme allowlist | data:, file: and custom schemes are channels |
| Host allowlist | Destinations come from configuration, never from the model |
| No tainted query strings | Closes reflection through a permitted host without having to enumerate which permitted hosts are safe |
Images in rendered output are stripped unconditionally. Where a product needs them, fetch server-side after checking the URL, and serve from your own host.
public sealed record GateRecord(
DateTimeOffset At,
string SessionId,
string TaskId,
string ToolName,
IReadOnlyList<(string Name, Provenance Origin, string SourceRef)> Arguments,
Outcome Outcome,
string DeniedBy,
string ReasonForAudit);Argument origins are recorded. Argument values are not. Logging values recreates the exposure chapters 6 and 10 exist to prevent, and makes the log store the thing an attacker wanted.
SourceRef points at the document, ticket or memory record that produced each tainted argument. It is what turns chapter 17's blast-radius assessment into a query.
Record denied and escalated proposals as well as allowed ones. The denials are the pattern, and a system that never reads its denial log has discarded its only signal about how often it is being probed.
Download the full PDF for free?
Free download — no account required