Skip to content

feat(extensions): add host-mediated workspace document reads and writes - #674

Merged
benvinegar merged 5 commits into
mainfrom
claude/inline-diff-editing-esm5b8-workspace
Aug 6, 2026
Merged

feat(extensions): add host-mediated workspace document reads and writes#674
benvinegar merged 5 commits into
mainfrom
claude/inline-diff-editing-esm5b8-workspace

Conversation

@benvinegar

@benvinegar benvinegar commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Adds a host-mediated workspace API to extension command contexts:

  • readDocument(fileId, side)
  • canWriteDocument(fileId)
  • writeDocument({ fileId, text })

Extensions could already access node:fs; this API instead makes file access supported, attributable, consented, and synchronized with the review. It provides the foundation for inline diff editing.

Behavior

  • Reads return only content already represented by the review and work across review types without prompting.
  • Writes are limited to reloadable, unstaged working-tree reviews. Revision, staged, stash, patch, file-pair, and non-reloadable agent-context reviews return a named "unavailable" reason.
  • Targets use reviewed file IDs, not arbitrary paths. Deleted, binary, oversized, missing, symlinked, or root-escaping targets are refused.
  • Filesystem confinement is verified both before showing consent and again after consent, preventing deletion or symlink-swap races while the dialog is open.
  • Every write uses the existing attributed FIFO consent dialog. Cancellation returns { ok: false, reason: "cancelled" }.
  • Successful writes soft-reload the session so the review immediately reflects the new content.

Validation

Coverage includes policy and request validation, real-filesystem confinement and symlink cases, consent-window target changes, and App-level Git flows for writes, cancellation, read-only reviews, symlink refusal, and read-transform-write behavior.

Typecheck, unit tests, PTY integration, TTY smoke tests, lint, package checks, documentation checks, and CI pass. Documentation and a changeset are included.

This PR description was generated by Pi using gpt-5.6-sol

@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hunk-web Ready Ready Preview Aug 6, 2026 12:50pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a public ctx.workspace API for reading reviewed document sides and performing consented working-tree writes, with policy tests, App-level integration coverage, package checks, and documentation.

  • Adds whole-document read, write-availability, and write-result contracts to the extension API.
  • Centralizes review-kind, file-state, and lexical root-confinement policy in extensionWorkspace.ts.
  • Routes writes through attributed extension dialogs and reloads the active review after successful filesystem replacement.
  • Extends consumer fixtures, integration tests, changeset metadata, and both documentation sites.

Confidence Score: 4/5

The PR should not merge until host-mediated writes cannot escape through symlinks or target a path rewritten only by a presentation transform.

The new write path can replace an external symlink target despite lexical root confinement, and it trusts mutable post-transform file paths rather than an authoritative mapping to the reviewed working-tree document.

Files Needing Attention: src/ui/lib/extensionWorkspace.ts, src/ui/App.tsx, src/extensions/apply.ts

Security Review

A reviewed path that is a symlink can pass the lexical root check and cause the host write to overwrite the symlink target outside the repository. The write path must resolve or safely reject symlink traversal while ensuring the checked object is the same object written.

Important Files Changed

Filename Overview
src/ui/lib/extensionWorkspace.ts Introduces the central read/write policy, but lexical confinement permits symlink escapes and post-transform paths are treated as authoritative write destinations.
src/ui/App.tsx Wires workspace controls into command contexts, consent dialogs, filesystem writes, and reloads; the final write inherits unsafe target decisions from the policy.
src/extension-api/types.ts Adds the documented public workspace request, result, and command-context types without importing internal modules.
src/ui/lib/extensionWorkspace.test.ts Covers review-kind, malformed-input, file-state, side, and lexical traversal cases, but lacks symlink and transformed-path authority cases.
src/ui/AppHost.workspace.test.tsx Exercises real reads, accepted and declined writes, reload behavior, and unavailable review kinds, without covering the two unsafe target-resolution mechanisms.

Sequence Diagram

sequenceDiagram
  participant E as Extension command
  participant W as ctx.workspace
  participant P as Workspace policy
  participant D as Attributed dialog queue
  participant FS as Filesystem
  participant H as AppHost reload
  E->>W: readDocument(fileId, side)
  W->>P: resolve reviewed source fetcher
  P-->>W: bound read or null
  W-->>E: text or null
  E->>W: writeDocument(fileId, text)
  W->>P: resolve working-tree target
  P-->>W: target or unavailable
  W->>D: confirm extension and path
  D-->>W: accepted or cancelled
  W->>FS: writeFile(target, text)
  FS-->>W: success or failure
  W-->>E: write result
  W-->>H: soft reload after success
Loading
Prompt To Fix All With AI
### Issue 1
src/ui/lib/extensionWorkspace.ts:160-161
**Symlinks bypass workspace confinement**

When a reviewed path is a symlink to a file outside the review root, the lexical `resolve`/`relative` check accepts it and `writeFile` follows the symlink, causing confirmation for an in-repository path to overwrite an external file.

**How this was verified:** The target path is never canonicalized or rejected as a symlink before it reaches `writeFile`.

### Issue 2
src/ui/lib/extensionWorkspace.ts:141-160
**Transformed paths redirect workspace writes**

When a changeset transform preserves a file's id and render metadata but rewrites its path, workspace target resolution treats that presentation path as authoritative and writes it, causing a different working-tree file to be overwritten than the document represented by the reviewed source.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "chore: add changeset for workspace reads..." | Re-trigger Greptile

