pub trait WriteObserver:
Send
+ Sync
+ Debug {
// Required methods
fn before_write<'life0, 'life1, 'async_trait>(
&'life0 self,
path: &'life1 Path,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
fn after_write<'life0, 'life1, 'async_trait>(
&'life0 self,
path: &'life1 Path,
) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait;
}Expand description
P5-9 (design §2 module 20 checkpoint, §2.1 D-5 “write-path
interception seam shared with formatters”): the ONE well-defined
interception point around every file-mutating built-in tool
(write_file/edit_file/apply_patch) — installed on
ToolContext::write_observer, None by default. Both hooks fire
AFTER ToolContext::check_write has already approved the call (so an
observer never sees a write the sandbox itself refused) and BEFORE/AFTER
the actual mutation:
Self::before_write— pre-image capture.crate::checkpoint’scrate::checkpoint::CheckpointObserveris the only implementation today: it snapshotspath’s current on-disk content (or records “did not exist”) so a latercheckpoint restorecan undo the write.Self::after_write— post-write. A true no-op in every implementation shipped so far; reserved forformatters(P5-11, design line 510 “shared seam with checkpoint”) to run format-on-write from, without needing a SECOND interception point wired through the same three tools.
None (the default — [capabilities.checkpoint] off and no formatters
module yet) means neither hook is ever consulted: every write-tool
call-site’s observer check is if let Some(obs) = &ctx.write_observer,
a branch that’s simply never taken, so behavior is byte-identical to
before this seam existed.
P5-11 (§2 modules 28/29 lsp/formatters, C10): async_trait (rather
than the plain sync methods P5-9 originally shipped) because BOTH new
observers need real async I/O in after_write — formatters spawns and
awaits a subprocess, lsp writes/reads framed JSON-RPC over a child’s
stdio — and neither can block the tokio runtime thread the way a
synchronous call from inside an already-async fn execute() would.
CheckpointObserver’s own hooks stay synchronous internally (plain
blocking std::fs calls); wrapping them in async fn changes nothing
observable for it, since that blocking work already ran on the calling
task before this signature changed. after_write now RETURNS
Option<String> — an annotation to append to the calling tool’s result
string (formatter diff-back content, or LSP diagnostics) — None when
the observer has nothing to report, which is the only value
CheckpointObserver::after_write (still a no-op) ever returns, keeping
today’s tool-result text byte-identical whenever checkpoint is the only
observer installed.
Required Methods§
Sourcefn before_write<'life0, 'life1, 'async_trait>(
&'life0 self,
path: &'life1 Path,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn before_write<'life0, 'life1, 'async_trait>(
&'life0 self,
path: &'life1 Path,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
path (already resolved + sandbox-checked) is about to be
created/overwritten/deleted. Implementations must be fast and must
never propagate a failure as a tool error — a capture failure should
degrade the OBSERVER (e.g. disable itself with a one-time warning),
never block or fail the user’s actual edit.
Sourcefn after_write<'life0, 'life1, 'async_trait>(
&'life0 self,
path: &'life1 Path,
) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
fn after_write<'life0, 'life1, 'async_trait>(
&'life0 self,
path: &'life1 Path,
) -> Pin<Box<dyn Future<Output = Option<String>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
path was just written/deleted successfully. Not called when the
tool call itself failed (e.g. the write errored before completing).
Returns an optional annotation for the calling tool’s result text —
see the trait doc comment above.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".