Skip to content

rollout: keep backups under AMAP_ROOT, never in sandy's features directory - #134

Open
rappdw wants to merge 4 commits into
mainfrom
rollout-backups-outside-features-r2
Open

rappdw wants to merge 4 commits into
mainfrom
rollout-backups-outside-features-r2

Conversation

@rappdw

@rappdw rappdw commented Nov 6, 2026

Copy link
Copy Markdown
Member

Supersedes #98: the same patch, rebased onto the current base, on a new branch (no force-push).

What

amap-codex-rollout.py apply wrote its backups to $SANDY_HOME/features/amap.rollout-backups. Sandy reads every directory under features/ as a feature. It refuses one that has subdirectories and no feature.json, and then mounts no feature at all:

[sandy] A feature manifest could not be applied; no feature was mounted.
[sandy]   amap.rollout-backups    has subdirectories but no feature.json; a whole-directory mount would expose every other sandbox instance tree

So every sandbox launched after an apply started without AMAP. Running sandboxes were unaffected.

  • Backups now go to AMAP_ROOT/rollout-backups, default ~/amap/rollout-backups. The set-aside copy that rollback makes goes there too.
  • Every verb (check, apply, rollback) refuses while features/amap.rollout-backups still exists, and names where to move its contents. It also refuses a backup root that resolves inside the features directory.

Host remedy (already given to the operator)

mkdir -p ~/amap/rollout-backups && mv "$SANDY_HOME/features/amap.rollout-backups"/* ~/amap/rollout-backups/ && rmdir "$SANDY_HOME/features/amap.rollout-backups"

Then relaunch any sandbox launched since the apply.

Tests

  • test_apply_and_rollback_leave_nothing_beside_the_feature_in_sandys_features_directory: after apply and after rollback, features/ lists only amap.
  • test_a_backup_directory_left_in_sandys_features_directory_refuses_every_verb: all three verbs refuse with the move named, and nothing installs.
  • test_a_backup_root_inside_sandys_features_directory_refuses.
  • Each new guard was broken one at a time, and each time its named test failed.
  • On this head, tests/test_codex_rollout.py passes under unittest. The two features-directory tests fail against the base's code: they are the regression this fix removes.
  • The full suite runs in this PR's CI.

Not covered: a host run. I haven't checked sandy's own rule for which names under features/ it reads as a feature. This fix keeps everything of this tool's out of that directory, so the rule doesn't matter here.

🤖 Generated with Claude Code

rappdw and others added 4 commits November 5, 2026 21:22
…ctory

apply wrote its backups to $SANDY_HOME/features/amap.rollout-backups. Sandy
reads every directory under features/ as a feature, refuses one with
subdirectories and no feature.json, and then mounts no feature at all, so
every sandbox launched after an apply started without AMAP.

Backups now go to AMAP_ROOT/rollout-backups. Every verb refuses while a
directory of this tool's backups is still in the features directory, naming
where to move it, and refuses a backup root that resolves inside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…before naming a move

features_clear() refused a left-over features/amap.rollout-backups before it
checked where the backup root resolves. With AMAP_ROOT inside that directory,
the refusal told the operator to move its contents into the root and then
remove the directory, which deletes the moved backups and keeps them in the
features directory. The root is now checked first, and the move is named
only once the root is proven outside.

The root resolves strictly: its existing part follows every link and a loop
or an unreadable component is a refusal (RuntimeError included, as older
Pythons raise for a loop). A missing tail is accepted, since backup and
rollback create it, but a link to nothing or a '..' in it is refused.
Containment compares spelling and the device and inode of each existing
ancestor, so a spelling that differs only in case on a case-insensitive disk
is still inside.

Codex T2 blocker on d42870d6; Opus N1-N3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y spelling

The containment check read the features directory's identity and refused
when it could not, so a host whose sandy features directory did not exist
yet refused every verb with "cannot read". Such a directory has no
identity to compare: a backup root is inside it when one of its ancestors
is spelt as the features directory, with case ignored, which covers the
spelling a case-insensitive disk would make the same directory once it
is created. Tests: a missing features directory, accepted outside and
refused by spelling and by case; a stat error other than ENOENT on the
features directory or an ancestor is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry not created yet

Comparing spellings with case ignored decides containment only for ASCII:
a disk that also ignores Unicode normalization makes NFC and NFD spellings
one directory, and Unicode case folding maps some non-ASCII letters (the
Kelvin sign) to ASCII ones. When the features directory does not exist,
a backup root or features directory spelt outside ASCII is now refused,
with a remedy, instead of compared. An existing features directory is
still compared by identity. Tests: NFC against NFD both ways, a non-ASCII
root against an ASCII directory, and a Kelvin-sign directory against an
ASCII root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant