Skip to content
allsrc.dev
Go back

Pattern 5: Redaction Boundary

A Redaction Boundary is an architectural security pattern that recursively masks sensitive data before it crosses trust boundaries—such as model inputs, audit logs, or user interfaces—to prevent unauthorized exposure. Mask sensitive values before data crosses any boundary where the raw value should not appear — the model, the display, or the audit store.

Understanding Redaction Boundaries

Every agent that calls an LLM sends a prompt. That prompt contains context pulled from logs, databases, user inputs, and tool responses. Some of that context contains information that should never leave your environment: PII, credentials, internal topology, confidential business data.

The attack surface is not only the model provider. It is your own agent, faithfully passing sensitive information into the context window because nothing in the pipeline told it not to. These leaks are often accidental — an agent retrieves a log line with an email, a bearer token appears in headers, or a tool result includes a credit-card-like value. If the harness passes that payload unchanged, the leak has already happened. The model does not need malicious intent for this to be a problem. It only needs unfiltered context.

Implementing the Redaction Boundary Pattern

A redaction boundary recursively masks sensitive values before data crosses a boundary where the raw value should not appear. It is enforced by the harness, not by hoping the agent makes good choices about what to include.

flowchart TD
  A[Raw user input] --> R[redact]
  B[Retrieved context] --> R
  C[Tool result] --> R
  R --> D[Prompt-safe payload]
  R --> E[Audit-safe payload]
  R --> F[Display-safe payload]

In LangGraph, redaction runs at more than one boundary. A redact_input node scrubs the inbound message before the model sees it, and a redact_output node scrubs the model’s answer before it leaves the graph — the agent is bracketed on both sides:

def redact_input(state: AgentState) -> dict:
    last = state["messages"][-1]
    if isinstance(last, HumanMessage):
        return {"messages": [last.model_copy(update={"content": redact(last.content)})]}
    return {}

def redact_output(state: AgentState) -> dict:
    last = state["messages"][-1]
    return {"messages": [last.model_copy(update={"content": redact(last.content)})]}

builder.add_edge(START, "redact_input")
builder.add_edge("redact_input", "agent")
builder.add_edge("agent", "redact_output")
builder.add_edge("redact_output", END)

Limitations and Considerations for Redaction Boundaries

Consider the following factors when you implement this pattern:

References



Previous Post
Pattern 4: Cost and Tool Budgeting
Next Post
Pattern 6: Prompt Injection and Goal Hijacking