Skip to content

fix(server): guard project_root writes against mock/relative coercion (TAP-4573)#197

Merged
wtthornton merged 1 commit into
masterfrom
tap-4573-fs-leak-guard
Jul 2, 2026
Merged

fix(server): guard project_root writes against mock/relative coercion (TAP-4573)#197
wtthornton merged 1 commit into
masterfrom
tap-4573-fs-leak-guard

Conversation

@wtthornton

Copy link
Copy Markdown
Owner

What

Harden the two production write paths that mkdir/write under Path(settings.project_root) so a non-real (mock- or relative-coerced) project_root can never create a real filesystem tree.

Root cause

agent_identity._write_uuid (via get_stable_agent_id) and server_helpers.write_session_start_marker both mkdir(parents=True) under project_root with no guard. ~70 test call sites pass a bare MagicMock(); os.fspath(MagicMock().project_root) coerces to the relative path MagicMock/mock.project_root/<id> (verified empirically), so every full-suite run created real agent.id / .session-start-marker trees in the pytest CWD (the repo root) — 1,848 dirs accumulated, invisible to git status because .tapps-mcp/ is gitignored at any depth.

Fix

Add a shared guard is_real_writable_root(project_root) in tapps_core.agent_identity accepting only absolute filesystem paths (os.fspathPath.is_absolute()). Both write paths short-circuit when it fails:

  • get_stable_agent_id returns a valid in-memory id (no persistence).
  • write_session_start_marker returns early.

No-op for real deployments (roots are always absolute). No edits to the 70 test sites — the guard stops the leak at the single production write path.

Tests

  • New guard unit test + "MagicMock settings creates zero files under CWD" for both modules.
  • No test deleted/skipped/xfailed.
  • Full package unit run: 7368 passed, and the run's post-check reports no MagicMock leak. The one ordering-flake failure (test_cli_memory::test_search_json_output) is pre-existing and unrelated — passes in isolation and in its own file, and has zero linkage to the changed code.

Closes TAP-4573.

🤖 Generated with Claude Code

… (TAP-4573)

Two production write paths mkdir/write under Path(settings.project_root)
with no guard: agent_identity._write_uuid (via get_stable_agent_id) and
server_helpers.write_session_start_marker. ~70 test sites pass a bare
MagicMock(); os.fspath(MagicMock().project_root) coerces to the relative
path 'MagicMock/mock.project_root/<id>', so full-suite runs created real
agent.id / session-start-marker trees in the pytest CWD (the repo root),
invisible to git status because .tapps-mcp/ is gitignored at any depth.

Add a shared guard is_real_writable_root() in tapps_core.agent_identity
that accepts only absolute filesystem paths. Both write paths short-circuit
when it fails: get_stable_agent_id returns a valid in-memory id (no
persistence), write_session_start_marker returns early. No-op for real
deployments (roots are always absolute); no edits to the 70 test sites.

Verified: full package unit run reports 'no MagicMock leak' and 7368 passed
(the single ordering-flake failure in test_cli_memory is pre-existing and
unrelated — passes in isolation and has zero linkage to the changed code).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@wtthornton wtthornton merged commit 4318968 into master Jul 2, 2026
4 checks passed
@wtthornton wtthornton deleted the tap-4573-fs-leak-guard branch July 2, 2026 18:40

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57ff94c44e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

raw = os.fspath(project_root) # type: ignore[arg-type]
except (TypeError, ValueError):
return False
return Path(raw).is_absolute()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve real relative roots before rejecting them

In the Claude Code configuration path the project root is intentionally emitted as TAPPS_MCP_PROJECT_ROOT="." (packages/tapps-mcp/src/tapps_mcp/distribution/setup_generator.py:380-383), and load_settings() preserves that env value as Path('.') without resolving it (packages/tapps-core/src/tapps_core/config/settings.py:1517-1518). With this new absolute-only predicate, real Claude Code sessions take the non-persistent fallback in get_stable_agent_id(), so repeated calls generate different UUIDs instead of reading/writing .tapps-mcp/agent.id; that changes X-Agent-Id/Hive registration on every request. The guard should resolve legitimate relative roots (or normalize settings earlier) while still rejecting mock paths.

Useful? React with 👍 / 👎.

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