Merge nucleic/zesty-ancient-koala-i9if into dev

This commit is contained in:
2026-07-27 18:31:14 -07:00
parent 195ac00d0b
commit f7755f53b4
2 changed files with 40 additions and 2 deletions
+34 -2
View File
@@ -398,6 +398,30 @@ fn read_back(record: &brush_core::gate::RedirectRecord) -> Option<(u64, Vec<u8>,
Some((total, buf, total > n as u64)) Some((total, buf, total > n as u64))
} }
/// Resolve a redirection target against the directory its command ran in, so the
/// reported path is meaningful to a host that never saw that cwd. Purely lexical
/// (`.`/`..` collapsed, no symlink resolution, no filesystem access): the target of a
/// truncating write may not exist yet, and observation must never stat its way into a
/// slow or hanging path.
fn absolute_path(path: &Path, cwd: &Path) -> PathBuf {
let joined = if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
};
let mut out = PathBuf::new();
for component in joined.components() {
match component {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
out.pop();
}
other => out.push(other.as_os_str()),
}
}
out
}
/// Build a preview string (redacted, base64) from captured bytes. Binary data is /// Build a preview string (redacted, base64) from captured bytes. Binary data is
/// previewed as a hex head rather than mangled UTF-8. /// previewed as a hex head rather than mangled UTF-8.
fn preview_b64(data: &[u8]) -> String { fn preview_b64(data: &[u8]) -> String {
@@ -502,6 +526,14 @@ impl brush_core::gate::Gate for RecordingGate {
// Data-flow read-back for this command's redirects (docs/NASH.md §5.2). // Data-flow read-back for this command's redirects (docs/NASH.md §5.2).
for record in &pending.redirects { for record in &pending.redirects {
// The host resolves a redirect target against the session's worktree to decide
// which file was edited (and which lock that is), so report the path absolutely:
// `> out.txt` is only meaningful next to the directory the command ran in, and the
// redirect event — unlike `exec` — carries no cwd of its own.
let target = record
.path
.as_ref()
.map(|p| absolute_path(p, &pending.cwd).to_string_lossy().into_owned());
let Some((total, data, truncated)) = read_back(record) else { let Some((total, data, truncated)) = read_back(record) else {
// Special file / unreadable — emit metadata only. // Special file / unreadable — emit metadata only.
obs.send(Event::Redirect { obs.send(Event::Redirect {
@@ -510,7 +542,7 @@ impl brush_core::gate::Gate for RecordingGate {
cmd_seq: exec_seq, cmd_seq: exec_seq,
op: record.op.clone(), op: record.op.clone(),
fd: record.fd, fd: record.fd,
target: record.path.as_ref().map(|p| p.to_string_lossy().into_owned()), target,
bytes: 0, bytes: 0,
truncated: false, truncated: false,
hash: String::new(), hash: String::new(),
@@ -524,7 +556,7 @@ impl brush_core::gate::Gate for RecordingGate {
cmd_seq: exec_seq, cmd_seq: exec_seq,
op: record.op.clone(), op: record.op.clone(),
fd: record.fd, fd: record.fd,
target: record.path.as_ref().map(|p| p.to_string_lossy().into_owned()), target,
bytes: total, bytes: total,
truncated, truncated,
hash: fnv1a_hex(&data), hash: fnv1a_hex(&data),
+6
View File
@@ -384,6 +384,12 @@ fn redirect_readback_captures_each_operator() {
let trunc = redirs.iter().find(|e| e["op"] == ">").expect("truncate redirect"); let trunc = redirs.iter().find(|e| e["op"] == ">").expect("truncate redirect");
assert_eq!(decode_preview(trunc), "first\n"); assert_eq!(decode_preview(trunc), "first\n");
assert_eq!(trunc["bytes"], 6); assert_eq!(trunc["bytes"], 6);
// The target is reported ABSOLUTE (docs/NASH.md §5.2). A redirect event carries no cwd of its
// own, and the host maps the path to a worktree file — and therefore to a file lock — so a bare
// `d.txt` would be unresolvable there. `run_nash` runs in a fresh temp workspace.
let target = trunc["target"].as_str().expect("redirect target");
assert!(target.starts_with('/'), "target not absolute: {target}");
assert!(target.ends_with("/d.txt"), "target lost its file name: {target}");
let append = redirs.iter().find(|e| e["op"] == ">>").expect("append redirect"); let append = redirs.iter().find(|e| e["op"] == ">>").expect("append redirect");
// Append captures ONLY the added bytes, not the whole file. // Append captures ONLY the added bytes, not the whole file.