security

package
v0.17.20 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultTimeout = 30 * time.Minute

DefaultTimeout is the maximum time a request will block waiting for a webui response before applying the fallback (reject for tool kind, defaultResponse for prompt kind).

Set generously (30 min) so a user who steps away from an interactive session can still return and approve — a false-deny after 5 minutes was a recurring UX complaint. Non-interactive runs should not hit this path at all (they fast-fail before calling the manager), so the long timeout has no downside for automation.

Variables

View Source
var (
	// SetGlobalPromptManager is a backward-compatible alias for SetGlobalApprovalManager.
	SetGlobalPromptManager = SetGlobalApprovalManager
	// GetGlobalPromptManager is a backward-compatible alias for GetGlobalApprovalManager.
	GetGlobalPromptManager = GetGlobalApprovalManager
)

Backward-compatible aliases so existing code referencing the old names continues to compile during the migration window.

Functions

func CheckAllSymlinks(configDir string) []string

CheckAllSymlinks checks all files in the config directory for symlinks pointing outside.

func CheckFileSecurity deprecated

func CheckFileSecurity(
	relativePath string,
	fileContent string,
	isNew bool,
	isChanged bool,
	existingSecurityConcerns []string,
	existingIgnoredSecurityConcerns []string,
	cfg *configuration.Config,
	eventBus *events.EventBus,
	userID string,
	promptManager *ApprovalManager,
) (
	updatedSecurityConcerns []string,
	updatedIgnoredSecurityConcerns []string,
	skipLLMSummarization bool,
)

CheckFileSecurity analyzes a file's content for security concerns, prompts the user for confirmation on new detections, and returns the updated lists of security concerns and ignored concerns, along with a boolean indicating if local summarization should be skipped.

Deprecated: This function has no callers. Use Agent.CheckFileContentSecurity instead, which uses the injected ApprovalManager.

func CheckSymlinkSafety

func CheckSymlinkSafety(path string, configDir string) string

CheckSymlinkSafety checks if a path is a symlink and warns if it points outside the config dir.

func DetectSecurityConcerns

func DetectSecurityConcerns(content string) ([]string, map[string]string)

DetectSecurityConcerns analyzes content for security-related patterns and returns a sorted, deduplicated list of concern types plus a map from each concern type to its first matched snippet.

func DetectSecurityConcernsWithContext

func DetectSecurityConcernsWithContext(content, filePath string) ([]string, map[string]string)

DetectSecurityConcernsWithContext analyzes content with file-path context to reduce false positives. The path is consulted via isTestFile to apply stricter filtering in obvious test/example files.

func GetDirMode

func GetDirMode(path string) (string, error)

GetDirMode returns the permission bits of a directory as an octal string.

func GetFileMode

func GetFileMode(path string) (string, error)

GetFileMode returns the permission bits of a file as an octal string.

func GetPermissionError

func GetPermissionError(path string, expectedMode os.FileMode) error

GetPermissionError returns a descriptive error for common permission issues.

func IsGroupReadable

func IsGroupReadable(path string) (bool, error)

IsGroupReadable returns true if the file has group-readable permissions.

func IsWorldReadable

func IsWorldReadable(path string) (bool, error)

IsWorldReadable returns true if the file has world-readable permissions.

func RunStartupCheck

func RunStartupCheck(configDir string) bool

RunStartupCheck performs a full permission check at startup and logs warnings.

First call attempts to fix insecure permissions automatically, then checks again and warns only about issues that persist after the fix attempt. Subsequent calls return the cached result (deduplication across callers). The configDir argument is only consulted on the first call; both the CLI and WebUI callers pass configuration.GetConfigDir(), so this is safe. Returns true if any warnings were issued.

func SetGlobalApprovalManager deprecated

func SetGlobalApprovalManager(mgr *ApprovalManager)

SetGlobalApprovalManager sets the global singleton (called by webui setup).

Deprecated: use dependency injection via Agent.InjectWebUIManagers instead.

Types

type ApprovalDecision

type ApprovalDecision int

