Skip to content

feat(op): make the session own filesystem access behind accessors - #414

Merged
David-Noble-at-work merged 1 commit into
developfrom
session-root-accessors
Aug 14, 2026
Merged

David-Noble-at-work merged 1 commit into
developfrom
session-root-accessors

Conversation

@David-Noble-at-work

Copy link
Copy Markdown
Contributor

Summary

Phase 2b, PR 2b.2b of windows-native-permissions.md
(#405), completing 2b.2 after
#413 added CreateTemp / MkdirTemp.

RuntimeEnvironment.Root is no longer a field anyone can set or read. The session owns filesystem
access 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 the
answer cannot change mid-session.

Root() panics when HasRoot() is false. A rootless session is legal and advertised —
cmd/lore/lore/builder.go builds one in production — so the panic reports a caller that never
asked
. A bare nil was rejected in design: fsroot.Root is an interface, so nil doesn't 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. Created 0700, so phase 2 gives it a protected DACL on
Windows. No HasScratch: nothing about scratch is configurable, so it could only return true.

Preflight is what licenses the asserts

probeRootAnchor opens and releases the root; probeScratchAnchor creates and removes a temp
directory. 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.Once each, because gather dispatches bodies concurrently and two goroutines can reach an
accessor 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.go has a local
triadEnv fixture with its own Root field, so env.Root there is not the session — 20
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.

Tests

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.

make test zero failures; make lint-all zero issues on linux, darwin, and windows.
test (windows-latest) is expected to hold at its known 28.

Assisted-by: Claude

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 David-Noble-at-work self-assigned this Aug 14, 2026
@David-Noble-at-work
David-Noble-at-work merged commit 83701c3 into develop Aug 14, 2026
6 of 8 checks passed
@David-Noble-at-work
David-Noble-at-work deleted the session-root-accessors branch August 14, 2026 23:05
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]>
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