Skip to content

feat(read-file): record read scope and report clipping separately (U4, #1375) - #1913

Open
easonLiangWorldedtech wants to merge 26 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fws/u4-read-scope-recording
Open

easonLiangWorldedtech wants to merge 26 commits into
Zoo-Code-Org:mainfrom
easonLiangWorldedtech:fws/u4-read-scope-recording

Conversation

@easonLiangWorldedtech

@easonLiangWorldedtech easonLiangWorldedtech commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Split unit U4 of #1833, under the plan on this issue (5993969784 / 5994039786 / 5994053776). Base is U2 (#1912) per the merge order.

Scope (one gate scope): the read side — what a read records about its own scope, and reporting truncation and clipping as two separate notices.

Content source of record: kind: commit, base 7c291bb08 → head 6768ccfaf, replayed on the current main tip 9af61f87e so this branch carries nothing that main already has.

Budget (own delta, not the stacked view): 932 a+d / 105 changed executable lines. Inside both caps.

Verification at this head: 94 passed (readFileTool) and 48 passed (indentation-reader); ESLint --max-warnings=0 clean on every file in the unit; Prettier clean; src/eslint-suppressions.json never increased.

The GitHub view also carries the unmerged base, so the numbers above are this unit's own delta.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 28 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: Zoo-Code-Org/Zoo-Code/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 5613c841-fe15-4f92-9993-22bca5a442c6
📥 Commits

Reviewing files that changed from the base of the PR and between 31ffa4b and 244f4b6.

📒 Files selected for processing (4)
  • src/services/file-safety/__tests__/safeWriteText.spec.ts
  • src/services/file-safety/safeWriteText.ts
  • src/utils/__tests__/safeWriteJson.test.ts
  • src/utils/safeWriteJson.ts
📝 Summary

Summary by CodeRabbit

  • New Features
    • File writes now use staged files for atomic publication and preserve existing file permissions. Optional backups protect the original file if a write fails before commit.
    • Writes through symbolic links consistently target the linked file. Writes can also be restricted to a specified directory.
  • Bug Fixes
    • File reads now indicate when returned lines are clipped, including when the full file is read but some lines exceed the display limit.
    • Reads that start partway through a file or return incomplete content are identified as partial.

Walkthrough

The change adds task-local file observations and read-completeness reporting. It adds staged text publication with backup handling, then updates safeWriteJson to use the new writer and resolved lock targets.

Changes

File read observations

Layer / File(s) Summary
Observation registry and task ownership
src/core/task/Task.ts, src/core/task/observationRegistry.ts, src/core/task/__tests__/observationRegistry.spec.ts
Tasks now own an observation registry. The registry stores file-version tokens, timestamps, and completeness values, and provides lookup, membership, clearing, and size access.
Read completeness and clipping
src/integrations/misc/indentation-reader.ts, src/integrations/misc/__tests__/indentation-reader.spec.ts, src/core/tools/ReadFileTool.ts, src/core/tools/__tests__/readFileTool.spec.ts
Read results distinguish clipped lines from omitted lines. Native and legacy paths classify complete and partial views. Tests cover clipping, truncation, offsets, and invalid read positions.
Stable read observations
src/core/tools/ReadFileTool.ts, src/core/tools/__tests__/readFileTool.spec.ts, src/eslint-suppressions.json
Native and legacy text reads record observations only when pre-read and post-read stat tokens match. Stat failures do not fail successful reads. Lossy decoding and partial views produce incomplete observations.

Safe text and JSON writes

Layer / File(s) Summary
Resolve targets and stage writes
src/services/file-safety/safeWriteText.ts, src/services/file-safety/__tests__/safeWriteText.spec.ts
Adds target and lock-key resolution, staging-path validation, and staged-file setup. Tests cover symlinks, missing targets, and staging cleanup.
Publish and preserve staged content
src/services/file-safety/safeWriteText.ts, src/services/file-safety/__tests__/safeWriteText.spec.ts, src/services/file-safety/__tests__/safeWriteText.integration.spec.ts
Adds staged publication with optional backups, permission preservation, file and directory syncing, and best-effort Windows DACL handling. Tests cover successful writes and failure cleanup.
Integrate safe JSON writes
src/utils/safeWriteJson.ts, src/utils/__tests__/safeWriteJson.test.ts, src/utils/__tests__/safeWriteJson.lockKey.spec.ts, src/eslint-suppressions.json
safeWriteJson resolves and locks the publish target, checks path confinement, and delegates publication to safeWriteText. Tests cover locking, symlinks, confinement, and cleanup.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ReadFileTool
  participant FileSystem
  participant ObservationRegistry
  ReadFileTool->>FileSystem: stat before read
  ReadFileTool->>FileSystem: read file
  ReadFileTool->>FileSystem: stat after read
  ReadFileTool->>ObservationRegistry: record matching version token and completeness
Loading
sequenceDiagram
  participant safeWriteJson
  participant proper-lockfile
  participant safeWriteText
  participant FileSystem
  safeWriteJson->>FileSystem: resolve lock key and publish target
  safeWriteJson->>proper-lockfile: acquire lock
  safeWriteJson->>safeWriteText: publish staged JSON with backup enabled
  safeWriteText->>FileSystem: write, sync, and rename staged content
  safeWriteJson->>proper-lockfile: release lock
Loading

Suggested reviewers: hannesrudolph

Merge Risk: 🔵 Low · up to 31ffa

The remaining concerns are a narrow write-failure edge case and a read-notice test gap. They do not establish a broad failure that blocks merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to a1b98

Read authorization remains intact, but JSON writes now follow file symlinks. A project configuration update can therefore modify configuration outside that project, including global tool-approval settings, when a workspace-controlled link points there.

Retained concerns

  • Medium · security · inferred: Project MCP updates inherit newly expanded write scope. If workspace .roo/mcp.json is a symlink to global MCP settings, a project-scoped tool-approval or server-setting action now changes the global referent rather than replacing the project link as the base did. The inspected caller authorizes the logical project source but does not check or obtain approval for the resolved destination. This creates a conditional cross-project configuration-integrity and approval-policy concern.
Security review details

Security Blast Radius

  • inferred — The new write exposure is local to destinations writable by the extension process, but is not limited to the logical project configuration path. A controlled file symlink can redirect a compatible configuration update to global MCP settings, making its effects persist across projects. Workspace-link control and an update action are required; automatic remote exploitation was not established.

Security Findings and Attack Paths

  • inferred — A project mcp.json link to an existing global MCP configuration is read as project configuration. A project-scoped always-allow toggle then passes that same logical path to safeWriteJson, which now publishes onto the global referent. Reading through links predates the PR; mutation of the referent through this JSON publication path is the introduced change.

Trust Boundaries and Controls

  • observed — Read-file access retains RooIgnore filtering and approval before processing approved files. Publication rejects dangling terminal symlinks and non-regular caller staging files. These controls do not authorize an existing resolved destination against a project boundary.

Resilience and Maintainability Implications

  • observed — Canonical advisory locks coordinate JSON writers using symlink aliases and referents. The general withFileLock helper still uses lexical path identity; equivalent coordination for every maintenance caller was not established, although the inspected task-history deletion caller uses its normal task-file path.

Hardening Proposals

  • proposed — Keep intentional symlink support in the general writer, but require project configuration callers to authorize the resolved destination: reject destinations outside the project policy boundary or obtain explicit destination-specific consent before publication.

Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore (reviewers only)

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Security Boundaries ❌ Error safeWriteJson now resolves symlinks and publishes to the referent (src/utils/safeWriteJson.ts:162, 204-222). The project MCP update path selects <workspace>/.roo/mcp.json (`src/services/mcp/McpH… Pass the canonical workspace directory as confineTo for every project-scoped MCP configuration write, and reject referents outside it before reading or staging. Keep global settings writes scoped to their intended settings directory, or p…
Regression Evidence ⚠️ Warning The new confinement check has an uncovered boundary case. safeWriteJson.ts rejects a resolved target equal to the confinement root when path.relative(scopeRoot, resolvedTarget) === "" (lines 170–1… Add a focused safeWriteJson test where filePath resolves to confineTo. Assert that it rejects with ConfinedPathEscapeError before invoking the merge callback or staging/publishing a file. Keep the existing outside-root and in-scope …
Lifecycle Resource Cleanup ⚠️ Warning The new backup lifecycle can leave persistent orphan files. safeWriteText creates a backup at src/services/file-safety/safeWriteText.ts:381-420, then swallows unlink failures after commit at lines… Do not silently abandon failed backup cleanup. Preserve or report the orphan path and arrange a reliable retry or recovery cleanup, while keeping the committed write successful if that remains the intended contract.
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Persistence Integrity ✅ Passed No changed persistence path meets the failure condition. safeWriteJson awaits JSON staging and safeWriteText (src/utils/safeWriteJson.ts); safeWriteText fsyncs staged content before an atomic …
Title check ✅ Passed The title clearly identifies the read-side changes: recording read scope and reporting clipping separately.
Description check ✅ Passed The description explains the scope and implementation context, references the related issues, and reports test and lint results. It does not provide reproducible test steps or complete the template ch…
Full details: Regression Evidence

Explanation

The new confinement check has an uncovered boundary case. safeWriteJson.ts rejects a resolved target equal to the confinement root when path.relative(scopeRoot, resolvedTarget) === "" (lines 170–179). The added tests cover targets outside the root and symlinks that resolve outside it, but no test passes the root itself as the target (safeWriteJson.test.ts, lines 668–753). An omitted equality check could therefore allow publishing to the declared root without failing the existing outside-path tests.

Resolution

Add a focused safeWriteJson test where filePath resolves to confineTo. Assert that it rejects with ConfinedPathEscapeError before invoking the merge callback or staging/publishing a file. Keep the existing outside-root and in-scope symlink tests.

Full details: Security Boundaries

Explanation

safeWriteJson now resolves symlinks and publishes to the referent (src/utils/safeWriteJson.ts:162, 204-222). The project MCP update path selects &lt;workspace&gt;/.roo/mcp.json (src/services/mcp/McpHub.ts:623-632) and writes through safeWriteJson without confineTo (src/services/mcp/McpHub.ts:2035-2042, 2083-2095). If a repository places .roo/mcp.json as a symlink to an external, valid MCP config, a project setting update writes to that external file. The prior implementation renamed over the supplied path, so this symlink-following write is introduced by the PR. The new confinement option does not protect this caller because it is not supplied.

Resolution

Pass the canonical workspace directory as confineTo for every project-scoped MCP configuration write, and reject referents outside it before reading or staging. Keep global settings writes scoped to their intended settings directory, or preserve the prior non-following behavior where no scope is declared.

Full details: Lifecycle Resource Cleanup

Explanation

The new backup lifecycle can leave persistent orphan files. safeWriteText creates a backup at src/services/file-safety/safeWriteText.ts:381-420, then swallows unlink failures after commit at lines 469-475. Its error cleanup also swallows unlink failures and clears the path at lines 497-503. The changed test explicitly simulates EPERM and confirms the backup remains on disk (src/services/file-safety/__tests__/safeWriteText.spec.ts:328-348). Repeated writes under this condition can accumulate backup files.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the has-conflicts PR has merge conflicts with the base branch label Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Review status

Thanks for contributing. This comment tracks the review sequence and the next action.

Current step: Required CI passed. Waiting for automated review of the latest commit.

If automated review does not start, a maintainer must restart it.

Review-state labels are managed by this workflow; do not edit them manually. community-approved is managed the same way — do not add or remove it manually. It signals a fresh community code approval for the current head as an advisory priority only; maintainer review is still required.

@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fws/u4-read-scope-recording branch from 9774b0a to 60b9221 Compare October 5, 2026 12:35
@github-actions github-actions Bot removed the has-conflicts PR has merge conflicts with the base branch label Oct 5, 2026
…ve (U1, issue 1375)

Split unit U1 of PR 1833. Three changes, each with a test that fails without it:
- a caller-supplied staging path is checked for location and file type before anything is written, so an arbitrary path or a symlink cannot be published onto the target;
- a failed parent-directory fsync on POSIX is reported as PostCommitDurabilityError instead of being swallowed, so a successful return never claims durability the filesystem did not grant;
- the staged file and this write's own staging directory are released before RollbackFailureError is thrown.

Focused coverage for resolveLockKey added: canonical parent directory, a dangling-link chain, and termination at the bounded depth on a two-link cycle.
@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.27481% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/utils/safeWriteJson.ts 78.26% 4 Missing and 6 partials ⚠️
src/services/file-safety/safeWriteText.ts 97.17% 1 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fws/u4-read-scope-recording branch from 60b9221 to bb64d87 Compare October 5, 2026 12:55
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

…ishTarget (U1, issue 1375)

The resolver may fall back to the given path only when lstat also reports the path as absent. An EACCES or EIO failure says nothing about whether the path is a link, so falling back would publish through a link we were not allowed to inspect. Focused tests added for both branches.
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fws/u4-read-scope-recording branch from bb64d87 to 6dd95ce Compare October 5, 2026 13:15
easonLiangWorldedtech added 2 commits October 5, 2026 22:30
… type-sound

compile failed at the unit head on three points:
- RollbackFailureError needs a string backupPath, but the throw now happens after cleanup, so the
  `string | null` narrowing was lost. The failure is now held as { error, backupPath }.
- The async lstat stand-in is built on the Stats prototype so it satisfies fsSync.Stats.
- The realpath/readlink mocks are typed to the real signatures; the readlink mock answers once
  because only the link path is read.

tsc clean, 50 tests pass, ESLint --max-warnings=0 clean, no suppression change.
@easonLiangWorldedtech
easonLiangWorldedtech force-pushed the fws/u4-read-scope-recording branch from 6dd95ce to 5bdf5b0 Compare October 5, 2026 14:39
easonLiangWorldedtech added 5 commits October 5, 2026 22:47
The any usage this entry covered is gone in the rewritten file, so the count drops 4 -> 3.
eslint --prune-suppressions --max-warnings=0 confirms it.
The read tools record the observed on-disk version through task.observationRegistry, but the field
was only declared in a later unit, so at this head the call dereferences undefined and the mocked
e2e run fails on the read_file smoke tests. The registry is introduced by this unit, so the field
belongs here.

tsc clean on this unit, 11 observationRegistry tests pass, ESLint --max-warnings=0 clean.
The two any usages this entry covered are gone in the rewritten spec, so the count drops 98 -> 96.
eslint --prune-suppressions --max-warnings=0 confirms it.
The staging-identity guard compared the caller-staged file with the target through fs.lstat(targetPath).catch(() =>
null). Any error other than ENOENT - EACCES, ENOTDIR, ELOOP - was therefore indistinguishable from "there is no
target", so a staging path that is a hard link of the target would pass the identity check, reach the commit rename,
and let the failure handler unlink the only copy of the content.

Only ENOENT now yields "no target"; anything else fails closed with a StagingPathError before anything is opened,
staged or renamed. Test: the staging path resolves to a hard link of the target (same ino/dev) and the target lstat
rejects with EACCES; the write is rejected with the comparison error and neither openSync nor rename is called.
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Series alignment in 467cfda: the staging-identity guard no longer reads a failed target lstat as a missing target. fs.lstat(targetPath).catch(() => null) made EACCES / ENOTDIR / ELOOP indistinguishable from "there is no target", so a caller-staged path that is a hard link of the target would pass the identity check, reach the commit rename, and let the failure handler unlink the only copy of the content. Only ENOENT now means "no target"; anything else fails closed with a StagingPathError before anything is opened, staged or renamed.

Regression test in safeWriteText.spec.ts: the staging path resolves to a hard link of the target (same ino/dev) and the target lstat rejects with EACCES — the write is rejected with the comparison error and neither openSync nor rename is called. Verified as a real regression test: reverting the catch to () => null makes it fail.

Local: 85 passed / 4 skipped across services/file-safety/__tests__ + safeWriteJson + lockKey; eslint --max-warnings=0 clean, no suppression drift.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 13 minutes.

@github-actions github-actions Bot added coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit and removed awaiting-author PR is waiting for the author to address requested changes labels Oct 7, 2026
The staging-identity guard read fs.Stats.ino/dev as JS numbers. On NTFS and ReFS those identifiers can exceed
Number.MAX_SAFE_INTEGER, and the rounding makes two different files look identical - a valid caller-staged file is
then rejected with StagingPathError (and a real alias could be missed). Both lstats now request { bigint: true } and
the comparison checks for bigint values before comparing them.

The spec stand-in carries bigint ino/dev, matching what lstat({ bigint: true }) returns at runtime (fs.BigIntStats is
a type-only export, so the double is documented at its single assertion).
@github-actions github-actions Bot removed coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Oct 7, 2026
easonLiangWorldedtech added 2 commits October 7, 2026 12:58
The identity stand-in returns bigint identifiers regardless of the options, so the same-file tests could still pass if
either lstat dropped { bigint: true } - which is exactly the case that matters on NTFS/ReFS. Both tests now assert
that every identity lstat requested bigint stats.

The calls are filtered by their options rather than by path spelling: path.resolve prefixes a drive letter to
/tmp/... on Windows, so a path filter would only see one of the two reads on that platform.
…served

On win32 the DACL of an existing target is saved before the commit rename so it can be reapplied afterwards. Two
paths silently skipped that step and still published:
- icacls /save failed (saved === false): the dump is cleaned up and the rename proceeds, so the new file inherits
  different access rights;
- fs.access(targetPath) failed with something other than ENOENT (EACCES, ...): the catch treated "cannot check" as
  "target absent" and skipped DACL handling entirely.

Both now report through a new onWarning sink (default console.warn): the write still proceeds - a missing or failing
icacls must not leave the user unable to save, which is the documented fallback - but the caller is told the
replacement may inherit different access rights instead of discovering it later.

Tests: icacls save failure still commits the write and yields exactly one access-rights warning; an EACCES on the
target yields the could-not-check warning and no icacls call. Verified as real regression tests - neutralizing the two
warn calls makes both fail. 272 passed / 4 skipped locally; tsc and eslint clean.
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Series alignment in 1331a91: a win32 replacement whose DACL could not be saved (failed icacls /save) or could not be checked (fs.access failing with something other than ENOENT) is now reported through a new onWarning sink instead of happening silently; the write still commits, per the documented fallback. Two regression tests added (verified by neutralizing the two warn calls). Same change as on #1915, where the finding was raised. @coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

@easonLiangWorldedtech Thanks for the update. I’ll request a full review of the current head, including commit 1331a910f. The review will check both DACL warning paths, the documented commit fallback, and the two regression tests.


✏️ Learnings added
Learnt from: easonLiangWorldedtech
URL: https://github.com/Zoo-Code-Org/Zoo-Code/pull/1913

Timestamp: 2026-10-07T05:14:09.701Z
Learning: For Windows replacements in safeWriteText, the documented fallback permits the write to commit when DACL preservation cannot be completed. A failed icacls /save or an fs.access failure other than ENOENT must report a warning through the onWarning sink rather than fail silently.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 35 minutes.

@github-actions github-actions Bot added coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Oct 7, 2026
onWarning is documented as the sink for non-fatal safety notices, but delivery was not isolated from the write: an
onWarning callback that threw propagated to the outer failure handler before fs.rename, turning a non-fatal notice into a
failed save. The warn binding now catches callback failures and logs them. The restore-failure notice is routed through
the same binding so a caller supplying onWarning receives it. Same fix as the u6 unit, kept aligned across the series.

Local: safeWriteText 58/58 green; eslint clean, no suppression growth.
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Series alignment with the u6 unit (#1915): onWarning delivery is now isolated from the write, so a callback that throws can no longer abort fs.rename after a documented non-fatal safety notice, and the DACL restore failure is routed through the same binding instead of console.warn. New test: a throwing onWarning leaves the write succeeding.

Pushed as e412fce. Local: safeWriteText 58/58 green, eslint clean, no suppression-count growth.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 6 minutes.

@github-actions github-actions Bot removed coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Oct 7, 2026
proper-lockfile creates ${lockKey}.lock beside the lock key, and the key is the
symlink referent. A repository that plants .roo/mcp.json -> ~/.ssh/config
therefore made a confined write create a lock directory OUTSIDE the declared
scope, and when that directory was not writable the caller got a
lock-acquisition error after up to five retries instead of
ConfinedPathEscapeError. The confinement check now runs on the lock key before
acquireFileLock and is repeated on the resolved publish target inside the lock
(a peer writer may have moved the referent in between); both checks share
_assertWithinScope so they canonicalize identically.

Ported across the file-safety unit series so every unit that declares a scope
carries the same guarantee (same fix as fws/u3-observation-completeness).
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Series alignment: the confinement check now runs before the advisory lock (bcd178c61). proper-lockfile creates ${lockKey}.lock beside the lock key, and the key is the symlink referent, so a repository-planted link out of the declared scope (.roo/mcp.json -> ~/.ssh/config) previously created a lock directory outside the scope — and an unwritable referent directory surfaced a lock-acquisition error after retries instead of ConfinedPathEscapeError. The check is repeated on the resolved publish target inside the lock, and both share _assertWithinScope.

One ordering test added (a lock mock that throws if reached, no symlinks so it runs on every lane); safeWriteJson 25 passed / 4 skipped locally, eslint clean. Same fix as #1912 so the units stay identical when they merge in order.

@github-actions github-actions Bot added coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Oct 7, 2026
Series alignment for the two review findings fixed on fws/u6-apply-patch-wiring:

1. safeWriteText's warn wrapper could not catch a rejection from an async
   onWarning sink - TypeScript accepts a value-returning callback where a void one
   is expected - so the rejected promise was left unhandled, which under Node's
   default mode can end the process after a write that already succeeded. The
   wrapper now attaches a catch handler without awaiting (awaiting would let
   warning delivery delay a committed write, or stall it on a hung sink) and
   reports the rejection through the fallback sink.

2. safeWriteJson created the target's parent directory BEFORE the preflight
   confinement check, so a confined write to an out-of-scope path with a missing
   parent still created a directory outside confineTo. resolveLockKey and the check
   need no directory to exist, so the order is now lock key, confinement, mkdir;
   the in-lock check on the resolved publish target stays.
@easonLiangWorldedtech

Copy link
Copy Markdown
Contributor Author

Series alignment with #1915: two review findings fixed here in 244f4b62c.

  • safeWriteText's warning wrapper now handles an async onWarning sink: a returned promise gets a catch handler without awaiting, so a rejection is reported through the fallback sink instead of surfacing as an unhandled rejection (which under Node's default mode can end the process after a write that already succeeded).
  • safeWriteJson now runs the confinement preflight before creating the target's parent directory (order: lock key → confinement → mkdir), so an out-of-scope target with a missing parent no longer creates a directory outside confineTo; the in-lock check on the resolved publish target stays.

Tests ported; local suites green (services/file-safety + safeWriteJson), eslint clean.

@github-actions github-actions Bot added coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit and removed coderabbit-review-active Required CI passed; CodeRabbit review is active awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit labels Oct 7, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-coderabbit Waiting for CodeRabbit to approve the latest commit coderabbit-review-active Required CI passed; CodeRabbit review is active

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant