Repository navigation
feat(op): make the session own filesystem access behind accessors - #414
Merged
Merged
Conversation
Phase 2b, PR 2b.2b of the native-Windows-permissions plan (#405), completing 2b.2 after #413 put CreateTemp/MkdirTemp on the interface. RuntimeEnvironment.Root is no longer a field anyone can set or read directly: the session owns filesystem access and hands it out. HasRoot() answers from the spec's anchor rather than from whether a handle has been minted, so its answer cannot change mid-session. Root() panics when HasRoot() is false -- a rootless session is legal and advertised (cmd/lore builds one in production), so the panic reports a caller that never asked rather than an impossible state. Returning a bare nil was rejected during design: fsroot.Root is an interface, so nil does not avoid the panic, it relocates it into fsroot with a stack that never names the mistake. Modeled on reflect.Value.IsValid, whose doc states the rule once and whose getters panic. Scratch() returns a per-session private directory INSIDE the OS temp directory -- never a root over the temp directory itself, which is shared with every process, cannot be mode-controlled, and could not be removed on Close. It is created 0700, so phase 2 gives it a protected DACL on Windows, which matters because scratch holds the most sensitive transient data in the system. There is no HasScratch: nothing about scratch is configurable, so the predicate could only ever return true. Both anchors are PREFLIGHTED at construction and released again -- probeRootAnchor opens and closes the root, probeScratchAnchor creates and removes a temporary directory. That is what licenses both accessors to assert: validation has already passed, so a later failure is an invariant violation, matching the repository's checksum trust boundary. Allocation stays lazy behind a sync.Once each, because gather dispatches bodies concurrently and two goroutines can reach an accessor at once. A planning-only session holds no handle and closes nothing. 105 sites across 25 files moved, and the seven that nil-checked the field became HasRoot() guards. The 82-site estimate counted production only and was accurate there; test files added the rest. The mechanical rewrite over-matched once and the compiler caught it: triad_test.go has a local triadEnv fixture with its own Root field, so env.Root there is not the session. Twenty rewrites reverted. It failed loudly only because a field is not callable -- an interface with a Root() method would have compiled and silently changed meaning, which is the risk phase 6's rename runs over a much larger surface. One existing test specified the old contract (Root() != nil on a rootless session) and now specifies the new one, with recover() proving the panic. A new test pins scratch: private tree inside os.TempDir, same instance on every call, 0700, removed on Close. Verified: make test zero failures; make lint-all zero issues on linux, darwin, and windows. Assisted-by: Claude Signed-off-by: David Noble <[email protected]>
David-Noble-at-work
added a commit
that referenced
this pull request
Aug 14, 2026
…415) internal/lorepackage's TestRegistry_SyncIntegration clones https://github.com/NobleFactor/devlore-registry.git from CI runners. On PR #414 the macOS runner failed DNS -- "Could not resolve host: github.com" -- and produced a red check that had nothing to do with the change under review, 30 seconds after the clone started. It matters more than a single flake because this repository currently runs on a known-failure baseline: the Windows leg is expected to fail at exactly 28, verified name by name, and a 29th name is a real regression. That discipline works only while a failure is attributable. A red check that sometimes means "the network was unwell" makes "is this red mine?" a judgment call on every PR, and judgment calls at that frequency decay into reflex. THE SOLUTION IS DELIBERATELY NOT CHOSEN. The charter requires three discoveries first: enumerate every network-reaching test by BEHAVIOR rather than by name (a https:// grep is a proxy -- it hits URL-parsing tests that never fetch, and misses anything reaching the network through a helper or provider); measure how often this actually fires from CI history rather than impression; and establish whether the registry's contract needs a real remote, any git remote, or a fake transport. Six candidate directions are recorded with the residual each one buys -- local fixture repo, record/replay, gated integration tag, skip-on-unreachable, retry, scheduled job -- and the charter says explicitly that the answer may be none of them. Worth starting from: cmd/devlore-index/main_test.go already builds a LOCAL devlore-registry fixture and chdirs away so a real sibling checkout cannot leak in. The precedent exists in-repo and should be understood before anything new is invented. Assisted-by: Claude Signed-off-by: David Noble <[email protected]> Co-authored-by: David Noble <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2b, PR 2b.2b of
windows-native-permissions.md(#405), completing 2b.2 after
#413 added
CreateTemp/MkdirTemp.RuntimeEnvironment.Rootis no longer a field anyone can set or read. The session owns filesystemaccess and hands it out — the invariant this phase exists to establish.
The accessors
HasRoot()answers from the spec's anchor, not from whether a handle has been minted, so theanswer cannot change mid-session.
Root()panics whenHasRoot()is false. A rootless session is legal and advertised —cmd/lore/lore/builder.gobuilds one in production — so the panic reports a caller that neverasked. A bare
nilwas rejected in design:fsroot.Rootis an interface, so nil doesn't avoidthe panic, it relocates it into
fsrootwith a stack that never names the mistake. Modeled onreflect.Value.IsValid, whose doc states the rule once and whose getters panic.Scratch()returns a per-session private directory inside the OS temp directory — never aroot over the temp directory itself, which is shared with every process, cannot be mode-controlled,
and could not be removed on
Close. Created0700, so phase 2 gives it a protected DACL onWindows. No
HasScratch: nothing about scratch is configurable, so it could only returntrue.Preflight is what licenses the asserts
probeRootAnchoropens and releases the root;probeScratchAnchorcreates and removes a tempdirectory. Validation passes at construction, so a later failure is an invariant violation, not a
condition — the same trust boundary the checksum rule draws. Allocation stays lazy behind a
sync.Onceeach, becausegatherdispatches bodies concurrently and two goroutines can reach anaccessor at the same moment. A planning-only session holds no handle and closes nothing.
Migration
105 sites across 25 files, and the seven that nil-checked the field became
HasRoot()guards. The 82-site estimate counted production only and was accurate there; test files added the
rest.
The mechanical rewrite over-matched once and the compiler caught it:
triad_test.gohas a localtriadEnvfixture with its ownRootfield, soenv.Rootthere is not the session — 20rewrites reverted. It failed loudly only because a field is not callable. An interface with a
Root()method would have compiled and silently changed meaning, which is the risk phase 6'srename runs over a much larger surface.
Tests
One existing test specified the old contract (
Root() != nilon a rootless session) and nowspecifies the new one, with
recover()proving the panic. A new test pins scratch: private treeinside
os.TempDir(), same instance on every call,0700, removed onClose.make testzero failures;make lint-allzero issues on linux, darwin, and windows.test (windows-latest)is expected to hold at its known 28.Assisted-by: Claude