Skip to content

fix(shell): make touch set file times and accept its options - #445

Closed
zastrowm wants to merge 4 commits into
strands-agents:mainfrom
zastrowm:fix/shell-touch-times
Closed

zastrowm wants to merge 4 commits into
strands-agents:mainfrom
zastrowm:fix/shell-touch-times

Conversation

@zastrowm

@zastrowm zastrowm commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

Description

The box shell's touch reported success on an existing file and changed nothing: it opened the file for append and closed it, which writes no byte and so moves no time. It also refused every option, so touch -d, -r, -t, -a, -m, and -c failed with invalid option. An agent that touches a file to mark it changed, for example for make, got exit 0 and stale times.

The vendored shell had no way to set a file's times, so this adds one along the same path chmod takes: a Kernel::set_times method, a FsOperation::SetTimes effect that admission sees, and a host branch that calls utimensat on the identity admission approved. touch then parses its options on top of that.

Decisions this depends on (both approved by @zastrowm before implementation):

  • Policy sees a time change as an ordinary write. SetTimes maps to the existing fs:write operation write_content, so the authored policy vocabulary does not change. A rule that permits writing a file also permits touching it. The cost: an operator cannot permit writes but forbid time changes. A new Box::FsWriteOperation::"set_times" value was the alternative; it would add to the frozen policy vocabulary for a distinction nobody has asked for.
  • vfs::Inode gains a public atime field. The in-memory VFS recorded no access time, so touch -a and -r could not work there. A struct literal of Inode outside the crate no longer compiles; nothing in this repo builds one outside vfs.rs. A private side table in VfsKernel was the alternative; it is more code for the same behavior.

Interfaces that moved, all in the vendored strands-shell crate: the required trait method Kernel::set_times; the types FileTimes and TimeChange; FsOperation::SetTimes; FileStat::accessed; vfs::Inode::atime. Box's own frozen surfaces are unchanged. crates/shell/UPSTREAM.md records the divergence.

Limits: -d accepts ISO 8601 and @SECONDS only, since the crate has no date parser; a date with no zone is UTC, as date assumes. The macOS file-attributes switch does not govern this: the shell runs in box's trusted process, outside the agent's Seatbelt profile.

Related Issues

Fixes #361

Type of Change

Bug fix

Testing

  • crates/shell/tests/touch_times.rs (12 cases) covers every option on a host bind, creating a missing file without truncation, a read-only bind refused while the next file still updates, and -d, -a, -r and creating a file at a date on the in-memory VFS. touch_creates_a_missing_in_memory_file_at_the_date fails unless touch waits for the new file's write (settle_writes) before setting its times; that write would otherwise reset the modification time to now. With the host branch made a no-op, 7 fail; with the in-memory branch keeping the access time, the 2 in-memory -a/-r cases fail.

  • an_invalid_date_is_refused includes non-ASCII dates; without the ASCII check parse_date panics on them.

  • crates/policy/tests/shell_fs_vocabulary.rs shows a rule on write_content decides touch.

  • On Linux x86_64 after merging main: build, test, clippy, and fmt --check with --all-features for strands-shell (2050 passed), strands-box-policy (676 passed), and strands-box (994 passed, 1 failed), plus the site-manifest test. The one failure, a_leafs_shebang_resolves_past_the_alias_directory, comes from this host's /usr/bin/env being a link to the multicall /usr/bin/coreutils; this change does not touch containment. Clippy reports no warnings in changed files.

  • Not yet run (why this is a draft): an end-to-end touch inside a real box on Linux aarch64 and on macOS, and the macOS build.

  • I ran the relevant suites (just check, or cargo test --workspace --all-features)

  • If I touched Rust, I ran cargo fmt --all and cargo clippy --workspace --all-targets --all-features -- -D warnings

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • My changes generate no new warnings

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Agent-assisted PR.

## Problem

The box shell's `touch` did not change the times of an existing file. It opened the file for
append and closed it. Nothing was written, so no time changed, and `touch` exited 0. An agent that
uses `touch` to mark a file as changed, for example for `make`, saw success and got nothing.
`touch` also refused every option except `--help`, so `touch -d`, `-r`, `-t`, `-a`, `-m`, and `-c`
all failed with `invalid option`. The vendored shell had no operation that sets a file's times.
Fixes strands-agents#361.

## Solution

- The vendored shell gets a time operation on the same path as `chmod`:
  `Kernel::set_times(proc, Resolved, FileTimes)`, `Mediated::set_times`, `io::set_times`, and the
  effect `FsOperation::SetTimes`, admitted with `Follow::Yes`. `FileTimes` holds one `TimeChange`
  (`Keep`, `Now`, or `To(SystemTime)`) for each of the two times.
- On a host bind, `VfsKernel` calls `utimensat` pinned to the identity that admission checked, as
  `set_permissions` does. A read-only bind refuses it. On the in-memory VFS, permission follows
  `utimensat`: the owner can set any time, and a writer can set the current time.
- `touch` accepts `-a`, `-m`, `-c`/`--no-create`, `-d`/`--date` (ISO 8601 and `@SECONDS`),
  `-r`/`--reference`, and `-t STAMP`. A missing file is created without truncation. A failure on
  one file names that file, and `touch` continues and exits 1.
- The policy adapter maps `SetTimes` to the existing `fs:write` operation `write_content`. A rule
  that permits writing a file also permits touching it. The authored policy vocabulary does not
  change.
- Interfaces that moved, all in the vendored `strands-shell` crate: the required trait method
  `Kernel::set_times`; the types `FileTimes` and `TimeChange`; the variant `FsOperation::SetTimes`;
  the field `FileStat::accessed`; and the field `vfs::Inode::atime`. The new `Inode` field breaks
  a struct literal of `Inode` outside the crate; no code in this repository builds one. The user
  approved each of these. Box's call kernel passes `set_times` through.
- `crates/shell/UPSTREAM.md` records the divergence, and `docs/user/shell.md` states that `touch`
  requests `fs:write` / `write_content`.

## Tests

- `crates/shell/tests/touch_times.rs` (11 cases) pins `-d`, `-t`, `-a`, `-m`, `-r`, and `-c` on a
  host bind, creation of a missing file, a read-only bind refused while the next file still
  updates, and `-d`, `-a`, and `-r` on the in-memory VFS. With the host branch made a no-op, seven
  cases fail. With the in-memory branch keeping the access time,
  `touch_a_changes_only_the_access_time_of_an_in_memory_file` and
  `touch_r_copies_the_reference_times_of_an_in_memory_file` fail.
- `commands::touch::tests` pins the `-t` and `-d` parsers. `an_invalid_date_is_refused` includes
  non-ASCII dates; without the ASCII check it panics.
- `crates/policy/tests/shell_fs_vocabulary.rs` shows that a rule on `write_content` decides `touch`.
  `admission_window_guard_present.rs` and `kernel_effect_interception.rs` list the new operation.
- Ran on Linux x86_64: `cargo build`, `test`, `clippy`, and `fmt --check` for `strands-shell`,
  `strands-box-policy`, and `strands-box` with `--all-features`, and the site-manifest test.
  `strands-box` fails one test, `a_leafs_shebang_resolves_past_the_alias_directory`, because
  `/usr/bin/env` on this host is a link to the multicall `/usr/bin/coreutils`. The change does not
  touch that code.
- Not run: the macOS build, `test-integ/run.sh`, and an end-to-end run in a box.
@ferdingler

Copy link
Copy Markdown
Collaborator

Thanks Mackenzie! Box no longer vendors Shell since #414, so the Shell half of this lives in strands-agents/shell#303 now. The Box half, sending a time change through the policy check as a write, still needs doing, but against the new crates/shell kernel: a set_times override, a SetTimes FsOperation, and mapping it to fs:write. That has to land together with the pin bump that picks up #303, otherwise touch breaks in a box because upstream's default set_times returns unsupported. Closing this one.

@ferdingler ferdingler closed this Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] The box shell's touch takes no options and does not update an existing file's times

2 participants