ApprovalDecision is the user's choice from the 4-option approval dialog (SP-058 follow-up). The classic yes/no path collapses to ApprovalDeny or ApprovalApproveOnce so legacy bool wrappers still work; the two extra outcomes — ApprovalApproveAlways and ApprovalElevate — power the "always approve this command" and "elevate session permissions" buttons added to the WebUI dialog and the CLI prompt.

const (
	// ApprovalDeny rejects the operation. Caller surfaces a security error.
	ApprovalDeny ApprovalDecision = iota
	// ApprovalApproveOnce approves this single invocation. Subsequent
	// invocations of the same command still prompt.
	ApprovalApproveOnce
	// ApprovalApproveAlways approves this invocation AND persists the
	// exact command string to Config.ApprovedShellCommands so future
	// runs (across restarts) skip the prompt for the same literal command.
	ApprovalApproveAlways
	// ApprovalElevate approves this invocation AND sets the agent's
	// risk profile override to "permissive" for the rest of this
	// session. Critical-tier ops still block. Persistence is session
	// only — the CLI/WebUI should tell the user to use /risk-profile
	// permissive if they want this to survive restart.
	ApprovalElevate

	// ApprovalAllowFolderSession is the filesystem-tier-B outcome:
	// approve this invocation AND add the prompt's target folder to
	// the agent's session-allowed folder list (in-memory). Subsequent
	// accesses under that folder skip the prompt for the rest of the
	// session. The folder is conveyed via the approval request's
	// `folder` extra so the caller knows which path to record.
	ApprovalAllowFolderSession

	// ApprovalAlwaysAsk approves this invocation AND persists the
	// command as an "ask" rule in Config.CommandPolicies so future
	// matching commands always force an interactive prompt, even in
	// permissive mode or when the classifier says SAFE. SP-123-2b.
	ApprovalAlwaysAsk
)

func ApprovalDecisionFromString

func ApprovalDecisionFromString(s string) ApprovalDecision

ApprovalDecisionFromString parses a wire-format decision string back into the typed enum. Unknown values resolve to ApprovalDeny for safety.

func (ApprovalDecision) Approved

func (d ApprovalDecision) Approved() bool

Approved reports whether the operation should proceed (any non-Deny decision approves the current invocation).

func (ApprovalDecision) String

func (d ApprovalDecision) String() string

String returns a stable lowercase identifier for the decision, used in event payloads and tests.

type ApprovalKind

type ApprovalKind int

ApprovalKind distinguishes between the two approval flows that share this manager. The kind determines the event type published, the request ID prefix, and the default response used on timeout / nil event-bus.

const (
	// ApprovalKindTool is used for tool execution approval (shell_command,
	// write_file, git ops, etc.). Timeout or nil event-bus rejects for safety.
	ApprovalKindTool ApprovalKind = iota

	// ApprovalKindPrompt is used for file content security prompts (API keys,
	// passwords found in files). Timeout or nil event-bus returns DefaultResponse.
	ApprovalKindPrompt
)

type ApprovalManager

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

ApprovalManager coordinates security approval requests between the agent and the webui. It subsumes the former SecurityApprovalManager (tool approvals) and SecurityPromptManager (file security prompts) into a single unified manager, eliminating duplicated infrastructure.

The manager is safe for concurrent use. Requests block until a response is received from the webui, a timeout elapses, or the event bus is nil.

func GetGlobalApprovalManager deprecated

func GetGlobalApprovalManager() *ApprovalManager

GetGlobalApprovalManager returns the global singleton.

Deprecated: use dependency injection via Agent.InjectWebUIManagers instead.

func NewApprovalManager

func NewApprovalManager() *ApprovalManager

NewApprovalManager creates a new ApprovalManager with the default timeout.

func (*ApprovalManager) RequestApproval

func (am *ApprovalManager) RequestApproval(eventBus *events.EventBus, req ApprovalRequest) bool

RequestApproval publishes a security approval/prompt event to the event bus and blocks until the webui responds, a timeout elapses, or the event bus is nil.

