You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(validate): judge the socket bound against the runtime catalog
#442 added a socket-path guard that read its bound off whatever tree was being
validated. Publication does not validate the tree that will run: `st2 agent
publish` admits a candidate through a disposable projection nested in the
catalog's control directory, and validation canonicalizes that root, so every
identity was charged for the projection's own depth. Declarations whose real
socket is bindable were refused, and CI showed it on an 11-byte task id:
agents/host/worker/agent.kdl [socket-path-too-long]: task 'host.worker'
would bind a 108-byte session socket at
.../catalog/.st2/catalog-admission-okX01u/pty/host.worker.sock
On a host whose catalog root is 49 bytes the projection leaves a 15-byte
identity budget, so once authoring resumed this would have failed closed on
essentially every managed declaration. A fail-closed guard on the wrong
measurement is worse than the defect it replaced.
The bound is a host-local runtime fact, so it now travels with the caller that
knows where the supervisor will bind sockets. `RuntimeRoot` makes that explicit
in the validator's contract: `Catalog(root)` names the catalog that will run, and
`Unknown` is the deliberately context-free mode that omits the host-local guard.
Publication supplies the real catalog while validating the projection; catalog
transactions supply it while validating captures, stages and admission
projections; direct validation of a live catalog supplies itself.
Two publish-path regression tests cover the boundary the original tests missed.
Both pin `PTY_ROOT` off, because an ambient pty root wins over the
catalog-relative default and makes the guard unobservable — which is why this
regression was visible in CI and invisible locally.
Refs #432
Copy file name to clipboardExpand all lines: INVARIANTS.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -42,6 +42,6 @@ materialization, messaging, DING, or presence must preserve them.
42
42
| **Atomic resource proposal publication** | Every changed resource publication is one host-owned compare-and-swap fenced by binding generation, state revision, and prior carrier digest. A persistent cross-process lock admits at most one proposal from the same prior. The content-derived proposal ID binds the accepted carrier digest and semantic outbox envelope; the durable intent becomes eligible only with the exact canonical carrier, then folds into one authoritative catch-up state. A pre-carrier crash exposes old state, a post-carrier crash catches up on restart, and retry after a lost acknowledgement returns the durable receipt without another transition. Ordinary reconciliation fails closed on out-of-band divergence after a committed intent; only an explicit generation-advance recovery may re-adopt the canonical carrier or its absence while invalidating the old intent and fence. | `src/resource_profile.rs::tests::atomic_publication_fences_races_and_survives_crash_restarts`; `src/resource_profile.rs::tests::generation_advance_explicitly_recovers_diverged_or_missing_carrier` |
43
43
|**Pass progress is independent of publication**| A reconcile pass completes whether or not resync publication is making progress. Publication runs on its own thread, so a pass's per-seat watch-set handshakes never wait for a catalog lock, a stream lock, or a refused publication. One publication is outstanding per subscription at a time, and a queued publication is dropped when its subscription is deactivated or removed by a refresh, so nothing is published to a seat the pass has already decided receives no events. |`src/run/tests.rs::reconcile_pass_completes_while_a_resync_publication_is_blocked`; `src/resync/tests.rs::a_flush_never_hands_off_a_subscription_whose_publication_is_outstanding`; `src/resync/tests.rs::deactivation_drops_only_that_recipients_queued_publication`; `src/resync/tests.rs::a_refresh_drops_a_queued_publication_for_a_subscription_it_removed`|
44
44
|**Terminal refusals are classified, not retried**| A refused resync publication is classified by what could admit it later. A recipient that is declared but not running parks its reservation: it is attempted once, captures and schedules nothing while parked, and re-arms with its exact reserved bytes when a refresh carries that recipient again. An ambiguous recipient, a foreign owning host, and an undeclared stream are permanently refused: the reservation is dropped and the carrier baseline advances so the same transition is not re-captured. Everything else, including an absent declaration, stays retryable. |`src/resync/tests.rs::a_not_running_recipient_parks_its_reservation_and_is_attempted_once`; `src/resync/tests.rs::a_parked_reservation_re_arms_and_replays_when_its_recipient_runs_again`; `src/resync/tests.rs::a_permanently_refused_reservation_is_dropped_and_not_recaptured`; `src/resync/tests.rs::refusals_are_classified_by_what_could_admit_them_later`|
45
-
|**Unbindable session sockets fail at admission**| Host-scoped validation rejects a pty task whose session socket path would exceed the portable 104-byte `sun_path` bound, because `pty` refuses that bind and the task can then never spawn. The bound is derived from the pty root resolved for the selected host rather than a fixed identity length, only that host's pty tasks are judged against it, and the diagnostic names the resolved path and the byte overage. A park whose cause is that same unbindable path does not advise `st2 unpark`, which would relaunch into the identical failure. |`tests/validate.rs::an_unbindable_session_socket_path_is_rejected_at_admission`; `tests/validate.rs::another_hosts_long_identity_is_not_judged_against_this_hosts_pty_root`; `tests/validate.rs::a_long_exec_task_id_is_not_a_socket_path_issue`; `src/run/tests.rs::session_socket_overage_is_derived_from_the_resolved_root`; `tests/run.rs::a_structurally_unrecoverable_park_does_not_advise_unpark`|
45
+
| **Unbindable session sockets fail at admission** | Host-scoped validation rejects a pty task whose session socket path would exceed the portable 104-byte `sun_path` bound, because `pty` refuses that bind and the task can then never spawn. The bound comes from the pty root resolved for the RUNTIME catalog the caller names, never from the tree under inspection: publication admits a candidate through a disposable projection nested in the catalog, and measuring that tree charges every identity for the projection's own depth and refuses declarations whose real socket is bindable. Only a deliberately context-free validation omits the guard. It is not a fixed identity length either, only that host's pty tasks are judged, and the diagnostic names the resolved path and the byte overage. A park whose cause is that same unbindable path does not advise `st2 unpark`, which would relaunch into the identical failure. | `tests/agent_publish.rs::publication_judges_the_socket_bound_against_the_runtime_catalog_not_the_projection`; `tests/agent_publish.rs::publication_still_refuses_an_unbindable_canonical_socket_path`; `tests/validate.rs::an_unbindable_session_socket_path_is_rejected_at_admission`; `tests/validate.rs::another_hosts_long_identity_is_not_judged_against_this_hosts_pty_root`; `tests/validate.rs::a_long_exec_task_id_is_not_a_socket_path_issue`; `src/run/tests.rs::session_socket_overage_is_derived_from_the_resolved_root`; `tests/run.rs::a_structurally_unrecoverable_park_does_not_advise_unpark` |
46
46
| **Marker-matched declaration authority** | Every st2 write path that rewrites a declaration carrying `meta { managed-by "nix" }` admits it only from a caller asserting exactly the marker that declaration carries: the lifecycle verb and the compare-and-swap `agent publish` both refuse a mismatched marker, an unmarked subject, an unresolvable multi-marker declaration, and an empty or padded assertion before any write, while presentation, address, stream, and Resource authoring refuse a Nix-owned declaration outright. Publication reads that authority from the incumbent, so creating a declaration and republishing byte-identical bytes stay open — neither replaces bytes another writer owns. A marker-matched edit rewrites nothing but the lifecycle line and is admitted through the same full-catalog gate as a compare-and-swap publication of the same bytes, so it cannot commit a declaration the catalog would reject. | `src/agent_author.rs::marker_matched_lifecycle_authority_is_exact_and_source_preserving`; `src/agent_author.rs::marker_matched_retirement_refuses_a_candidate_admission_would_reject`; `tests/agent_desired_state.rs::cli_managed_by_authority_retires_a_projected_seat_and_refuses_every_inexact_claim`; `tests/agent_publish.rs::publication_honours_the_incumbent_ownership_marker`; `tests/agent_publish.rs::publication_refuses_an_unresolvable_owner_and_repairs_unreadable_bytes` |
47
47
|**Proof references resolve**| Every qualified test named in this table exists in its named source file, so stale invariant claims fail the suite instead of silently surviving a refactor. |`tests/invariants.rs::qualified_proof_references_resolve`|
0 commit comments