newt-core 0.8.0

Newt-Agent core types, errors, and the NeMoCode-style tier router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
//! Object-bound workspace filesystem capability (step-52.1).
//!
//! # Why this exists
//!
//! Filesystem authorization in newt has historically been *pathname*-bound: a
//! predicate decides "is this string inside the workspace?" (`tui_permits_path`,
//! `is_workspace_contained`, `is_safe_worktree_path`) and then a *separate*
//! `std::fs` call opens the path. Two structural flaws follow from that split:
//!
//! 1. **TOCTOU.** The name is checked, then re-resolved at open time; a rename or
//!    symlink swap between the two makes the checked name and the opened object
//!    differ.
//! 2. **Symlink escape.** A lexical check sees `ws/link/secret` as "inside `ws`";
//!    the kernel, following `link -> /etc`, opens `/etc/secret`. `#522` /
//!    `fs-canonical-containment` names this the known residual.
//!
//! [`WorkspaceDir`] removes the split. It owns an `O_DIRECTORY` file descriptor
//! for the workspace root and resolves every relative path *through that fd* with
//! `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)`. Authorization is then bound
//! to the **object the kernel actually opened**, atomically: a path that would
//! leave the root — via `..`, an absolute component, or a symlink (under the
//! workspace or not) whose target is outside it — is refused *by the kernel at
//! resolve time*, in the same syscall that opens it. There is no separate name to
//! check and no window to swap.
//!
//! `RESOLVE_BENEATH` rejects any resolution that would ascend above the root fd
//! (so `..`, absolute paths, and absolute/escaping symlinks cannot leave it);
//! `RESOLVE_NO_MAGICLINKS` rejects `/proc`-style magic links. In-tree symlinks
//! that stay beneath the root are still permitted — the fence is *containment*,
//! not a blanket symlink ban.
//!
//! # Scope (step-52.1)
//!
//! This slice lands the capability and proves the containment property (see
//! `tests/fs_cap_object_bound.rs`). Reading ([`open`](WorkspaceDir::open)),
//! writing ([`create`](WorkspaceDir::create)), and directory traversal
//! ([`open_dir`](WorkspaceDir::open_dir)) are object-bound here. Rewiring the
//! existing file tool arms and the write primitives (`newt-core` `tools.rs`,
//! `newt-tools` `patch.rs`) onto it — and the matching flip of the residual
//! `tui_permits_path_symlink_escape_is_the_known_residual` test — is step-52.2 /
//! step-52.3. Mutating-name operations (`unlinkat` / `mkdirat`) need the
//! open-parent-then-operate pattern to stay beneath-safe and land with the
//! write-arm rewire that consumes them.
//!
//! Linux uses `openat2`; macOS uses a descriptor-relative `openat` walk with
//! `O_NOFOLLOW` for every component. macOS conservatively rejects even in-tree
//! symlinks; operator-supplied root aliases (such as /var) remain supported.
//! Other platforms retain their existing consumer fallback.

use std::fs::File;
use std::io;
use std::os::fd::OwnedFd;
use std::path::{Component, Path};

use rustix::fs::{mkdirat, open, unlinkat, AtFlags, Mode, OFlags};
#[cfg(target_os = "linux")]
use rustix::fs::{openat2, ResolveFlags};

/// A capability handle to a workspace root directory. Every method resolves its
/// relative path argument *beneath* the held root fd; a path that would escape is
/// an error, never an open of an object outside the root.
///
/// The handle *is* the authority: holding a `WorkspaceDir` grants access to that
/// subtree and no more, and a subtree handle from [`open_dir`](Self::open_dir)
/// can only narrow — never widen — that authority.
#[derive(Debug)]
pub struct WorkspaceDir {
    /// `O_DIRECTORY` fd for the workspace root. The private field is the
    /// capability: a `WorkspaceDir` can exist only for a directory that was
    /// actually opened, and every resolve is anchored to *this* fd.
    root: OwnedFd,
}

impl WorkspaceDir {
    /// Open `path` as the workspace root, returning a capability anchored to it.
    ///
    /// `path` is resolved with the caller's ambient authority — it is the
    /// operator-supplied root, not a model-supplied path. Every *subsequent*
    /// access goes through the returned handle and is contained beneath it.
    #[cfg(target_os = "linux")]
    pub fn open_root(path: &Path) -> io::Result<Self> {
        let root = open(
            path,
            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
            Mode::empty(),
        )
        .map_err(io::Error::from)?;
        Ok(Self { root })
    }

    /// Authenticate fixed system aliases, then acquire every component with
    /// O_NOFOLLOW. A mutable root or ancestor symlink never becomes authority,
    /// including a replacement between the lexical gate and this open.
    #[cfg(target_os = "macos")]
    pub fn open_root(path: &Path) -> io::Result<Self> {
        let path = path.to_str().ok_or_else(|| {
            io::Error::new(io::ErrorKind::InvalidInput, "non-UTF-8 authority root")
        })?;
        let path = crate::caveats::lexically_normalize(path);
        let anchor = if path.is_absolute() { "/" } else { "." };
        let root = open(
            anchor,
            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
            Mode::empty(),
        )
        .map_err(io::Error::from)?;
        Self { root }.open_dir(path.strip_prefix("/").unwrap_or(&path))
    }

    /// Build a capability over an **already-held** directory descriptor —
    /// `dup`'d from `root`'s own fd (`F_DUPFD_CLOEXEC`, via
    /// [`std::os::fd::BorrowedFd::try_clone_to_owned`]), never by resolving
    /// `root`'s pathname a second time (#2686 review round 5, P1). A prior
    /// version of this capability independently opened its own
    /// `WorkspaceDir` via [`Self::open_root`] against the SAME intended
    /// pathname an [`agent_bridle_fdguard::GrantedRoot`] had just been
    /// acquired from — two opens of one intended object, bound only by the
    /// argument that nothing hostile ran between them, not by a kernel
    /// guarantee. `dup` makes it structurally ONE object: the returned
    /// handle's fd and `root`'s fd name the identical kernel directory, so
    /// there is no second pathname resolution left for anything to redirect.
    pub fn from_granted_root(root: &agent_bridle_fdguard::GrantedRoot) -> io::Result<Self> {
        Ok(Self {
            root: root.as_fd().try_clone_to_owned()?,
        })
    }

    /// The single choke point: resolve `rel` beneath the root fd and return the
    /// opened fd, or an error if resolution would escape. Every public method
    /// flows through here, so the containment property has one owner.
    #[cfg(target_os = "linux")]
    fn resolve(&self, rel: &Path, oflags: OFlags, mode: Mode) -> io::Result<OwnedFd> {
        // Containment policy for every resolve: stay beneath the root fd (so `..`,
        // absolute paths, and escaping symlinks are refused), and reject magic
        // links. In-tree symlinks that stay beneath still resolve.
        let resolve = ResolveFlags::BENEATH | ResolveFlags::NO_MAGICLINKS;
        openat2(&self.root, rel, oflags | OFlags::CLOEXEC, mode, resolve).map_err(io::Error::from)
    }

    /// macOS needs O_DIRECTORY and caller-selected nonblocking/create flags
    /// that Bridle's current read/write-only GrantedRoot API cannot express.
    /// Walk normal components through held descriptors and never follow links.
    #[cfg(target_os = "macos")]
    fn resolve(&self, rel: &Path, oflags: OFlags, mode: Mode) -> io::Result<OwnedFd> {
        let mut directory = self.root.try_clone()?;
        let mut names = Vec::new();
        for component in rel.components() {
            match component {
                Component::Normal(name) => names.push(name),
                Component::CurDir => {}
                _ => return Err(io::Error::from_raw_os_error(libc::EXDEV)),
            }
        }
        let Some((leaf, parents)) = names.split_last() else {
            return rustix::fs::openat(
                &directory,
                ".",
                oflags | OFlags::CLOEXEC | OFlags::NOFOLLOW,
                mode,
            )
            .map_err(io::Error::from);
        };
        for name in parents {
            directory = rustix::fs::openat(
                &directory,
                Path::new(name),
                OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
                Mode::empty(),
            )
            .map_err(io::Error::from)?;
        }
        rustix::fs::openat(
            &directory,
            Path::new(leaf),
            oflags | OFlags::CLOEXEC | OFlags::NOFOLLOW,
            mode,
        )
        .map_err(io::Error::from)
    }

    /// Open a file for reading, contained beneath the root.
    pub fn open(&self, rel: &Path) -> io::Result<File> {
        Ok(File::from(self.resolve(
            rel,
            OFlags::RDONLY,
            Mode::empty(),
        )?))
    }

    /// Open a regular file without blocking on a FIFO. Diagnostic snapshots
    /// pass `nofollow` to distinguish a final symlink from its target; mutation
    /// verification may follow a contained link, matching the write policy.
    pub fn open_regular(&self, rel: &Path, nofollow: bool) -> io::Result<File> {
        let flags = OFlags::RDONLY
            | OFlags::NONBLOCK
            | if nofollow {
                OFlags::NOFOLLOW
            } else {
                OFlags::empty()
            };
        let file = File::from(self.resolve(rel, flags, Mode::empty())?);
        if !file.metadata()?.is_file() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "path is not a regular file",
            ));
        }
        Ok(file)
    }

    /// Append only to a regular file beneath the held root. Nonblocking open
    /// refuses a readerless FIFO; descriptor validation also rejects a FIFO
    /// with a reader, before any bytes are written. Never follow the final link.
    pub(crate) fn append_regular(&self, rel: &Path) -> io::Result<File> {
        let file = File::from(self.resolve(
            rel,
            OFlags::WRONLY | OFlags::CREATE | OFlags::APPEND | OFlags::NOFOLLOW | OFlags::NONBLOCK,
            Mode::from_raw_mode(0o644),
        )?);
        if !file.metadata()?.is_file() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "path is not a regular file",
            ));
        }
        Ok(file)
    }

    /// Read a granted regular file, including a grant naming the file itself.
    /// An exact grant exposes only its basename, never the parent capability;
    /// following a final link would grant a different object, even next door.
    pub(crate) fn open_granted_file(root: &Path, rel: &Path, nofollow: bool) -> io::Result<File> {
        if rel == Path::new(".") {
            let (parent, name) = Self::exact_file_parent(root)?;
            parent.open_regular(&name, true)
        } else {
            Self::open_root(root)?.open_regular(rel, nofollow)
        }
    }

    /// Create or replace the granted file without granting its siblings.
    /// Exact-file parents must exist; truncation follows regular-file validation
    /// on the opened descriptor, with no second pathname resolution.
    pub(crate) fn create_granted_file(root: &Path, rel: &Path) -> io::Result<File> {
        if rel == Path::new(".") {
            let (parent, name) = Self::exact_file_parent(root)?;
            let file = File::from(parent.resolve(
                &name,
                OFlags::WRONLY | OFlags::CREATE | OFlags::NOFOLLOW | OFlags::NONBLOCK,
                Mode::from_raw_mode(0o644),
            )?);
            if !file.metadata()?.is_file() {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "path is not a regular file",
                ));
            }
            file.set_len(0)?;
            Ok(file)
        } else {
            let directory = Self::open_root(root)?;
            if let Some(parent) = rel.parent().filter(|path| !path.as_os_str().is_empty()) {
                directory.create_dir_all(parent)?;
            }
            directory.create(rel)
        }
    }

    fn exact_file_parent(path: &Path) -> io::Result<(Self, std::path::PathBuf)> {
        let name = path
            .file_name()
            .ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?;
        let parent = path
            .parent()
            .filter(|path| !path.as_os_str().is_empty())
            .unwrap_or(Path::new("."));
        Ok((Self::open_root(parent)?, Path::new(name).to_path_buf()))
    }

    /// Create (or truncate) a file for writing, contained beneath the root.
    ///
    /// The final component is created *inside* the resolved-beneath path or the
    /// whole call fails — there is no `workspace.join(rel)` that could land the
    /// write elsewhere, and no separate containment check to skip.
    pub fn create(&self, rel: &Path) -> io::Result<File> {
        Ok(File::from(self.resolve(
            rel,
            OFlags::WRONLY | OFlags::CREATE | OFlags::TRUNC,
            Mode::from_raw_mode(0o644),
        )?))
    }

    /// Create `rel` exclusively, contained beneath the root — `EEXIST` if it is
    /// already there. The lockfile half of git's own ref-update protocol
    /// (`<ref>.lock`): a bounded `create` would silently truncate a lock another
    /// writer already holds, so this is a distinct flag, not `create` with a
    /// different argument.
    pub fn create_new(&self, rel: &Path) -> io::Result<File> {
        Ok(File::from(self.resolve(
            rel,
            OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL,
            Mode::from_raw_mode(0o644),
        )?))
    }

    /// Create `rel` and any missing parent directories, each contained beneath
    /// the root. The walk opens (or creates) one component at a time on a fd
    /// resolved *beneath* the previous one, so a symlink or `..` in any component
    /// is refused by the kernel — there is no `mkdir -p` over an un-resolved path.
    /// An existing directory is fine; a non-directory in the path errors.
    pub fn create_dir_all(&self, rel: &Path) -> io::Result<()> {
        // `try_clone` so the walk owns its cursor without consuming `self.root`.
        let mut cur: OwnedFd = self.root.try_clone()?;
        let dir_flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC;
        for comp in rel.components() {
            let name = match comp {
                Component::Normal(n) => n,
                Component::CurDir => continue,
                // A beneath-safe relative path has no `..`, root, or prefix.
                _ => return Err(io::Error::from_raw_os_error(libc::EXDEV)),
            };
            let step = Path::new(name);
            let child = match (Self {
                root: cur.try_clone()?,
            })
            .resolve(step, dir_flags, Mode::empty())
            {
                Ok(fd) => fd,
                Err(error) if error.kind() == io::ErrorKind::NotFound => {
                    mkdirat(&cur, step, Mode::from_raw_mode(0o755)).map_err(io::Error::from)?;
                    (Self {
                        root: cur.try_clone()?,
                    })
                    .resolve(step, dir_flags, Mode::empty())?
                }
                Err(e) => return Err(e),
            };
            cur = child;
        }
        Ok(())
    }

    /// Resolve the directory that holds `rel`'s final component (object-bound —
    /// a symlink / `..` in the parent path is refused) and return it with that
    /// final component name. A bare filename resolves against the root itself.
    /// The shared owner of the name-granularity mutation ops ([`unlink`], [`rename`]).
    ///
    /// [`unlink`]: Self::unlink
    /// [`rename`]: Self::rename
    fn parent_dir_and_name(&self, rel: &Path) -> io::Result<(OwnedFd, std::ffi::OsString)> {
        let name = rel
            .file_name()
            .ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))?
            .to_os_string();
        let parent = rel.parent().unwrap_or(Path::new(""));
        let dir = if parent.as_os_str().is_empty() {
            self.root.try_clone()?
        } else {
            self.resolve(parent, OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty())?
        };
        Ok((dir, name))
    }

    /// Remove a file entry, contained beneath the root. The parent is resolved
    /// object-bound, then the final component is removed with `unlinkat` relative
    /// to the parent fd — so the removal cannot be redirected outside the root,
    /// and a symlink at the final component is removed *as the link* (its target
    /// is not followed).
    pub fn unlink(&self, rel: &Path) -> io::Result<()> {
        let (parent_dir, name) = self.parent_dir_and_name(rel)?;
        unlinkat(&parent_dir, Path::new(&name), AtFlags::empty()).map_err(io::Error::from)
    }

    /// Rename `from` → `to`, both contained beneath the root. Each path's parent
    /// is resolved object-bound, then `renameat` moves the final component between
    /// the parent fds — so neither endpoint can be redirected outside the root by
    /// a symlink / `..` in its path. Used for atomic temp-then-rename writes.
    pub fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
        let (from_dir, from_name) = self.parent_dir_and_name(from)?;
        let (to_dir, to_name) = self.parent_dir_and_name(to)?;
        rustix::fs::renameat(
            &from_dir,
            Path::new(&from_name),
            &to_dir,
            Path::new(&to_name),
        )
        .map_err(io::Error::from)
    }

    /// Check that a path still names this held directory (optimistic observation).
    #[cfg(feature = "ast")]
    pub(crate) fn still_named_by(&self, path: &Path) -> io::Result<bool> {
        use std::os::unix::fs::MetadataExt;
        let held = File::from(self.root.try_clone()?).metadata()?;
        let named = std::fs::metadata(path)?;
        Ok(held.dev() == named.dev() && held.ino() == named.ino())
    }

    /// Publish a staged regular file without replacing an existing destination.
    /// Both parents stay bound to this capability; linkat fails with EEXIST.
    #[cfg(feature = "ast")]
    pub(crate) fn link_new(&self, from: &Path, to: &Path) -> io::Result<()> {
        let (from_dir, from_name) = self.parent_dir_and_name(from)?;
        let (to_dir, to_name) = self.parent_dir_and_name(to)?;
        rustix::fs::linkat(
            &from_dir,
            Path::new(&from_name),
            &to_dir,
            Path::new(&to_name),
            AtFlags::empty(),
        )
        .map_err(io::Error::from)
    }

    /// Open a subdirectory as its own contained [`WorkspaceDir`]. Traversal stays
    /// beneath the original root; the returned handle cannot reach outside it.
    pub fn open_dir(&self, rel: &Path) -> io::Result<Self> {
        Ok(Self {
            root: self.resolve(rel, OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty())?,
        })
    }

    /// List the entry names of a subdirectory, contained beneath the root. The
    /// directory is resolved object-bound — a symlink-escape directory is refused
    /// by the kernel — and its entries are read straight off the returned fd, so
    /// there is no second path to re-resolve. `.` and `..` are filtered; the
    /// order is filesystem order (the caller sorts). Names only, matching the
    /// `list_dir` tool's output.
    pub fn read_dir(&self, rel: &Path) -> io::Result<Vec<std::ffi::OsString>> {
        use std::os::unix::ffi::OsStringExt;
        let dirfd = self.resolve(rel, OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty())?;
        let dir = rustix::fs::Dir::read_from(&dirfd).map_err(io::Error::from)?;
        let mut names = Vec::new();
        for entry in dir {
            let entry = entry.map_err(io::Error::from)?;
            let bytes = entry.file_name().to_bytes();
            if bytes == b"." || bytes == b".." {
                continue;
            }
            names.push(std::ffi::OsString::from_vec(bytes.to_vec()));
        }
        Ok(names)
    }
}

// Model: GPT-6 | Harness: Codex | Operator: Shawn Hartsock | Time: 14:19 EDT | Date: 2026-09-16