You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
At commit 49a606bc, the FileSystem service provides atomic writeText and editText operations but ends without a delete primitive (packages/fs/fs/src/index.ts:223-263).
The existing mutation paths resolve the requested path, obtain the per-call sandbox policy, run the appropriate intent waterfall, and call the provider (packages/fs/tool-fs/src/write.ts:101-113, packages/fs/tool-fs/src/edit.ts:111-132). The sandboxing provider re-resolves the target and enforces writable-root containment immediately before mutation (packages/fs/fs-sandbox/src/index.ts:80-108,122-143). The service defines fs/write-intent and fs/edit-intent so policy plugins can guard those mutations (packages/fs/fs/src/index.ts:49-66).
Plugin use case
A workspace-restore plugin may record that a DSH write created a previously absent file. Restoring the earlier state requires removing that file.
Without a service method, the plugin must call processPath(), repeat containment and sandbox-policy checks, and use host unlink. That works only for host-local providers, bypasses the filesystem mutation intent path, and cannot support remote or sandbox-native providers.
delete() could mirror writeText by performing the guarded mutation atomically inside the provider and enforcing the supplied per-call sandbox policy.
fs/delete-intent could be a single-slot waterfall over the resolved target and opaque actor, returning an optional version guard consistently with fs/edit-intent.
This request is only for a file primitive that plugins can compose; it does not require a model-facing delete tool, recursive directory removal, rename, or copy.
Verified every anchor against 49a606bc (I now have the commit in a local checkout, so these are file-exact): the service really does end at editText with no delete primitive (fs/src/index.ts), and your mental model of the mutation template matches the code — resolvePolicy → fs.resolve → intent waterfall → provider call → error remediation → fs/observed (tool-fs/write.ts, tool-fs/edit.ts), with the sandboxing provider's checkedTarget re-canonicalizing and enforcing writable-root containment per call (fs-sandbox/src/index.ts). A few facts from the tree sharpen the design and expose one genuine trap.
Your "host-local only" claim is concretely the fs-e2b provider
There are two in-tree backends extending FileSystem: fs-local (bare) and fs-e2b (E2B). fs-e2b/src/index.ts implements writeText (:376) and editText (:405) but no delete — so today a plugin's "host unlink" workaround cannot touch a file living inside an E2B sandbox at all, not merely bypass policy. A service-level delete is the only route that reaches both providers uniformly. This strengthens the "missing primitive" framing: it is not just an API gap, it is a provider-surface asymmetry (write/edit are native, delete is not).
The version guard is cheap and fits the existing machinery
fs-local's version token is dev:ino:size:mtimeNs:ctimeNs derived from one bigint stat (fsio.ts, versionOf) — a probe, not a content hash. So delete(expected: { version }) can verify staleness with a single stat, no content read, and slots into the same pre-mutation guard editText already uses (fs-local checks existing.version !== expected.version before mutating). The restore-safety semantic works: your plugin records the FsWriteOutcome.version its write produced (write outcomes and fs/observed presence both carry it), and a later delete(expected) refuses with FS_STALE_VERSION when the user has modified the file in between. That is the property that makes "restore = remove what I created" non-destructive, and it composes with the opaque-version discipline (consumers never manufacture versions — they replay recorded ones).
The genuine design gap: absence semantics and outcome shape
expected when the file is absent is underspecified, and restore replay makes it load-bearing. If the plugin recorded "write created file at V" and the file is now absent, the goal is already achieved (a prior restore ran, or the workspace was reset) — re-delete should not throw. But present-at-a-different-version must refuse. Recommend the outcome carry that distinction rather than forcing a pre-stat:
delete(target, expected) → { kind: 'deleted' } when it removed a file (present, version matched, or unconditional), and { kind: 'absent' } when nothing was there.
Absent + expected → success (absent), because the guarded intent ("this file, at this version, no longer exists") already holds.
That gives the restore command idempotent replay with no pre-stat and a truthful report (files removed vs already gone) for its structured outcome.
The trap: delete containment must not follow the final symlink
checkedTarget (shared by write/edit) realpaths the whole path and returns the fresh target — correct for content mutation, wrong for unlink. POSIX unlink removes the directory entry and never follows the final component. If an in-root path is a symlink pointing outside the writable root, whole-path realpath containment either denies the delete (the entry is inside the root and should be removable) or resolves outside. The backend already models this distinction — fs-local has both stat-based probe and lstat-based probeNoFollow, and lstat is exposed on the service. The fs-sandbox delete override should canonicalize the parent (catches a swapped symlink ancestor) and unlink the leaf entry as-is — it cannot reuse checkedTarget verbatim. Worth nailing in the proposal before implementation, because it is the one place the delete path diverges structurally from write/edit.
Placement and shape
Everything else lines up with your sketch and the code's own conventions:
Abstract delete(target, expected?: { version }, signal?, sandboxPolicy?) on FileSystem after editText — argument order matches writeText/editText (guard before signal before policy).
'fs/delete-intent' as a single-slot waterfall over (target, actor, next) mirroring fs/edit-intent (fs/src/index.ts:66), returning { version } | undefined; policy plugins refuse by throwing (that is how fs-observation-policy refuses edits of unobserved targets). No listener → next() = unconditional, subject only to sandbox containment — same default parity as write/edit, and for a restore plugin workspace-write is the right policy anyway.
Scope boundary is right: no model-facing delete tool means the escalation vocabulary (sandbox_permissions targets) stays untouched. If a model-facing delete is ever added later, the presentation vocabulary already has a 'delete' tool-call kind waiting for it.
One integration note: fs-observation-policy is event-only and absence-aware, keyed by the actor's session — but service methods do not emit fs/observed (the tool layer does, with exec as actor), and a plugin call has no tool-execution actor. Since your restore plugin records its own durable domain events (per your other thread), the plugin-owned record is the right home for the negative observation; just be explicit in the API docs that a service-level delete does not publish to the observation layer, so callers who need policy coherence emit the absence observation themselves (or a future model-facing tool does, following the write/edit template).
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Request
At commit
49a606bc, theFileSystemservice provides atomicwriteTextandeditTextoperations but ends without a delete primitive (packages/fs/fs/src/index.ts:223-263).The existing mutation paths resolve the requested path, obtain the per-call sandbox policy, run the appropriate intent waterfall, and call the provider (
packages/fs/tool-fs/src/write.ts:101-113,packages/fs/tool-fs/src/edit.ts:111-132). The sandboxing provider re-resolves the target and enforces writable-root containment immediately before mutation (packages/fs/fs-sandbox/src/index.ts:80-108,122-143). The service definesfs/write-intentandfs/edit-intentso policy plugins can guard those mutations (packages/fs/fs/src/index.ts:49-66).Plugin use case
A workspace-restore plugin may record that a DSH
writecreated a previously absent file. Restoring the earlier state requires removing that file.Without a service method, the plugin must call
processPath(), repeat containment and sandbox-policy checks, and use hostunlink. That works only for host-local providers, bypasses the filesystem mutation intent path, and cannot support remote or sandbox-native providers.Suggested API
delete(target: FsTarget, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<void>delete()could mirrorwriteTextby performing the guarded mutation atomically inside the provider and enforcing the supplied per-call sandbox policy.fs/delete-intentcould be a single-slot waterfall over the resolved target and opaque actor, returning an optional version guard consistently withfs/edit-intent.This request is only for a file primitive that plugins can compose; it does not require a model-facing delete tool, recursive directory removal, rename, or copy.
All reactions