Skip to content

fix(op): mint each runtime environment's Root from the spec's anchor - #402

Merged
David-Noble-at-work merged 3 commits into
developfrom
env-minted-root
Aug 13, 2026
Merged

David-Noble-at-work merged 3 commits into
developfrom
env-minted-root

Conversation

@David-Noble-at-work

@David-Noble-at-work David-Noble-at-work commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements the amended #393 ruling
(option 4): RuntimeEnvironmentSpec carries no live fsroot.Root — only the anchor path and
access mode — and every RuntimeEnvironment mints its own Root and closes it.

The confined Root's directory handle leaked on every path that minted one without a matching
close. Windows refuses RemoveAll on an open directory, so those leaks surface as t.TempDir()
cleanup failures on the windows leg; POSIX unlink hides the identical leak, which is why none
of this was visible on the ubuntu-only gate.

What changed

  • pkg/fsroot: new Mode (zero value ModeConfined) and Open(dir, mode) — one
    mode-dispatched constructor over the three existing implementations.
  • pkg/op: Spec.Root → RootPath + RootMode; WithRoot(path, mode);
    NewRuntimeEnvironment mints and returns an error; Close releases (the single-owner contract
    it already held). A mint failure at Run lands preflight_failed before any dispatch. Resume
    re-mints from the anchor — all that survives a cross-process resume anyway.
  • plan.Provider.Spec: keeps its fail-fast contract via an open-and-release probe, so the
    mint itself remains the only definition of a valid root.
  • Hosts: six writ spec factories became infallible (their only error source was the mint);
    adopt and migrate-register collapse their planning/execute spec pairs into one shared spec.
  • Tests: the fifteen plan-package/migrate test environments that never closed now do.

Defects fixed en passant

  • cmd/lore/lore/commands.go handed one spec to N executors in a loop, so every iteration
    after the first dispatched against a Root the previous run had closed.
  • devlore-test aliased the spec's root into TestContext for post-close reads plus a double
    close — benign only because unconfined Close is a no-op.
  • writ verify and writ readback never closed their loading environments.

Measurement, corrected

The first windows run on this branch returned 48 → 45, not the ~30 this PR originally
predicted. The framework change alone delivered −3. Fifteen failures still carried the leak
signature, and they were not the production paths #393 diagnosed — they were test
environments that are never closed, which relocating the mint could not fix. The natural
experiment: the three plan-package helpers that close their environment have no failing tests;
the five that do not, account for exactly fifteen.

The follow-up commit closes those fifteen. Expected: 45 → ~30, and this estimate names its
mechanism rather than inheriting one.

Verification

  • make test — zero failures repository-wide
  • make vet-all, make lint-all, make build-all — green on linux, darwin, and windows
    (lint reports 0 issues per GOOS)
  • gofmt clean; both mechanical style detectors report zero findings
  • TestRun_SequentialExecutorsShareOneSpec — two executors, one spec, both runs green; the
    cmd/lore bug shape, observable on every platform
  • First windows measurement: 48 → 45 (−3), uncapped, against the head commit's check-runs
  • Second windows measurement: expected ~30

Refs #393. Campaign: #373 / docs/plans/platform-test-matrix.md.
Plan: docs/plans/env-minted-root.md.

Assisted-by: Claude

David Noble added 2 commits August 13, 2026 14:03
A RuntimeEnvironmentSpec carried a live fsroot.Root, so every path that
minted one without reaching a closing Run leaked the confined Root's
directory handle. Windows refuses RemoveAll on an open directory, which
is why 18 of the standing 48 windows-leg failures are t.TempDir()
cleanup failures rather than assertion failures; POSIX unlink hides the
identical leak.

Per the amended #393 ruling (option 4), the spec now carries only the
anchor path and access mode -- serializable values, never a handle.
NewRuntimeEnvironment mints the Root through the new fsroot.Open(dir,
mode) and RuntimeEnvironment.Close releases it, the single-owner
contract it already held. Nothing mints before environment build, so the
pre-Run error paths cannot leak. Resume is no special case: every Run
mints fresh from the anchor, which is all that survives a cross-process
resume anyway.

The deciding fact was verified across the tree before the ruling was
amended: nothing between spec construction and Run dereferences the
spec's Root. The field had exactly two production readers, all nine host
mint-to-Run gaps were clean, and no .star script or doc touches
spec.root.

NewRuntimeEnvironment now returns an error, and a mint failure at Run
lands the preflight-failed terminal before any dispatch. Provider.Spec
keeps its fail-fast contract by probing the anchor open-and-release, so
the mint itself stays the only definition of a valid root. Six writ spec
factories lost their only error source and are now infallible; adopt and
migrate-register collapse their planning and execute spec pairs into one
shared spec.

Three defects heal with it. cmd/lore's deploy loop handed one spec to N
executors, so every iteration after the first dispatched against a Root
the previous run had closed. devlore-test aliased the spec's root into
TestContext for post-close reads plus a double close, benign only
because unconfined Close is a no-op. verify and readback never closed
their loading environments at all; both now do, through iox.Close.

Riders: cmd/lore/lore/integration_test.go removed (//go:build ignore,
referencing op.contextBase and other long-deleted APIs); and a go-style
sweep over the touched set -- 77 multi-line doc summaries split, 37
over-120-column lines rewrapped, one blank-line-after-signature added.

Verified: make test reports zero failures repository-wide; make vet-all,
lint-all, and build-all are green on linux, darwin, and windows (lint
reports 0 issues per GOOS); gofmt clean over all 41 files. The -18 on
the windows leg is CI-side proof -- POSIX cannot observe the leak.

Assisted-by: Claude
Signed-off-by: David Noble <[email protected]>
loadRecipientsFile discarded its close error in a defer lambda while
loadAgeIdentities, ten lines up in the same file, already used the
iox.Close idiom. Named returns plus defer iox.Close(&err, file) joins a
close failure onto the result instead of dropping it.

Assisted-by: Claude
Signed-off-by: David Noble <[email protected]>
@David-Noble-at-work David-Noble-at-work self-assigned this Aug 13, 2026
The first windows measurement on this PR came back 48 -> 45, not the
predicted ~30. Fifteen failures still carried the leak signature, and
none of them were the production paths #393 diagnosed: they are test
environments that are never closed. Whoever constructs a runtime
environment closes it; these tests never did -- before this branch or
after -- so relocating the mint could not fix them.

A natural experiment inside the plan package proves it. lifecycle_e2e,
lifecycle_starlark, and result_flow_starlark each close their
environment and none of their tests appear in the failure list;
provider_test (2 mints), lifecycle_api (10), gather_api (1), git_resume
(1), and receipt_integration (1) close nothing and all of their tests
do. Fifteen unclosed mint sites, fifteen leak failures.

Add t.Cleanup(func() { _ = environment.Close() }) at all fifteen,
matching the idiom the three passing helpers already use. Verified by
enumeration rather than assumption: repo-wide, every test file that sets
a root now closes it. The files that mint without a root hold no handle,
and TestNewRuntimeEnvironment_BadAnchorFails mints nothing because the
mint is what fails.

The plan document records the miss rather than quietly restating the
target: the option-4 verification asked whether anything dereferences
spec.Root between spec construction and Run (it does not, and that
holds), but never asked who closes the environment in the failing tests.
The -18 estimate was inherited from the #393 diagnosis and repeated
without its own enumeration.

Expected windows effect: 45 -> ~30. Unlike the first estimate, this one
names its mechanism -- fifteen unclosed handles, fifteen cleanups.

Assisted-by: Claude
Signed-off-by: David Noble <[email protected]>
@David-Noble-at-work
David-Noble-at-work merged commit 810745c into develop Aug 13, 2026
7 of 8 checks passed
@David-Noble-at-work
David-Noble-at-work deleted the env-minted-root branch August 13, 2026 23:47
David-Noble-at-work added a commit that referenced this pull request Aug 13, 2026
…403)

PR #402 closed the handle-leak cluster: the leak signature on the windows
leg went 18 -> 0, with zero panics and zero [build failed] packages,
measured uncapped against head 27d23e8's check-runs. The campaign
burn-down is now 85 -> 57 -> 48 -> 50 -> 48 -> 28.

The correction matters more than the number. This campaign predicted -18
from the #393 ruling; the framework change alone delivered -3, and the
remaining -17 came from closing fifteen test runtime environments that
were never closed. The production leaks #393 diagnosed were real and are
fixed -- lore's deploy loop ran N executors off one spec against a Root
the first run had closed, writ verify and readback never closed their
loading environments, devlore-test aliased the spec's root into
TestContext -- but they were not what those eighteen tests were failing
on. A natural experiment inside the plan package proved it: the three
helpers that close their environment had no failing tests; the five that
did not accounted for exactly fifteen.

A right number reached by wrong reasoning is still a miss, and the
campaign's counts are only as trustworthy as their attributions, so both
plans record it rather than restating the target. env-minted-root.md is
marked complete with the three-point measurement table; the 48-row in
platform-test-matrix.md now points at the correction instead of the
superseded diagnosis.

Remaining 28: file-provider write/permission semantics, path semantics,
chown, git argv, 2 CLI error-text expectations, and singles including
#376's Starlark path escape.

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