localcontrol

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Overview

Package localcontrol is the in-process policy layer: detectors that decide without a network round trip, so an obviously-destructive call is stopped at the edge and an offline agent is not an ungoverned one.

It is the Go port of the Python SDK's protection middleware pipeline (flyedge/core/middleware/protection_middlewares.py + security_middleware.py, wired in protection.py:_setup_middlewares). The detector set, their default patterns, and the way `mode` selects each detector's action are deliberately the same — an org that moves a workload from the Python SDK to the Go one should not silently lose a guardrail.

Local is additive, never subtractive

A local verdict may add a faster NO. It must never turn the server's NO into a yes. Guard.Check still fires for every call: session risk accumulation, the audit trail, and every ML- or session-state-backed control live server-side and are untouched by anything here. That also means a compromised endpoint gains nothing by lying about local evaluation — the authoritative record is still made server-side.

Consequently the only rules that belong here are unambiguous and parameter-free: a DROP TABLE, a pinned secret, an exhausted budget. Anything tunable, statistical, or that needs cross-session state stays on the server, where it can be reasoned about centrally and changed without redeploying every agent.

Regex portability

Go's regexp is RE2: no backreferences, no lookaround. Several Python patterns use negative lookahead (notably "UPDATE ... SET ... not followed by WHERE"). Those are re-expressed as a match plus an explicit Go-side check rather than transliterated — see detectors.go. A pattern that silently fails to compile would be a guardrail that looks present and enforces nothing, so compilation errors are surfaced at construction time, never swallowed.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Version is the platform's revision of this document. It is echoed in reports so an operator
	// can tell which endpoints have converged.
	Version int `json:"version"`
	// Mode is the local posture. Empty → warn.
	Mode Mode `json:"mode"`

	// Detectors toggles and tunes each built-in detector. A detector absent from the payload keeps
	// its default (enabled), because a truncated or partially-written config must fail toward
	// protection rather than away from it.
	DatabaseSafety  *DatabaseSafetyConfig  `json:"databaseSafety,omitempty"`
	SecretScan      *SecretScanConfig      `json:"secretScan,omitempty"`
	PromptInjection *PromptInjectionConfig `json:"promptInjection,omitempty"`
	TokenBudget     *TokenBudgetConfig     `json:"tokenBudget,omitempty"`

	// Scope narrows which components the local layer inspects at all. Exclusions win over
	// inclusions. Empty include lists mean "everything not excluded".
	IncludeComponents []string `json:"includeComponents,omitempty"`
	IncludePatterns   []string `json:"includePatterns,omitempty"`
	IncludeTypes      []string `json:"includeTypes,omitempty"`
	ExcludeComponents []string `json:"excludeComponents,omitempty"`
	ExcludePatterns   []string `json:"excludePatterns,omitempty"`
	ExcludeTypes      []string `json:"excludeTypes,omitempty"`
}

Config is the synced local-control configuration — the payload the platform distributes through the edgesync channel, and the same shape callers can set directly for local development.

It is intentionally a compiled/distilled document rather than raw policy YAML: the edge should not be re-deriving which controls are client-evaluable, and a smaller payload keeps the conditional-GET fast path cheap.

type DatabaseSafetyConfig

type DatabaseSafetyConfig struct {
	// Disabled turns the detector off. Expressed as an opt-OUT so that the zero value of an
	// omitted config block is "enabled", matching Config's fail-toward-protection rule.
	Disabled bool `json:"disabled,omitempty"`
	// AllowedPatterns are escape hatches: a query matching one is exempt even if it also matches a
	// danger pattern. This is how a known-safe `DELETE FROM session_cache WHERE ...` gets through.
	AllowedPatterns []string `json:"allowedPatterns,omitempty"`
	// BlockedPatterns extend the built-in danger set with org-specific rules.
	BlockedPatterns []string `json:"blockedPatterns,omitempty"`
	// ToolPatterns overrides which component names are treated as database tools. Empty keeps the
	// built-in list.
	ToolPatterns []string `json:"toolPatterns,omitempty"`
}

DatabaseSafetyConfig tunes the destructive-query detector.

type Detector

type Detector interface {
	// Name is the stable identifier used in verdicts and config.
	Name() string
	// Stages the detector runs at. A detector is never consulted at other stages.
	Stages() []enforce.Stage
	// Priority orders execution, higher first. It matters because the engine stops at the first
	// blocking verdict, so the cheapest and most certain rules should run before the fuzzier ones.
	Priority() int
	// Inspect returns a finding, or nil when there is nothing to report.
	Inspect(req *Request) *Verdict
}

Detector is one local rule. Implementations must be safe for concurrent use: one Engine is shared by every goroutine making calls through the Guard.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine runs the configured detectors for a request and reduces their findings to one verdict.

It is immutable after construction: rebuilding on config change (see Apply) rather than mutating a live engine is what lets the hot path run lock-free while a sync goroutine swaps configuration underneath it.

func New

func New(cfg Config) (*Engine, error)

New builds an Engine from a synced configuration.

This is the Go counterpart of the Python SDK's _setup_middlewares: mode selects each detector's action at BUILD time rather than being re-checked per call, so the hot path is a plain loop over already-configured detectors.

It returns an error when a configured pattern does not compile. That is deliberate: a guardrail that silently failed to compile would look present in the UI and enforce nothing, which is worse than a loud failure at config-apply time. Callers should keep the previously-good engine on error.

