Skip to content

Commit 0114f21

Browse files
Harden PTY resource target observation
agent-identity: mbp2025.direct.omp.5tek2r48 agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.0.11 agent-runtime: OMP 18.0.11 tooling-profile: dotfiles@4cc7b25
1 parent 0278191 commit 0114f21

5 files changed

Lines changed: 366 additions & 212 deletions

File tree

docs/vrs/.decisions/0017-task-resource-targets-are-strict-observations.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ process-generation fence. One exact unified `0::` entry preserved its
3636
slash-prefixed path, including spaces and colons, while absent, duplicate,
3737
relative, repeated-separator, trailing-separator, and traversal-capable shapes
3838
were rejected. Stable start-token observations admitted the candidate target.
39-
A deterministic PTY regression replaced the PID generation between the first
40-
backend snapshot and start-token capture; the second snapshot's changed
41-
creation generation rejected that token. A token mismatch before target read,
42-
a token change after it, and process exit each emitted only a bounded
43-
unavailable result and never the candidate locator.
39+
A deterministic PTY regression replaced the PID generation between the
40+
registry snapshot and start-token capture; the socket-backed stats row's
41+
changed creation generation rejected that token. A two-live-task fixture
42+
proved one unscoped stats snapshot serves the whole inventory. A token mismatch
43+
before target read, a token change after it, and process exit each emitted only
44+
a bounded unavailable result and never the candidate locator.
4445

4546
A live Darwin exec observation joined the current PID as
4647
`darwinProcessTree.rootPid` without rewriting its retained generation record,
@@ -70,13 +71,14 @@ Unavailable reasons are exactly `notRunning`, `runtimeIndeterminate`,
7071

7172
On Linux, st2 reads exactly one unified `0::<path>` entry from the observed
7273
process's `/proc/<pid>/cgroup`. Exec compares the target fence to the token in
73-
its generation record. PTY captures a token after its first backend snapshot
74-
and admits it only after a second snapshot confirms the same task, running
75-
state, PID, and backend creation generation; that admitted token then fences
76-
the target read. A missing, exited, or recycled process never publishes the
77-
candidate locator. Darwin fences the current process in the same way before
78-
exposing its PID as a best-effort tree root. All targets are rediscovered on
79-
each inventory command.
74+
its generation record. PTY captures each candidate daemon token after the
75+
registry snapshot, then takes one unscoped, socket-backed `pty stats --json`
76+
snapshot for the entire inventory. A candidate is admitted only when its named
77+
live stats row reports the same daemon PID and creation generation and a second
78+
token read is unchanged; that admitted token then fences the target read. A
79+
missing, exited, or recycled process never publishes the candidate locator.
80+
Darwin fences the current process in the same way before exposing its PID as a
81+
best-effort tree root. All targets are rediscovered on each inventory command.
8082

8183
Runtime ID remains stable task identity. PID, creation time, generation ID,
8284
cgroup path, root PID, unit, and incarnation remain observation locators only.

docs/vrs/.experiments/2026-09-05-task-resource-target-observation.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ The inventory resource-target boundary was split into two narrow operations:
1616
strict parsing of the Linux `/proc/<pid>/cgroup` payload and a process-start
1717
fence around target acquisition. Deterministic unit fixtures supplied stable,
1818
changed, and disappearing start tokens around a candidate target. A PTY fixture
19-
then reused the same numeric PID with a different backend creation generation
20-
between the first snapshot and token capture; a confirming second snapshot
21-
tested that exact pre-fence window. Parser
19+
then reused the same numeric daemon PID with a different creation generation
20+
between the registry snapshot and token capture; the live socket-backed stats
21+
row tested that exact pre-fence window. A second fixture gave one inventory two
22+
live candidates, recorded the single unscoped stats invocation, and changed
23+
target proof from unavailable to available without changing either PTY
24+
generation ID. Parser
2225
fixtures covered a unified-only record, a hybrid record, root membership,
2326
spaces and colons, duplicate unified entries, missing v2 entries, malformed
2427
fields, carriage returns, repeated/trailing separators, and traversal
@@ -37,7 +40,8 @@ Focused commands:
3740

3841
```sh
3942
nix develop -c cargo test --lib task_inventory::tests
40-
nix develop -c cargo test --lib run::tests::pty_pid_reuse_between_snapshot_and_start_token_capture_is_rejected
43+
nix develop -c cargo test --lib run::tests::pty_stats_rejects_pid_reuse_between_registry_snapshot_and_start_token_capture
44+
nix develop -c cargo test --lib run::tests::pty_resource_observation_uses_one_stats_snapshot_and_preserves_generation_id
4145
nix develop -c cargo test --test task_inventory_cli
4246
```
4347

@@ -48,7 +52,8 @@ nix develop -c cargo test --test task_inventory_cli
4852
| Unified cgroup parser | Preserved one exact slash-prefixed `0::` path, including spaces and colons; rejected ambiguous, absent, relative, or traversal-capable shapes. |
4953
| Stable start token | Published the candidate resource target. |
5054
| Changed start token | Published only `unavailable/generationChanged`; the candidate locator was discarded. |
51-
| PTY reuse before token capture | A second backend snapshot observed the changed creation generation and returned `unavailable/generationChanged`; the replacement token was never admitted. |
55+
| PTY reuse before token capture | The socket-backed stats row observed the changed creation generation and returned `unavailable/generationChanged`; the replacement token was never admitted. |
56+
| PTY stats fan-out and identity | Two live candidates used one `pty stats --json` invocation with no session argument; transient target-proof failure then success preserved each PTY generation ID. |
5257
| Process disappeared during read | Published only `unavailable/processUnavailable`; the candidate locator was discarded. |
5358
| Stable target read failed | Published the bounded degraded reason `unavailable/cgroupV2Unavailable`. |
5459
| Live Darwin process | Published `{"type":"darwinProcessTree","rootPid":<current-pid>}` for the proven current generation. |
@@ -57,8 +62,9 @@ nix develop -c cargo test --test task_inventory_cli
5762

5863
## Conclusion
5964

60-
A resource target can remain a pure observation. PTY first captures the kernel
61-
token and then proves the backend generation unchanged before admitting it;
65+
A resource target can remain a pure observation. PTY captures the daemon token,
66+
then uses one socket-backed stats snapshot to prove every candidate's live
67+
daemon PID and creation generation before rereading and admitting the token;
6268
the token subsequently fences the target read. This prevents publication
6369
across the measured pre-fence reuse, exit, and generation-change races. Strict
6470
cgroup parsing supplies the exact kernel locator without scope-name

docs/vrs/requirements.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -291,13 +291,15 @@ accepted.
291291
exposes its exact unified cgroup-v2 path read from that process's
292292
`/proc/<pid>/cgroup`; unit and scope names are not locators. A proved live
293293
Darwin generation exposes its PID as a best-effort process-tree root. For
294-
PTY, the kernel start token captured after the first backend snapshot becomes
295-
admissible only when a second backend snapshot confirms the same PID and
296-
creation generation. That token is then sampled around target observation,
297-
as it is for exec. A changed, recycled, or exited process is never associated
298-
with the target.
299-
Non-running, indeterminate, degraded, and unsupported observations expose a bounded
300-
explicit unavailable reason rather than a nullable or guessed locator.
294+
PTY, st2 captures the candidate daemon's kernel start token, takes one
295+
socket-backed `pty stats --json` snapshot for all candidates, and admits the
296+
token only when the named live session reports the same daemon PID and
297+
creation generation and a second token read is unchanged. That token fences
298+
target observation, as the recorded token does for exec, but its transient
299+
availability never changes the PTY generation ID. A changed, recycled, or
300+
exited process is never associated with the target.
301+
Non-running, indeterminate, degraded, and unsupported observations expose a
302+
bounded explicit unavailable reason rather than a nullable or guessed locator.
301303
Resource-target unavailability is itself a complete observation and does not
302304
turn a trustworthy task inventory into an incomplete one.
303305
A proved non-routable retired subject has a null bus address without making

docs/vrs/spec.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -852,15 +852,18 @@ validate ──► materialize ──► host-local st2 scheduler/reconciler
852852
with no unified entry. It does not derive the path from a systemd unit,
853853
scope name, runtime ID, or naming convention.
854854

855-
PTY observation captures a kernel process-start token after its first backend
856-
snapshot, then takes a second backend snapshot and admits that token only
857-
when task ID, running state, PID, and backend creation time still identify
858-
the same generation. Resource observation reads the token again before and
859-
after reading the target. Exec compares those two reads directly to the start
860-
token already bound into its generation record. A missing process is
861-
`processUnavailable`; a backend or token mismatch, or a token that changes
862-
across the read, is `generationChanged`. Neither result exposes the candidate
863-
path or root PID. Darwin uses the same fence around its root PID. A bounded
855+
PTY observation captures each candidate daemon's kernel process-start token
856+
after the registry snapshot, then executes `pty stats --json` exactly once
857+
without a session argument. The returned array is socket-backed live
858+
evidence for every candidate. A token is admitted only when exactly one
859+
stats row has the task name, `process.alive=true`, the same `daemon.pid`, and
860+
the same `createdAt`, and a second kernel-token read equals the first.
861+
Resource observation then reads the token before and after the target. Exec
862+
compares those target-fence reads directly to the token already bound into
863+
its generation record. A missing process is `processUnavailable`; a stats or
864+
token mismatch, or a token that changes across a read, is
865+
`generationChanged`. Neither result exposes the candidate path or root PID.
866+
Darwin uses the same fence around its root PID. A bounded
864867
`unavailable` target is a truthful successful field and does not by itself
865868
make the envelope incomplete. Locators are sampled afresh on each command:
866869
st2 stores no scope registry, performs no resource sampling, and gives no

0 commit comments

Comments
 (0)