Documentation
¶
Overview ¶
ABOUTME: ExecutionEnvironment interface abstracting where agent tools run. ABOUTME: Enables local execution (default) with future extensibility to Docker/SSH/K8s.
ABOUTME: Cross-platform pure Go helpers for the writable_paths fs-jail (issue #272). ABOUTME: ValidateWritablePaths runs at session setup before any syscall jail mechanism.
ABOUTME: Shared error sentinels for the writable_paths fs-jail (issue #272). ABOUTME: Used by both Linux jail implementation and non-Linux passthrough stubs.
ABOUTME: LocalEnvironment implements ExecutionEnvironment for local filesystem and process execution. ABOUTME: Enforces path containment within the working directory to prevent traversal attacks.
Index ¶
- Variables
- func OpenForWrite(anchor, relPath string, perm os.FileMode) (*os.File, error)
- func ProbeLandlock() error
- func RunJailExec(args []string) int
- func SafeMkdirAll(anchor, relDir string, perm os.FileMode) error
- func SafeRemove(anchor, relPath string) error
- func ValidateWritablePaths(workingDir string, globs []string, processCwd string) error
- func WrapBashCmd(cmd *exec.Cmd, anchor string, writable []string) *exec.Cmd
- type CommandResult
- type ExecutionEnvironment
- type LocalEnvironment
- func (e *LocalEnvironment) ExecCommand(ctx context.Context, command string, args []string, timeout time.Duration) (CommandResult, error)
- func (e *LocalEnvironment) ExecCommandWithLimit(ctx context.Context, command string, args []string, timeout time.Duration, ...) (CommandResult, error)
- func (e *LocalEnvironment) Glob(ctx context.Context, pattern string) ([]string, error)
- func (e *LocalEnvironment) ReadFile(ctx context.Context, path string) (string, error)
- func (e *LocalEnvironment) RemoveFile(ctx context.Context, path string) error
- func (e *LocalEnvironment) WorkingDir() string
- func (e *LocalEnvironment) WriteFile(ctx context.Context, path string, content string) error
Constants ¶
This section is empty.
Variables ¶
ErrLandlockUnavailable is returned by ProbeLandlock when the host kernel doesn't support Landlock ABI v3 (kernel 6.7+), or when the binary is built for a non-Linux target. The codergen handler refuses to start a session with non-empty WritablePaths on this error.
var ErrPathEscape = errors.New("write path escapes session root")
ErrPathEscape is returned by OpenForWrite when the requested path resolves outside the session anchor (via absolute path, parent traversal, or symlink escape). The kernel returns EXDEV/ELOOP for openat2 with RESOLVE_BENEATH; the helper translates to this sentinel for typed handling upstream.
var ErrPathNotAllowed = errors.New("write path not in writable_paths")
ErrPathNotAllowed is returned by OpenForWrite when the resolved path is beneath the session anchor but does not match any writable_paths glob.
Functions ¶
func OpenForWrite ¶
OpenForWrite opens (or creates + truncates) a file under anchor for writing, using openat2(2) with RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS. The kernel binds path resolution to anchorFD; symlink chains rejected at the syscall — no userspace TOCTOU window.
Returns ErrPathEscape (wrapped) when the kernel returns EXDEV / ELOOP indicating the resolved path is outside anchor. EACCES is propagated as a regular permission error — it can be a real escape signal, but also fires for ordinary mode/ownership issues, so we don't claim escape unless the kernel's resolve-specific errno was returned (#275 review, Copilot jail_linux.go:113).
Used by LocalEnvironment.WriteOpener when SessionConfig.WritablePaths is non-empty. The codergen handler (Task 14) installs the configured OpenForWrite closure on the env. Closes the parallel-branch symlink race vector documented in spec D6.
func ProbeLandlock ¶
func ProbeLandlock() error
ProbeLandlock verifies the host kernel supports Landlock ABI v3 (kernel 6.7+, June 2023). Called eagerly at session setup. Failure = refuse-to-start.
ABI v3 brings LANDLOCK_ACCESS_FS_REFER (hardlinks across rulesets) and LANDLOCK_ACCESS_FS_TRUNCATE; both are needed for the spec's "Bash + children bounded" contract. Strict — no BestEffort fallback.
Uses the non-destructive landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION) probe. The VERSION flag causes the kernel to return the highest supported ABI version number rather than creating a ruleset FD, so this call has no side effects on the calling process.
func RunJailExec ¶
RunJailExec is the entry point for the `tracker __jail-exec` subcommand. Argv shape (already stripped of "__jail-exec" by cmd/tracker/main.go's dispatch in Task 12):
-- <anchor> <glob1> <glob2> ... -- <cmd> <args>...
The function:
- Parses argv into anchor + globs + command tail.
- Computes Landlock RWDirs from the static-prefix ancestor of each glob (per spec D2; Landlock is path-prefix on resolved paths, not glob-aware, so we bound at the directory ancestor of each glob's literal prefix).
- Applies Landlock ABI v3 to the current process. Strict — no BestEffort.
- syscall.Exec's into the command tail with the parent's environment. Landlock is preserved through exec; bash + all descendants are bounded.
Returns the process exit code on failure; on success (post-exec), this function does not return. Exit codes:
2 = argv parse failure 3 = landlock_restrict_self failure 4 = exec failure
func SafeMkdirAll ¶
SafeMkdirAll creates the directory tree rooted at anchor + relDir without following symlinks or procfs magic-links at any intermediate component. Each path component is resolved via openat2(RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS) against the running parent fd; missing components are created via mkdirat. A symlink at any intermediate path causes the resolution to fail with EXDEV/ELOOP, which surfaces as ErrPathEscape. Same Resolve flags as OpenForWrite so the in-process write seam and the on-the-side dir creation share one hardening contract (#275 review, Copilot jail_linux.go:282).
Closes the #275 review gap where os.MkdirAll inside the WriteOpener closure would follow agent-placed symlinks before openat2 saw the leaf path (Copilot codergen_jail.go:92).
func SafeRemove ¶
SafeRemove deletes the file at anchor + relPath without following symlinks or procfs magic-links at any intermediate path component. Uses openat2 to resolve the parent directory with RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKS, then unlinkat on the final component. A symlink anywhere in the parent chain causes EXDEV/ELOOP which surfaces as ErrPathEscape. Same Resolve flags as OpenForWrite / SafeMkdirAll for consistent hardening across the jail's destructive operations (#275 review, Copilot jail_linux.go:346).
Closes the #275 review gap where os.Remove inside env.Remover would follow agent-placed symlinks (Copilot codergen_jail.go:103).
func ValidateWritablePaths ¶
ValidateWritablePaths is the cross-platform gate that runs at session setup before the jail is wired. It catches three classes of refusal:
- working_dir escapes tracker's process cwd / session root (the "working_dir: /tmp/atk relocation" attack described in the spec § 8.1).
- The glob list is empty (fail-closed; a present-but-empty value is already rejected by dippin's parser, but tracker backstops).
- A glob entry is bad in some way (absolute, starts with ~, escapes via parent traversal, uses an unsupported shape, etc.). These are mostly caught by dippin DIP142, but tracker is the runtime backstop.
Error sentinels:
- ErrPathEscape wraps class-1 errors AND the escape-flavored class-3 subclass: absolute / `~` / parent-traversal / Windows-absolute / inward `..` segments / non-existent intermediate. errors.Is(err, ErrPathEscape) answers "did the operator (or attacker) try to point the jail outside the session root?"
- Plain (non-sentinel) errors carry the malformed-pattern class-3 cases: empty / brace usage / multiple ** / metachars before ** / glued `foo/**bar` / unbalanced character classes. These are "bad glob shape" rather than "escape attempt" — the codergen handler refuses to start on either, but the sentinel distinction lets upstream consumers differentiate if they care.
- Class-2 (empty list) is a plain error too.
func WrapBashCmd ¶
WrapBashCmd rewrites cmd's argv to invoke `/proc/self/exe __jail-exec` with the writable_paths jail rules, then the original command after a `--` separator.
The wrapped command runs in three stages:
- tracker re-execs itself as `tracker __jail-exec`.
- The __jail-exec child applies Landlock ABI v3 with the static-prefix ancestor directories of each writable_paths glob.
- The child syscall.Exec's into the original command (e.g. `sh -c <agentCmd>`), replacing its image. Landlock is preserved through exec; bash + all descendants are bounded.
Argv layout — the two `--` separators are unambiguous boundaries that RunJailExec's parseJailExecArgs (Task 11) splits on:
/proc/self/exe __jail-exec -- <anchor> <glob1> ... <globN> -- <origArgs...>
All other Cmd fields (Dir, Env, Stdin/Stdout/Stderr, SysProcAttr, ctx) are preserved by in-place mutation — the wrapper returns the same *Cmd.
Types ¶
type CommandResult ¶
type CommandResult struct {
Stdout string
Stderr string
ExitCode int
StdoutTruncated bool
StdoutBytesDropped int
StderrTruncated bool
StderrBytesDropped int
}
CommandResult holds the output and exit status of an executed command.
When the command was run via ExecCommandWithLimit and a stream exceeded the per-stream cap, StdoutTruncated / StderrTruncated are set and the matching BytesDropped field carries how many bytes were elided from the head of the stream. The captured strings always contain the tail of the stream up to the cap. Truncation flags default to false / 0 when the stream did not overflow or when ExecCommand (unbounded) was used.
type ExecutionEnvironment ¶
type ExecutionEnvironment interface {
ReadFile(ctx context.Context, path string) (string, error)
WriteFile(ctx context.Context, path string, content string) error
// RemoveFile deletes a file relative to the working directory. Used by
// tools that mutate the workspace (e.g. apply_patch's delete and
// move-cleanup paths). Implementations enforce the same containment
// rules as WriteFile so the writable_paths fs-jail (#272) can intercept
// destructive operations through a single seam.
RemoveFile(ctx context.Context, path string) error
ExecCommand(ctx context.Context, command string, args []string, timeout time.Duration) (CommandResult, error)
Glob(ctx context.Context, pattern string) ([]string, error)
WorkingDir() string
}
ExecutionEnvironment abstracts filesystem and process operations so that agent tools can run locally, in containers, or over SSH without changes.
type LocalEnvironment ¶
type LocalEnvironment struct {
// CommandWrapper, when non-nil, is applied to every *exec.Cmd that
// ExecCommand and ExecCommandWithLimit construct, after all standard
// fields (Dir, SysProcAttr, Cancel, WaitDelay) are set but before
// the command runs. The writable_paths fs-jail (issue #272) uses this
// to rewrite Bash invocations through tracker's __jail-exec self-re-exec,
// applying Linux Landlock before the agent command runs.
// Default nil — the environment behaves as before.
CommandWrapper func(*exec.Cmd) *exec.Cmd
// WriteOpener, when non-nil, replaces the os.WriteFile call in WriteFile.
// Receives the absolute path (already validated by safePath) and the
// file mode; returns an *os.File for writing. The writable_paths fs-jail
// sets this to an openat2-backed opener that enforces RESOLVE_BENEATH +
// RESOLVE_NO_SYMLINKS against a session-root file descriptor — the
// kernel atomic-checks the chain, closing the parallel-branch symlink
// race vector (spec D6).
// Default nil — WriteFile uses os.WriteFile as before.
WriteOpener func(abs string, perm os.FileMode) (*os.File, error)
// Remover, when non-nil, replaces the os.Remove call in RemoveFile.
// Receives the absolute path (already validated by safePath). The
// writable_paths fs-jail (#272) sets this with the same exact-glob
// check WriteOpener uses, so destructive operations (apply_patch's
// delete and move-cleanup paths) are bounded to the declared globs
// just like writes are.
// Default nil — RemoveFile uses os.Remove as before.
Remover func(abs string) error
// contains filtered or unexported fields
}
LocalEnvironment runs commands and accesses files on the local machine, scoped to a specific working directory.
func NewLocalEnvironment ¶
func NewLocalEnvironment(workDir string) *LocalEnvironment
NewLocalEnvironment creates a LocalEnvironment rooted at workDir. The path is resolved to an absolute path on creation.
func (*LocalEnvironment) ExecCommand ¶
func (e *LocalEnvironment) ExecCommand(ctx context.Context, command string, args []string, timeout time.Duration) (CommandResult, error)
ExecCommand runs a command with the given arguments and timeout. Non-zero exit codes are returned in CommandResult without an error. An error is returned only for timeouts or execution failures.
func (*LocalEnvironment) ExecCommandWithLimit ¶
func (e *LocalEnvironment) ExecCommandWithLimit(ctx context.Context, command string, args []string, timeout time.Duration, outputLimit int, env ...[]string) (CommandResult, error)
ExecCommandWithLimit runs a command with output capped at outputLimit bytes per stream. If outputLimit <= 0, output is unbounded (same as ExecCommand). Optional env parameter sets the subprocess environment (nil = inherit parent).
func (*LocalEnvironment) Glob ¶
Glob returns file paths matching a pattern relative to the working directory.
func (*LocalEnvironment) ReadFile ¶
ReadFile reads a file relative to the working directory and returns its contents.
func (*LocalEnvironment) RemoveFile ¶
func (e *LocalEnvironment) RemoveFile(ctx context.Context, path string) error
RemoveFile deletes a file relative to the working directory. The writable_paths fs-jail (#272) hooks here via Remover so destructive operations (apply_patch's delete and move-cleanup paths) are bounded to the declared globs.
func (*LocalEnvironment) WorkingDir ¶
func (e *LocalEnvironment) WorkingDir() string
WorkingDir returns the absolute path of the environment root.
func (*LocalEnvironment) WriteFile ¶
WriteFile writes content to a file relative to the working directory, creating intermediate directories as needed.
When WriteOpener is non-nil, the opener is solely responsible for both policy (e.g. writable_paths glob check) AND mkdir+open. The opener performs the policy check BEFORE any filesystem mutation so rejected writes leave no empty intermediate directories behind (#272 review, codex P2). The unjailed path (WriteOpener nil) does mkdir then os.WriteFile as before.