Documentation
¶
Overview ¶
Package approvals: the ApprovalAgent seam (SP-141 phase 3, increment 5). ApprovalAgent is the narrow interface the approval broker operates on — the same pattern as changes.AgentView and workflow.LoopAgent. *Agent satisfies it directly: most members are existing exported accessors; the seam accessors in pkg/agent/approval_seam_accessors.go expose the private surface (debug logger, interrupt ctx, security-analysis cache, effective cwd, the shell-approval picker). The import arrow is one-way: pkg/agent -> approvals.
Package approvals: the approval broker (SP-141 phase 3, increment 5). RequestApproval is the unified approval flow for a RiskAssessment: command-policy, allowlist, unsafe/elevated bypasses, optional LLM security analysis, then the WebUI / CLI interactive surfaces, with permissive auto-approve as the last resort in non-interactive mode. It operates on the ApprovalAgent seam (approval_agent.go) so it lives here, out of the god package.
Package approvals: CLI approval-choice mapping (SP-141 phase 3, increment 5).
Package approvals: filesystem path-tier classification (risk inputs).
Pure, agent-free: no *Agent dependency. The LLM/client and the session allowlist live in pkg/agent; this package owns only the path -> tier mapping.
Package approvals: the RiskAgent seam (SP-141 phase 3, increment 6). RiskAgent is the narrow interface the risk resolver (ResolveToolRisk) operates on — the same pattern as ApprovalAgent, changes.AgentView, and workflow.LoopAgent. *Agent satisfies it directly: most members are pre-existing exported methods; the seam accessors in pkg/agent/risk_seam_accessors.go expose the private surface (the persona git-write capability check and the test-overridable home-dir hook). The import arrow is one-way: pkg/agent -> approvals.
Package approvals: the risk resolver (SP-141 phase 3, increment 6). ResolveToolRisk produces the unified risk assessment for a tool call by folding all security inputs onto the Low/Medium/High/Critical scale: the static classifier, the persona cascade, the git gates, the workspace security policy, and the filesystem path tiers. It operates on the RiskAgent seam (risk_agent.go) so it lives here, out of the god package.
Package approvals: LLM-augmented security analysis for shell commands.
The package is deliberately free of any *Agent dependency: the LLM client and the model name are injected by the caller (pkg/agent), which owns the session-model override and client locking.
Package approvals: session-scoped cache for LLM security analyses.
Index ¶
- Constants
- Variables
- func AccessModeForTool(toolName string) string
- func ApprovalDecisionFromCLIChoice(c utils.ApprovalChoice) security.ApprovalDecision
- func CanonicalPath(p string) string
- func ChainCacheKey(input string) string
- func EvaluateCommandPolicy(command string, policies *configuration.CommandPolicies) (configuration.CommandPolicyAction, string, bool)
- func IsGitRebaseCommand(command string) bool
- func IsGitStashCommand(command string) bool
- func IsGitWriteCommand(command string) bool
- func IsShellCommandAllowlisted(cfg *configuration.Config, command string) bool
- func IsUnderPrefix(path, prefix string) bool
- func NormalizeChain(chain Chain) string
- func NormalizePath(p string) string
- func PersistShellCommandAllowlist(mgr *configuration.Manager, command string) error
- func PersistShellCommandAskPolicy(mgr *configuration.Manager, command string) error
- func PersistShellCommandPattern(mgr *configuration.Manager, pattern string) error
- func ResolveOldDecision(res tools.SecurityResult) string
- func ResolveUnifiedDecision(ra RiskAssessment) string
- type ApprovalAgent
- type BrokerDecision
- type Chain
- type PathTier
- type RiskAgent
- type RiskAssessment
- type RiskSource
- type SecurityAnalysis
- func AnalyzeChain(ctx context.Context, client api.ClientInterface, model string, chain Chain, ...) (*SecurityAnalysis, error)
- func AnalyzeChainFallback(ctx context.Context, client api.ClientInterface, model string, chain Chain, ...) (*SecurityAnalysis, error)
- func AnalyzeShellCommand(ctx context.Context, client api.ClientInterface, model string, ...) (*SecurityAnalysis, error)
- type SecurityAnalysisCache
Constants ¶
const MaxChainSubcommandsForBatchPrompt = 10
MaxChainSubcommandsForBatchPrompt caps chain length for the batch prompt; longer chains fall back to per-subcommand analysis.
Variables ¶
var DetectHomeDir = func() string { h, err := os.UserHomeDir() if err != nil { return "" } return h }
DetectHomeDir returns the user's home directory or empty if it can't be resolved (very rare). Wrapped so tests can stub it.
var ErrNilAgent = agenterrors.NewPermission("nil agent", nil)
ErrNilAgent is the error the *Agent forwarders return for the persist methods when the agent is nil (matching the pre-move "nil agent" Permission error).
Functions ¶
func AccessModeForTool ¶
AccessModeForTool returns "write" for mutating tools and "read" for read-only tools.
func ApprovalDecisionFromCLIChoice ¶
func ApprovalDecisionFromCLIChoice(c utils.ApprovalChoice) security.ApprovalDecision
ApprovalDecisionFromCLIChoice maps the CLI prompt's typed choice onto the shared ApprovalDecision.
func CanonicalPath ¶
CanonicalPath is NormalizePath without the Windows case folding — the form to show a user, since it keeps the path's real spelling.
func ChainCacheKey ¶
ChainCacheKey returns the cache key for storing/retrieving analyses of a shell chain. The key is normalized so that equivalent chains (modulo whitespace and outer trimming) collide, but distinct operators keep distinct keys.
func EvaluateCommandPolicy ¶
func EvaluateCommandPolicy( command string, policies *configuration.CommandPolicies, ) (configuration.CommandPolicyAction, string, bool)
EvaluateCommandPolicy checks user-defined command policies against a shell command. Returns the matched action, the matched pattern, and whether a match was found.
Algorithm:
- Split the command on &&, ||, ;, | (quote-aware) using SplitChainedCommand.
- For each subcommand, check rules in order (first-match-wins).
- Pattern matching uses Go path.Match (glob), case-insensitive.
- Return the highest-severity action across all subcommands: deny > ask > allow.
- If no subcommand matched any rule, return ("", "", false).
func IsGitRebaseCommand ¶
IsGitRebaseCommand reports whether `command` contains a `git rebase` invocation that rewrites history (i.e. NOT `git rebase --abort`).
func IsGitStashCommand ¶
IsGitStashCommand checks whether `command` contains a `git stash` invocation that is not purely read-only. `git stash` (bare or with `push`) saves the working tree and reverts it to HEAD — the save itself isn't destructive, but the typical pattern is stash + build + pop, and the pop is where merge conflicts silently revert files. `stash pop`, `stash apply`, `stash drop`, and `stash clear` are all destructive. `stash list` and `stash show` are read-only and handled by shellLooksReadOnly, but we include them here for completeness — the gate blocks all stash subcommands except list/show.
The gate is intentionally broad: any `git stash` that isn't `list` or `show` is blocked, because even `git stash push` sets up the pop-that-can-corrupt that we want to prevent.
func IsGitWriteCommand ¶
IsGitWriteCommand reports whether `command` contains a git invocation whose intent (not safety) requires the orchestrator git-write flow: `commit`, `push`, branch/tag CREATE operations (not delete — those go through the history-rewrite gate), and pushes via `clone`/`init` of existing-repo destinations.
Tier A working-tree-mutating ops (`checkout`, `switch`, `restore`, `reset` without history-loss, `clean`, `rm`, `mv`, `am`, `apply`, `cherry-pick`, `revert`, `fetch`, `pull`, `stash` mutations) used to be gated here; they're now allowed because the change tracker captures before-content and recover_file / recover_bulk are the recovery path. Tier B history-loss ops (`rebase`, `reset --hard <commit-ish>`, `branch -D`, `tag -d`) are routed through isGitHistoryRewriteCommand instead — that gate has its own opt-in flag (AllowGitHistoryRewrite) and prompts the user (auto-approved when AllowGitHistoryRewrite=true).
func IsShellCommandAllowlisted ¶
func IsShellCommandAllowlisted(cfg *configuration.Config, command string) bool
IsShellCommandAllowlisted reports whether the command matches an approved literal or glob pattern. Critical-tier commands are still blocked regardless of allowlist matches.
func IsUnderPrefix ¶
IsUnderPrefix reports whether `path` equals `prefix` or sits in a subdirectory of it. Both args must already be normalized. The check is path-component aware: "/foobar" is NOT under "/foo".
func NormalizeChain ¶
NormalizeChain returns a normalized cache key for a chain. It walks chain.Original to recover operators and produces distinct keys for "a && b" and "a || b". Chains with identical subcommands and operators normalize to the same key regardless of internal whitespace.
func NormalizePath ¶
NormalizePath cleans the path so prefix comparisons aren't fooled by trailing slashes or "./" segments. Symlinks aren't resolved here — that's the filesystem layer's job. On Windows the path becomes case-insensitive for comparison. NormalizePath cleans the path so prefix comparisons aren't fooled by trailing slashes or "./" segments. Symlinks aren't resolved here — that's the filesystem layer's job. On Windows the path becomes case-insensitive for comparison.
func PersistShellCommandAllowlist ¶
func PersistShellCommandAllowlist(mgr *configuration.Manager, command string) error
PersistShellCommandAllowlist appends command to the user's persistent approved-commands list (Config.ApprovedShellCommands) and saves to disk. Used by the "Always approve this command" choice on the approval dialog. Idempotent: re-adding an existing entry is a no-op but still triggers a save so the file's mtime updates (cheap).
func PersistShellCommandAskPolicy ¶
func PersistShellCommandAskPolicy(mgr *configuration.Manager, command string) error
PersistShellCommandAskPolicy adds a "always ask" command policy rule for the given command.
func PersistShellCommandPattern ¶
func PersistShellCommandPattern(mgr *configuration.Manager, pattern string) error
PersistShellCommandPattern appends pattern to the user's persistent approved-command-pattern list (Config.ApprovedShellCommandPatterns) and saves to disk. Patterns use Go path.Match glob syntax (`*`, `?`, `[]`). Idempotent: re-adding an existing entry is a no-op but still triggers a save so the file's mtime updates (cheap).
func ResolveOldDecision ¶
func ResolveOldDecision(res tools.SecurityResult) string
ResolveOldDecision derives a one-word gating decision from the old dual-gate path's SecurityResult for shadow-mode comparison.
func ResolveUnifiedDecision ¶
func ResolveUnifiedDecision(ra RiskAssessment) string
ResolveUnifiedDecision derives a one-word gating decision from a RiskAssessment for shadow-mode comparison with the old path.
Types ¶
type ApprovalAgent ¶
type ApprovalAgent interface {
// Config + approval-policy state.
GetConfig() *configuration.Config
IsShellCommandAllowlisted(cmd string) bool
GetUnsafeMode() bool
GetUnsafeShellMode() bool
IsSessionElevated() bool
IsSubagent() bool
// Interactive-approval surfaces.
HasActiveWebUIClients() bool
GetSecurityApprovalMgr() *security.ApprovalManager
GetEventBus() *events.EventBus
GetEventClientID() string
GetEventUserID() string
MarkWorkflowApprovedInSession(workflow string)
// Seam accessors (see pkg/agent/approval_seam_accessors.go).
Client() api.ClientInterface
GetModel() string
InterruptCtx() context.Context
EffectiveCwd() string
DebugEnabled() bool
DebugLogf(format string, args ...interface{})
IsNonInteractive() bool
GetSecurityAnalysisCache() *SecurityAnalysisCache
LogSecurityDecision(tool string, args map[string]interface{}, assessment RiskAssessment, action string)
ApplyApprovalDecision(decision security.ApprovalDecision, command string)
ApproveShellCommandParts(ctx context.Context, command string) (map[string]bool, []string, error)
}
ApprovalAgent is the surface the approval broker (RequestApproval) needs from the agent.
type BrokerDecision ¶
type BrokerDecision struct {
Approved bool
Decision security.ApprovalDecision
Outcome security.ApprovalOutcome
Surface string // "webui" or "cli" — which surface answered
Assessment RiskAssessment // echoed for caller diagnostics
Analysis *SecurityAnalysis // LLM-derived security analysis when available; nil otherwise
}
BrokerDecision is the typed verdict returned by RequestApproval.
func RequestApproval ¶
func RequestApproval(a ApprovalAgent, assessment RiskAssessment, toolName string, args map[string]interface{}) (BrokerDecision, error)
RequestApproval performs the unified approval flow for a RiskAssessment. Low-risk auto-approves. Critical/hard-blocks deny unconditionally. Medium/High/IntentConfirmation checks bypass paths then tries WebUI, CLI, or falls back to permissive auto-approve in non-interactive mode.
type Chain ¶
type Chain struct {
Original string
Operators []string // len(Subcommands)-1
Subcommands []string // len >= 1 (split via SplitChainedCommand)
}
Chain is a top-level decomposition of a shell command. For unchained input, Subcommands has length 1. Operators carries the chain operator between each adjacent pair of subcommands.
func ParseChain ¶
ParseChain splits a shell command string into a Chain value, delegating to SplitChainedCommand.
type PathTier ¶
type PathTier int
PathTier classifies a filesystem path for approval purposes. See ClassifyPathAccess for the resolution rules. The tiers are ordered from least to most restrictive.
const ( // PathTierUnknown is the zero value and shouldn't be returned; // it exists so a missing tier shows up loudly in tests rather // than silently behaving as "allow". PathTierUnknown PathTier = iota // PathTierWorkspace — the path is inside the agent's workspace // root (or the sprout config dir). No approval required. PathTierWorkspace // PathTierExternal — outside the workspace but not in a system // or off-CWD home directory. Eligible for the "Allow this folder // for the rest of the session" approval choice. Once a parent // folder is in the agent's session allowlist, future accesses // under it auto-approve. PathTierExternal // PathTierSensitive — system directories (/etc, /usr, ...) OR // home-directory paths when the agent's CWD is outside the user's // home. These ALWAYS prompt and CANNOT be added to the session // allowlist. The "Allow folder this session" choice is hidden // from the dialog for this tier. PathTierSensitive )
func ClassifyPathAccess ¶
ClassifyPathAccess decides which approval tier a path falls into. All three input paths should be absolute (or empty for unset fields). Behavior:
- Inside workspaceRoot → PathTierWorkspace.
- Under a known system directory (e.g. /etc, /usr, /var on Unix; C:\Windows, C:\Program Files on Windows) → PathTierSensitive.
- Under the user's home dir AND the agent's CWD is NOT under home → PathTierSensitive (working in /tmp, accessing ~ is unusual and shouldn't get session-wide allowlisting).
- Anything else outside the workspace → PathTierExternal.
homeDir and cwd are passed explicitly so tests can drive each branch deterministically. Production callers use os.UserHomeDir and the agent's effective CWD.
Symlinks: this classifier compares cleaned path strings; it does NOT call filepath.EvalSymlinks. The filesystem layer (pkg/filesystem) resolves symlinks before returning ErrOutsideWorkingDirectory, so the path we receive here is already the symlink-resolved target — the comparison is correct for cases where the filesystem layer hands us a real path. For non-filesystem callers (e.g. the WebUI file API consulting IsFolderSessionAllowed), the path is whatever the caller resolved. The classifier is therefore advisory: if you've crafted a clever symlink to dodge tier classification, the filesystem layer still enforces its own checks at write time.
type RiskAgent ¶
type RiskAgent interface {
// Workspace + session state.
GetWorkspaceRoot() string
HasPasswordPrompter() bool
IsFolderSessionAllowed(absPath string) bool
GetConfig() *configuration.Config
// Persona cascade.
EvaluateOperationRisk(command string) configuration.RiskLevel
// Seam accessors (see pkg/agent/risk_seam_accessors.go +
// approval_seam_accessors.go).
IsGitWriteAllowed() bool
EffectiveCwd() string
HomeDir() string
DebugEnabled() bool
DebugLogf(format string, args ...interface{})
}
RiskAgent is the surface the risk resolver (ResolveToolRisk) needs from the agent.
type RiskAssessment ¶
type RiskAssessment struct {
Level configuration.RiskLevel
// IsHardBlock is true for critical-tier operations that no approval can
// override (rm -rf /, fork bombs, mkfs).
IsHardBlock bool
RequiresIntentConfirmation bool
Sources []RiskSource
Reason string
// PathTier and FileMode are structured fields for file-touching tools.
PathTier PathTier
FileMode string
}
RiskAssessment is the canonical, single-vocabulary verdict for a tool call.
func AssessmentFromClassifier ¶
func AssessmentFromClassifier(res tools.SecurityResult) RiskAssessment
AssessmentFromClassifier maps a static-classifier SecurityResult onto the canonical scale: SAFE→Low, CAUTION→Medium, DANGEROUS→High, hard-block→Critical.
func AssessmentFromPersonaCascade ¶
func AssessmentFromPersonaCascade(level configuration.RiskLevel, reason string) RiskAssessment
AssessmentFromPersonaCascade builds an assessment from the persona/risk- profile cascade's RiskLevel verdict for a command.
func ResolveToolRisk ¶
func ResolveToolRisk(a RiskAgent, toolName string, args map[string]interface{}) RiskAssessment
func (RiskAssessment) Combine ¶
func (ra RiskAssessment) Combine(other RiskAssessment) RiskAssessment
Combine folds two assessments into one, taking the most restrictive Level. Critical always hard-blocks. Preserves PathTier and FileMode from the original.
func (RiskAssessment) Explain ¶
func (ra RiskAssessment) Explain() string
Explain renders a one-line human-readable summary of the assessment for diagnostics ("why was this gated?"). Sources are listed alphabetically for a stable rendering regardless of combination order.
type RiskSource ¶
type RiskSource string
RiskSource identifies which check contributed to an assessment.
const ( RiskSourceClassifier RiskSource = "classifier" RiskSourcePersonaCascade RiskSource = "persona-cascade" RiskSourceCriticalOp RiskSource = "critical-op" RiskSourceGitHistoryRewrite RiskSource = "git-history-rewrite" RiskSourceGitRebase RiskSource = "git-rebase" RiskSourceGitWrite RiskSource = "git-write" RiskSourceFSTier RiskSource = "fs-tier" RiskSourceWorkspacePolicy RiskSource = "workspace-policy" RiskSourceHandler RiskSource = "handler" RiskSourcePasswordPrompter RiskSource = "password-prompter" )
func MergeRiskSources ¶
func MergeRiskSources(a, b []RiskSource) []RiskSource
MergeRiskSources concatenates two source lists, de-duplicating while preserving first-seen order so Explain() reads deterministically.
type SecurityAnalysis ¶
type SecurityAnalysis struct {
// Summary is a one-sentence plain-language description of what the
// command does. Required.
Summary string `json:"summary"`
// Modifies lists files, directories, or system resources the command
// touches (e.g. "No local files; executes arbitrary code from URL").
Modifies string `json:"modifies"`
// RiskAssessment is one of "low", "moderate", "high" — the LLM's own
// assessment. Independent of (often overrides) the static classifier.
RiskAssessment string `json:"risk_assessment"`
// Recommendation is one of "approve", "review", "reject".
Recommendation string `json:"recommendation"`
// ChainLength is the number of subcommands in the analyzed chain.
// 0 means single-command path or analyzer didn't run.
ChainLength int `json:"chain_length,omitempty"`
// ChainSubcommands are the per-subcommand strings, in order. Used by the UI stepper.
ChainSubcommands []string `json:"chain_subcommands,omitempty"`
// ChainClassifications holds the per-subcommand risk classification for the stepper dots.
ChainClassifications []string `json:"chain_classifications,omitempty"`
}
SecurityAnalysis is the structured output of AnalyzeShellCommand.
func AnalyzeChain ¶
func AnalyzeChain(ctx context.Context, client api.ClientInterface, model string, chain Chain, classifications []agenttools.ChainedClassification, cwd string) (*SecurityAnalysis, error)
AnalyzeChain analyzes a command chain using the LLM. Single subcommands use the single-command prompt; chains up to MaxChainSubcommandsForBatchPrompt use the chain-aware prompt with per-subcommand classifications; longer chains fall back to per-subcommand analyses via AnalyzeChainFallback.
func AnalyzeChainFallback ¶
func AnalyzeChainFallback(ctx context.Context, client api.ClientInterface, model string, chain Chain, classifications []agenttools.ChainedClassification, cwd string) (*SecurityAnalysis, error)
AnalyzeChainFallback handles chains longer than MaxChainSubcommandsForBatchPrompt. It runs per-subcommand single-command analysis on each subcommand and synthesizes a single SecurityAnalysis: max severity, worst recommendation, deduped modifies.
func AnalyzeShellCommand ¶
func AnalyzeShellCommand(ctx context.Context, client api.ClientInterface, model string, command, cwd string) (*SecurityAnalysis, error)
AnalyzeShellCommand sends a shell command to the agent's LLM for plain-language analysis.
type SecurityAnalysisCache ¶
type SecurityAnalysisCache struct {
// contains filtered or unexported fields
}
SecurityAnalysisCache caches LLM security analyses keyed by command string.
func NewSecurityAnalysisCache ¶
func NewSecurityAnalysisCache() *SecurityAnalysisCache
NewSecurityAnalysisCache creates an empty cache.
func (*SecurityAnalysisCache) Clear ¶
func (c *SecurityAnalysisCache) Clear()
Clear resets the cache to empty.
func (*SecurityAnalysisCache) Get ¶
func (c *SecurityAnalysisCache) Get(normalizedKey string) (*SecurityAnalysis, bool)
Get returns the cached analysis for a normalized key, or false if not found.
func (*SecurityAnalysisCache) Set ¶
func (c *SecurityAnalysisCache) Set(normalizedKey string, sa *SecurityAnalysis)
Set stores an analysis under a normalized key.