Cases/Open-source engineering case

ADPS Open-Source Engineering Case

DeerFlow Guardrails: From Pre-Call Interception to Two-Layer Authorization

Five public pull requests moved tool control from run-time denial to assembly-time visibility filtering, while bringing identity, policy, and evidence into one execution path.

Through-line task

How a read-only research task receives its minimum tool set

  1. 01Task

    An authenticated read-only user searches, reads an allowed directory, and writes a summary without file or shell access.

  2. 02First divergence

    The first interceptor sees tool name and arguments but does not receive a trusted Principal.

  3. 03Architecture change

    Five PRs add an enforcement point, identity propagation, journal events, independent AuthZ, and assembly filtering.

  4. 04Acceptance

    The model cannot see unauthorized tools; a forged call still stops before the real handler and leaves an event.

Case scope

System studiedGuardrail and authorization changes in the public DeerFlow repository
Core questionWhich tools should a task receive, and when should tools that the principal cannot use disappear from the candidate set?
EvidencePRs #1240, #3665, #3837, #4260, and #4370, plus current source, tests, and public documentation
ADPS patternsA1 Tool Dispatch, A4 Guardrail Sandwich, A5 Minimal Tool Set, G1 Approval Gate, G2 Blast-Radius Control, X1 Observability, and G5 Hooks Pipeline

1. Which tools should one task receive?

DeerFlow does not delegate this question to one unconstrained generation. The model can choose only among tool schemas already present in its run context. Before a capability enters that list, agent declarations, the active Skill, and principal permissions narrow the candidates.

Effective capability = global candidates ∩ agent declaration ∩ active Skill ∩ principal permission.

DeerFlow tool assembly and two-layer authorization
Figure 1 · Relevance narrows candidates; authorization decides what the current principal may see and call.

Tool relevance and authorization are separate decisions. Is the tool relevant? is handled by tool_groups, subagent allow/deny lists, Skill activation, and deferred discovery. May this principal use it? is handled by the AuthorizationProvider. Relevance does not grant permission, and authorization does not guess the best tool for the task.

2. Assembly filtering and run-time interception share one policy

apply_tool_authorization resolves the provider, constructs the principal, and filters candidates through one entry point. It returns both the filtered list and the provider instance, which the caller then wires into run-time middleware.

def apply_tool_authorization(tools, *, context, app_config,
                             authorization_provider=None):
    if app_config.authorization.enabled is not True:
        return tools, None

    provider = authorization_provider or resolve_authorization_provider(
        app_config.authorization
    )
    principal = build_principal_from_context(context)
    filtered = filter_tools_by_authorization(
        tools, provider=provider, principal=principal,
        fail_closed=app_config.authorization.fail_closed,
    )
    return filtered, provider
  1. Layer 1, assembly time: remove tools that can never be used from schemas and the deferred catalog. The model never sees them, and tool_search cannot promote them later.
  2. Layer 2, call time: when the model proposes a concrete call, authorize again against current identity, arguments, and dynamic resources. Explicit guardrails run after that decision.

Reusing one provider prevents a split in which a capability is hidden in one layer but callable in another, or the two layers evaluate different policy versions.

3. Five pull requests changed five boundaries

Five stages in the evolution of DeerFlow guardrails
Figure 2 · The evolution moved from a call boundary to a visibility boundary. Dates follow Git merge history.
MergedPRCapability addedStill missing at that point
2026-03-23#1240Pre-call GuardrailMiddleware and pluggable providersTrusted identity, durable audit, resource policy
2026-06-21#3665User, role, run, and channel identity in each requestIndependent RBAC and assembly filtering
2026-07-03#3837Denials and provider failures in RunJournalConsistent journal inheritance across all paths
2026-07-21#4260AuthorizationProvider, built-in RBAC, provider factoryModel-visible tools could still exceed permission
2026-07-23#4370Assembly filter and run-time check sharing one providerAsk decisions, general post-checks, business compensation

4. PR #1240: establish a non-bypassable call point

The first version placed authorization in wrap_tool_call and awrap_tool_call. Middleware constructs a GuardrailRequest before execution and delegates the decision to a structural provider. A denial becomes a ToolMessage with a reason code, allowing the agent to adapt. Deployments choose fail-closed or fail-open behavior for provider failures.

decision = provider.evaluate(guardrail_request)
if not decision.allow:
    return ToolMessage(
        content="Guardrail denied: ...",
        status="error",
        tool_call_id=tool_call_id,
    )
return handler(request)

Control-flow exceptions such as GraphBubbleUp pass through unchanged. Middleware must not swallow pause and resume signals simply because it occupies the tool boundary.

5. PR #3665: identity must come from a trusted injection point

A tool name alone is insufficient for production authorization. The same write_file call may have different permissions for a regular user, an internal workload, and a delegated subagent. The request therefore gained user and role identity, OAuth provenance, run and tool-call IDs, channel identity, and an internal-workload flag.

These fields come from gateway and run-time context, not from prompts or model output. The system also keeps the requesting principal distinct from the executing agent, making it possible to answer who requested the action, which agent executed it, and in which run.

6. PR #3837: a denial must remain discoverable

Security-relevant decisions enter RunJournal with the tool name, call ID, role, policy ID, reason codes, fail-closed setting, and provider-error flag. Normal allows do not produce a security event for every call, which keeps the stream focused on interventions.

The event intentionally omits raw tool_input and user identifiers. Arguments may contain credentials or business data; an audit stream should not create a second exposure surface. Journal persistence is best-effort. A storage failure emits a warning but does not change the authorization result.

