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 ¶
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 ¶
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 ¶
Detectors returns the configured detector names, for diagnostics and the sync report. Order is undefined; callers that display it should sort.
func (*Engine) Evaluate ¶
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.
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.
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.