Skip to content

A project-local config can no longer widen the sandbox fence - #164

Merged
yusiwen merged 1 commit into
masterfrom
fix/162-project-config-cannot-widen
Oct 9, 2026
Merged

yusiwen merged 1 commit into
masterfrom
fix/162-project-config-cannot-widen

Conversation

@yusiwen

@yusiwen yusiwen commented Oct 9, 2026

Copy link
Copy Markdown
Owner

What changed

  • config/config.go: SandboxBoundary plus UserSandboxBoundary() (code defaults + the user's own file) and ProjectSandboxBoundary() (the same keys as the project file spells them, so a caller can report what it dropped).
  • main.go: one installSandboxBoundary() installs the boundary root, the working-directory auto-allow and the allowed paths from the user layer, and logs project_local_boundary_keys_ignored when the project file asked for either widening key. The duplicated later block is gone, and the language server workspace now reads the same installed root instead of the merged config.
  • config/config_test.go, main_test.go: three tests — the layer rule, the wiring (with a control), and the other column of the table.
  • docs/sandbox.md: a "which layer may set what" table recording the decision this issue asked for.

Why

The configuration layers defaults → user → project, and the project layer arrives with the repository, so the agent may run with it before anyone has read it. The sandbox wiring read sandbox.project_root and sandbox.allowed_paths out of the merged result, which made a checked-out repository able to widen the fence that is supposed to contain it.

Measured with the real binary (one-shot, build mode, redirected HOME with no grant, writing /tmp/stub-outside-write.txt):

./.tinycode/config.json on master d351ee8 on this branch
{"sandbox":{"project_root":"/"}} CREATED refused
{"sandbox":{"allowed_paths":["/tmp/stub-outside-write.txt"]}} CREATED refused
{"default_mode":"build"} refused refused
the same allowed_paths in the user's file (control) CREATED CREATED

The LSP workspace root had the same source, so a repository could also point the language server at / while the fence still called the repository the project; it now takes the installed root, so "inside the project" means one thing to both.

Verification

  • The table above, before and after, with a fresh stub per case; the ignored-key warning is in the run log (project_local_boundary_keys_ignored).
  • go test ./... -count=1 → 13 packages ok; -count=1 -race → 13 packages ok; go vet ./... clean; gofmt -l nothing.
  • make test-tui-scenarios → exit 0, 31 scenario files, 27 ok, 4 skipped by design (confine-bash-write, live-answer, lsp-diagnostics, lsp-tools, each naming its make target).
  • Mutation: making UserSandboxBoundary() return the merged config fails both fence tests — boundary root = "/", is allowed: the project-local config widened the fence (and the config-layer test: user boundary root = "/", user allowed paths = [/srv/user-cache /etc]). Restored, all pass.
  • CI: recorded in the checks below.

Honest scope

  • make test-tui-confine cannot run on this macOS host (no Landlock; the target skips loudly), so the kernel-boundary job remains CI-only. The change here is wiring that runs everywhere.
  • gopls is not installed here, so make test-lsp / make test-tui-lsp were not run locally; the LSP workspace change is covered by CI's LSP integration (gopls + typescript) job and by the ungated scenario suite, in which the two LSP scenarios are skipped locally by design.
  • What a project-local sandbox block may still do is now written down rather than inferred: deny_commands accumulates, and the two hardening booleans can be turned on by any layer but never off (enforced by merge() since the sandbox landed).

Follow-ups

The configuration is layered defaults -> user -> project, and the sandbox wiring
read the two keys that can widen the fence out of the merged result. The project
layer arrives with the repository, so a checked-out repository carrying
{"sandbox":{"project_root":"/"}} built the fence around the whole filesystem, and
one carrying allowed_paths handed the agent a path nobody allowed.

Measured on master d351ee8 with the real binary in one-shot build mode, writing a
path outside the project: project-local project_root "/" -> written;
project-local allowed_paths -> written; project-local config with neither ->
refused; the same allowed_paths in the user's own file -> written (the control
that has to keep working).

installSandboxBoundary() now takes both keys from config.UserSandboxBoundary()
(the code defaults plus the user's own file) and the working directory, and it
logs project_local_boundary_keys_ignored when the project file asked for either,
so a person who wrote such a key sees that it did not take effect. The language
server workspace is the same root, so it reads the installed one instead of the
merged config: a repository could otherwise point the server at / while the fence
still called the repository the project.

Deny rules and the two hardening booleans are untouched: merge() already lets a
project layer add refusals and turn the requirements on, never off.

After the change the table flips — both project-local widening keys are refused,
the user-level control is still written. Tests: TestSandboxBoundaryIgnoresThe
ProjectLayer (the layer rule), TestProjectLocalConfigCannotWidenTheFence (the
wiring, with a path inside the project as the control) and
TestUserConfigMayWidenTheFence (the other column). Mutation: making
UserSandboxBoundary read the merged config fails both fence tests ("boundary
root = \"/\"", "is allowed: the project-local config widened the fence");
restored, all pass.

Refs #162, #155.
@yusiwen yusiwen added bug Something isn't working security Security hardening, sandbox or SSRF residual labels Oct 9, 2026
@yusiwen
yusiwen merged commit aafa9dc into master Oct 9, 2026
9 checks passed
@yusiwen
yusiwen deleted the fix/162-project-config-cannot-widen branch October 9, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working security Security hardening, sandbox or SSRF residual

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[security] A repository-local config can widen or remove the sandbox fence

1 participant