Current code also records an open boundary: native subagents do not automatically inherit __run_journal. Custom runtimes can supply it, but cross-agent evidence still needs further consolidation.

7. PR #4260: separate policy decisions from enforcement

GuardrailMiddleware owns the execution point. AuthorizationProvider owns resource decisions. A provider receives a Principal, resource, action, target, and context. Built-in RBAC validates and compiles role policy during construction, leaving deterministic lookups on the request path.

Deny wins over allow. Unknown roles, empty targets, and misspelled configuration keys fail. allow: false and an empty allow list both mean deny-all. Only an absent policy for a resource type means unrestricted. Tests, rather than operator intuition, fix these semantics.

8. PR #4370: the model should not see tools it can never use

With run-time denial alone, the model still sees unauthorized schemas. It can spend tokens planning a path that must fail, and deferred search may rediscover the tool. Assembly-time filtering makes visibility part of authorization.

The change had to cover lead-agent, subagent, and embedded-client construction. Missing one path would give the same role a different capability set depending on its entry point.

tool_search is a special boundary. It may defer MCP schemas, but the deferred catalog must itself be filtered first. A generated tool_search result can reuse that filtered catalog. An ordinary tool with the same name receives no exemption from run-time authorization.

9. “Dynamic configuration” has four meanings

LevelWhen it takes effectTypical changes
ConfigurationProcess start or explicit reloadProvider type, role policy, fail-closed
Agent buildNew lead agent, subagent, or embedded clientTool groups, middleware, provider instance
Per requestPrincipal and GuardrailRequest constructionUser, role, run, channel, attributes
Per callImmediately before tool executionArguments, dynamic resources, external policy, risk

The built-in RBAC provider compiles policy at construction time. Editing an external file does not mutate an existing instance. A hot-update design must explicitly choose reload, agent reconstruction, or a provider that reads dynamic policy, and preserve the policy version in evidence.

10. Middleware order determines cost and semantics

Assembly filtering runs before the model sees schemas. At call time, the AuthorizationAdapter is the outer authorization check and explicit guardrails provide inner business or external-policy checks. The tool and sandbox follow. Cheap, stable checks with a high rejection rate belong early; remote or argument-heavy checks belong later.

A sandbox isolates processes and resources. Authorization answers whether this principal may call a capability. A guardrail asks whether this invocation satisfies additional constraints. The three controls are complementary.

Reassemble and execute the same read-only research task

This replay checks model visibility and real execution as separate boundaries.

  1. 01Collect candidates

    Load search, read, write, bash, and MCP tools while preserving provider coordinates.

  2. 02Apply Agent and Skill

    Remove unrelated product tools while retaining capabilities the research task may need.

  3. 03Resolve Principal

    The gateway injects the trusted read-only subject; lead and subagent inherit the delegation.

  4. 04Filter at assembly

    write_file and bash disappear; the deferred catalog is built only from authorized MCP tools.

  5. 05Recheck at runtime

    read_file is authorized for its actual path; a forged write_file call receives deny.

  6. 06Record intervention

    A restrained ToolMessage returns while RunJournal links subject, run, tool_call, and policy.

Commit conditionUnauthorized schemas stay out of the model, and every real invocation still crosses the same enforcement point.

11. Mapping the implementation to ADPS patterns

PatternDeerFlow implementationBoundary
A5 Minimal Tool SetAssembly removes invisible tools; Skills and tool groups narrow candidates furtherTask relevance remains a separate declaration and activation concern
A1 Tool DispatchThe model selects only within the admitted setDoes not define routing quality or ranking
A4 Guardrail SandwichA strong PRE interception pathNo general POST business verification or compensation
G1 Approval GateAllow/deny decisions with structured reasonsAsk, durable intent, and resume-time revalidation remain separate work
G2 Blast-Radius ControlLeast visibility, sandboxing, and fail-closed behaviorBusiness quotas, scope, rate limits, and circuit breakers are deployment policy
X1 ObservabilityDenials and provider failures enter RunJournalUnified evidence across subagents remains incomplete
G5 Hooks PipelineA shared middleware point with short-circuit semanticsThe feature does not by itself provide a complete pre/post hook pipeline

12. Boundaries the test suite should hold

  • Authorization disabled preserves tool order and identity.
  • Deny overrides allow; an empty allow list cannot mean “unset.”
  • Unknown roles, empty targets, and invalid policy keys fail closed.
  • Lead agent, subagent, and embedded client receive consistent filtering.
  • Layer 1 and Layer 2 reuse one provider instance.
  • The deferred catalog is filtered before tool_search can promote schemas.
  • Provider failures cover both fail-open and fail-closed behavior.
  • Pause, resume, and GraphBubbleUp signals pass through middleware.
  • Denial events carry policy IDs and reason codes without raw sensitive arguments.
  • Journal persistence failure does not change the decision.

13. Work that remains outside this implementation

The public implementation covers PRE authorization before tool calls. High-risk actions also need POST business verification, idempotency keys, external receipts, and compensation. Human decisions require an ask state, approval expiry, resume-time state checks, and single consumption. Policy hot updates need versioning, atomic cutover, and rollback.

This guardrail implementation connects capability assembly, trusted identity, policy decisions, execution interception, and durable evidence.

Public code evidence

Trace the first enforcement point through two-layer authorization

Public sources

DeerFlow source code is available under the MIT License. This article and its diagrams are released under CC BY 4.0.

Chronicle

Recorded source
Willem Jiang's DeerFlow Guardrail architecture presentation at the Governance workshop; checked by ADPS against public pull requests, source code, and documentation
Source date
First published on ADPS

View in the ADPS Chronicle