Skip to content

alloc unit 2: workspace request lifecycle - #13092

Merged
gunbai-bot[bot] merged 45 commits into
mainfrom
alloc/unit2-workspace-lifecycle
Oct 4, 2026
Merged

gunbai-bot[bot] merged 45 commits into
mainfrom
alloc/unit2-workspace-lifecycle

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Unit 2 of docs/plans/allocation-main-landing.md: the workspace request lifecycle, extracted from #12505 (codex/allocation-vertical) without retargeting, merging or rewriting #12465 / #12505 / #12691.

Base: main, with #13004 (unit 1, auth + protected state) now landed. This diff is the unit-2 delta only. Unit 3 (initial commissioning, from #12691) will be a separate PR stacked on this branch.

What this carries

Sized durable workspace requests and their HTTP read routes (GET /allocations.json and /allocations/{id}, wired into gunbc.roadmap_serve; create/release are not mounted — see below), owner admission through a dedicated allocation authority, resource-budget selection over eligible hosts, money + slot reservation binding (workspace_allocation_plan, durable exclusive hold), workspace guests through the existing microVM lifecycle and slot controller (extended, not forked), recoverable discovery, expiry / cancel retirement including never-launched requests, fleet-managed SSH access through the gateway, and cleanup before release. The srv1 sudoers projection gains the three workspace grants declared in gunbc.runner_host_grants; nothing is installed on a host.

Fixes for GitHub reviews 5403489707 and 5403494247

  1. Writes are unmounted, policy unchanged. POST /allocations and /allocations/{id}/release require tailnet_path_required: true, but roadmap_serve_request_security reads no serve route, so AccessPathTailnetProxy cannot be constructed at that seam and a mounted write could never admit. Rather than weaken the policy, both routes and their wrappers are removed from the served table until that seam executes a serve-route/ACL readback (a change to the shared unit-1 seam, which affects every write route). workspace_http_create_over / workspace_http_release_over remain the handlers that mount then. allocation_write_requires_an_observed_tailnet_route runs them under the required policy through the real request_access_path_of producer: observed route admits; no readback refuses; LocalDev refuses. workspace_demo_image_identity, consumed only by the create route, is removed with it.
  2. Dead elected actor no longer wedges an owner. workspace_actor_takeover_decision (pure) reads the prior actor's standing from the slot unit's InvocationID, then a fresh hold observation: matching hold → recover; no/free hold + terminal → CAS the actor slot from its exact recorded generation to this invocation (wanted) or to a cancelled_after_actor tombstone (retired); running or unreadable → retain. workspace_take_over_actor orchestrates; the retired path settles through it. Claims: every decision arm (hermetic) and, in the wet lane, terminal actor + no hold settles retirement, running actor retains, and two replacements on one generation admit exactly one (no double hold). The wanted re-arm path is witnessed at the decision and CAS-fence level; its end-to-end form needs a real systemd invocation the wet lane does not have.
  3. Allocation ids are one route segment. WorkspaceAllocationId (sole_constructor, [A-Za-z0-9_-]{1,128}, the AuthStoreKey alphabet) is the only constructor for the HTTP submission and the durable-record decode. unaddressable_allocation_keys_are_refused (a/b, a?b, a b, %2F, unicode, empty, a.b) and admitted_allocation_id_round_trips_through_the_release_route (real path matcher).

All executed on ea948db55c (floor: 9 hermetic planned-and-passed, 5 wet hermetic-route-gap-held-and-wet-passed).

Changes made during extraction (beyond moving code)

  • Defect fixed — a withdrawn cell could receive a new workspace. workspace_slot_from_current_ledger sent CellWithdrawn / HostWithdrawn readiness down the launch path through a wildcard arm. The decision is now the pure workspace_slot_readiness_route: withdrawn readiness refuses a new incarnation and still permits retirement. Witnessed by test.claim.runner.workspace_slot_readiness_route_witness_test: a discriminating red (withdrawn cell/host never launches a wanted workspace; false before the fix), a positive control (admitted + wanted launches), retirement, and one claim through the real cell_incarnation_admission producer over a quarantined record.
  • NonFoldResidue: 46 unrostered wildcard sites, zero enrolled. Every arm over a closed coproduct is now explicit; the JSON decoders read through the existing extdeps.languages.json.parse json_field* authority instead of restating the lookup coproduct. The two stale rows were renames of already-rostered sites (stage_jit_device → stage_bootstrap_device, observe_firecracker_host_standing_wet → …_at_binary_wet) and moved with them.
  • Allocation route actions ServeActionCreateAllocation / ServeActionReleaseAllocation added to the generic human write gate as refusals; their authority is gunbc.workspace_allocation_admission.
  • Import debt: two UnimportedBareProviderDisposition rows retired as ImportsFixed (the slice imports them); none added.
  • Wet routing (existing mechanism, not debt): eight claims whose first effect is shell.Mktemp.Dir (no mock_response by ruling) are routed exactly as login_callback_hold_wet already is — LocalRepoWetLane exclusion per file, every function on local_repo_wet_schedule, and the measured Dir/NoMockResponse gaps as floor_route_gap_expectation_chunk_34. Floor log on 75199db: each shows hermetic-route-gap-held-and-wet-passed.
  • Two test fns that Discover workspace requests and settle unplanned expiry before placement #12505 placed inside gunbc.runner_microvm_cell_readiness now live in runner_microvm_cell_readiness_release_witness_test, minting through the admitted cell_reservation_released_write.
  • extdeps.ssh.openssh_client_commands gains openssh_client_program() (admitted in the uncataloged_program caller roster) for the stdio-forward command; install_file_owned_command uses install_program().

Evidence

Required floor green on 75199db296 and on 1fd9360603 (before #13004 landed) and on the post-landing merge b6cadd78a8; review fixes on ea948db55c (run 37139096600; 75199db was run 37136141493). On 75199db the floor log shows every new identity planned-and-passed within the 302 ms enrolment margin, including the four route claims and the two release claims. On the earlier head 9d9da3d the floor adjudicated 107 newly enrolled workspace/commissioning/network/gateway/request-consumer witnesses with zero claim failures (it refused only on the eight route gaps now routed above). Generated artifacts adopted from the sealed repair candidate of run 37131097100.

Omissions vs the integration checkpoint (codex/allocation-main-landing e7c4426)

Compared over the files the integration branch itself changed (git diff $(git merge-base CP main) CP, excluding docs/plans/docs/notes), not an endpoint diff — 254 differ from this branch:

  • Unit 3 — initial commissioning (Journal initial commissioning and gate workspace supply on committed readback #12691 child history), deferred to the next PR: 110 files. Commissioning executor/journal/fence/recovery/dispatch modules, fleet converge plan/workflow/terminal/publication changes, live-deploy apply/emit receipts, slot start/unit, boot probe, and their witnesses. workspace_commissioning and workspace_commissioning_evidence are carried here at their Discover workspace requests and settle unplanned expiry before placement #12505 revision because runner_microvm_cell_readiness consumes the readiness permit.
  • Tracker / task-history / presentation (excluded by the landing plan): 17 not carried, 6 kept at alloc unit 1: auth + protected state (browser-bound Google login, cookie transport, owner admission, protected fabric state) #13004's revision.
  • dag/gunbc/git_use_census/roadmap_event_git_execution.dag
  • dag/gunbc/roadmap/roadmap_event_carrier_instance.dag
  • dag/gunbc/roadmap/roadmap_event_migration.dag
  • dag/gunbc/roadmap/roadmap_issue_routes.dag
  • dag/gunbc/roadmap/roadmap_task_editorial.dag
  • dag/gunbc/roadmap/roadmap_task_migration.dag
  • dag/gunbc/roadmap/roadmap_task_projection.dag
  • dag/gunbc/roadmap/roadmap_task_routes.dag
  • dag/gunbc/roadmap/roadmap_task_wire.dag
  • dag/test/claim/roadmap/roadmap_approval_transport_witness_test.dag
  • dag/test/claim/roadmap/roadmap_event_carrier_wet_witness_test.dag
  • dag/test/claim/roadmap/roadmap_task_projection_witness_test.dag
  • dag/test/claim/roadmap/roadmap_task_wire_witness_test.dag
  • dag/test/manual/roadmap_carrier_process_wet.dag
  • dag/test/manual/roadmap_import_audit.dag
  • dag/test/manual/roadmap_served_storage_hold_wet.dag
  • tools/tests/roadmap_preview_readiness.py
  • Unit 1 domain left to alloc unit 1: auth + protected state (browser-bound Google login, cookie transport, owner admission, protected fabric state) #13004's revision (17 files) or not carried (8):
  • dag/gunbc/auth/approval_transport.dag
  • dag/gunbc/auth/serve_route_readback.dag
  • dag/gunbc/fabric/fabric_storage_state_bootstrap.dag
  • dag/test/claim/auth/login_callback_hold_witness_test.dag
  • dag/test/manual/fabric_state_hold_server.dag
  • dag/test/manual/fabric_state_socket_hold_server.dag
  • dag/test/manual/login_consumer_race.dag
  • tools/tests/allocation_login_race.py
  • No consumer even on Journal initial commissioning and gate workspace supply on committed readback #12691 — omitted rather than landed dangling (§3c):
  • dag/gunbc/workspace_preparation.dag
  • dag/gunbc/workspace_allocation_budget.dag
  • dag/gunbc/workspace_allocation_supply.dag
  • dag/gunbc/workspace_tool_observer.dag
  • dag/gunbc/workspace_commissioning_observe.dag (consumed by unit 3)
  • dag/gunbc/workspace_request_consumer_cli.dag
  • dag/test/claim/workspace_supply_host_boundary_witness_test.dag
  • dag/test/manual/workspace_preparation_typecheck.dag
  • dag/test/manual/workspace_image_demo_wet.dag
    (workspace_offer_producer / workspace_prepare_convergence belong to unit 3's file set.) Automatic preparation and supply exposure therefore arrive with unit 3, which owns "read back committed provenance and initial readiness before exposing supply".
  • Superseded by main: fabric_floor_policy.dag (main's gunbc.floor_demand owns floor requirements) and its import re-routes in fabric_witness_run, fabric_floor_dispatch, floor_cold_build_receipt and three witnesses; rfc_8118, e0599_probe_census, v2.lens.synthesis, github_merge_state_witness_test import edits; and the spark/v41_* repairs from the integration's main-reconcile commit dd5df8d — main's current revisions are kept.
  • Integration narrative notes not carried: the seven docs/notes/allocation-*.md, approval-gcp-access.md and workspace-runtime-handoff.md receipts (transcribed run results describing the integration tree; review 75077).
  • Ad-hoc qualification scripts not carried: tools/tests/allocation_served_storage.py, tools/tests/dag_validation_closure.py, tools/tests/allocation_login_race.py, tools/tests/roadmap_preview_readiness.py, dag/test/claim/fabric/fabric_closure_budget_witness_test.dag (storage-layer, unit 1).
  • Kept with different content (reconciled to current main / alloc unit 1: auth + protected state (browser-bound Google login, cookie transport, owner admission, protected fabric state) #13004, or rewritten here): 23 files, listed above under the extraction changes (wildcard closure, program identities, route wiring, sudoers projection).

Known residue, stated rather than hidden

  • extdeps.linux.tun_fdinfo (from Discover workspace requests and settle unplanned expiry before placement #12505) carries an inline Python program in a shell transport to count tap holders. That is raw shell standing where a modeled observation could; it is consumed by runner_microvm_network_observe and its witnesses, so it is carried rather than dropped, and its replacement by a modeled /proc fdinfo reader is a follow-up.
  • No VM, host, deploy or commissioning action was performed.

🤖 Generated with Claude Code

Brian Searls and others added 18 commits October 2, 2026 18:31
…ner admission, protected fabric state

Extracted from the allocation integration checkpoint (codex/allocation-main-landing) onto
current main; see the PR body for the composed-tree comparison and omission list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rfc_8118 Optional/any, gunbhub_serve filter, http_serve_route_witness any)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ledger-Repair-Judged: docs/design-rung-drops.md
Ledger-Rows-Repaired: docs/design-rung-drops.md fabric_storage_append_principal_unrefused
Heal-Candidate-Run: 37052560455
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-state hold servers)

No CI lane, instrument row, doc or module reaches them (DESIGN §3c dangling; §6 hand-authored
orchestration tell, review 74255), and they carried the four unrostered NFR sites. The routes they
exercised stay covered by enrolled witnesses: login_callback_hold, login_browser_binding,
fabric_state_auth, fabric_state_namespace, fabric_state_provision.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ghten the sequential replay to NothingToDelete

The removed race harness was the only route to the create-new claim's losing arm. The new claim
plants the winner's .consumed marker and requires DeleteRefused with the transaction file intact
(RED if the claim write stops being create-new); the sequential claim no longer accepts an I/O
refusal as the replay answer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…er case; real-file claims to the wet lane

- login_browser_binding: auth_login_browser_presents(binding, cookie) split from the digest; the
  cookie claim supplies the binding; the real digest runs in login_callback_hold_wet_witness.
- approved_secret_grants / fabric_state_provision: the intent is derived once (secret_grants_intent,
  fabric_state_provision_intent) and the request only digests it; binding claims compare intents,
  one field per claim; ONE real-path claim per constructor, typed cost-debt admission pending grant.
- approved_gcp_access: approved_gcp_access_with_credential split from the variable read; the
  both-required claim supplies the credential over a real gate admission.
- consume (both claims), callback hold and private-namespace alias move to *_wet_witness files,
  enrolled route-gap + wet schedule + LocalRepoWetLane row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-crypto mechanism (granted, snappy-gull-889 msg_75d78dfe)

Own list floor_eval_step_cost_drop_approval_intent_digest_rows (exactly the two real-path claims),
own rung drop approval_intent_digest_new_witness_eval_step_cost whose restoration trigger names the
capability -- SHA-256 executing as natively emitted code on the merge path. Typed admissions cite it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ledger-Repair-Judged: docs/design-rung-drops.md
Ledger-Rows-Repaired: docs/design-rung-drops.md approval_intent_digest_new_witness_eval_step_cost
Heal-Candidate-Run: 37069365931
…ew 74314); regenerate projections

uri_percent_decode_component was four uppercase replaces with a bare String result: lowercase or
any other escape passed through still encoded, malformed escapes silently. It is now the inverse
of the encode half -- any %HH in either hex case, escaped octets folded as shortest-form UTF-8
(no surrogate, <= U+10FFFF), literal characters as scalars -- returning Decoded | Refused
{ TruncatedEscape | NonHexDigit | NotUtf8 }. The Google-alphabet note leaves extdeps; the callback
refuses an undecodable code with a 400 instead of forwarding it. Witness:
test.claim.uri_percent_decode_witness (positive, exactly-once, multi-octet, and the three refusals;
all six held locally). extdeps_uri.rs is regenerated with gunbc compile --entry dag/extdeps/uri.dag
--target rust (main's mirror reproduced byte-identical apart from the new declarations);
docs/design-rung-drops.md regenerated via docs_projection_gate regen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… take main's versions of every shared auth file

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#12787 landed the OIDC/session/request-authentication/principal core and named 'the
session-cookie channel and the OIDC login routes' as its follow-up; this unit is that follow-up.
- main's versions of every shared file are kept; this delta re-applies only what the follow-up
  needs: session_cookie_id_of_header takes the cookie name (login-binding cookie), the mint admits
  gunbc.roadmap_auth_routes auth_callback_mint_session, roadmap_serve serves /auth/login,
  /auth/google/callback, /auth/logout and threads cookie_header to the security context, whose
  evidence now resolves the session cookie.
- main's roadmap_serve witness 'the login routes are not served' flips to
  the_session_and_login_routes_select_their_handlers (the seed now writes response headers).
- dropped as superseded by main: approval_transport (the broker takes RequestAuthenticationEvidence)
  and serve_route_readback (main deliberately performs no readback on this revision).
- extdeps.http.client PostFormFromFile: -sk -> -sS. The OAuth code exchange carried the client
  secret with certificate verification disabled.
- headers: [] on the two ServeHttpResponse literals main added; server.dag prose that contradicted
  the headers field is corrected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…; Set-Cookie as typed parts (review 74495)

- auth_base64url_wire returns NonEmptyStr?; a non-qualifying octet list refuses up the entropy
  and PKCE paths instead of answering "" (DESIGN §5).
- the hand-rolled auth_hex_* decoder is replaced by a linear fold over extdeps.uri
  uri_hex_digit_value (one authority for a hex digit's value).
- extdeps.http.server models RFC 6265 Set-Cookie (SetCookie, CookieSameSite) and renders it once
  (set_cookie_header_line); ServeResponseHeaderSetCookie carries the typed cookie. The session
  cookie, its clearing cookie and the login-binding cookie (login_browser_cookie_name, one data
  row for parse and set) construct values; no caller spells an attribute.
- witness: test.claim.auth.login_pkce_wire_witness_test (RFC 7636 Appendix B vector over a
  supplied digest; either hex case; odd/non-hex refuse). All held locally, with
  auth_session_witness, login_callback_hold_wet and login_browser_binding.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eed renders them (review 74532)

ServeWireResponse.headers is List<ServeWireHeader { name: ServeWireHeaderName, value }>, the
name a closed coproduct (Location | Set-Cookie | Cache-Control): the framing headers the seed
writes itself have no constructor, so a duplicate Content-Type or a smuggled Content-Length is
unwritable rather than caught. cli_run's serve_response_header_line_admitted and its reserved-name
list are deleted; serve_wire_header_line renders the RFC 9110 name from the variant and refuses
only a value carrying CR, LF or NUL (external data at the realization boundary). set_cookie_value
renders the Set-Cookie field value. Smoke-tested through gunbc serve: Location, Set-Cookie and
Cache-Control are written in order and the inbound cookie reaches the handler; a CRLF Location
refuses with a typed 500 and writes no header.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Extracts the workspace request lifecycle from #12505 (codex/allocation-vertical)
onto #13004: sized durable requests, resource grants, eligible-host selection,
reservation binding, workspace guests through the existing VM lifecycle,
recoverable discovery, expiry/cancel retirement, fleet-managed SSH and cleanup
before release. Auth/protected-state hunks defer to #13004; tracker,
task-history and presentation changes are excluded; modules with no consumer
even on #12691 (automatic preparation/offer production, request-consumer CLI)
are omitted rather than landed dangling.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot changed the base branch from alloc/unit1-auth-protected-state to main October 3, 2026 08:21
@gunbai-bot gunbai-bot Bot closed this Oct 3, 2026
@gunbai-bot gunbai-bot Bot reopened this Oct 3, 2026
Brian Searls and others added 9 commits October 3, 2026 08:49
…rt debt the slice fixed

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

# Conflicts:
#	docs/design-rung-drops.md
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he canary oracle

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

# Conflicts:
#	dag/gunbc/auth/approval_decision_store.dag
NonFoldResidue named 46 wildcard arms over closed coproducts in the
#12505 sites this slice carries. None are enrolled: each arm is now
explicit, and the JSON decoders read through the existing
extdeps.languages.json.parse json_field authority rather than restating
the lookup coproduct.

One arm was a defect, not style: workspace_slot_from_current_ledger sent
CellWithdrawn and HostWithdrawn readiness down the launch path. A
withdrawn cell or host may still be retired but never receives a new
workspace incarnation (workspace_slot_withdrawn).

The two stale rows were renames of rostered sites (stage_jit_device ->
stage_bootstrap_device, observe_firecracker_host_standing_wet ->
..._at_binary_wet); the rows move with their sites.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brian Searls and others added 4 commits October 3, 2026 13:54
…-repo wet lane

Eight #12505 claims reach shell.Mktemp.Dir, which has no mock_response by
ruling. They are routed the way login_callback_hold_wet already is: a
LocalRepoWetLane exclusion per file, every function on
local_repo_wet_schedule, and the measured Dir/NoMockResponse gaps as
FloorRouteGapExpectation rows -- so each claim still executes in the
required floor, in the lane that can perform the effect.

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

Registry projections regenerated from their .dag authority: the srv1
sudoers gains the workspace launch-credential and slot-start grants
declared in gunbc.runner_host_grants, and design-rung-drops re-derives.

Ledger-Repair-Judged: docs/design-rung-drops.md
Ledger-Rows-Repaired: docs/design-rung-drops.md approval_intent_digest_new_witness_eval_step_cost
Ledger-Rows-Repaired: docs/design-rung-drops.md fabric_storage_append_principal_unrefused

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

# Conflicts:
#	src/v2/workflow/floor_route_gap.dag
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review October 3, 2026 17:43
…-lifecycle

# Conflicts:
#	dag/gunbc/auth/login_transaction_store.dag
#	dag/gunbc/ci/ci_layer_roots.dag
#	dag/test/claim/auth/login_transaction_consume_wet_witness_test.dag
#	docs/design-rung-drops.md
#	src/v1/stage0/src/cli_run.rs
#	src/v2/workflow/floor_route_gap.dag
#	src/v2/workflow/local_repo_wet_terminal.dag
@gunbai-bot gunbai-bot Bot changed the title alloc unit 2: workspace request lifecycle (stacked on #13004) alloc unit 2: workspace request lifecycle Oct 3, 2026
Brian Searls and others added 3 commits October 3, 2026 22:12
…12505

The seven docs/notes allocation receipts describe the integration tree,
not this unit: they transcribe run results into prose (DESIGN §6), name
modules this unit omits, and the index tells readers not to merge. The
landing plan and the PR body are the narrative; CI is the evidence.
Nothing references them. Review 75077.

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

workspace_lease_bound() -> Minute (one hour via minutes_per_hour); the two
expiry checks compare against its second conversion. Review 74938.

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

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HOLD at exact head 033f89a055cd842441239f93aa103d9f29625489.

The core architecture is directionally right: sized 2/4-vCPU and 4/8-GiB requests issue exact memory grants; resource selection is explicit and host-agnostic over a supplied dedicated roster; the durable plan binds request/work/demand/offer/slot/generation/grant; the workspace path extends the existing microVM controller rather than forking it; fleet-managed SSH evidence is incarnation- and host-key-bound; and release remains behind durable shared teardown/readiness plus exact hold-generation settlement. The new workspace_slot_readiness_route is also correct: CellWithdrawn / HostWithdrawn cannot launch a wanted workspace, while retirement remains possible.

Two blockers remain.

P1 — the mounted create/release routes cannot satisfy their own deployment-path conjunct

workspace_route_create and workspace_route_release both call the write admission with DeploymentAccessPolicy { tailnet_path_required: true }. But the one actual server seam, roadmap_serve_request_security, still constructs every request with ServeStatusUnreadable and serve_route: none. request_access_path_of therefore cannot produce AccessPathTailnetProxy; it yields AccessPathLocalDev on a loopback bind or AccessPathDirectHttps otherwise, and deployment_path_decision rejects both when the policy requires tailnet.

So the GET routes can read an owner ledger, but no request through the mounted production handler can create or release an allocation. The current allocation HTTP witness avoids this exact join by supplying AccessPathLocalDev with tailnet_path_required: false.

Please either connect the real serve-route/ACL readback before mounting these writes, or leave the mutating routes out until that observation exists. Do not weaken the policy merely to make the test pass. Add an exact server-seam control showing a valid logged-in allocation POST reaches the protected store under the intended deployment path; the no-readback neighbor must refuse.

P1 — an elected acquisition actor can wedge an owner forever if it dies before the hold CAS

The immutable actor record is written before workspace_reservation_apply. A replacement slot-controller invocation cannot win that actor slot because the stored value names the prior InvocationID. It then falls back to read-only hold recovery. If the old controller died after actor election but before the hold operation committed, recovery observes no matching hold and refuses. After release/expiry, workspace_settle_unelected_retirement also refuses because an actor exists. The committed control explicitly establishes that an elected actor cannot be cancelled from an absent-hold observation, but there is no subsequent path that proves the old invocation terminal and either rearms acquisition or settles retirement.

That is a permanent quota/request wedge in a real crash window, despite no VM ever launching and no hold existing. It conflicts with this unit's restart/recoverable-never-launched claim.

Please add a generation-fenced takeover/settlement path based on the exact old systemd invocation being terminal plus a fresh authoritative hold observation:

  • matching hold present -> recover and publish that reservation;
  • hold absent/free and old invocation terminal -> CAS the actor from the exact old record to a new actor or cancellation tombstone;
  • hold or invocation unreadable -> retain the obligation.

Controls should kill/replace the actor after actor publication but before hold acquisition and prove wanted retry, retired settlement, matching-hold recovery, and unreadable-state retention without a duplicate hold.

I am not holding this unit for the explicitly deferred unit-3 supply producer or full commissioning transaction. Carrying the lightweight readiness-permit dependency is acceptable, provided unit 3 remains the only production route that exposes commissioned supply. Exact-head CI is green, but the current claims do not execute either failing end-to-end join above.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supplemental finding on the same exact head:

P1/P2 — the API admits allocation IDs that its own GET/release routes cannot address

workspace_create_submission accepts any nonempty idempotency_key up to 128 characters. The durable allocation uses that value as its public allocation_id. But /allocations/{allocation_id} and /allocations/{allocation_id}/release bind one raw nonempty path segment; the route matcher splits on /, strips ?…, and does not percent-decode the bound value.

Therefore an admitted key such as a/b, a?b, a b, or a Unicode value can be durably recorded but cannot be presented back as the same route parameter through an ordinary encoded URL. This can leave the owner unable to GET or release the allocation through the API that created it.

Please give the public allocation/idempotency key one path-segment-safe constructor (the same unreserved/base64url-style alphabet is sufficient), use it in the submission and durable request, and add a route round-trip control. Alternatively change the API to carry the exact key in a decoded request body and keep the route on a separately derived safe allocation ID. Do not rely on a future frontend to generate only safe strings when the server currently admits more.

Brian Searls and others added 2 commits October 4, 2026 00:14
… actors, path-safe allocation ids

Addresses GitHub reviews 5403489707 and 5403494247.

1. POST /allocations and /allocations/{id}/release required the tailnet
   path, but roadmap_serve_request_security reads no serve route, so
   AccessPathTailnetProxy was unconstructible and no write could admit.
   The writes are unmounted (policy unchanged) until that seam executes a
   serve-route/ACL readback; the handlers stay witnessed under the required
   policy through the real request_access_path_of producer: observed route
   admits, no readback refuses, LocalDev refuses.

2. An elected acquisition actor that died before its hold CAS wedged the
   owner in Acquiring with quota held. workspace_actor_takeover_decision
   reads the prior invocation's standing (slot InvocationID) then a fresh
   hold: matching hold -> recover; no hold + terminal -> CAS the actor from
   its exact recorded generation to this invocation (wanted) or a
   cancellation tombstone (retired); running/unreadable -> retain.
   Witnessed per arm, plus wet: terminal actor settles retirement, running
   actor retains, and two replacements on one generation admit exactly one.

3. Allocation ids are one route segment: WorkspaceAllocationId admits
   [A-Za-z0-9_-]{1,128} and is the only constructor for the submission and
   the durable-record decode. RED over a/b, a?b, a b, %2F, unicode, empty;
   control round-trips an id through the release route matcher.

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

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at exact head ea948db55c476075ef85e15b71f4b2551b245eb8.

The three blockers from reviews 5403489707/5403494247 are addressed:

  1. Create/release remain unmounted while retaining tailnet_path_required: true; the handler controls now exercise the real request_access_path_of boundary and distinguish observed matching proxy readback from absent readback and LocalDev.
  2. The dead acquisition actor has a generation-fenced recovery decision over the recorded invocation and a fresh hold observation. Matching holds recover, terminal/no-hold actors may rearm or tombstone by desired state, and active/unreadable/foreign states retain the obligation. The disclosed wanted-rearm real-systemd E2E remains live acceptance, not a source blocker for this unit.
  3. Public allocation IDs are admitted through a sole-constructor [A-Za-z0-9_-]{1,128} wall, with route round-trip and invalid-spelling controls.

The sized grant, immutable selected-slot plan, existing microVM lifecycle extension, fleet-managed SSH, withdrawn-readiness launch wall, and sanitation-before-release structure remain sound. Exact-head required checks are green. This approval does not claim live commissioning, VM boot, SSH, release, or same-slot reuse.

LAND.

Brian Searls and others added 3 commits October 4, 2026 02:16
…-lifecycle

# Conflicts:
#	src/v2/workflow/floor_route_gap.dag
…lost in the merge

Both sides' chunk_34 bodies ended in the same '}' line, which git placed after the
conflict markers; splicing main's chunk ahead of the renumbered chunk_35 left
chunk_34 unterminated and the module index refused floor_route_gap.dag.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Oct 4, 2026
…-lifecycle

# Conflicts:
#	src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag
@gunbai-bot
gunbai-bot Bot removed this pull request from the merge queue due to a manual request Oct 4, 2026
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Oct 4, 2026
@gunbai-bot
gunbai-bot Bot removed this pull request from the merge queue due to a manual request Oct 4, 2026
…-lifecycle

# Conflicts:
#	dag/gunbc/ci/ci_layer_roots.dag
@gunbai-bot
gunbai-bot Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 69b0527 Oct 4, 2026
4 checks passed
@gunbai-bot
gunbai-bot Bot deleted the alloc/unit2-workspace-lifecycle branch October 4, 2026 11:15
gunbai-bot Bot pushed a commit that referenced this pull request Oct 4, 2026
… store's retention parameter (KeepAllGenerations)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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