Skip to content

security/policy: a missing workspace is reported as a path-traversal escape #5416

Description

@oxoxDev

What happens

SecurityPolicy::validate_parent_path refuses a write with:

[policy-blocked] Resolved parent path escapes workspace: <path>

for two conditions that have nothing to do with each other:

  1. A genuine escape — a symlink inside the sandbox pointing out, where the
    immediate parent cannot be canonicalized up front (e.g. escape/nested/x.txt
    with escape -> /outside and nested/ not yet created). The ancestor walk
    lands outside. This is an attack, and refusing it is right.
  2. A workspace that does not exist. With workspace_dir absent, the walk in
    path_checks.rs (loop { if existing_ancestor.exists() { break } … }) climbs
    straight past the sandbox to a parent that genuinely is outside it, and the
    guard refuses a plainly-inside relative path such as notes.md.

The two are refused in identical words. Only the resolved path differs, and
nothing in the message tells a reader which condition they are in.

The refusal also appends the standard workaround text:

Raise the agent's access tier / autonomy (Settings → Agent access …)

which is actively wrong for condition 2 — no autonomy tier creates a directory,
and an operator or agent following it burns the turn.

Why this matters more than it looks

In a workspace_only sandbox, condition 2 is by far the more likely of the two.
Every easy escape is already caught earlier and more clearly by
is_path_string_allowed: .. components, absolute paths, and symlinks whose
immediate parent resolves all come back as Path not allowed by security policy,
which is unambiguous. The resolved-parent arm is reached mainly when nothing up
front can be canonicalized — and "the workspace was never created" is exactly
that shape.

We hit this in OpenCompany (tinyhumansai/opencompany#409). An agent granted file
tools but not shell could not write a relative path at all, because nothing had
created its sandbox directory; an agent with a shell grant never saw it, because
the first command creates the directory as a side effect. The message sent
several people looking for a traversal attempt that did not exist. We have fixed
the provisioning on our side, so the condition should no longer arise for us —
but the guard will keep reporting a missing directory as an escape for anyone
else who hands it one.

Suggested change

In validate_parent_path, before or alongside the containment check, distinguish
the case where the workspace root itself does not exist (or where the ancestor
walk left the workspace subtree only because nothing inside it exists yet), and
say so:

[policy-blocked] Workspace directory does not exist: <workspace_dir>.
Nothing can be written until it is created; this is not a path-traversal refusal.

Two things worth keeping:

  • The refusal itself. Failing closed on a missing sandbox is correct — this is a
    wording and diagnosis change, not a permission change.
  • The distinct workaround text. The autonomy-tier suggestion should not be
    attached to the missing-directory case.

A test coupling the two conditions to two different messages would keep them from
converging again.

Where

  • src/openhuman/security/policy/path_checks.rsvalidate_parent_path, the
    ancestor walk and the is_resolved_path_allowed_for refusal.
  • Same arm reached from tools/impl/filesystem/file_write.rs and
    tools/impl/filesystem/csv_export.rs.

Notes

Filed from tinyhumansai/opencompany#409, which fixed the provisioning half in
OpenCompany. No OpenHuman change was made there — the vendored checkout was left
untouched deliberately.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions