--- fp-driver-d2-r4.sh +++ fp-driver-d2-r5.sh @@ -1,7 +1,7 @@ #!/bin/bash # ============================================================================= # D2 DRIVER — #297 bounded out-of-band capture, run BY THE EXISTING FIELD DRIVER. -# hertz 2026-09-13. FOR SOURCE REVIEW. NOT RUN IN THE FIELD. +# hertz 2026-09-13. r5 under 3XE67XYP: owned-job implementation; field runs still HELD. # NO BUILD GRANT. NO FIELD GRANT. NO ELEVATION GRANT. NO RESIDUAL-PID GRANT. # # PROVENANCE: this is the FROZEN r10 driver, staged-r10-queue-guard.sh, sha256 @@ -204,13 +204,9 @@ # LEDGER_RESERVE_S the report cannot be starved by the work before it. Every cleanup step # is bounded by WORK_DEADLINE = REPORT_DEADLINE - this; the ledger owns # the remainder ALONE and emits without waiting for any child. -# KILL_GRACE_S the seconds every bound holds back from its own deadline. It was named -# for `timeout -k N`'s TERM-then-KILL escalation, and NOTHING ESCALATES -# ANY MORE: no `timeout` stands on the launch boundary, and termination is -# held -- no kill, no tree kill. The reserve now covers the LAUNCHER'S OWN -# RETURN (writing the outcome into the identity record and reporting), so -# a bound still completes BY the deadline rather than past it, and a step -# that cannot cover it is NOT STARTED. +# KILL_GRACE_S caller reporting reserve; native setup, termination and confirmation +# live inside the launcher's own deadline. Only retained created handles +# authorize termination. No PID-tree sweep or guard bypass exists. # HANDOFF_CAP_S a POLICY ALLOCATION of the activity window -- how much of the 300s this # run will spend waiting on another agent. 94s and 134s are two samples, # not a distribution, and nothing here is derived from them. @@ -319,8 +315,7 @@ IDENT_CAPTURE_RC=1 # the capture command's own exit; 1 until it has run CAPTURE_USABLE=no # a capture supports CLEAR only when proven complete CAPTURE_INTEGRITY='' # why it is or is not usable, in words -DAEMON_STOP_REFUSED=0 # IR-122's expected refusal, recorded with its native exit -DAEMON_STOP_RC=NOT_RUN # the refused command's OWN exit, preserved in every case +DAEMON_STOP_RC=NOT_RUN # run-job stop/confirmation outcome, not the guarded daemon-stop verb LISTENER_STATE=UNMEASURED # unread is read as LIVE: an unread port is not an empty one CAPTURE_CONTRACT=NOT_SATISFIED # earned, never defaulted to satisfied CLEANUP_VERDICT=PENDING # PENDING until VERIFIED is measured; a clock never earns it @@ -352,31 +347,6 @@ mark_state() { echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $2" > "$ST/$1"; record "STATE_MARKED $1 — $2"; } has_state() { [ -f "$ST/$1" ]; } -# ---- run_cmd: launch, capture and product-exit are THREE different answers - -# Pre-creating the capture files answers "can I capture at all". rc 126/127 answers -# "did it launch". Everything else is the product's own exit. An EMPTY capture is -# neither of those — it is a missing-row OUTCOME, classified where rows are counted. -run_cmd() { - local tag="$1" out="$2" err="$3"; shift 3 - [ "$1" = '--' ] && shift || { record "HARNESS_BUG run_cmd $tag called without --"; exits "${tag}_HARNESS=bad-call"; return 90; } - if ! : > "$out" 2>/dev/null; then - exits "${tag}_CAPTURE=SETUP_FAILED_STDOUT"; record "CAPTURE_SETUP_FAILED $tag stdout=$out"; return 91 - fi - if ! : > "$err" 2>/dev/null; then - exits "${tag}_CAPTURE=SETUP_FAILED_STDERR"; record "CAPTURE_SETUP_FAILED $tag stderr=$err"; return 91 - fi - stamp "${tag}_START" - "$@" > "$out" 2> "$err" - local rc=$? - case "$rc" in - 127) exits "${tag}_LAUNCH=NOT_FOUND"; record "NOT_LAUNCHED $tag — command not found (rc 127), NOT a product exit" ;; - 126) exits "${tag}_LAUNCH=NOT_EXECUTABLE"; record "NOT_LAUNCHED $tag — not executable (rc 126), NOT a product exit" ;; - *) exits "${tag}_LAUNCH=OK" ;; - esac - exits "${tag}_EXIT=$rc" - stamp "${tag}_END rc=$rc" - return $rc -} die() { set_original 1 "$1" @@ -828,158 +798,136 @@ [ "$left" -lt 0 ] && left=0 echo "$left" } -# ---- bounded(): the ONLY enforced bound, and it MEASURES what it stopped ----- -# THE PREDECESSOR ENFORCED NOTHING EXCEPT THE CAPTURES. deadline_check fired at two sites in -# the whole 20260913T071131Z run, and 271 of the 300 seconds ran with no budget read at all: -# a budget checked BETWEEN steps bounds a sequence, not a process. -# -# FOUR THINGS, NOT ONE, and each is a correction doyle made to my first proposal: -# 1. the bound subtracts KILL_GRACE_S, so the LAUNCHER'S OWN RETURN completes BY the -# deadline rather than past it, and a step that cannot cover it is NOT STARTED; -# 2. the subject is REGISTERED FROM THE ACT OF LAUNCHING IT. The register no longer asks -# Win32 for a wrapper's children: that query could only ever reach the wrapper itself -# (A7, A6, probe-a3-parent), because the winpid bash hands back is a pre-exec stub's. -# The identity kept is the DIRECT child's, and it carries NO claim over anything that -# child may itself have started; -# 3. on expiry the driver MEASURES the subject's disposition by pid AND creation instant, -# and TERMINATES NOTHING. Tree kill is withdrawn until containment behaviour is -# reviewable, so an expired step's subject MAY STILL BE RUNNING and is reported -# SURVIVING rather than stopped. `timeout` sent TERM to a native Windows process that -# owed it no POSIX answer, and whose descendants sat in no job object the kill reached; -# dropping it removes a wrapper, not a guarantee; -# 4. whatever the killed step wrote is PRESERVED under a name that says it is unverified. -# An expired bound says the COMPLETION is unverified; it says nothing about the disk. -# -# NOTHING HERE KILLS ANYTHING THIS DRIVER DID NOT SPAWN, and nothing here acquires an -# authority over an elevated leg. Stopping a wait and stopping an operation are different acts. -BOUNDED_STATE=NOT_REACHED # the outcome of the most recent bounded() call +# r5: actual argv vector, owned job confirmation, direct state kept separate. +BOUNDED_STATE=NOT_REACHED LAUNCH_SEQ=0 +RUN_JOB_EXPECTED=0 +SUBJECT_STATE=NOT_STARTED launch_file() { echo "$R/launched-identities.txt"; } -# winpid_of() IS GONE, and not merely unused. Its only consumer was the descendant query, and -# the mapping it supplied -- an msys pid's winpid -- is the very reading that misattributed the -# subject: on cygwin the winpid bash hands back belongs to the PRE-EXEC STUB, so a -# ParentProcessId query fed from it could only ever reach the exec'd wrapper. The launcher now -# reports the identity it CREATED, so nothing needs the map, and leaving the function behind -# would leave the next author a tool whose answer looks authoritative and is not. -# THE REGISTER READS THE LAUNCHER'S OWN IDENTITY RECORD. It queries no parent link, and it -# enumerates no descendant. -# -# WHY THE QUERY IS GONE, measured. The predecessor read the backgrounded job's winpid out of -# msys `ps` and asked Win32 for THAT pid's children. On cygwin the pid bash hands back is the -# PRE-EXEC STUB's, so the only process that ever answered was the exec'd `timeout.exe`: A7's -# single kind=native row named the WRAPPER, A6 got no row at all, and -# disposition-20260913T092245Z measured five wrapper identities GONE while the subjects they -# bounded had no identity at all and stay UNREADABLE for good. A deeper walk down the same -# links inherits the same broken premise (doyle JDCJYANF). -# -# WHAT REPLACES IT. launch_bounded.ps1 CREATES the subject and writes the identity it created -# -- metadata only -- before it waits. The register copies that in. The identity comes from the -# act of launching, not from a query afterwards, and the launcher keeps the handle so the pid -# cannot be recycled under the record (doyle NRTFH236 #1). -# -# COVERAGE IS NEVER COMPLETE (doyle NRTFH236 #2). Nothing enforces containment, so no launch -# can establish that it has seen everything its subject started. A scan that found nothing -# would not raise this either -- that is the absence of a reading. COMPLETE is not emissible -# anywhere in this driver, and CONFIRMED_GONE therefore is not emissible either. -SUBJECT_STATE=NOT_MEASURED -register_launched() { # LABEL RECORD_FILE - local label="$1" rec="$2" lf; lf=$(launch_file) +launcher_record() { python "$BIN/launch-record.py" field "$2" "$1"; } +launch_admission() { # LABEL -> audited creation-chain scope, never inferred from a scan + case "$1" in + identity_capture_*|portfields_*|census_*|d1_*_render|d1_*_exec|d2_*_capture) echo process-tree ;; + *) echo none ;; + esac +} +register_launched() { # LABEL RECORD_JSON LAUNCH_SEQ=$((LAUNCH_SEQ + 1)) - local now; now=$(date -u +%Y-%m-%dT%H:%M:%SZ) - echo "LAUNCHED seq=$LAUNCH_SEQ label=$label kind=launcher exe=[$BIN/launch_bounded.ps1] registered=$now" >> "$lf" - if [ ! -s "$rec" ]; then - echo "LAUNCHED seq=$LAUNCH_SEQ label=$label coverage=NONE reason=no-identity-record" >> "$lf" - record " LAUNCH($label) the launcher wrote no identity record: coverage=NONE. That is the ABSENCE OF A READING, and it is never the statement that nothing was launched." - return 0 - fi - cat "$rec" >> "$R/launch-records.txt" 2>/dev/null - local subj; subj=$(grep -m1 '^subject pid=' "$rec" 2>/dev/null) - if [ -z "$subj" ]; then - echo "LAUNCHED seq=$LAUNCH_SEQ label=$label coverage=NONE reason=record-has-no-subject-row" >> "$lf" - record " LAUNCH($label) the identity record names no subject: coverage=NONE, and nothing about this launch can be re-queried." - return 0 - fi - echo "LAUNCHED seq=$LAUNCH_SEQ label=$label kind=subject ${subj#subject }" >> "$lf" - local st; st=$(grep -m1 '^subject_state=' "$rec" 2>/dev/null) - [ -n "$st" ] && echo "LAUNCHED seq=$LAUNCH_SEQ label=$label $st" >> "$lf" - echo "LAUNCHED seq=$LAUNCH_SEQ label=$label coverage=PARTIAL reason=direct-subject-only-no-enforced-containment" >> "$lf" - record " LAUNCH($label) subject registered from the launcher's own identity record; coverage=PARTIAL -- the DIRECT child only, because nothing here enforces containment" + python "$BIN/launch-record.py" register "$(cygpath -w "$R")" "$1" "$(cygpath -w "$2")" "$LAUNCH_SEQ" >> "$R/findings.txt" +} +measure_termination() { # LABEL SEQ DEADLINE: status is owned-job evidence, not a pid scan + local label="$1" seq="$2" rec + rec="$R/.launch-rec.$seq.json" + SUBJECT_STATE=$(launcher_record subject.state "$rec") + TERMINATION=$(launcher_record termination "$rec") + LAUNCH_COVERAGE=$(launcher_record coverage "$rec") + if [ "$LAUNCH_COVERAGE" != COMPLETE ]; then TERMINATION=ATTRIBUTION_INCOMPLETE; fi + record "SUBJECT_STATE($label)=$SUBJECT_STATE (direct subject only)" + record "TERMINATION($label)=$TERMINATION coverage=$LAUNCH_COVERAGE (whole admitted operation only)" +} +bounded() { # LABEL DEADLINE_EPOCH OUT ERR -- COMMAND... + local label="$1" dl="$2" out="$3" err="$4"; shift 4 + if [ "${1:-}" != -- ]; then BOUNDED_STATE=HARNESS_BUG; return 90; fi + shift + BOUNDED_STATE=NOT_REACHED; TERMINATION=NOT_CONFIRMED; SUBJECT_STATE=NOT_STARTED + case "$dl" in (''|*[!0-9]*) dl=0 ;; esac + local left; left=$(( dl - $(date +%s) - KILL_GRACE_S )) + if [ "$left" -lt 1 ]; then + BOUNDED_STATE=NOT_STARTED; exits "${label}_BOUND=NOT_STARTED"; return 125 + fi + # Every invocation has fresh captures. Preserve prior uses of a shared caller path. + local myseq; myseq=$((LAUNCH_SEQ + 1)) + local rec="$R/.launch-rec.$myseq.json" argf="$R/.launch-args.$myseq.json" + local previous + for previous in "$out" "$err"; do + if [ -e "$previous" ]; then + if [ -e "$previous.before-$myseq" ]; then BOUNDED_STATE=SETUP_FAILED; return 91; fi + mv "$previous" "$previous.before-$myseq" || { BOUNDED_STATE=SETUP_FAILED; return 91; } + fi + done + local lexe="$1" lwin admission; shift + lwin=$(win_exe "$lexe") + if [ -z "$lwin" ]; then BOUNDED_STATE=SETUP_FAILED; return 91; fi + python "$BIN/launch-record.py" argv "$(cygpath -w "$argf")" "$@" || { BOUNDED_STATE=SETUP_FAILED; return 91; } + admission=$(launch_admission "$label") + stamp "${label}_START deadline_epoch=$dl admission=$admission" + pwsh -NoProfile -File "$(cygpath -w "$BIN/launch-v2.ps1")" \ + -Label "$label" -Seconds "$left" -Scope step -Admission "$admission" \ + -RecordFile "$(cygpath -w "$rec")" -ArgsFile "$(cygpath -w "$argf")" -Exe "$lwin" \ + -OutFile "$(cygpath -w "$out")" -ErrFile "$(cygpath -w "$err")" > "$R/.launch-log.$myseq" 2>&1 + local rc=$? + register_launched "$label" "$rec" + local reason; reason=$(launcher_record completion_reason "$rec") + measure_termination "$label" "$myseq" "$dl" + stamp "${label}_END rc=$rc reason=$reason" + exits "${label}_EXIT=$rc reason=$reason" + case "$reason" in + deadline_expired) BOUNDED_STATE=EXPIRED ;; + subject_exit) BOUNDED_STATE=RAN ;; + setup_refused) BOUNDED_STATE=SETUP_FAILED ;; + *) BOUNDED_STATE=UNREADABLE ;; + esac + return "$rc" +} +coverage_verdict() { + OPERATION_COVERAGE=INCOMPLETE; OPERATION_TERMINATION=UNREADABLE; DIRECT_SUBJECT_STATES=UNREADABLE + local summary="$R/.coverage-values.txt" + python "$BIN/launch-record.py" aggregate "$(cygpath -w "$R")" "${LAUNCH_SEQ:-0}" "${RUN_JOB_EXPECTED:-0}" > "$summary" || return 1 + local k v + while read -r k v; do + case "$k" in coverage) OPERATION_COVERAGE="$v" ;; termination) OPERATION_TERMINATION="$v" ;; direct) DIRECT_SUBJECT_STATES="$v" ;; esac + done < "$summary" +} +run_daemon_start() { + # This launcher owns a separate run job. It is NEVER a bounded step's child. + [ "${RUN_JOB_EXPECTED:-0}" -eq 0 ] || return 1 + [ -d "$H" ] && [ ! -e "$H/daemon.pid" ] || { record "RUN_DAEMON_REFUSED home missing or prior daemon breadcrumb present"; return 1; } + RUN_JOB_EXPECTED=1 + local rec="$R/run-daemon.json" args="$R/run-daemon-args.json" envf="$R/run-daemon-env.json" + python "$BIN/launch-record.py" argv "$(cygpath -w "$args")" daemon run || return 1 + python "$BIN/run-daemon-control.py" environment "$(cygpath -w "$envf")" "$(cygpath -w "$H")" || return 1 + local seconds; seconds=$(( EXP_DEADLINE + CLEANUP_REPORT_S - $(date +%s) - LEDGER_RESERVE_S )) + [ "$seconds" -gt 10 ] || return 1 + pwsh -NoProfile -File "$(cygpath -w "$BIN/launch-v2.ps1")" \ + -Label run-daemon -Seconds "$seconds" -Scope run -Admission process-tree \ + -RecordFile "$(cygpath -w "$rec")" -ArgsFile "$(cygpath -w "$args")" \ + -EnvironmentFile "$(cygpath -w "$envf")" -StopFile "$(cygpath -w "$R/run-daemon.stop")" \ + -Exe "$(cygpath -w "$EXE")" -OutFile "$(cygpath -w "$R/run-daemon.out")" \ + -ErrFile "$(cygpath -w "$R/run-daemon.err")" > "$R/run-daemon-launcher.log" 2>&1 & + RUN_JOB_LAUNCH_PID=$! + mark_state daemon_may_run "run-scoped owned job launched; no step may autostart a daemon" + run_daemon_require_ready +} +run_daemon_require_ready() { + [ "${RUN_JOB_EXPECTED:-0}" -eq 1 ] || { record "AUTOSTART_REFUSED no run-scoped daemon job"; return 1; } + pwsh -NoProfile -File "$(cygpath -w "$BIN/run-daemon-ready.ps1")" \ + -RecordFile "$(cygpath -w "$R/run-daemon.json")" -Home "$(cygpath -w "$H")" \ + -Seconds 20 > "$R/run-daemon-ready.latest.txt" 2>&1 + local rc=$? + cat "$R/run-daemon-ready.latest.txt" >> "$R/findings.txt" + [ "$rc" -eq 0 ] || { record "AUTOSTART_REFUSED owned supervisor/brain not freshly proven ready"; return 1; } +} +run_daemon_stop() { + if [ "${RUN_JOB_EXPECTED:-0}" -ne 1 ]; then DAEMON_STOP_STATE=NOT_MARKED; return 0; fi + # The stop channel reaches only our retained run-job handle; no daemon-stop guard bypass. + : > "$R/run-daemon.stop" + local end; end=$(cur_deadline) + local state=running + while [ "$(date +%s)" -lt "$end" ]; do + state=$(launcher_record completion_reason "$R/run-daemon.json") + [ "$state" != running ] && [ "$state" != UNREADABLE ] && break + sleep 0.2 + done + local confirmed; confirmed=$(launcher_record termination "$R/run-daemon.json") + DAEMON_STOP_STATE="$confirmed" + if [ "$confirmed" != CONFIRMED_GONE ]; then + DAEMON_STOP_RC=126; record "RUN_DAEMON_STOP_UNCONFIRMED reason=$state"; return 1 + fi + if [ -n "${RUN_JOB_LAUNCH_PID:-}" ]; then wait "$RUN_JOB_LAUNCH_PID"; DAEMON_STOP_RC=$?; fi + record "RUN_DAEMON_STOP termination=$confirmed; coverage=$(launcher_record coverage "$R/run-daemon.json")" return 0 } -# WHAT STOPPED, REPORTED AS TWO THINGS THAT ARE NEVER SUMMED (doyle NRTFH236 #2). The DIRECT -# subject's disposition is measured by pid AND creation instant. The WHOLE OPERATION stays -# ATTRIBUTION_INCOMPLETE while coverage is short of COMPLETE -- which, under this design, it -# always is. CONFIRMED_GONE is not emissible. -# -# THE ANCHOR IS NOT DECORATION: control L7 (controls-out/20260913T100902Z-L) caught pid 53072 -# recycled 24 seconds after that subject exited. A pid-only re-query would have called the -# subject SURVIVING. -# -# THE RE-QUERY IS STILL BOUNDED BY `timeout`, DELIBERATELY. That is a bound on a MEASUREMENT -# whose identity is never registered; it is not the launch boundary, and nothing here can be -# mistaken for the subject. -measure_termination() { # LABEL SEQ DEADLINE -> sets TERMINATION and SUBJECT_STATE - local label="$1" seq="$2" dl="$3" lf; lf=$(launch_file) - TERMINATION=ATTRIBUTION_INCOMPLETE - SUBJECT_STATE=UNREADABLE - local rows; rows=$(grep "^LAUNCHED seq=$seq .* kind=subject pid=" "$lf" 2>/dev/null) - if [ -z "$rows" ]; then - record " SUBJECT_STATE($label)=UNREADABLE -- no subject identity was registered for this launch, so nothing can be re-queried. This is NOT 'confirmed gone'." - record " TERMINATION($label)=ATTRIBUTION_INCOMPLETE" - return - fi - local q_left=$(( dl - $(date +%s) )) - if [ "$q_left" -le 3 ]; then - record " SUBJECT_STATE($label)=UNREADABLE -- no budget remained to re-query the registered identity. UNREADABLE is never CLEAR." - record " TERMINATION($label)=ATTRIBUTION_INCOMPLETE" - return - fi - [ "$q_left" -gt 10 ] && q_left=10 - local live=0 gone=0 unread=0 p c now_c - while IFS= read -r r; do - p=$(echo "$r" | sed -n 's/.*[ ]pid=\([0-9]*\).*/\1/p') - c=$(echo "$r" | sed -n 's/.*[ ]created=\([^ ]*\).*/\1/p') - [ -n "$p" ] || continue - if [ -z "$c" ] || [ "$c" = UNREADABLE ]; then - unread=$((unread + 1)) - record " SUBJECT_STATE($label) pid=$p carries NO creation anchor, so it cannot be re-queried. An unanchored pid is never reported gone." - continue - fi - now_c=$(SPT_D2_P="$p" timeout -k 2 "$q_left" pwsh -NoProfile -Command ' - $p = [int]$env:SPT_D2_P - try { $x = @(Get-CimInstance Win32_Process -Filter "ProcessId=$p" -ErrorAction Stop) } - catch { "UNREADABLE"; exit 0 } - if ($x.Count -eq 0) { "GONE"; exit 0 } - try { $x[0].CreationDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ") } catch { "UNREADABLE" } - ' 2>/dev/null | tr -d ' \r\n') - case "$now_c" in - GONE) gone=$((gone + 1)) ;; - UNREADABLE|'') unread=$((unread + 1)) ;; - *) - # COMPARED AT SECOND GRANULARITY. The launcher records the creation instant to 100ns - # and this re-query reads the same field, but the two readers round the last digits - # differently (L1a: ...8749085 recorded, ...8749080 re-queried). A pid recycled within - # the same second is not a case this box produces -- L7's measured reuse took 24s -- - # and both raw values are written down, so the comparison can be re-derived. - if [ "${now_c%.*}" = "${c%.*}" ]; then - live=$((live + 1)) - record " SUBJECT_STATE($label) SURVIVING pid=$p created=$c -- the bound elapsed and this process did not stop. NOTHING WAS TERMINATED: termination is held." - else - gone=$((gone + 1)) - record " SUBJECT_STATE($label) pid=$p was REUSED (recorded created=$c, now created=$now_c): a different process answers on that pid, so ours is gone" - fi ;; - esac - done < file` would file -# this function's own log lines into the subject's output. run_cmd already takes OUT and ERR -# for exactly this reason, and this helper keeps the same shape. +# this function's own log lines into the subject's output. bounded takes explicit OUT and ERR +# channels so the instrument's own logs cannot become subject evidence. # WHICH DEADLINE A STEP IS UNDER IS A PROPERTY OF THE PHASE, NOT OF THE CALL SITE. One # helper answers it, so a probe called from preparation, from the window and from cleanup # cannot silently re-grant itself the wrong budget -- which is exactly what a remaining-seconds @@ -1017,72 +965,6 @@ p=$(command -v "$prog" 2>/dev/null) || return 1 [ -n "$p" ] || return 1 cygpath -w "$p" 2>/dev/null -} -bounded() { # LABEL DEADLINE_EPOCH OUT ERR -- COMMAND... - local label="$1" dl="$2" out="$3" err="$4"; shift 4 - if [ "$1" = '--' ]; then shift; else - record "HARNESS_BUG bounded '$label' called without --"; exits "${label}_HARNESS=bad-call"; BOUNDED_STATE=HARNESS_BUG; return 90 - fi - if ! : > "$out" 2>/dev/null || ! : > "$err" 2>/dev/null; then - record "CAPTURE_SETUP_FAILED $label out=$out err=$err"; exits "${label}_CAPTURE=SETUP_FAILED"; BOUNDED_STATE=SETUP_FAILED; return 91 - fi - BOUNDED_STATE=NOT_REACHED - TERMINATION=NOT_MEASURED - case "$dl" in (''|*[!0-9]*) dl=0 ;; esac - local now left; now=$(date +%s); left=$(( dl - now - KILL_GRACE_S )) - if [ "$dl" -le 0 ] || [ "$left" -lt 1 ]; then - BOUNDED_STATE=NOT_STARTED - exits "${label}_BOUND=NOT_STARTED" - record "BOUND_NOT_STARTED $label — ${left}s would remain after the ${KILL_GRACE_S}s launcher reserve. The step is NOT RUN." - record " TRUNCATED phase=$label got 0s. What it would have observed is NOT CAPTURED, which is not the statement 'not present'." - return 125 - fi - stamp "${label}_START bound=${left}s deadline_epoch=$dl" - # THE LAUNCHER IS THE BOUNDARY (doyle NRTFH236 #3). No `timeout` and no `env` stand between - # this driver and the process it means to run: launch_bounded.ps1 creates the subject - # DIRECTLY, holds its handle for the child's whole life, writes the identity it created, and - # enforces the bound itself. The environment reaches the subject by INHERITANCE -- a call site - # that needs a variable exports it around this call. - # - # ARGUMENTS CROSS ON DISK, ONE PER LINE. An array handed through `pwsh -File` collapses into a - # single string, which is how quoting defects are built. (Ceiling: an argument containing a - # newline would not survive this protocol. No call site has one, and one would have to be - # written deliberately.) - local myseq=$((LAUNCH_SEQ + 1)) - local rec="$R/.launch-rec.$myseq" argf="$R/.launch-args.$myseq" - rm -f "$rec" "$argf" - local lexe="$1"; shift - local lwin; lwin=$(win_exe "$lexe") - if [ -z "$lwin" ]; then - BOUNDED_STATE=SETUP_FAILED - exits "${label}_LAUNCH=UNRESOLVABLE" - record "LAUNCH_UNRESOLVABLE $label — the program [$lexe] does not resolve on PATH. The step is NOT RUN and NOTHING WAS LAUNCHED, which is a different statement from a step that ran and failed." - return 91 - fi - if [ $# -gt 0 ]; then printf '%s\n' "$@" > "$argf"; else : > "$argf"; fi - pwsh -NoProfile -File "$(cygpath -w "$BIN/launch_bounded.ps1")" \ - -Label "$label" -Seconds "$left" \ - -RecordFile "$(cygpath -w "$rec")" -ArgsFile "$(cygpath -w "$argf")" \ - -Exe "$lwin" \ - -OutFile "$(cygpath -w "$out")" -ErrFile "$(cygpath -w "$err")" \ - > "$R/.launch-log.$myseq" 2>&1 - local rc=$? - # THE LAUNCHER'S OWN STREAMS GO TO ITS OWN FILE. The SUBJECT's output is written by the - # launcher straight into OUT and ERR, so anything the launcher itself says would otherwise - # contaminate either the subject's capture or this driver's log (`record` tees to stdout -- - # r10's defect, and it cost IR-123 an unpassable gate). - [ -s "$R/.launch-log.$myseq" ] && record " LAUNCHER($label) wrote to .launch-log.$myseq: $(head -3 "$R/.launch-log.$myseq" | tr '\n' ' ')" - register_launched "$label" "$rec" - stamp "${label}_END rc=$rc" - exits "${label}_EXIT=$rc" - if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then - BOUNDED_STATE=EXPIRED - record "BOUND_EXPIRED $label after ${left}s (launcher rc=$rc). The bound stopped THIS DRIVER'S WAIT AND NOTHING ELSE: no termination is authorized here, so the subject MAY STILL BE RUNNING. What it is actually doing is measured below." - measure_termination "$label" "$myseq" "$dl" - return 124 - fi - BOUNDED_STATE=RAN - return $rc } cleanup_left() { [ "$CLEANUP_EPOCH" -eq 0 ] && { echo "$CLEANUP_REPORT_S"; return; } @@ -1207,7 +1089,7 @@ record "D2($point) NOT RUN — the remaining budget could not cover the ${KILL_GRACE_S}s launcher reserve. NOT CAPTURED is not 'not present'." return 1 fi - if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then + if [ "$BOUNDED_STATE" = EXPIRED ]; then echo "TIMEOUT" > "$exf" # COMPLETION UNVERIFIED IS NOT AN ABSENT DOCUMENT (doyle O2VBV5LC). The serialize, the # JSON write and the base64 write are separate stages in 69b8506a; an expired bound can @@ -1664,9 +1546,20 @@ elif [ "$RESIDUE_LIVE" -gt 0 ]; then CLEANUP_STOP_STATE=ADMITTED record "CLEANUP_STOP_ADMITTED - $RESIDUE_LIVE tracked process(es) measured LIVE by pid + creation time; the stop has something to stop" - run_cmd cleanup_stop "$R/cleanup-stop.out" "$R/cleanup-stop.err" -- \ - env SPT_INSTALL_NO_FIREWALL=1 SPT_HOME="$H" "$EXE" serve lan --stop - [ $? -eq 0 ] || cfail "unelevated opt-out stop returned nonzero; see cleanup-stop.err" + if run_daemon_require_ready; then + local old_home="${SPT_HOME-}" had_home="${SPT_HOME+x}" + local old_optout="${SPT_INSTALL_NO_FIREWALL-}" had_optout="${SPT_INSTALL_NO_FIREWALL+x}" + export SPT_HOME="$H" SPT_INSTALL_NO_FIREWALL=1 + bounded cleanup_stop "$(cur_deadline)" "$R/cleanup-stop.out" "$R/cleanup-stop.err" -- \ + "$EXE" serve lan --stop + local stop_rc=$? + if [ "$had_home" = x ]; then export SPT_HOME="$old_home"; else unset SPT_HOME; fi + if [ "$had_optout" = x ]; then export SPT_INSTALL_NO_FIREWALL="$old_optout"; else unset SPT_INSTALL_NO_FIREWALL; fi + [ "$stop_rc" -eq 0 ] || cfail "contained opt-out stop returned nonzero; see cleanup-stop.err" + else + CLEANUP_STOP_STATE=WITHHELD_UNREADABLE + cfail "autostart-capable stop refused: owned daemon readiness unavailable" + fi elif [ "$RESIDUE_UNREADABLE" -gt 0 ]; then CLEANUP_STOP_STATE=WITHHELD_UNREADABLE record "CLEANUP_STOP_WITHHELD residue=UNREADABLE ($RESIDUE_UNREADABLE identity/identities) - NO autostarting stop is issued on an unreadable measurement. Disposition PENDING/review: this run can neither say the listener is gone nor act as though it is present." @@ -1802,63 +1695,10 @@ record " CLEANUP=VERIFIED is not available on this path either: nothing was created, so there is nothing whose removal could be measured. The verdict below says which of the two it is." fi - # 3. Isolated daemon. OWL_SESSION_ID IS LEFT INTACT: clearing it bypasses a refusal - # guard doyle has not approved. A refusal is REPORTED; nothing is force-reaped. - # DAEMON_STOP_REFUSED is EXPECTED here (IR-122): the rig's isolated-daemon stop is - # structurally unrunnable from an spt-hosted session, and that refusal is reported, - # never worked around. - # THE SAME GATE AS THE LISTENER (doyle 2026-09-13). daemon_may_run is marked BEFORE the setup - # is dispatched, so it says what this run INTENDED, not what the host holds. The marker decides - # whether the question is asked; a FRESH measurement decides whether the command runs. - # attributed LIVE -- a tracked identity of this run is measured running: the reviewed stop path - # may be entered, because there is something to stop. - # measured ABSENT -- nothing of this run is running: SKIP. No command. - # UNREADABLE -- attribution could not be established: PENDING/review, and no action. - # THIS IS NOT A CLAIM THAT `daemon stop` SPAWNS ANYTHING. Nothing here measures that, and the - # gating mismatch alone would not establish it. The reason for the gate is narrower and enough: - # acting on a marker that was never a measurement is not justified by the marker. - if has_state daemon_may_run; then - residue_scan pre-daemon-stop no - if [ "$SETUP_OUTSTANDING" = yes ]; then - DAEMON_STOP_STATE=WITHHELD_OUTSTANDING_SETUP - CLEANUP_VERDICT=PENDING - record "DAEMON_STOP_WITHHELD setup=$SETUP_COMPLETION — the elevated leg dispatches through the daemon (ensure_running, serveverb.rs:191), so stopping it while that leg may still be acting races the processes it is starting. NO command is issued; disposition PENDING/review." - cfail "daemon stop withheld: the setup is outstanding ($SETUP_COMPLETION)." - elif [ "$RESIDUE_LIVE" -gt 0 ]; then - DAEMON_STOP_STATE=ADMITTED - record "DAEMON_STOP_ADMITTED - $RESIDUE_LIVE tracked identity/identities measured LIVE by pid + creation time; the stop has something to stop" - elif [ "$RESIDUE_UNREADABLE" -gt 0 ]; then - DAEMON_STOP_STATE=WITHHELD_UNREADABLE - CLEANUP_VERDICT=PENDING - record "DAEMON_STOP_WITHHELD residue=UNREADABLE ($RESIDUE_UNREADABLE identity/identities) - NO stop is issued on an unreadable measurement. Disposition PENDING/review." - else - DAEMON_STOP_STATE=NOT_NEEDED - record "DAEMON_STOP_NOT_NEEDED - daemon_may_run was marked, but every tracked identity measured GONE (live=0 unreadable=0). NO COMMAND IS ISSUED." - fi - fi - if [ "${DAEMON_STOP_STATE:-NOT_REACHED}" = ADMITTED ]; then - run_cmd rig_daemon_stop "$R/rig-daemon-stop.out" "$R/rig-daemon-stop.err" -- \ - env SPT_HOME="$H" "$EXE" daemon stop - DAEMON_STOP_RC=$? - # THE REFUSED COMMAND'S NATIVE EXIT IS PRESERVED IN EVERY CASE (doyle, 2026-09-13) -- - # in exits.txt, here, and in the ledger. A refusal reported without its own exit is a - # story about a command rather than a record of one. - record "RIG_DAEMON_STOP native_exit=$DAEMON_STOP_RC (preserved verbatim; see rig-daemon-stop.err)" - if [ "$DAEMON_STOP_RC" -ne 0 ]; then - # REPORTED, NOT FORCED, and NOT BY ITSELF A CLEANUP FAILURE (D2 change, measured): - # IR-122 makes this refusal STRUCTURAL from an spt-hosted session -- it fires on the - # caller's OWL_SESSION_ID whatever SPT_HOME is targeted. r10 counted it as a cleanup - # failure, which under the r8 exit rule would raise EVERY D2 run to exit 6 and empty - # that code of meaning. What decides whether it MATTERS is the residue measurement - # immediately below: a refusal that left no process associated with this home left - # nothing behind, and a refusal that left one is reported as an identity with the - # verdict PENDING. The refusal is on the record either way. - record "DAEMON_STOP_REFUSED (expected, IR-122) — reported, never worked around. Whether anything REMAINS is decided by the residual-identity measurement below, not by this exit." - DAEMON_STOP_REFUSED=1 - fi - elif ! has_state daemon_may_run; then - DAEMON_STOP_STATE=NOT_MARKED - record "CLEANUP_SKIP daemon — no daemon_may_run state was ever marked" + # 3. Close only this run's daemon job. Never invoke daemon stop or clear its guard. + if ! run_daemon_stop; then + CLEANUP_VERDICT=PENDING + cfail "owned run daemon job did not confirm empty" fi # 4. RESIDUAL IDENTITY -- RUN-BOUND, NOT STRING-MATCHED (doyle's admission blocker, @@ -1950,7 +1790,7 @@ cov_n=$(grep -c ' coverage=' "$(launch_file)" 2>/dev/null) case "$subj_n" in (''|*[!0-9]*) subj_n=0 ;; esac case "$cov_n" in (''|*[!0-9]*) cov_n=0 ;; esac - record "LAUNCHED_IDENTITIES $(grep -c '^LAUNCHED ' "$(launch_file)" 2>/dev/null) rows: $subj_n DIRECT subjects across $cov_n launches, and every launch is coverage=PARTIAL or NONE — see launched-identities.txt. No launch in this driver can claim COMPLETE coverage of what it started, so no whole operation is reported CONFIRMED_GONE; a direct subject's own disposition is reported separately and is never summed into one." + record "LAUNCHED_IDENTITIES $subj_n direct subjects across $cov_n launches; actual enforcement and confirmation are in each indexed v2 record. Missing records and PARTIAL/NONE coverage cannot support CLEAR." else record "LAUNCHED_IDENTITIES none recorded — no bounded child was ever launched, or the register could not be written" fi @@ -2035,17 +1875,22 @@ # one happened: LIVE is a process or a listener we SAW, UNREADABLE is a measurement we could # not make. Collapsing them would let an instrument failure be reported as a dirty host, or # a dirty host as an instrument failure. - local residual=CLEAR - if [ "${RESIDUE_COUNT:-UNMEASURED}" != 0 ] && [ "${RESIDUE_COUNT:-UNMEASURED}" != UNMEASURED ]; then residual=LIVE - elif [ "${LISTENER_STATE:-UNMEASURED}" = PRESENT ]; then residual=LIVE - elif [ "${RESIDUE_COUNT:-UNMEASURED}" = UNMEASURED ] || [ "${LISTENER_STATE:-UNMEASURED}" != ABSENT ]; then residual=UNREADABLE + coverage_verdict || true + local residual=UNREADABLE + if [ "${RESIDUE_LIVE:-0}" -gt 0 ] || [ "${LISTENER_STATE:-UNMEASURED}" = PRESENT ]; then residual=LIVE + elif [ "${RESIDUE_COUNT:-UNMEASURED}" != 0 ] && [ "${RESIDUE_COUNT:-UNMEASURED}" != UNMEASURED ]; then residual=LIVE + elif [ "${RESIDUE_COUNT:-UNMEASURED}" = 0 ] && [ "${RESIDUE_UNREADABLE:-0}" = 0 ] \ + && [ "${LISTENER_STATE:-UNMEASURED}" = ABSENT ] \ + && [ "$OPERATION_COVERAGE" = COMPLETE ] && [ "$OPERATION_TERMINATION" = CONFIRMED_GONE ]; then residual=CLEAR fi + echo " operation_coverage=$OPERATION_COVERAGE operation_termination=$OPERATION_TERMINATION" + echo " direct_subject_states=$DIRECT_SUBJECT_STATES (never sufficient for whole-operation clearance)" echo " residual_state=$residual (CLEAR = measured gone; LIVE = seen; UNREADABLE = not measurable, and NOT the same claim)" echo " tracked_process_identities: live=${RESIDUE_LIVE:-0} gone=${RESIDUE_GONE:-0} unreadable=${RESIDUE_UNREADABLE:-0} (re-queried by pid + creation time + executable + command line)" - echo " identity_capture=${CAPTURE_USABLE:-not-attempted} (${CAPTURE_INTEGRITY:-no capture was needed}) -- an incomplete capture can never support CLEAR" + echo " identity_capture=${CAPTURE_USABLE:-not-attempted} (${CAPTURE_INTEGRITY:-no capture was needed}); whole-operation clearance is decided by the separate coverage and confirmation predicates above" echo " cleanup_stop_gate=${CLEANUP_STOP_STATE} daemon_stop_gate=${DAEMON_STOP_STATE:-NOT_REACHED} teardown_dispatch=${TEARDOWN_DISPATCH} removal_pre=${REMOVAL_PRE:-NOT_REACHED} removal_post=${REMOVAL_CHECK:-NOT_REACHED}" echo " rig_daemon_stop_exit=${DAEMON_STOP_RC:-NOT_RUN} listener_after_cleanup=${LISTENER_STATE:-UNMEASURED}" - echo " CLEANUP=$CLEANUP_VERDICT residue=${RESIDUE_COUNT:-UNMEASURED} daemon_stop_refused=${DAEMON_STOP_REFUSED:-0} (IR-122: expected from an spt-hosted session; the residue count is what decides whether anything remains)" + echo " CLEANUP=$CLEANUP_VERDICT residue=${RESIDUE_COUNT:-UNMEASURED} (residue and operation coverage are independent predicates)" echo " experimental_ended=[$EXPERIMENTAL_ENDED]" [ "$D2_T1" = UNAVAILABLE ] && echo " D2_T1=UNAVAILABLE — the seq-001 request named the elevated capture and it did not run. The contract is NOT satisfied and no conclusion may rest on that observation." [ "$CLEANUP_VERDICT" = PENDING ] && echo " CLEANUP=PENDING — residual identities above are the actionable record. A fresh pid-naming authorization is a STOP, not a step." @@ -2262,16 +2107,17 @@ # bracketed; d2_capture.ps1 and d2_validate.py are new. runner-census.ps1 STAYS: shared-host # admission is not workload, and its ancestry-authenticated census is what makes box_gate a # measurement instead of a name filter. -for inst in census.ps1 runner-census.ps1 portfields.ps1 d1_render.py d2_capture.ps1 d2_validate.py launch_bounded.ps1; do - [ -f "$BIN/$inst" ] || die "instrument $BIN/$inst is missing — the seven dependencies must all exist before any runnable claim" +for inst in census.ps1 runner-census.ps1 portfields.ps1 d1_render.py d2_capture.ps1 d2_validate.py launch-v2.ps1 launch-v2.cs launch-record.py run-daemon-ready.ps1 run-daemon-control.py; do + [ -f "$BIN/$inst" ] || die "instrument $BIN/$inst is missing — every dependency must exist before any runnable claim" done sha256sum "$BIN"/census.ps1 "$BIN"/runner-census.ps1 "$BIN"/portfields.ps1 "$BIN"/d1_render.py \ - "$BIN"/d2_capture.ps1 "$BIN"/d2_validate.py "$BIN"/launch_bounded.ps1 \ + "$BIN"/d2_capture.ps1 "$BIN"/d2_validate.py "$BIN"/launch-v2.ps1 "$BIN"/launch-v2.cs \ + "$BIN"/launch-record.py "$BIN"/run-daemon-ready.ps1 "$BIN"/run-daemon-control.py \ > "$R/INSTRUMENTS.sha256" 2>"$R/instruments.err" irc=$? exits "instruments_sha_EXIT=$irc" [ "$irc" -eq 0 ] || die "could not hash the instruments — directory placement is not provenance (C); the hashes must ride the evidence" -record "INSTRUMENTS hashed into INSTRUMENTS.sha256 (7 files)" +record "INSTRUMENTS hashed into INSTRUMENTS.sha256 (11 files)" # SCOPE OF PRIOR INSTRUMENT EXERCISE, recorded accurately (doyle 2026-09-12, extended # 2026-09-13 for the two D2 instruments): # d1_render.py was exercised by LIVE LOCAL EXECUTION against blob c28874ef plus three @@ -2518,7 +2364,7 @@ bounded "d1_${tag}_exec" "$(cur_deadline)" "$R/d1-$tag-snapshot.json" "$R/d1-$tag-snapshot.err" -- \ powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand "$(cat "$R/d1-$tag.b64")" local prc=$? - if [ "$prc" -eq 124 ]; then + if [ "$BOUNDED_STATE" = EXPIRED ]; then # COMPLETION UNVERIFIED, NOT AN ABSENT SNAPSHOT. The partial is preserved and left unread. preserve_unverified "d1_${tag}_exec" "$R/d1-$tag-snapshot.json" record "D1_FAILED($tag) the out-of-band capture did not complete within its bound; its partial output is PRESERVED UNREAD and this run makes no claim about what it contains" @@ -2687,9 +2533,12 @@ 1) die "port $GUARD_PORT already changed before the elevated setup" ;; *) die "the $GUARD_PORT instrument failed before the elevated setup" ;; esac +# The daemon has its own run-scoped job, alive before any autostart-capable dispatch. +run_daemon_start || die "run-scoped daemon did not establish owned readiness; no setup is dispatched" +run_daemon_require_ready || die "autostart-capable setup refused: no current owned daemon" # MARKED BEFORE THE DISPATCH (C): a REFUSED setup can still leave rules behind. mark_state fw_mutation_may_have_occurred "an elevated reconcile is about to be REQUESTED; rules may exist from this moment even if the setup is refused" -mark_state daemon_may_run "serve dispatches through the daemon (ensure_running, serveverb.rs:191); the elevated leg may leave one running" +mark_state daemon_may_run "run-scoped job owns the prestarted daemon; setup may not autostart one" mark_state listener_may_exist "the elevated setup binds a listener on port $PORT" # A TIMESTAMP THIS DRIVER DID NOT TAKE (D2 successor). The setup command and the elevated # capture both run in a leg this driver never enters, so their bounds can only be stamped