func NewEngine

func NewEngine(mode Mode, sc scope, detectors []Detector) *Engine

NewEngine builds an engine from an explicit detector list. Most callers want New(cfg) instead, which maps a synced configuration onto the detector set the way the Python SDK's _setup_middlewares does.

func (*Engine) Detectors

func (e *Engine) Detectors() []string

Detectors returns the configured detector names, for diagnostics and the sync report. Order is undefined; callers that display it should sort.

func (*Engine) Evaluate

func (e *Engine) Evaluate(req Request) Verdict

Evaluate runs the detectors for req's stage and returns the strongest finding.

Ordering is by priority and evaluation stops at the first block, so a definite DROP TABLE does not pay for a fuzzy injection scan. Warnings do not stop the chain: a later detector may still block, and reporting the block matters more than reporting the warning that preceded it.

A nil Engine evaluates to allow, so "local controls not configured" needs no nil check at any call site.

func (*Engine) Mode

func (e *Engine) Mode() Mode

Mode reports the posture the engine was built with.

type Mode

type Mode string

Mode is the local posture. It gates ONLY the local detectors in this package; a server deny or kill from Guard.Check enforces regardless of mode. That asymmetry is deliberate and matches the Python SDK: mode is how loudly the local layer speaks, not whether the platform is obeyed.

The TS SDK gates cloud denies by mode; that is a bug, not a precedent worth matching.

const (
	// ModeOff disables local evaluation entirely.
	ModeOff Mode = "off"
	// ModeAudit records findings and never blocks — the dry run for a new rule set.
	ModeAudit Mode = "audit"
	// ModeWarn (default) surfaces findings as warnings without blocking.
	ModeWarn Mode = "warn"
	// ModeEnforce blocks on findings that meet each detector's blocking bar.
	ModeEnforce Mode = "enforce"
)

type PromptInjectionConfig

type PromptInjectionConfig struct {
	Disabled bool `json:"disabled,omitempty"`
	// BlockThreshold is the minimum severity that blocks in enforce mode. Empty → high, matching
	// the Python SDK's block_threshold=ThreatLevel.HIGH.
	BlockThreshold Severity `json:"blockThreshold,omitempty"`
	// ExtraPatterns add org-specific injection phrasings.
	ExtraPatterns []string `json:"extraPatterns,omitempty"`
}

PromptInjectionConfig tunes the injection detector.

type Request

type Request struct {
	Stage enforce.Stage
	// ComponentType is "TOOL" or "LLM", matching the wire vocabulary.
	ComponentType string
	// ComponentName is the tool or model name, used for scoping and for the audit message.
	ComponentName string
	// Content is the text to inspect: tool arguments at tool_call, model output at post_llm.
	Content string
	// SessionID scopes stateful detectors (the token budget). Empty means unscoped, which the
	// budget detector treats as a single shared bucket rather than as "no budget".
	SessionID string
	// Tokens is this call's token count, when the caller knows it. Zero means unknown, not free.
	Tokens int
}

Request is one thing to inspect. It is deliberately flat and content-first: detectors match on text, and the component fields exist so scoping (which tools a detector applies to) is decidable without the caller pre-classifying anything.

type SecretScanConfig

type SecretScanConfig struct {
	Disabled bool `json:"disabled,omitempty"`
	// ExtraPatterns add org-specific credential shapes (an internal token prefix, say).
	ExtraPatterns []string `json:"extraPatterns,omitempty"`
}

SecretScanConfig tunes the secret/credential detector.

type Severity

type Severity string

Severity ranks a finding independently of what was done about it. A CRITICAL finding in warn mode is still CRITICAL — the severity describes the thing found, the Action describes the posture applied to it.

const (
	SeverityLow      Severity = "low"
	SeverityMedium   Severity = "medium"
	SeverityHigh     Severity = "high"
	SeverityCritical Severity = "critical"
)

func (Severity) AtLeast

func (s Severity) AtLeast(min Severity) bool

AtLeast reports whether s is as severe as min. Unknown severities rank 0, so an unrecognized value never satisfies a threshold — an unparseable config must not silently disarm a detector.

type TokenBudgetConfig

type TokenBudgetConfig struct {
	Disabled bool `json:"disabled,omitempty"`
	// MaxTokens is the per-session ceiling. Zero means no ceiling — the budget detector is inert
	// rather than blocking everything, which is what an unset limit has to mean.
	MaxTokens int `json:"maxTokens,omitempty"`
}

TokenBudgetConfig caps token spend per session.

type Verdict

type Verdict struct {
	// Action is what the posture says to do about this finding.
	Action enforce.Action
	// Detector names the rule that fired, so an operator can find and tune it.
	Detector string
	// Reason is a stable machine-readable code ("destructive_query", "prompt_injection"). It is
	// part of the contract with the audit trail — rename it and dashboards break.
	Reason string
	// Message is the human sentence shown to whoever is blocked.
	Message string
	// Severity of the finding itself, independent of Action.
	Severity Severity
	// Matched is the excerpt that triggered the rule, bounded by clipMatch. Included because a
	// block with no evidence is unactionable, truncated because the content can be a whole prompt
	// and this ends up in logs.
	Matched string
}

Verdict is one detector's finding. A nil *Verdict means "nothing to say" — detectors return nil on the overwhelmingly common clean path so the engine allocates nothing for it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL