Skip to content

setup: a failed open before the first step 7 unlocks setup (#468) - #469

Merged
EricAndrechek merged 5 commits into
v1from
v1-setup-latch-468
Oct 6, 2026
Merged

EricAndrechek merged 5 commits into
v1from
v1-setup-latch-468

Conversation

@EricAndrechek

@EricAndrechek EricAndrechek commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

refs #468

Option 2 as refined on #468 before merge ("unlock", not "clear"), in all four bindings, with no name or signature changes.

The rule

  • A failed attempt unlocks. While no library has completed load step 7, an open that attempted a load and failed keeps the setup record but makes it replaceable. That covers the resolve or fetch, the signature, an incompatible artifact, a missing symbol, or step 7.
    • A retry with no new setup runs under the recorded zone and defaults, never the empty setup.
    • A different setup is accepted and replaces the record.
  • Locked again. A replaced record is locked again, and so is any record an open commits for its load, so no setup can replace a record underneath a load.
  • Misuse unlocks nothing. The binding's own input checks fail before anything is attempted: a refused version spelling, and an unverified open without both opt-ins.
  • Two setups before any open are still a UsageError (the Q6 ruling).
  • After the first successful step 7, nothing changes.

The generation guard is still needed, and it stays. A failed open never unlocks a setup that was recorded or replaced after the open began. Without the guard, a slow fetch that fails could unlock a setup that another caller recorded meanwhile, by replacing a record that was already unlocked. That would reopen the silent last-wins this rule exists to prevent.

What each binding does

binding where
Go go/chtypes/setup.go: replaceable. Setup replaces only a replaceable record. commitSetup and latchSetup lock it, and failedOpen (under images.mu, generation-guarded) unlocks it. registry.go treats an ocifetch.SpellingError as misuse. library.go OpenUnverified asks the new abi1.CheckUnverifiedAllowed (go/internal/abi1/loader.go) before anything is attempted.
Python python/src/chtypes/_setup.py: _replaceable, open_failed (under LOCK). registry.py Registry._open parses the request before the attempt begins. library.py open_unverified checks the opt-ins before it.
TypeScript ts/src/setup.ts: replaceable, settleFailedOpen. registry.ts: the spelling check is back before the settled opening. library.ts openUnverified asks the loader's newly exported checkUnverifiedAllowed (ts/src/abi1/loader.ts) before the commit.
Rust rust/src/setup.rs: replaceable, unlock_after_failed_open. registry.rs Registry::open parses the spelling (VersionRequest::parse) before the attempt. library.rs Library::open_unverified checks the opt-ins before it.

The shared cases (tests/fixtures/abi-v1/setup-cases.json)

  • setup.latches_only_on_success:
    1. Q6: two different setups before any open; the second is a UsageError.
    2. A refused spelling (v26.8, through a registry over an empty cache), then a different setup, which is still a UsageError.
    3. An unverified open without the caller's opt-in, then a different setup, which is still a UsageError.
    4. A load refused at step 4 (fingerprint-other, artifact_incompatible) unlocks the record.
    5. A different setup replaces it, and is locked again.
    6. A step 7 refusal, then a plain retry, which is refused again under the recorded zone.
    7. The corrected setup opens, and the image reports that zone.
    8. Latched.
  • setup.a_retry_after_a_failed_open_keeps_the_recorded_setup: setup Asia/Tokyo; a refused load; a plain retry with no new setup, which must load under Asia/Tokyo; then latched.
  • How the zone is read back. The stub gains a zone probe (_stubshared.ZONE_PROBE): chs_type_validate of exactly !Z: returns the zone the image holds. Each runner reads the zone back through its public validate_type, so the retry assertion is the image's own zone, not an inference.
  • Runner changes. Open steps may carry request (a registry open) and allow. The Rust runner now runs every case before its verdict.
  • Unit tests (Go, Python, TypeScript and the Rust api_v1_setup_failed_open.rs):
    • a refused spelling unlocks nothing;
    • a failed resolve keeps and unlocks the record;
    • a replacement is locked again;
    • the generation guard holds.

Proof

  • Before, on the "clear" semantics (v1-abi run 37430039241, head dbbf4b2: the new cases and runners, with binding code exactly as at 6c11397; measured): all 12 v1-abi-conformance legs failed, on both new paths, in every binding.
    • setup.latches_only_on_success, step 3 (setup "Europe/Berlin" after the refused spelling): want a usage error, got success. The refused spelling had cleared the record.
    • setup.a_retry_after_a_failed_open_keeps_the_recorded_setup, step 2 (the plain retry): the image was set up with zone "", want "Asia/Tokyo". The retry had loaded under the empty setup.
    • Python (×4) and TypeScript (×3) print both lines. Rust (×3) prints both, since its runner now runs every case before failing. Go (×2) fails exactly TestSetupCases/setup.latches_only_on_success and TestSetupCases/setup.a_retry_after_a_failed_open_keeps_the_recorded_setup; locally, Go printed the same two step messages.
    • Run 37429407136 (head b8c3e6b) shows the same, except that Rust stopped at the first case.
  • After the fix (head 069a6ec, v1-abi run 37430600288, success): all 12 legs pass, and each one ran both cases.
    • Go census 281 passed, 4 skipped, 0 failed, and the census requires TestSetupCases.
    • Python census 189 passed, 1 skipped (the other OS's ctor-marker case).
    • TypeScript ✓ … setup.latches_only_on_success and ✓ … setup.a_retry_after_a_failed_open_keeps_the_recorded_setup.
    • Rust …: passed in its own process for both cases.
    • The no-artifact ci jobs ran the new unit tests: Rust a_failed_open_before_step_7_unlocks_the_setup_and_misuse_does_not ... ok, and TypeScript ✓ setup: a failed open before the first step 7 unlocks the record ×3.
    • Locally: go test -race ./... with stubs and go test ./... without them; uv run pytest -q with and without stubs; golangci-lint; ruff; tsc --noEmit; biome 2.5.13; cargo fmt; clippy -D warnings; gen.py --check and --selftest; check-no-hand-decls; every always-gate; markdownlint and dprint. All green.
  • Required contexts on v1: all 21 pass.
  • Two checks that are not required are red, and neither is this change (measured from their own output):
    • docs — support matrix: release tag drift at 1.0.1.
    • security (report only): pnpm audit (ts).

🤖 Generated with Claude Code

EricAndrechek and others added 5 commits October 6, 2026 02:52
…on 1.0.2)

The shared setup case now also covers the ruling on #468:
- a second, different setup before any open is attempted is a UsageError;
- an open the loader refuses before step 7 (the stub's fingerprint-other
  variant: an incompatible artifact, reason "fingerprint") must clear the
  setup record while no image has completed step 7, so the next, different
  setup is accepted.

An open step may now name its own stub variant; each runner copies every
variant a case opens once, and checks a loader refusal by its class and
reason. The variant, its reason and its class come from _stubshared.plan and
sdk.json's loader.refusals, not from a list here.

This commit carries no fix: on 1.0.2 code the case fails at step 3, where the
setup after the refused load is still a UsageError.

refs #468

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1.0.2 settled the setup around load step 7 only, so a setup stayed recorded
after an open that failed earlier (the fetch, the signature, an incompatible
artifact, a missing symbol), and a different setup was refused until the
process restarted. Option 2 as ruled: while no image has completed step 7,
ANY failed open clears the record; once one has, today's rule holds; and a
second, different setup before any open is attempted is still a UsageError.

Each binding now settles around the whole public open, not step 7:
- an image that completes step 7 latches the setup, under the image lock
  that already holds its commit and load;
- every public open (Go Registry.For/OpenUnverified/OpenLinked, Python
  Registry.for_version/open_unverified, TS registry.for/openUnverified, Rust
  Registry::for_version/Library::open_unverified) reads a generation when it
  begins and, on any failure, clears the record under that same lock, unless
  the setup has latched or a setup was recorded after the open began.
The step-7-only callbacks from 1.0.2 are gone (Python's loader settle=, TS
ImageSetup.settle). TS moves the spelling check inside the settled open, so a
refused spelling clears the record there as it does in the other three.

docs/reference/bindings-v1.md section 6 rules 1, 2 and 4 and the settings
guide say so, including that an open with no setup after a failure records the
empty setup. Each CHANGELOG has an [Unreleased] Fixed entry; no version bump.

refs #468

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…head)

The ruling on #468 changed before merge from "clear" to "unlock": after an
open that attempted a load and failed, the setup record is KEPT but becomes
replaceable, so a plain retry runs under the recorded zone, never the empty
setup; and the binding's own input checks (a refused spelling, an unverified
open without the caller's opt-in) unlock nothing.

The shared cases now pin that:
- setup.latches_only_on_success: a refused spelling and a withheld opt-in
  each leave a different setup refused; a refused load makes the record
  replaceable; a replaced record is locked again; a plain retry after a
  step 7 refusal is refused again under the recorded zone; the corrected
  setup opens, and the image reports that zone.
- setup.a_retry_after_a_failed_open_keeps_the_recorded_setup: a refused
  load, then a plain retry with no new setup, which must load under the
  recorded zone.

The stub gains a zone probe (_stubshared.ZONE_PROBE): chs_type_validate of
exactly "!Z:" returns the zone the image holds, so each runner reads it back
through its public validate_type. Open steps may now carry "request" (a
registry over an empty cache) and "allow" (the caller's opt-in).

This commit carries no binding change: on the clear head both cases fail,
at the setup after the refused spelling and at the retry's image zone.

refs #468

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The parent used to stop at the first failing case, so a later case's result
was never seen. It now runs each case in its own process, reports every
failure, then fails once. Still no binding change: on the clear head both
cases must fail in Rust too.

refs #468

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The lead refined the ruling before merge: clearing the record failed open,
because a plain retry after a transient failure silently loaded under the
empty setup (UTC) instead of the zone the caller chose.

Now, while no image has completed load step 7:
- an open that attempted a load and failed (resolve or fetch, signature, an
  incompatible artifact, a missing symbol, step 7) KEEPS the record but marks
  it replaceable: a retry with no new setup runs under the recorded zone and
  defaults, and a different setup replaces the record;
- a replaced record is locked again, and so is any record an open commits
  for its load, so no setup replaces a record under a load;
- the binding's own input checks fail before anything is attempted and
  unlock nothing: a refused version spelling (checked before the attempt
  begins) and an unverified open without both opt-ins (checked before the
  commit, through a gate the loader now exposes);
- two different setups before any open is attempted are still a UsageError.
After the first successful step 7 nothing changes.

The generation guard stays: a failed open still never unlocks a setup that
was recorded or replaced after it began (another caller's setup, replacing
an unlocked record during a slow fetch, must stay locked).

- Go: setup.go replaceable; failedOpen unlocks; Registry.open treats an
  ocifetch.SpellingError as misuse; OpenUnverified asks the new
  abi1.CheckUnverifiedAllowed first.
- Python: _setup.py _replaceable; open_failed unlocks; Registry._open parses
  the request before the attempt; open_unverified checks opt-ins first.
- TypeScript: setup.ts replaceable; settleFailedOpen unlocks; the spelling
  check is back before the settled opening; openUnverified asks the loader's
  exported checkUnverifiedAllowed first.
- Rust: setup.rs replaceable and unlock_after_failed_open; Registry::open
  parses the spelling first; open_unverified checks opt-ins first.

Section 6 rules 1, 2 and 4, the settings guide and the four [Unreleased]
CHANGELOG entries say so.

refs #468

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@EricAndrechek EricAndrechek changed the title setup: any failed open before the first step 7 clears setup (#468) setup: a failed open before the first step 7 unlocks setup (#468) Oct 6, 2026
@EricAndrechek
EricAndrechek marked this pull request as ready for review October 6, 2026 07:49
@EricAndrechek
EricAndrechek merged commit e72a5e8 into v1 Oct 6, 2026
@EricAndrechek
EricAndrechek deleted the v1-setup-latch-468 branch October 6, 2026 07:49
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