For ToolKind: returns true only if explicitly approved; false on rejection, timeout, or nil event-bus. For PromptKind: returns the user response, or DefaultResponse on timeout / nil event-bus.

Callers that need the richer 4-option outcome (ApproveAlways / Elevate) should call RequestApprovalDecision; this wrapper collapses to bool.

func (*ApprovalManager) RequestApprovalDecision

func (am *ApprovalManager) RequestApprovalDecision(eventBus *events.EventBus, req ApprovalRequest) ApprovalDecision

RequestApprovalDecision is the same as RequestApproval but returns the full ApprovalDecision so the caller can distinguish ApproveOnce from ApproveAlways and Elevate. The 4-option UI is currently only wired for shell_command Gate 1/2 callers; everyone else collapses through the bool wrapper above.

func (*ApprovalManager) RequestApprovalDecisionWithOutcome added in v0.16.7

func (am *ApprovalManager) RequestApprovalDecisionWithOutcome(eventBus *events.EventBus, req ApprovalRequest) (ApprovalDecision, ApprovalOutcome)

RequestApprovalDecisionWithOutcome is the same as RequestApprovalDecision but also returns an ApprovalOutcome so the caller can tell whether the user actually answered (Responded) or the request fell back to its safe default via timeout / missing channel. Callers that have an alternate approval surface (e.g. a terminal prompt) use this to avoid treating an unanswered browser dialog as a deliberate deny.

func (*ApprovalManager) RequestPrompt

func (am *ApprovalManager) RequestPrompt(eventBus *events.EventBus, userID, prompt string, defaultResponse bool, extras map[string]string) bool

RequestPrompt is a convenience wrapper for ApprovalKindPrompt requests. It preserves the original SecurityPromptManager.RequestPrompt signature.

func (*ApprovalManager) RequestToolApproval

func (am *ApprovalManager) RequestToolApproval(eventBus *events.EventBus, clientID, userID, toolName, riskLevel, reasoning string, extras map[string]string) bool

RequestToolApproval is a convenience wrapper for ApprovalKindTool requests. It preserves the original SecurityApprovalManager.RequestApproval signature.

func (*ApprovalManager) RequestToolApprovalDecision

func (am *ApprovalManager) RequestToolApprovalDecision(eventBus *events.EventBus, clientID, userID, toolName, riskLevel, reasoning string, extras map[string]string) ApprovalDecision

RequestToolApprovalDecision is the 4-option variant: it returns the full ApprovalDecision so callers can react to "always approve" and "elevate" choices. Wire payload is the same SecurityApprovalRequest; the WebUI dialog opts into the extra buttons via the matching response schema (action field).

func (*ApprovalManager) RequestToolApprovalDecisionWithOutcome added in v0.16.7

func (am *ApprovalManager) RequestToolApprovalDecisionWithOutcome(eventBus *events.EventBus, clientID, userID, toolName, riskLevel, reasoning string, extras map[string]string) (ApprovalDecision, ApprovalOutcome)

RequestToolApprovalDecisionWithOutcome is the outcome-aware variant of RequestToolApprovalDecision (the 4-option Gate 2 path).

func (*ApprovalManager) RequestToolApprovalWithOutcome added in v0.16.7

func (am *ApprovalManager) RequestToolApprovalWithOutcome(eventBus *events.EventBus, clientID, userID, toolName, riskLevel, reasoning string, extras map[string]string) (bool, ApprovalOutcome)

RequestToolApprovalWithOutcome is the outcome-aware variant of RequestToolApproval: it returns whether the operation was approved AND how the request resolved (Responded / TimedOut / NoChannel), so a caller with a terminal fallback can avoid treating an unanswered browser dialog as a deny.

func (*ApprovalManager) RespondToApproval

func (am *ApprovalManager) RespondToApproval(requestID string, response bool) bool

RespondToApproval resolves a pending request with a boolean (legacy path: collapses to ApprovalApproveOnce or ApprovalDeny). Returns true if the request existed and was responded to.

func (*ApprovalManager) RespondToApprovalDecision

func (am *ApprovalManager) RespondToApprovalDecision(requestID string, decision ApprovalDecision) bool

RespondToApprovalDecision resolves a pending request with the full 4-option decision. The WebUI handler uses this to forward the user's "always approve" or "elevate" choice; the bool wrapper RespondToApproval stays for any caller (CLI bridge, tests) that only knows yes/no.

func (*ApprovalManager) RespondToPrompt

func (am *ApprovalManager) RespondToPrompt(requestID string, response bool) bool

RespondToPrompt is a backward-compatible alias for RespondToApproval.

func (*ApprovalManager) SetApprovalTimeout

func (am *ApprovalManager) SetApprovalTimeout(d time.Duration)

SetApprovalTimeout is a backward-compatible alias for SetTimeout.

func (*ApprovalManager) SetPromptTimeout

func (am *ApprovalManager) SetPromptTimeout(d time.Duration)

SetPromptTimeout is a backward-compatible alias for SetTimeout.

func (*ApprovalManager) SetTimeout

func (am *ApprovalManager) SetTimeout(d time.Duration)

SetTimeout sets the maximum duration requests will block. A zero or negative value resets to the default.

type ApprovalOutcome added in v0.16.7

type ApprovalOutcome int

ApprovalOutcome reports HOW an approval request resolved, independent of the decision itself. It lets callers distinguish a deliberate user "deny" from a dialog that was never answered — so an unattended browser tab can fall back to the terminal prompt instead of dead-ending on a 5-minute timeout that silently denies.

const (
	// ApprovalOutcomeResponded — the user (browser) actually answered.
	// The accompanying decision is authoritative; honor it.
	ApprovalOutcomeResponded ApprovalOutcome = iota
	// ApprovalOutcomeTimedOut — no answer arrived within the timeout
	// window. The decision is the safe default; callers may retry on
	// another surface.
	ApprovalOutcomeTimedOut
	// ApprovalOutcomeNoChannel — the event bus was nil or the response
	// channel closed without a reply (e.g. the browser disconnected).
	// The decision is the safe default; callers may retry on another surface.
	ApprovalOutcomeNoChannel
)

type ApprovalRequest

type ApprovalRequest struct {
	Kind ApprovalKind

	// On timeout / nil event-bus: PromptKind returns DefaultResponse;
	// ToolKind always returns false (reject for safety).
	DefaultResponse bool

	// Tool kind fields
	ToolName  string
	RiskLevel string
	Reasoning string
	ClientID  string
	UserID    string // User ID for multi-tenant isolation

	// Prompt kind fields
	Prompt string

	// Shared extras forwarded to the event payload.
	Extras map[string]string
}

ApprovalRequest captures all parameters for a single approval request.

type DetectedSecret

type DetectedSecret struct {
	Type    string // e.g. "Env Var Value", "Bearer Token", "API Key"
	Snippet string // Matched text, truncated to ~40 chars for display
	Line    int    // 1-based line number where found (0 if not line-scannable)
}

DetectedSecret describes a single secret found in tool output.

type ElevationGate

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

ElevationGate manages user elevation decisions for secret detection. It maintains two session-scoped vaults:

  • vault: per-secret decisions keyed by type + first 8 chars of snippet
  • sourceAllow: whole-source allowlist (e.g. "read_file: /tmp/example.html")

The user is prompted only once per unique secret pattern OR once per source (whichever they choose).

SAFETY: A single Gate instance is used from one agent goroutine (sequential tool execution driven by seed core.ToolRegistry via pkg/agent/processQueryWithSeed), so no mutex is required. If parallel tool execution is ever introduced, this type must be protected with a sync.Mutex or sync.RWMutex.

func NewElevationGate

func NewElevationGate(prompter SecretPrompter) *ElevationGate

NewElevationGate creates a gate that consults prompter for new detections. Pass nil to default to SecretRedact for uncached secrets (subagent usage).

func (*ElevationGate) Evaluate

func (g *ElevationGate) Evaluate(secrets []DetectedSecret, source string) (SecretAction, error)

Evaluate checks detected secrets against the session vault, prompts for any new ones, and returns the aggregated action to take.

Returns SecretAllow when no secrets were found OR the source has been whitelisted; otherwise the strictest action (any SecretBlock wins, then any SecretRedact, then SecretAllow).

func (*ElevationGate) IsSourceAllowed

func (g *ElevationGate) IsSourceAllowed(source string) bool

IsSourceAllowed reports whether the user has whitelisted this source for the rest of the session. Exported for use by callers that want to skip detection entirely on whitelisted sources.

func (*ElevationGate) SetDefault

func (g *ElevationGate) SetDefault(action SecretAction, envVarName string)

SetDefault pre-seeds the vault for known patterns. Pass envVarName="" for a global catch-all, or e.g. "CommitSecret" to match vault keys of that type.

type OutputRedactor

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

OutputRedactor scans tool output for secrets using two strategies:

  1. Environment value matching — literal secret values from env vars
  2. Pattern matching — regex-based detection of common credential formats

func NewOutputRedactor

func NewOutputRedactor() *OutputRedactor

NewOutputRedactor constructs an OutputRedactor by scanning os.Environ() for env vars whose names suggest they hold credentials (per credentials.IsSensitiveEnvName).

func (*OutputRedactor) RedactFileContent

func (r *OutputRedactor) RedactFileContent(content string, filePath string) RedactionResult

RedactFileContent is a convenience wrapper equivalent to RedactToolOutput but with toolName set to "read_file" and filePath attached for context.

func (*OutputRedactor) RedactToolOutput

func (r *OutputRedactor) RedactToolOutput(output string, toolName string, toolArgs map[string]interface{}) RedactionResult

RedactToolOutput scans tool output for secrets, returning a redacted version and any secrets detected. The toolName and toolArgs are logged but do not change redaction behaviour per tool; they are reserved for future context-aware filtering.

type PermissionChecker

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

PermissionChecker provides utilities to check file and directory permissions for security-sensitive files in the sprout configuration directory.

func NewPermissionChecker

func NewPermissionChecker(configDir string) *PermissionChecker

NewPermissionChecker creates a new PermissionChecker for the given config directory.

func (*PermissionChecker) CheckAllSecurityFiles

func (pc *PermissionChecker) CheckAllSecurityFiles() []string

CheckAllSecurityFiles checks all security-sensitive files in the config directory. Returns a list of warning messages for files with insecure permissions.

func (*PermissionChecker) CheckConfigDirPermissions

func (pc *PermissionChecker) CheckConfigDirPermissions() string

CheckConfigDirPermissions checks that the config directory has secure permissions (0700). Returns a warning message if permissions are too open.

func (*PermissionChecker) CheckFilePermissions

func (pc *PermissionChecker) CheckFilePermissions(filePath string) string

CheckFilePermissions checks that a file has secure permissions (0600). Returns a warning message if permissions are too open.

func (*PermissionChecker) FixPermissions

func (pc *PermissionChecker) FixPermissions() []error

FixPermissions attempts to fix insecure permissions on security-sensitive files. Returns a list of errors encountered.

type RedactionResult

type RedactionResult struct {
	Content string           // The (potentially) redacted output
	Secrets []DetectedSecret // What was found (if any)
}

RedactionResult holds the output after redaction and any secrets detected.

type SecretAction

type SecretAction int

SecretAction represents the user's decision for a detected secret.

const (
	SecretRedact      SecretAction = iota // Replace secret with [REDACTED] and continue
	SecretAllow                           // Pass through as-is (just this batch)
	SecretBlock                           // Stop the operation entirely
	SecretAllowSource                     // Pass through as-is AND whitelist the source for the rest of the session
)

type SecretPrompter

type SecretPrompter interface {
	// PromptSecretAction presents detected secrets to the user and returns
	// their decision. Returns error if prompting fails (e.g., non-interactive).
	PromptSecretAction(secrets []DetectedSecret, source string) (SecretAction, error)
}

SecretPrompter is implemented by the agent layer to ask the user what to do when secrets are detected. Defined here (in security) to avoid circular imports with the agent package.

Jump to

Keyboard shortcuts

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