Comment thread src/ui/lib/extensionWorkspace.ts Outdated
Comment on lines +160 to +161
const absolutePath = resolve(root, path);
const relativePath = relative(root, absolutePath);

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.

P1 security Symlinks bypass workspace confinement

When a reviewed path is a symlink to a file outside the review root, the lexical resolve/relative check accepts it and writeFile follows the symlink, causing confirmation for an in-repository path to overwrite an external file.

How this was verified: The target path is never canonicalized or rejected as a symlink before it reaches writeFile.

Context Used: security.mdc Cursor rule (source)

Knowledge Base Used: Extension System: API, Loading, and Host Integration

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/lib/extensionWorkspace.ts
Line: 160-161

Comment:
**Symlinks bypass workspace confinement**

When a reviewed path is a symlink to a file outside the review root, the lexical `resolve`/`relative` check accepts it and `writeFile` follows the symlink, causing confirmation for an in-repository path to overwrite an external file.

**How this was verified:** The target path is never canonicalized or rejected as a symlink before it reaches `writeFile`.

**Context Used:** security.mdc Cursor rule ([source](https://github.com/modem-dev/modem/blob/main/.cursor/rules/security.mdc))

**Knowledge Base Used:** [Extension System: API, Loading, and Host Integration](https://app.greptile.com/modem/-/custom-context/knowledge-base/modem-dev/hunk/-/docs/extension-system.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +141 to +160
const file = files.find((candidate) => candidate.id === fileId);
if (!file) {
return { writable: false, detail: `No reviewed file has the id "${fileId}".` };
}

const path = normalizeDiffPath(file.path) ?? file.path;

if (readMetadataChangeType(file.metadata) === "deleted") {
return { writable: false, detail: `${path} was deleted in this review; it has no new side.` };
}

if (file.isBinary) {
return { writable: false, detail: `${path} is binary; workspace writes are text-only.` };
}

if (file.isTooLarge) {
return { writable: false, detail: `${path} was skipped as too large to load.` };
}

const absolutePath = resolve(root, path);

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.

P1 Transformed paths redirect workspace writes

When a changeset transform preserves a file's id and render metadata but rewrites its path, workspace target resolution treats that presentation path as authoritative and writes it, causing a different working-tree file to be overwritten than the document represented by the reviewed source.

Knowledge Base Used: Extension System: API, Loading, and Host Integration

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/lib/extensionWorkspace.ts
Line: 141-160

Comment:
**Transformed paths redirect workspace writes**

When a changeset transform preserves a file's id and render metadata but rewrites its path, workspace target resolution treats that presentation path as authoritative and writes it, causing a different working-tree file to be overwritten than the document represented by the reviewed source.

**Knowledge Base Used:** [Extension System: API, Loading, and Host Integration](https://app.greptile.com/modem/-/custom-context/knowledge-base/modem-dev/hunk/-/docs/extension-system.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-refresh branch from 591e440 to 9575563 Compare August 4, 2026 14:53
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-workspace branch from 356e70a to 5cec20e Compare August 4, 2026 14:54
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-refresh branch from 9575563 to 466361a Compare August 4, 2026 16:37
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-workspace branch from 5cec20e to 41d71cc Compare August 4, 2026 16:40
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-refresh branch from 466361a to 7425c13 Compare August 5, 2026 22:04
claude added 4 commits August 5, 2026 19:15
Extension isolation is crash containment, not a sandbox, so nothing
stopped an extension from reaching node:fs directly; there was just no
supported contract. ctx.workspace.writeDocument gives writes an explicit
one: targets are named by reviewed-file id only, available exclusively
for working-tree reviews, confirmed through the attributed extension
dialog queue, confined to the review root, and followed by the same soft
reload the refresh key runs so the review reflects the write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015aJpBUupsP9L7Wtd7MEzmU
An extension composing replacement text had no mediated way to read the
current document from a command handler; reads existed only inside
file-view layout requests. ctx.workspace.readDocument closes the loop
(read, transform, writeDocument) through the same one-policy module.
Reads never prompt and work in every review kind: they expose exactly
what the review already shows, unlike writes, which stay working-tree
only and confirmed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015aJpBUupsP9L7Wtd7MEzmU
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015aJpBUupsP9L7Wtd7MEzmU
…le sessions

The write path's root confinement was purely lexical, but git tracks
symlinks-to-files as ordinary reviewable entries, so a confirmed write
through a reviewed link would follow it anywhere on disk while the
dialog named only the innocent repo-relative path. writeDocument now
runs a filesystem verification before ever prompting: lstat refuses
links and non-files, a deleted target is not recreated, and the
realpath'd parent must stay inside the realpath'd root. The policy
module stays lexical and fs-free; both confinements share one
containment predicate so they cannot drift.

Writes also now require a session that can reload. A review started
with --agent-context - can never refresh, which silently broke the
documented promise that a successful write reloads the review; the
reload predicate moved to src/core/inputReload.ts so the fs-free
policy can share it with App and startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015aJpBUupsP9L7Wtd7MEzmU
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-workspace branch from 4d9adcb to b2edc6f Compare August 5, 2026 23:35
@benvinegar
benvinegar changed the base branch from claude/inline-diff-editing-esm5b8-refresh to main August 5, 2026 23:35
@benvinegar
benvinegar force-pushed the claude/inline-diff-editing-esm5b8-workspace branch from 113a946 to 59b651f Compare August 6, 2026 12:50
@benvinegar
benvinegar merged commit 4a656c7 into main Aug 6, 2026
12 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.

2 participants