Skip to content

Interactive file reads reach as far as shell does (#1724) - #1770

Open
Aaronontheweb wants to merge 10 commits into
devfrom
feat/1724-file-read-shell-reach
Open

Interactive file reads reach as far as shell does (#1724)#1770
Aaronontheweb wants to merge 10 commits into
devfrom
feat/1724-file-read-shell-reach

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Interactive Personal-audience sessions get shell-equivalent read/attach reach.

Changes

  • file_read / file_list / attach_file resolve outside trust roots for interactive Personal sessions (autonomous, Team, Public unchanged)
  • ToolPathPolicy.IsReadDenied now also denies the shell indicator list (config dir, sqlite DB, pid, lock, restart manifest) — read tools cannot reach files shell cannot even reference
  • attach_file lifts its session-proximity gate only for interactive Personal; out-of-session files still copy into the session attachments dir
  • Spec: netclaw-tools carve-out for interactive Personal read reach
  • Tests: InteractivePersonalReadReachTests (theory matrix), updated AttachFileToolTests, ToolPathPolicyTests

Closes #1724

Interactive Personal-audience sessions now get shell-equivalent read and
attach reach: file_read, file_list, attach_file resolve outside the
configured trust roots, matching the approval-gated shell surface. This
kills the shell-workaround (cat, cp-into-session) for legitimate
out-of-roots files.

ToolPathPolicy.IsReadDenied now also denies the shell indicator list
(config dir, sqlite DB, pid, lock, restart manifest), so read tools cannot
reach control-plane files that shell cannot even reference.

Autonomous sessions, Team, and Public audiences keep their roots-scoped or
fail-closed behavior. attach_file lifts its session-proximity gate only for
interactive Personal; out-of-session files still copy into the session
attachments dir.

Spec: netclaw-tools carve-out for interactive Personal read reach.

public InteractivePersonalReadReachTests()
{
_sessionDir = Path.Combine(_dir.Path, "sessions", "s1");
public InteractivePersonalReadReachTests()
{
_sessionDir = Path.Combine(_dir.Path, "sessions", "s1");
_outsideDir = Path.Combine(_dir.Path, "outside");
var ctx = Ctx(audience, autonomous: !interactive);

var path = outsideRoots
? Path.Combine(_outsideDir, "notes.txt")

var path = outsideRoots
? Path.Combine(_outsideDir, "notes.txt")
: Path.Combine(_sessionDir, "notes.txt");
var ctx = Ctx(audience, autonomous: !interactive);

var path = outsideRoots
? Path.Combine(_outsideDir, "report.png")

var path = outsideRoots
? Path.Combine(_outsideDir, "report.png")
: Path.Combine(_sessionDir, "report.png");
bool interactive,
bool expectedAttached)
{
var outsideFile = Path.Combine(_outsideDir, "report.png");
…ches (#1724)

Adversarial review (PR #1770) found two blockers:
1. attach_file had no ToolPathPolicy, so the lifted proximity gate let
   interactive Personal attach secrets/keys/db/pid/lock — a full bypass
   of the read-deny surface. AttachFileTool now takes ToolPathPolicy and
   applies IsReadDenied after path resolution, matching file_read/list.
2. set_working_directory inherited shell-equivalent reach via the shared
   TryResolveReadPath, widening the safe-verb auto-approve zone and
   letting a planted AGENTS.md become system-prompt content. New
   TryResolveWorkingDirectory opts out of interactive Personal reach;
   SetWorkingDirectoryTool stays roots-scoped.

Also: ToolPathPolicyTests fixture now includes ConfigDirectory in shell
indicators to match production (Program.cs), and the ls regression test
asserts the production behavior (denied). New tests cover attach deny of
control-plane files and set_working_directory roots-scoping.
// BLOCKER regression (#1724): attach must use the same hard-deny surface
// as file_read/file_list, so interactive Personal reach cannot ship
// secrets/keys/db/pid/lock that shell cannot even reference.
var secretsPath = Path.Combine(_outsideDir, "secrets.json");
Comment thread src/Netclaw.Actors.Tests/Tools/InteractivePersonalReadReachTests.cs Fixed
…_working_directory in Mode.All (#1724)

Second adversarial review found:
1. IsReadDenied bypass via symlinked INTERMEDIATE directory (ln -s config
   /tmp/x then read /tmp/x/netclaw.json). IsDeniedAgainst now resolves
   intermediate symlinks segment-by-segment via TryResolveSymlinksInPath,
   mirroring the shell scanner. attach_file re-checks the deny against the
   resolved path as defense-in-depth.
2. set_working_directory opt-out was inert for the default Mode.All
   Personal profile — the Mode.All branch fired before the opt-out flag was
   consulted. The branch now clamps set_working_directory to the autonomous
   zone in every mode.
3. Prefix-collision gap: sqlite sidecar files (wal/shm/journal) held raw
   page data (secrets) but path-boundary matching allowed reads while shell
   denied them. Program.cs shell indicator list now includes the sidecars;
   fixture mirrors production.
4. CredentialReadDenied message now covers control-plane state, not just
   credentials/keys; ToolPathPolicy remarks updated to match production
   (directory-scoped shell entries are intentional).
5. Spec: attach_file and set_working_directory requirements added.

Tests: symlinked-dir read-deny regression, Mode.All working-dir clamp,
Roots-mode attach branch (was untested), sidecar read-deny cases.
Comment thread src/Netclaw.Actors.Tests/Tools/InteractivePersonalReadReachTests.cs Fixed
…ars on write (#1724)

Verification review (approve-with-nits) flagged a stale scenario in
openspec/specs/session-cwd/spec.md that promised set_working_directory
allows any valid directory under Personal Mode.All — the opposite of the
new autonomous-zone clamp. Updated the requirement + scenario to codify
the clamp and cross-reference the interactive read reach.

Also add sqlite sidecars to writeDenyList for defense-in-depth parity
with the shell indicator list (read already denied via the union).
TryResolveSymlinksInPath built partial paths with a bare directory
separator, so on Windows every probe looked like '\Users\...' instead of
'C:\Users\...' and symlink resolution silently no-oped. This surfaced in
IsReadDenied_blocks_symlinked_directory_traversal on the Windows CI job:
the symlink was created but IsReadDenied returned false.

Path.GetPathRoot preserves the full root on every platform ('C:\' on
Windows, '/' on Unix), so the segment walk resolves symlinks correctly
everywhere. The shell scanner (CommandReferencesDeniedPath) shares this
function and was affected on Windows too.
The test IsReadDenied_blocks_symlinked_directory_traversal fails on
Windows CI. We do not know the exact cause yet.

This diagnostic shows the paths that IsReadDenied compares. It shows
the raw link path, the resolved link target, the candidate path, and
the denied directory. It shows the result of the StartsWith check.

The message appears only when the assert fails. On Linux the assert
passes, so the message stays hidden. This proves the diagnostic code
compiles and does not throw.
The old diagnostic showed the two paths only. It did not show which
segment of TryResolveSymlinksInPath diverges on Windows.

The new diagnostic replays the segment walk. It logs, for each
segment, the directory and file existence check, the
ResolveLinkTarget outcome, and the rebuilt path. The message prints
only when the assert fails.
Comment thread src/Netclaw.Security.Tests/ToolPathPolicyTests.cs Fixed
Comment thread src/Netclaw.Security.Tests/ToolPathPolicyTests.cs Fixed
TryResolveSymlinksInPath seeded the builder with the path root and then
split the full path. On Windows the first split token is the drive, so
the walk built "C:\C:\Users\..." and every existence probe missed. The
symlink walk then no-oped and IsReadDenied failed open. Now the split
covers only the remainder after the root. This also removes the
temporary CI diagnostic block from the regression test for #1724.
var policy = new ScopedFileAccessPolicy(config, _paths);
var ctx = Ctx(TrustAudience.Personal, autonomous: false);

var outside = Path.Combine(_outsideDir, "notes.txt");
var policy = new ScopedFileAccessPolicy(new ToolConfig(), _paths);
var ctx = Ctx(TrustAudience.Personal, autonomous: false);

var outside = Path.Combine(_outsideDir, "notes.txt");
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.

Interactive file_read/file_list/attach_file should reach as far as shell does

1 participant