Skip to content

fix(api): reject environment folder paths outside the workspace volume - #651

Merged
pikann merged 1 commit into
Paca-AI:masterfrom
siedlegroup:fix/environment-folder-path-root
Oct 28, 2026
Merged

pikann merged 1 commit into
Paca-AI:masterfrom
siedlegroup:fix/environment-folder-path-root

Conversation

@kayahk

@kayahk kayahk commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #650.

Symptom: every attach to a durable environment failed instantly with
acp: session/new: Invalid params: invalid directory path.

Cause: AddFolder accepted any absolute path and stored it as the folder's Path, which becomes the conversation workdir and session/new's cwd. A path outside the mounted workspace volume is mkdir'd into the container's writable layer, so it disappears on the next Pod recreation and the stored folder then points at a nonexistent directory — session/new rejects it forever after. The api's Browse and agent-runner's own mkdir/delete guards were already scoped to environmentHomeRoot; only the create path was not.

Change

  • services/api/internal/service/environment/environment_service.go — clean the path and require environmentHomeRoot or a path under it (the ErrFolderPathInvalid → 400 ENVIRONMENT_FOLDER_PATH_INVALID mapping already existed); store the cleaned path.
  • services/api/internal/domain/environment/errors.go — error text now names the real constraint.
  • apps/web/.../folder-create-dialog.tsx — the client-side pre-check uses the same root constant instead of "starts with /".
  • Table test TestAddFolder_PathMustBeUnderWorkspaceRoot: root, subfolder, trailing slash, outside the volume, sibling sharing the prefix (/home/paca/workspaces-other), .. traversal out of the root, relative, empty.

Verification (local)

  • go build ./..., go vet ./... clean in services/api.
  • go test ./internal/service/environment/ passes; sabotage run (guard reverted to the old HasPrefix("/")) fails exactly on the outside / sibling / traversal cases: Expected error with "folder path must be an absolute path" in chain but got nil.
  • npx @biomejs/biome@2.4.12 check on the changed tsx: clean. No tsc gate locally (no node_modules).
  • golangci-lint is not installed here — CI is the gate for it.
  • No e2e: it needs a provisioned environment; the ACP-level behaviour was instead reproduced directly against a live environment's agent-server with the same image (cwd outside the volume → -32602 invalid directory path; cwd under it → sessionId), see the issue.

Existing rows that already point outside the volume are not migrated — the directory they name is gone too, so they have to be corrected by hand.

AddFolder only required a leading "/", so a path outside the mounted
workspace volume (e.g. /home/paca-dev/workspaces) was persisted and
mkdir'd into the container's own writable layer. It disappears on the
next Pod/container recreation, after which the folder's Path is the cwd
of session/new and every attach fails with
ACP "session/new: Invalid params: invalid directory path".

Require the cleaned path to be the workspace root or under it, the same
scope agent-runner already applies to Browse and its mkdir/delete
guards, and store the cleaned path. The folder dialog's client-side
pre-check now uses that same root instead of "any absolute path".

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Minor suggestions only — the core fix is sound; two notes on the new guard and the comments it adds.

Reviewed changes

  • Workspace-root guard on AddFolder — environment_service.go cleans the incoming path with path.Clean and rejects anything not equal to, or under, /home/paca/workspaces, storing the cleaned path.
  • Error text — ErrFolderPathInvalid now names the workspace constraint (mapping to 400 ENVIRONMENT_FOLDER_PATH_INVALID already existed).
  • Client pre-check — folder-create-dialog.tsx validates against the same root constant instead of startsWith("/").
  • Table test — TestAddFolder_PathMustBeUnderWorkspaceRoot pins root, subfolder, trailing slash, outside, prefix-sibling, traversal, relative, and empty cases.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

// Mirrors agent-runner's environmentHomeRoot
// (internal/acpbridge/environment_handlers.go), which scopes Browse and
// the mkdir/delete guards to the same root.
if path != environmentHomeRoot && !strings.HasPrefix(path, environmentHomeRoot+"/") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowing the workspace root here is inconsistent with agent-runner, which refuses to mkdir it: forbiddenFolderTargets lists environmentHomeRoot itself, so the POST /internal/environments/{id}/folders this triggers returns 400. Because the folder row is created first, a running environment ends up with the row persisted while the request errors, and a retry then reports path taken — and the dialog's default path is the root, so this is the default submission. If root-as-folder is intended, agent-runner needs to permit it (or the mkdir should be skipped when path == environmentHomeRoot); otherwise drop the path != environmentHomeRoot carve-out and the "root" test case.

Technical details
# Root carve-out vs. agent-runner's mkdir guard

## Affected sites
- `services/api/internal/service/environment/environment_service.go:1009` — accepts `path == environmentHomeRoot`.
- `services/api/internal/service/environment/environment_service.go:1033-1041` — for a `StatusRunning` env the row is committed (`CreateFolder`, line 1022) *before* the agent-runner call, so the 400 leaves a persisted row.
- `services/agent-runner/internal/acpbridge/environment_handlers.go:597-618` — `forbiddenFolderTargets` includes `environmentHomeRoot`.
- `services/agent-runner/internal/acpbridge/environment_handlers.go:675-679` — `handleCreateEnvironmentFolder` calls `safeFolderTarget` unconditionally, which refuses it.
- `services/api/internal/service/environment/environment_service_test.go:1622` — pins `"root"` as valid; the test's env has `BackendRef == nil`, so the agent-runner leg is never exercised.

## Required outcome
- Root is either rejected consistently at the api boundary, or the full create flow (including the mkdir leg for a running env) succeeds for it.

## Also in this comment block
- `environment_service.go:1006-1008` states agent-runner "scopes Browse and the mkdir/delete guards to the same root." `safeFolderTarget` only refuses a fixed list of top-level directories; it does not scope mkdir to `environmentHomeRoot`. And there is no delete guard/endpoint — `DeleteFolder` is row-only (`environment_service.go:1110`).

## Open questions for the human
- Is a folder whose path is the workspace root a supported object (e.g. as a conversation workdir), or is the dialog default meant to force a subfolder?

// isValidPath already blocks a relative path, so
// EnvironmentFolderPathInvalid mainly guards a stale check;
// isValidPath is the same root check the server now enforces (folders
// outside the mounted workspace vanish on the next Pod recreation and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not quite "the same root check the server now enforces": a raw startsWith(ENVIRONMENT_HOME_ROOT) also accepts prefix siblings like /home/paca/workspaces-other and uncleaned .. segments, both of which the server rejects after path.Clean. The server is authoritative so nothing breaks, but the comment overstates the equivalence and EnvironmentFolderPathInvalid is reachable from a client-valid input — consider softening the wording rather than tightening the check.

@pikann pikann left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thank you for the contribution! 🚀

@pikann
pikann merged commit ce10489 into Paca-AI:master Oct 28, 2026
7 checks passed
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.

Environment folder path outside the workspace volume breaks every attach with ACP "invalid directory path"

2 participants