How Reasonix's Permission Layer Is Designed to Be Independent of the Model and CLI
Reasonix isolates permission handling from both the language model and command-line interface by implementing a self-contained internal/permission package that exposes a gate abstraction, ensuring policy decisions are made before requests reach either the model or CLI front-ends.
The permission layer in Reasonix serves as a strict security boundary that remains agnostic to both the underlying language model and user-facing interfaces. In the esengine/DeepSeek-Reasonix repository, this architecture is achieved through a pure Go module that intercepts tool calls via a gate pattern, enforcing policies without embedding logic into the model inference engine or CLI code. This design allows developers to swap models or add new front-ends without modifying permission logic.
Architectural Isolation in the internal/permission Package
All permission logic resides in internal/permission/permission.go, a self-contained module that defines only policy and gate abstractions. This package strictly avoids imports from internal/model or cmd/reasonix, ensuring zero coupling to inference engines or command-line handlers.
By confining permission logic to this isolated package, Reasonix maintains a thin, pure-Go layer that sits between user-facing front-ends and the underlying model. The module exposes two primary constructs: a Policy that defines rule sets, and a Gate that evaluates tool calls against those rules.
Declarative Policy Configuration
At startup, the controller reads permission configuration (typically from reasonix.example.toml) and constructs an immutable permission.Policy. This policy contains three distinct rule sets: allow, ask, and deny. Once constructed, the policy cannot be mutated by either the model or CLI components.
According to the source code in internal/control/controller.go (lines 73-88), the controller parses the configuration and builds the policy as follows:
// Build a policy that defaults to "ask" for writes, "allow" for reads.
p := permission.New(
"ask", // default mode
[]string{"read*"}, // auto-allow patterns
[]string{"write*"}, // ask for these
[]string{"bash(rm*)"}, // explicit deny
)
The immutability guarantee ensures that the model and CLI never modify permission rules at runtime, creating a predictable security boundary.
The Gate Pattern: Enforcing Permissions Before Execution
The controller creates a permission.Gate via permission.NewGate(policy, approver). Before any tool executes, the gate's Decide method evaluates the request against the policy, returning one of three decisions: Allow, Ask, or Deny.
As implemented in internal/permission/permission.go (lines 252-278), the gate intercepts tool calls before they reach the execution layer:
func (e *executor) runTool(name string, args json.RawMessage) error {
// Ask the gate before invoking the tool.
decision := e.gate.Decide(name, false, args)
switch decision {
case permission.Allow:
return e.invoke(name, args) // go ahead
case permission.Ask:
// Prompt the user (or an automated approver) for confirmation.
if e.approver.Confirm(name, args) {
return e.invoke(name, args)
}
return fmt.Errorf("permission denied by user")
case permission.Deny:
return fmt.Errorf("permission denied by policy")
}
}
This interception ensures that permission violations are caught before the model generates responses or the CLI displays output.
Shared Gate Between CLI and Desktop Interfaces
Both the CLI and desktop application entry points import the same controller, which constructs a single permission.Gate instance. As documented in site/src/pages/docs.astro (lines 186-194), this shared gate ensures identical permission semantics across all interfaces without code duplication.
The controller wires the gate as follows:
func (c *Controller) newInteractiveGate() *permission.Gate {
policy := c.cfg.Permission.Policy // parsed from config
// The controller itself implements the Approver interface.
return permission.NewGate(policy, gateApprover{c})
}
Consequently, whether users interact via terminal commands or the desktop client, they encounter the same permission enforcement logic, maintaining consistency across deployment modes.
Keeping the Model Pure and Untouched
The language model in Reasonix remains completely unaware of the permission layer. The model receives only the prompt that already respects the permission decision, generating output based on filtered requests without querying permission state.
As shown in internal/control/controller.go (lines 1245-1260), the model simply processes pre-filtered tool calls. This separation guarantees that improvements or changes to the model—whether swapping providers or updating inference parameters—never affect how permissions are evaluated. The permission layer acts as a pre-processor, not a post-processor.
Runtime Overrides via the Approver Interface
While the policy itself is immutable, the gate supports dynamic decision-making through an Approver interface implementation. The controller implements this interface to provide interactive prompts or automated approval workflows, but this logic remains confined to the gate layer.
The approver pattern allows programmatic grants or denials without bleeding logic into model or CLI code, maintaining the architectural boundary while supporting flexible runtime workflows.
Summary
- Reasonix's permission layer lives in
internal/permission/permission.goas a standalone module with no dependencies on the model or CLI packages. - Declarative policies define allow, ask, and deny rules that are immutable after construction, parsed from configuration files by the controller.
- The gate pattern intercepts all tool calls via
gate.Decide(), enforcing permissions before execution reaches the model or user interface. - Shared gate instances ensure CLI and desktop clients enforce identical security semantics without code duplication.
- Model purity is maintained by filtering requests before they reach the inference engine, keeping the model completely agnostic to permission logic.
- Runtime flexibility is provided through the
Approverinterface, allowing dynamic confirmations without compromising architectural isolation.
Frequently Asked Questions
What happens when the permission gate returns "Ask" instead of "Allow" or "Deny"?
When gate.Decide() returns permission.Ask, the system invokes the configured Approver implementation to request user confirmation or automated validation. This typically triggers an interactive prompt in the CLI or a confirmation dialog in the desktop application. If the approver confirms, the tool executes; otherwise, the operation returns a permission denied error.
Can I use Reasonix's permission layer with a different language model provider?
Yes. Because the permission layer in internal/permission has no imports from internal/model, you can swap the underlying inference engine or model provider without modifying permission code. The gate filters requests before they reach the model, making the architecture completely model-agnostic.
Where is the permission policy defined in a Reasonix deployment?
The permission policy is defined in configuration files such as reasonix.example.toml and parsed at startup by the controller in internal/control/controller.go. The policy supports glob patterns for tool names and accepts three rule categories: allow patterns (auto-approved), ask patterns (require confirmation), and deny patterns (explicitly blocked).
How does the shared gate ensure consistency between the CLI and desktop interfaces?
Both entry points instantiate the same controller type, which constructs a single permission.Gate instance using permission.NewGate(). Since both interfaces rely on this shared controller logic, they automatically enforce identical permission semantics. This architecture eliminates code duplication while ensuring that security policies apply uniformly across all user interaction modes.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →