- 删除未接线的 exec.FileSandbox(含 5 个用例):文件沙箱从此只有 plugins/tools/io/file.go 一处实现 - io 侧取更严一侧:NUL 拒绝(原 io 缺)、写前按最深存在祖先判定落点(避免越界建目录)、roots 归化到真实路径(修 roots 自身是 symlink 时的全误拒) - exec/sandbox.go 保留并注释说明:那是**终端工作目录守卫**的原语(terminal.go 在用),不是文件沙箱 - 修假阳性:原 NUL 用例断言错误文本含 "NUL",而 t.TempDir() 目录名自带测试函数名 "…RejectsNULBytePath…"、错误又内嵌整条临时路径 ⇒ 删掉安全检查用例照样绿。改为哨兵错误 ErrNullByte + errors.Is(错误文本逐字节不变),修复后同一变异必红 - 同类排查:file_test.go 三处文本断言判定无风险(含空格的串不可能出现在临时目录名里),保留
132 lines
6.2 KiB
Go
132 lines
6.2 KiB
Go
// Package exec implements the unified local execution abstraction for the
|
|
// Cyrene agent runtime (DevDocs/design/04-运行时.md §8 and design/02-会话与上下文模型.md §4).
|
|
//
|
|
// A Provider is the grant-based, sandboxed entry point through which the agent
|
|
// may run code on its own host. This milestone ships one concrete piece:
|
|
//
|
|
// - [TerminalExecutor], the default command provider, which runs allowlisted
|
|
// commands inside a locked-down terminal sandbox (command allowlist, working
|
|
// directory confinement, environment allowlist, timeout, output truncation).
|
|
//
|
|
// 文件读写的路径沙箱不在本包:按裁决 13.4.4(design/04 §4),文件访问的 roots
|
|
// 收敛、symlink 逃逸与 NUL 拒绝统一由 plugins/tools/io 文件工具承载;本包内
|
|
// sandbox.go 的收敛原语只服务于终端沙箱的工作目录 / roots 守卫。
|
|
//
|
|
// GUI operations and a real virtual machine are reserved for later milestones;
|
|
// they surface only through the [Provider] interface and are not implemented
|
|
// here. The sandbox is DISABLED BY DEFAULT: an [TerminalExecutor] created from
|
|
// a closed config refuses every request with [ErrExecutionDisabled], so the
|
|
// agent gains the ability to run code only after the owner explicitly enables
|
|
// it (see config.ExecSandbox).
|
|
package exec
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// DefaultEnabled is the sandbox red line: execution is off unless the owner
|
|
// explicitly enables it via config.ExecSandbox.Enabled.
|
|
const DefaultEnabled = false
|
|
|
|
// DefaultTimeout is the per-request deadline used when ExecRequest.Timeout is
|
|
// zero. 30s balances responsiveness with enough room for a real command.
|
|
const DefaultTimeout = 30 * time.Second
|
|
|
|
// DefaultMaxOutputBytes is the combined stdout+stderr cap used when
|
|
// ExecRequest.MaxOutputBytes is zero (1 MiB).
|
|
const DefaultMaxOutputBytes = 1 << 20
|
|
|
|
// Sentinel errors returned by this package. Use errors.Is to test for them;
|
|
// they are always wrapped with %w so the underlying cause stays available.
|
|
var (
|
|
// ErrExecutionDisabled is returned when the sandbox is closed (config
|
|
// Enabled is false). This is the security red line: default off.
|
|
ErrExecutionDisabled = errors.New("exec: sandbox disabled for this role")
|
|
// ErrEmptyCommand is returned when ExecRequest.Command is empty.
|
|
ErrEmptyCommand = errors.New("exec: command is empty")
|
|
// ErrCommandNotAllowed is returned when argv[0] is not in the allowlist.
|
|
ErrCommandNotAllowed = errors.New("exec: command not in allowlist")
|
|
// ErrCommandInjection is returned when a command argument contains shell
|
|
// metacharacters, control characters, or a path-traversal component.
|
|
ErrCommandInjection = errors.New("exec: command argument rejected")
|
|
// ErrWorkDirOutsideRoot is returned when the requested working directory
|
|
// escapes the allowed roots.
|
|
ErrWorkDirOutsideRoot = errors.New("exec: working directory outside allowed roots")
|
|
// ErrEnvNotAllowed is returned when ExecRequest.Env names a variable outside
|
|
// the configured environment allowlist.
|
|
ErrEnvNotAllowed = errors.New("exec: environment variable not in allowlist")
|
|
// ErrTimeout is returned when the command exceeds its deadline and is killed.
|
|
ErrTimeout = errors.New("exec: execution timed out")
|
|
// ErrExecFailed is returned when a command could not be started or finished.
|
|
ErrExecFailed = errors.New("exec: process failed")
|
|
// ErrNoAllowedRoot is returned when an operation needs a root but none is
|
|
// configured.
|
|
ErrNoAllowedRoot = errors.New("exec: no allowed root configured")
|
|
// ErrEmptyPath is returned when a path to resolve is empty.
|
|
ErrEmptyPath = errors.New("exec: path is empty")
|
|
// ErrPathOutsideRoot is returned when a path escapes the allowed roots.
|
|
ErrPathOutsideRoot = errors.New("exec: path outside allowed roots")
|
|
// ErrSymlinkEscape is returned when a path resolves through a symlink that
|
|
// points outside the allowed roots.
|
|
ErrSymlinkEscape = errors.New("exec: path escapes root via symlink")
|
|
// ErrNullByte is returned when a path contains a NUL byte.
|
|
ErrNullByte = errors.New("exec: path contains NUL byte")
|
|
// ErrNilLogger is returned by New when no logger is supplied.
|
|
ErrNilLogger = errors.New("exec: nil logger")
|
|
)
|
|
|
|
// Provider is the unified local execution abstraction. It covers command
|
|
// execution now and, in later milestones, GUI operations and virtual machines,
|
|
// all behind the same grant-based, sandboxed interface.
|
|
//
|
|
// Implementations must honor the context, which carries cancellation and
|
|
// deadlines, and must bound their output to ExecRequest.MaxOutputBytes,
|
|
// reporting truncation via ExecResult.Truncated.
|
|
type Provider interface {
|
|
// Execute runs req and returns its outcome, or an error that is one of the
|
|
// sentinels above (or wraps one). A non-zero exit code is NOT an error: it
|
|
// is reported in ExecResult.ExitCode.
|
|
Execute(ctx context.Context, req ExecRequest) (*ExecResult, error)
|
|
}
|
|
|
|
// ExecRequest describes a single local execution.
|
|
type ExecRequest struct {
|
|
// Command is the argv vector. It must be non-empty; argv[0] is the command
|
|
// name that must appear in the allowlist.
|
|
Command []string
|
|
// WorkDir is the process working directory. It must resolve inside an
|
|
// allowed root. When empty, the provider defaults to the first allowed root.
|
|
WorkDir string
|
|
// Env carries extra environment to set on the process. Only names in the
|
|
// environment allowlist are accepted.
|
|
Env map[string]string
|
|
// Timeout bounds the run. Zero uses DefaultTimeout.
|
|
Timeout time.Duration
|
|
// Stdin is the optional stdin payload.
|
|
Stdin []byte
|
|
// MaxOutputBytes caps combined stdout+stderr. Zero uses
|
|
// DefaultMaxOutputBytes. Output past the cap is dropped and Truncated is set.
|
|
MaxOutputBytes int
|
|
}
|
|
|
|
// ExecResult is the outcome of an execution.
|
|
type ExecResult struct {
|
|
// Stdout is the captured standard output (truncated to MaxOutputBytes).
|
|
Stdout string
|
|
// Stderr is the captured standard error (truncated to MaxOutputBytes).
|
|
Stderr string
|
|
// ExitCode is the process exit code. -1 when terminated by a signal.
|
|
ExitCode int
|
|
// Truncated reports whether output was dropped because MaxOutputBytes was
|
|
// reached.
|
|
Truncated bool
|
|
// StartedAt is when the process started.
|
|
StartedAt time.Time
|
|
// EndedAt is when the process finished (or was killed).
|
|
EndedAt time.Time
|
|
// PID is the process identifier assigned by the OS.
|
|
PID int
|
|
}
|