Skip to content

fix(validate): judge the socket bound against the runtime catalog - #509

Open
schickling-assistant wants to merge 1 commit into
mainfrom
schickling-assistant/2026-09-07-socket-bound-runtime-root
Open

fix(validate): judge the socket bound against the runtime catalog#509
schickling-assistant wants to merge 1 commit into
mainfrom
schickling-assistant/2026-09-07-socket-bound-runtime-root

Conversation

@schickling-assistant

Copy link
Copy Markdown
Contributor

Problem

#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.

main was red on both platforms, 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

The CI failure is the mild version. On a host whose catalog root is 49 bytes:

bytes
real catalog root 49
admission projection pty root 83
remaining identity budget 15

So once authoring resumed, publication would have failed closed on essentially every managed
declaration, including ones already published. A fail-closed guard on the wrong measurement is worse
than the defect it replaced: #432 was one seat that could never spawn, this is every publication on
the host.

Goal

The guard judges the socket path the supervisor will actually bind, and the validator's contract
says whose job it is to know that.

Decisions

  • The bound travels with the caller, as an explicit RuntimeRoot: Catalog(root) names the
    catalog that will run; Unknown is a 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, and direct validation of a
    live catalog supplies itself. st2 validate is not weakened — it still runs the guard.
  • Not by detecting the staging layout. Sniffing a .st2/catalog-admission-* ancestor would
    couple correctness to a directory name that is free to change.
  • Every call site had the canonical root already in scope, so no plumbing was invented for this;
    the parameter only makes explicit what each caller already knew.

Verification

Fail-before, via the faithful wiring mutant. The mutant makes the validator ignore the caller's
runtime root and measure the canonicalized tree under inspection, which is exactly the pre-fix
behaviour:

mutant result
validator measures the inspected tree (pre-fix wiring) caught by publication_judges_the_socket_bound_against_the_runtime_catalog_not_the_projection
the same mutant, against publication_still_refuses_an_unbindable_canonical_socket_path survives

That second row is the point, and it is why a clause mutant would not have been enough: the
canonical-rejection test cannot see a wrong-root defect, because an identity that is over the limit
is over it under either root. Only the wiring test discriminates.

What each test would show under the opposite implementation. Test 1 fails with
socket-path-too-long naming a catalog-admission-* path if the bound is ever read off the
inspected tree again. Test 2 fails, by publishing successfully, if the guard is disabled broadly
rather than re-rooted — so the repair cannot be satisfied by removing the check.

Sensitivity to staging depth, not to this staging layout. Test 1 places the canonical socket
path 0, 4 and 8 bytes under the limit and publishes at each. Any nesting deeper than that headroom
trips the bound if it is measured, whatever it is called and however deep it is, so the test does
not encode the current layout.

Two instrument failures found and fixed before trusting any of this, both of which would have
produced a green that could not have gone the other way:

  1. My first wiring mutant passed the raw shadow.path() and survived. The control directory is
    a /proc/self/fd/N path — short until canonicalization resolves it — so that mutant was inert,
    not evidence of a sensitive test. The faithful mutant measures the canonicalized root.
  2. Both new tests initially passed for no reason: this devshell exports PTY_ROOT, an ambient
    pty root wins over the catalog-relative default, and no catalog depth can then reach the limit.
    That is why the regression was visible in CI and invisible locally. Both tests now pin
    PTY_ROOT and PTY_SESSION_DIR off, and the helper says why.

Suites, targeted, in a warm target dir: lib 744 passed / 0 failed; --test agent_publish 17
passed / 0 failed / 9 ignored — the 4 tests CI failed on now pass, and the 9 ignored are the
sandbox-gated set; --test validate 64 / 0; --test run 60 / 0; --test invariants 2 / 0.

Held, not skipped: the full local Nix gate. Release condition — sequencing, since a heavy local
leg runs one at a time across the graph and this host is under load. CI will run once against this
candidate, which is the hermetic version of that evidence.

Complexity

One two-variant enum and one extra argument threaded to the call sites that already held the value.
No new dependency, no behaviour change for st2 validate.

Concerns

  • The guard's honesty depends on the caller passing the right root. That is why the parameter is
    an explicit type with a documented Unknown arm rather than an Option, and why the wiring mutant
    is part of the evidence rather than clause mutants alone.
  • Nine agent_publish tests fail locally with root-count: host 'host' must declare exactly one root agent; found 0. Pre-existing and unrelated — they are the set CI marks ignored, and they
    failed identically before this change.
  • My original fix(validate): reject an unbindable session socket path at admission #442 evidence could not have caught this. Nine killed clause mutants proved
    sensitivity to the clauses I wrote; none of them exercised a caller that supplied the wrong root.
    A mutation matrix measures clause sensitivity, not path coverage.

Friction & bottlenecks

  • PTY_ROOT being exported in the dev environment silently makes any socket-bound test vacuous.
    The new helper pins it, but nothing warns a future test author.

Follow-ups

References

Refs #432. Repairs the regression #442 introduced.

Posted on behalf of @schickling
field value
agent_identity dev3.compoundingtech.st2.resync-lock.worker
session dev3.1788d1eb
agent_persona worker
agent_supervisor dev3.compoundingtech-lead
agent_tool OMP
agent_tool_version 18.1.2
agent_runtime OMP 18.1.2
tooling_profile dotfiles@7534055

#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
@schickling-assistant
schickling-assistant marked this pull request as ready for review September 7, 2026 11:32
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