How the DeepSeek-Reasonix Permission Layer Implements Per-Call Gating with Allow/Ask/Deny Rules

The DeepSeek-Reasonix permission layer implements per-call gating through a deterministic precedence system where Deny rules override Ask rules, which override Allow rules, using a pure Policy structure evaluated by a runtime Gate that intercepts every tool invocation.

The permission system in the DeepSeek-Reasonix repository provides fine-grained access control for AI agent tool calls. It evaluates every invocation against configurable allow, ask, and deny rules before execution. This article examines the implementation details found in the source code, specifically how the Gate.Check method serves as the single entry point for permission decisions.

Core Architecture: Decision, Policy, and Gate

The system rests on three tightly-coupled concepts that separate configuration from runtime logic.

The Decision Enum

The Decision type defines three possible outcomes for any policy evaluation: Allow, Ask, and Deny. These constants represent the final verdict on whether a specific tool call may execute immediately, requires user confirmation, or is forbidden entirely.

Policy as Pure Configuration

The Policy struct is a pure, I/O-free data structure defined in internal/permission/permission.go. It contains a fallback Mode and three rule lists:

type Policy struct {
    Mode  Mode   // fallback when no rules match
    Allow []Rule // auto-allow patterns
    Ask   []Rule // prompt user patterns  
    Deny  []Rule // auto-deny patterns
}

Because Policy contains no I/O dependencies, it can be unit-tested in isolation without mocking external systems.

Gate as the Runtime Enforcer

The Gate struct wraps a Policy and an optional interactive Approver. It provides the Check method—the only function the agent calls to decide whether a tool may run. This design ensures that all permission logic funnels through a single controlled entry point.

Parsing Permission Rules

Rules are textual specifications such as Bash(go test:*) or Edit. The ParseRule function converts these strings into structured Rule objects containing Tool, Subject, and Literal fields.

The parser supports three syntax variants:

  • ToolName – matches any subject for that tool (e.g., Edit)
  • ToolName(glob) – matches a glob pattern against the subject (e.g., Bash(ls *))
  • ToolName=literal – matches the subject exactly (legacy form)

See the parsing logic in ParseRule located at internal/permission/permission.go lines 72-94.

Policy Evaluation and Precedence Logic

When a tool call arrives, the evaluation chain follows strict deterministic precedence.

The Decision Chain

The Policy.Decide method extracts subjects from JSON arguments and delegates to DecideSubjects. The precedence hierarchy is explicit:


Deny > Ask > Allow > (read-only ? Allow : Mode)

This means a single deny rule matching any subject immediately blocks the call, regardless of allow rules present.

Subject Extraction

For multi-endpoint tools like move_file, the Subjects function extracts all relevant paths (e.g., source_path and destination_path). The DecideSubjects method ensures that multi-subject tools are allowed only when every endpoint satisfies the same decision threshold, preventing partial permission loopholes.

The core evaluation logic lives in Policy.DecideSubject (lines 38-53) and the matchAny helper (lines 75-92) in internal/permission/permission.go.

Per-Call Gating with Gate.Check

Gate.Check serves as the definitive per-call gate. Its signature accepts the tool name, raw JSON arguments, and a read-only flag:

func (g *Gate) Check(ctx context.Context, toolName string,
    args json.RawMessage, readOnly bool) (bool, string, error)

Execution Flow

  1. Read-only override: If the tool is bash but the command is read-only (e.g., git status), the call is treated as readOnly = true (lines 64-68).

  2. Policy consultation: The gate calls g.Policy.Decide(toolName, readOnly, args) to determine the preliminary decision.

  3. Decision handling:

    • Deny: Immediately returns allow = false with a canned denial message.
    • Ask: If no Approver is configured (non-interactive runs), the gate auto-allows to maintain autonomy. Otherwise, it invokes Approver.Approve to solicit user input. If the user selects "remember," the rule is appended to g.Policy.Allow and the optional OnRemember callback fires (lines 84-99).
    • Allow (or fallback): Returns true to permit execution.

The complete implementation resides in Gate.Check at lines 60-102 of internal/permission/permission.go.

Subject Extraction and Pattern Matching

Glob Implementation

The matchGlob function (lines 105-133) implements shell-style pattern matching supporting * and ? wildcards, where * matches across path separators. This enables rules like Bash(go test:*) to match complex command strings.

Remember-Rule Generation

When users opt to persist an allowance, RememberRuleForScope (lines 140-176) constructs the appropriate rule string for the current scope—whether that's Bash(go test:*), Edit, or the bare tool name.

Practical Implementation Examples

Constructing a Policy

p := permission.New(
    "ask",                               // default mode for writers
    []string{"Bash(ls *)", "Edit"},      // allow list
    []string{"Bash(git push *)"},        // ask list  
    []string{"Bash(rm -rf *)"},          // deny list
)

Initializing the Gate (Interactive Mode)

gate := permission.NewGate(p, myApprover) // myApprover implements Approver interface

Querying a Specific Call

allowed, reason, err := gate.Check(ctx, "bash",
    []byte(`{"command":"git push origin main"}`), false)

In this example, Policy.Decide identifies Bash(git push *) in the Ask list, triggering the approver flow. If the user selects "always allow," the rule is dynamically added to p.Allow for the session duration.

Non-Interactive Auto-Allow

For automated environments, implement a nil approver that treats all Ask decisions as Allow without persistence:

type NilApprover struct{}
func (a *NilApprover) Approve(ctx context.Context, tool, subject string,
    args json.RawMessage) (bool, bool, error) {
    return true, false, nil // allow, don't remember
}

Summary

  • The permission layer uses a three-tier precedence system (Deny > Ask > Allow) enforced at internal/permission/permission.go.
  • Gate.Check is the sole entry point for per-call gating, handling read-only overrides and interactive approval flows.
  • Policy structs are pure data without I/O, enabling deterministic unit testing of permission logic in isolation.
  • Multi-subject tools require all endpoints to satisfy the same decision level, preventing security gaps in operations like file moves.
  • Rules support glob patterns with * and ? wildcards, parsed by the ParseRule function using shell-style matching semantics.

Frequently Asked Questions

What happens if a tool call matches both Allow and Deny rules?

Deny always takes precedence. The DecideSubjects method evaluates deny rules first via the matchAny helper, ensuring that a single deny match immediately returns Deny regardless of any matching allow rules.

How does the system handle multi-parameter tools like move_file?

The Subjects function extracts all relevant paths (source and destination), and DecideSubjects requires that every subject passes the same permission threshold. This prevents scenarios where the source is allowed but the destination is denied, or vice versa.

Can the permission policy be modified at runtime?

Yes. When a user selects "remember" during an interactive approval, the gate dynamically appends the new rule to the in-memory policy's Allow list via g.Policy.Allow = append(g.Policy.Allow, rule). The optional OnRemember callback persists this change to storage if configured.

What is the difference between Mode and the explicit rule lists?

Mode serves as the fallback decision when no explicit rules match. If a call is read-only and no rules apply, it defaults to Allow. For write operations with no matching rules, it follows the configured Mode (typically "ask" or "deny"). Explicit Allow/Ask/Deny rules always override the Mode.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →