Skip to content

Commit a4f735b

Browse files
committed
Add fail-closed task inventory
1 parent c6846f6 commit a4f735b

10 files changed

Lines changed: 1910 additions & 7 deletions

File tree

INVARIANTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ materialization, messaging, DING, or presence must preserve them.
66
| Invariant | Guarantee | Proof |
77
|---|---|---|
88
| **R11 control-plane replacement safety** | Normal stop or SIGKILL of `st2 up`, atomic binary replacement, and control-plane restart leave live PTY and exec agents usable with the same PID and creation identity. The replacement adopts each survivor exactly once, launches only genuinely missing work, and never duplicates a survivor; only explicit teardown kills an agent. | `tests/nomad_survival.rs::normal_stop_and_binary_replacement_adopt_exec_unchanged_without_duplicate`; `tests/nomad_survival.rs::forced_kill_and_binary_replacement_adopt_exec_unchanged_without_duplicate`; `tests/nomad_survival.rs::normal_stop_and_binary_replacement_adopt_pty_unchanged_without_duplicate`; `tests/nomad_survival.rs::forced_kill_and_binary_replacement_adopt_pty_unchanged_without_duplicate`; `tests/nomad_survival.rs::explicit_teardown_kills_exec_but_plain_stop_does_not`; `tests/nomad_survival.rs::explicit_teardown_kills_pty_but_plain_stop_does_not` |
9+
| **R23 fail-closed diagnostic inventory** | `st2 tasks --json` joins a semantically stable desired-catalog observation to exact PTY and conservatively proven exec generations without writes or reconciliation. Missing roots are positively absent without creation; declaration drift, malformed, unreadable, duplicate, PID-reused, or incomplete evidence is indeterminate, makes the typed envelope incomplete, and exits non-zero. The observational boundary does not serialize catalog writers or authorize cutover. | `src/task_inventory.rs::stable_wire_shape_maps_pty_exec_explicit_default_and_ignores_foreign_host`; `src/task_inventory.rs::complete_missing_runtime_is_absent_but_incomplete_is_indeterminate`; `src/exec_backend.rs::legacy_plain_pid_is_observed_without_rewriting_it`; `src/exec_backend.rs::record_older_than_current_process_generation_cannot_be_promoted`; `tests/task_inventory_cli.rs` |
910
| **Transport-decoupled lifecycle** | Each task is isolated from a supervisor/transport process-group or cgroup cascade. | `tests/transport_isolation.rs`; `tests/transport_isolation_macos.rs` |
1011
| **Clean exec teardown** | Killing an exec task reaps its whole process group. | `tests/exec_backend.rs::exec_kill_reaps_the_whole_process_group_not_just_the_leader` |
1112
| **Bounded restart diagnostics** | Relaunching an exec task preserves the just-finished log as one prior generation while bounding retained diagnostics to current plus prior. Final retirement removes the PID and both logs. | `tests/exec_backend.rs::exec_restart_reap_keeps_bounded_diagnostics_and_final_remove_cleans_them`; `tests/run.rs::up_once_finally_removes_dead_retired_tasks_without_restarting_them` |

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,25 @@ For a foreground supervisor on any host:
250250
st2 up --catalog "$CATALOG" --host <host>
251251
```
252252

253+
### Typed task inventory (diagnostic only)
254+
255+
Use the typed inventory instead of parsing `doctor` prose:
256+
257+
```sh
258+
st2 tasks --catalog "$CATALOG" --host <host> --json
259+
```
260+
261+
The `st2.task-inventory.v1` envelope joins the selected host's desired PTY and exec tasks to
262+
read-only runtime evidence. A complete observation exits zero. Catalog parse errors, declaration
263+
drift during observation, duplicate runtime IDs, timeouts, malformed output, PID reuse, and
264+
otherwise unprovable generations emit `complete: false` and exit non-zero. Missing runtime rows
265+
become `absent` only when the corresponding backend observation is complete; uncertainty remains
266+
`indeterminate`.
267+
268+
Observation never creates a missing PTY or exec state root and never rewrites an existing exec PID
269+
record. It also does not serialize catalog writers, reconcile tasks, or authorize a control-plane
270+
cutover. Consumers that require a transactional declaration boundary need a separate protocol.
271+
253272
### Staged control-plane replacement gate
254273

255274
`st2 up` is a replaceable control plane, not the lifetime owner of an agent. Stopping it normally

docs/vrs/requirements.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,14 @@ accepted.
122122
inspection exposes every Resource binding without interpreting its type or URI.
123123
Resource-only declaration changes do not alter a task's effective launch
124124
definition and do not stop, replace, or relaunch healthy work.
125+
- **R23 Fail-closed task inventory:** One read-only machine command exposes
126+
every desired local PTY and exec task by agent identity, task name, runtime
127+
id, kind, lifecycle, retirement, desired state, runtime state, PID, creation
128+
time, and opaque runtime-generation id. Unknown, duplicate, malformed,
129+
unreadable, timed-out, PID-reused, or otherwise unprovable evidence is
130+
indeterminate and makes the versioned envelope incomplete and the command
131+
unsuccessful; it is never reported as absence. Observation detects semantic
132+
declaration drift across its runtime probe, does not create a missing runtime
133+
root, and performs no reconciliation, cleanup, lifecycle change, or state
134+
rewrite. This diagnostic boundary is not transactionally serialized with
135+
catalog writers and is not control-plane cutover authority.

docs/vrs/spec.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,28 @@ validate ──► materialize ──► host-local st2 scheduler/reconciler
167167
lifecycle is the explicit authority to resume ordinary replacement.
168168
`retired #true` remains the separate explicit teardown path.
169169

170+
- **R23:** `st2 tasks --json` is a read-only diagnostic boundary. It emits one
171+
`st2.task-inventory.v1` envelope for the selected host. Rows are sorted by
172+
agent, task, and runtime id and cover both PTY and terminal-free exec tasks.
173+
`complete=false` plus a non-zero exit is a closed result: a consumer must not
174+
turn a missing row into absence. A running row always carries a PID, creation
175+
time, and opaque generation id derived from stable backend evidence.
176+
177+
Discovery runs before and after runtime observation. A semantic declaration
178+
change across those passes makes the result incomplete. This detects
179+
observed drift but does not serialize catalog writers or claim a
180+
transactional snapshot. A missing runtime root is positively empty and
181+
remains absent on disk; malformed state, PID reuse, timeouts, duplicate ids,
182+
and observer failures are indeterminate. Existing plain-PID exec records are
183+
verified against the process start token and record mtime without rewriting
184+
them. If that proof is unavailable on a supported OS, the generation remains
185+
indeterminate.
186+
187+
Inventory performs no reconciliation, launch, teardown, cleanup, lifecycle
188+
edit, state migration, or catalog write. It does not authorize a staged
189+
supervisor replacement; any cutover requiring transactional declaration
190+
authority needs a separate protocol.
191+
170192
- **Session registry:** A catalog owns the `pty` registry holding its tasks.
171193
`<catalog>/pty` is the default; a catalog may declare another so that one host
172194
can share a single registry across catalogs. Resolution is an exported

0 commit comments

Comments
 (0)