Skip to content

fix(agentloop): wire AllowedPaths into sandbox MountDirs for bash access#223

Merged
birdmanmandbir merged 5 commits intomainfrom
worker/fix-ttal-explore-project
Mar 12, 2026
Merged

fix(agentloop): wire AllowedPaths into sandbox MountDirs for bash access#223
birdmanmandbir merged 5 commits intomainfrom
worker/fix-ttal-explore-project

Conversation

@birdmanmandbir
Copy link
Contributor

Summary

  • ttal explore --project bash tool was blocked by seatbelt sandbox — the project directory had no file-read* rules
  • Root cause: cfg.AllowedPaths was never converted to sandbox.Mount entries in ExecConfig when building it in loop.go
  • Fix: convert each AllowedPaths entry to a read-only sandbox.Mount (Source == Target, as required by seatbelt)

Changes

  • pkg/agentloop/loop.go: populate ExecConfig.MountDirs from cfg.AllowedPaths with ReadOnly: true
  • pkg/agentloop/loop_test.go: add TestRun_AllowedPathsInMountDirs — verifies mounts are present in the context ExecConfig after Run()

Test plan

  • TestRun_AllowedPathsInMountDirs fails before fix, passes after
  • Full test suite passes (make test)
  • Lint clean (make ci)

All subagents need bash to run git diff, flicknote get, ls, etc.
The ttal tools config was missing it while claude-code config had it.
Convert cfg.AllowedPaths into read-only sandbox.Mount entries when building
ExecConfig so the seatbelt sandbox policy includes file-read* rules for project
directories. Previously bash commands were denied filesystem access to project
dirs because MountDirs was never populated.
@birdmanmandbir
Copy link
Contributor Author

PR Review

Scope: Changes in this PR only — pkg/agentloop/loop.go, loop_test.go, and agent template tool additions.

Critical Issues

None.

Important Issues

[loop.go:80-84] make([]sandbox.Mount, len(...)) produces non-nil empty slice when AllowedPaths is nil

When cfg.AllowedPaths is nil/empty, make([]sandbox.Mount, 0) sets MountDirs to a non-nil empty slice rather than nil. Downstream consumers checking cfg.MountDirs != nil (vs len > 0) would get a misleading truthy result for URL/web modes where no paths are configured. Idiomatic Go fix:

var mounts []sandbox.Mount
for _, p := range cfg.AllowedPaths {
    mounts = append(mounts, sandbox.Mount{Source: p, Target: p, ReadOnly: true})
}

[loop.go:80-84] No validation of AllowedPaths entries before mount construction

The current callers always pass validated absolute paths, but Config.AllowedPaths is a public API field. An empty string or relative path would silently produce a malformed Mount that causes a cryptic sandbox failure at runtime (bwrap exits non-zero, seatbelt injects a blank -D param) with no Go-level error returned. Pair with the guard pattern already used in Run:

for _, p := range cfg.AllowedPaths {
    if p == "" || !filepath.IsAbs(p) {
        return nil, fmt.Errorf("agentloop: AllowedPaths entry %q must be a non-empty absolute path", p)
    }
}

Suggestions

[loop_test.go] Missing test for nil/empty AllowedPaths

TestRun_AllowedPathsInMountDirs covers the two-path case well, but there's no test asserting that AllowedPaths: nil produces a working ExecConfig (no mounts, no panic). Worth adding given the nil-vs-empty-slice fix above.

[loop_test.go] No combined SandboxEnv + AllowedPaths test

The existing TestRun_SandboxEnvInContext and the new TestRun_AllowedPathsInMountDirs test each field in isolation. A single test verifying both fields survive the construction of execCfg together would guard against future refactors that drop one field.

Positive Observations

  • The capture-tool pattern (injecting a fantasy.AgentTool that reads ExecConfigFromContext) is an excellent way to test context propagation through the full Run loop without mocking the sandbox.
  • Source: p, Target: p is correct and satisfies the buildPolicy constraint that Source != Target is an error (policy.go line 35–39).
  • ReadOnly: true is appropriate for all current AllowedPaths callers (project/repo exploration).
  • The agent template bash tool additions are consistent across all 7 agent definitions.

VERDICT: LGTM

The core fix is correct. The two important issues above are worth addressing — the nil-vs-empty-slice is a quick one-liner and the validation guard matches the existing style in Run. The test additions are solid.

… empty slice

- Replace make([]sandbox.Mount, len(...)) with var + append so nil AllowedPaths
  produces nil MountDirs (not a non-nil empty slice)
- Validate each AllowedPaths entry is a non-empty absolute path before constructing
  mounts — returns an early error matching the existing guard pattern in Run()
- Add tests: nil AllowedPaths, invalid path errors, combined SandboxEnv+AllowedPaths
@birdmanmandbir
Copy link
Contributor Author

Triage Update

Fixed

  • make([]sandbox.Mount, len(...)) non-nil empty slice — replaced with var + append so nil AllowedPaths produces nil MountDirs (e83bd7c)
  • No validation of AllowedPaths entries — added early return error for empty or relative paths, matching existing guard pattern in Run() (e83bd7c)
  • Missing nil/empty AllowedPaths test — added TestRun_NilAllowedPathsProducesNilMounts and TestRun_InvalidAllowedPathReturnsError (e83bd7c)
  • No combined SandboxEnv + AllowedPaths test — added TestRun_SandboxEnvAndAllowedPathsBothWired (e83bd7c)
  • Duplicated stream helper logic (dupl lint) — extracted makeCaptureTool and toolCallThenDoneStream helpers (456017b)

@birdmanmandbir birdmanmandbir merged commit 4574169 into main Mar 12, 2026
1 check passed
@birdmanmandbir birdmanmandbir deleted the worker/fix-ttal-explore-project branch March 12, 2026 14:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant