docs(framing): make revision 13's consumer algorithm total
Close the last three approval blockers in revision 13. The shell algorithm now removes and disarms its guard-local capture on the safe path, selects the expected token through an explicit status case before any set-u-sensitive use, and sends every out-of-range status straight to boundary error. The load-bearing `|| status=$?` remains in place. A mechanical set-eu exercise covers safe, ignored, validated error, status 126 and capture-creation failure; every path returns the specified public status and leaves no capture residue. The conformance accounting now distinguishes the 45 cases shared by the shell and Rust validators from Rust's additional no-status spawn-error case. A shell exec failure necessarily becomes a shell status, so it cannot exercise that Rust-only input. The text also stops claiming that Rust uses file-backed capture: only the shell needs files, while Rust compares Command::output byte vectors directly. No remedy implementation. PR #241 remains blocked until revision 13 is recorded approved.
This commit is contained in:
parent
a546a85476
commit
5dece3e271
|
|
@ -777,9 +777,11 @@ every other status → `(2, error)`. Consumers do not parse the inner
|
||||||
**The consumer flow is pair-validation, not status inspection:**
|
**The consumer flow is pair-validation, not status inspection:**
|
||||||
|
|
||||||
1. Run the helper, capturing **status**, **stdout** and **stderr**
|
1. Run the helper, capturing **status**, **stdout** and **stderr**
|
||||||
separately. A spawn failure — the helper missing, not executable, or
|
separately. The two process boundaries differ and the contract keeps
|
||||||
unrunnable for any reason — is `error` immediately, with **no
|
that difference explicit: the shell consumer always receives a shell
|
||||||
status to inspect at all**.
|
status, including when `exec` fails; Rust's `Command` instead returns a
|
||||||
|
spawn error with **no status to inspect at all**. Either route can
|
||||||
|
produce boundary `error`, but they are not the same input.
|
||||||
2. **Compare stdout as BYTES against an exact grammar. There is no
|
2. **Compare stdout as BYTES against an exact grammar. There is no
|
||||||
trimming.**
|
trimming.**
|
||||||
|
|
||||||
|
|
@ -811,17 +813,20 @@ every other status → `(2, error)`. Consumers do not parse the inner
|
||||||
(verified). So `TOKEN NUL` validates in one shell and not another
|
(verified). So `TOKEN NUL` validates in one shell and not another
|
||||||
— a contract two consumers cannot implement identically.
|
— a contract two consumers cannot implement identically.
|
||||||
|
|
||||||
**Both consumers therefore capture to files and compare bytes.**
|
**The shell consumer therefore captures to files and compares bytes.**
|
||||||
|
Rust already receives byte vectors from `Command::output()` and compares
|
||||||
|
those directly; it does not need or create capture files.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# The guard runs BEFORE the gate's own temporary roots exist, so it
|
# The guard runs BEFORE the gate's own temporary roots exist, so it
|
||||||
# creates and owns its capture directory --- and arms the cleanup
|
# creates and owns its capture directory --- and arms the cleanup
|
||||||
# BEFORE the helper can be invoked, so no path can leave residue.
|
# BEFORE the helper can be invoked, so no path can leave residue.
|
||||||
capture=$(mktemp -d "${TMPDIR:-/tmp}/pmacs-sigint.XXXXXX") || {
|
capture=$(mktemp -d "${TMPDIR:-/tmp}/pmacs-sigint.XXXXXX") || {
|
||||||
echo 'gate: could not create the SIGINT guard capture directory' >&2
|
echo 'gate: could not create the SIGINT guard capture directory (status=unavailable token=missing)' >&2
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
trap 'rm -rf "$capture"' EXIT HUP INT TERM
|
cleanup_sigint_capture() { rm -rf "$capture"; }
|
||||||
|
trap cleanup_sigint_capture EXIT HUP INT TERM
|
||||||
|
|
||||||
# `|| status=$?` IS LOAD-BEARING under `set -eu`: a bare invocation
|
# `|| status=$?` IS LOAD-BEARING under `set -eu`: a bare invocation
|
||||||
# dies at the helper's non-zero exit and never reaches the
|
# dies at the helper's non-zero exit and never reaches the
|
||||||
|
|
@ -830,16 +835,59 @@ every other status → `(2, error)`. Consumers do not parse the inner
|
||||||
status=0
|
status=0
|
||||||
"$helper" >"$capture/out" 2>"$capture/err" || status=$?
|
"$helper" >"$capture/out" 2>"$capture/err" || status=$?
|
||||||
|
|
||||||
printf '%s' "$expected_token" >"$capture/want"
|
# Select an expected token only for public helper statuses. This case
|
||||||
printf '%s\n' "$expected_token" >"$capture/want_lf"
|
# MUST precede any use of expected_token: the gate runs under `set -u`,
|
||||||
if cmp -s "$capture/out" "$capture/want" \
|
# and an out-of-range shell status has no expected token.
|
||||||
|| cmp -s "$capture/out" "$capture/want_lf"
|
expected_token=
|
||||||
then token_ok=1; else token_ok=0; fi
|
case "$status" in
|
||||||
|
0) expected_token=pmacs-sigint-v1:safe ;;
|
||||||
|
1) expected_token=pmacs-sigint-v1:ignored ;;
|
||||||
|
2) expected_token=pmacs-sigint-v1:error ;;
|
||||||
|
esac
|
||||||
|
|
||||||
# On the SAFE path the guard tidies up and disarms, so the gate's own
|
token_ok=0
|
||||||
# later trap installation is undisturbed. Every refusing path exits,
|
if [ -n "$expected_token" ]; then
|
||||||
# and the armed trap removes the directory for it.
|
printf '%s' "$expected_token" >"$capture/want"
|
||||||
# rm -rf "$capture"; trap - EXIT HUP INT TERM
|
printf '%s\n' "$expected_token" >"$capture/want_lf"
|
||||||
|
if cmp -s "$capture/out" "$capture/want" \
|
||||||
|
|| cmp -s "$capture/out" "$capture/want_lf"
|
||||||
|
then token_ok=1; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -s "$capture/out" ]; then
|
||||||
|
token_state=missing
|
||||||
|
elif [ "$token_ok" -eq 1 ]; then
|
||||||
|
token_state=valid
|
||||||
|
else
|
||||||
|
token_state=unexpected
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$status:$token_ok" in
|
||||||
|
0:1)
|
||||||
|
# SAFE is the sole continuing path. Remove the guard-local
|
||||||
|
# directory and disarm its trap BEFORE the gate installs its
|
||||||
|
# later, unrelated cleanup trap.
|
||||||
|
cleanup_sigint_capture
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
;;
|
||||||
|
1:1)
|
||||||
|
cat "$capture/err" >&2
|
||||||
|
printf 'gate: SIGINT guard status=1 token=valid\n' >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
2:1)
|
||||||
|
cat "$capture/err" >&2
|
||||||
|
printf 'gate: SIGINT guard status=2 token=valid\n' >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# The captured stderr is untrusted here and is not surfaced as
|
||||||
|
# the diagnosis. EXIT runs cleanup_sigint_capture.
|
||||||
|
printf 'gate: SIGINT guard boundary error (status=%s token=%s)\n' \
|
||||||
|
"$status" "$token_state" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
```
|
```
|
||||||
|
|
||||||
**The capture directory is guard-local by necessity.** The guard sits
|
**The capture directory is guard-local by necessity.** The guard sits
|
||||||
|
|
@ -850,7 +898,7 @@ every other status → `(2, error)`. Consumers do not parse the inner
|
||||||
honour: a refused run leaves nothing behind.
|
honour: a refused run leaves nothing behind.
|
||||||
|
|
||||||
Files preserve every byte including NUL, `cmp` compares bytes, and
|
Files preserve every byte including NUL, `cmp` compares bytes, and
|
||||||
`status` is the helper's own. Rust does the same by comparing
|
`status` is the helper's own. Rust performs the same comparison on
|
||||||
`out.stdout` against `TOKEN` and `TOKEN + b"\n"`. Neither consumer
|
`out.stdout` against `TOKEN` and `TOKEN + b"\n"`. Neither consumer
|
||||||
trims, and neither routes stdout through a shell variable.
|
trims, and neither routes stdout through a shell variable.
|
||||||
|
|
||||||
|
|
@ -862,7 +910,7 @@ every other status → `(2, error)`. Consumers do not parse the inner
|
||||||
3. Accept **only** these three pairs; every other combination is
|
3. Accept **only** these three pairs; every other combination is
|
||||||
`error`:
|
`error`:
|
||||||
|
|
||||||
| status | normalised stdout | outcome |
|
| status | stdout bytes | outcome |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 0 | `pmacs-sigint-v1:safe` | `safe` |
|
| 0 | `pmacs-sigint-v1:safe` | `safe` |
|
||||||
| 1 | `pmacs-sigint-v1:ignored` | `ignored` |
|
| 1 | `pmacs-sigint-v1:ignored` | `ignored` |
|
||||||
|
|
@ -920,9 +968,10 @@ revision 13 each consumer **independently validates the pair**, in a
|
||||||
different language, so they can now disagree by validating differently.
|
different language, so they can now disagree by validating differently.
|
||||||
Revision 12's claim that they "can never disagree" is withdrawn.
|
Revision 12's claim that they "can never disagree" is withdrawn.
|
||||||
|
|
||||||
What replaces it is a **shared conformance matrix**: both validators
|
What replaces it is a **shared conformance matrix plus one
|
||||||
are exercised against the same generated case set below, and must agree
|
consumer-specific boundary case**. Both validators exercise the shared
|
||||||
on every case.
|
set and must agree on it; Rust alone exercises the no-status spawn-error
|
||||||
|
case that the shell boundary cannot represent.
|
||||||
|
|
||||||
**Two distinct failing outcomes**, which an earlier draft collapsed:
|
**Two distinct failing outcomes**, which an earlier draft collapsed:
|
||||||
|
|
||||||
|
|
@ -977,20 +1026,22 @@ Only **V validates** — `(0,safe)` → `safe`, `(1,ignored)` → `ignored`,
|
||||||
`(2,error)` → `error (validated)`. The other **36** are
|
`(2,error)` → `error (validated)`. The other **36** are
|
||||||
`error (boundary)`.
|
`error (boundary)`.
|
||||||
|
|
||||||
Out-of-band cases, which have no `(status, token)` form:
|
Out-of-band cases and their applicable consumers:
|
||||||
|
|
||||||
| # | case | expected |
|
| # | consumers | case | expected |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| X1 | status 126 with a correct token | `error (boundary)` — status outside 0–2 |
|
| X1 | shell + Rust | status 126 with a correct token | `error (boundary)` — status outside 0–2 |
|
||||||
| X2 | spawn failure (missing / unexecutable helper) | `error (boundary)`, **no status inspected** |
|
| X2 | **Rust only** | spawn failure (missing / unexecutable helper) | `error (boundary)`, **no status inspected** |
|
||||||
| X3 | status 1, empty stdout, stderr = canonical ignored text | `error (boundary)`, and the output **must not** present "SIGINT is ignored" as the diagnosis |
|
| X3 | shell + Rust | status 1, empty stdout, stderr = canonical ignored text | `error (boundary)`, and the output **must not** present "SIGINT is ignored" as the diagnosis |
|
||||||
| X4 | status 0, correct token, plus extra bytes on **stderr** | `safe` — stderr is not consulted for classification |
|
| X4 | shell + Rust | status 0, correct token, plus extra bytes on **stderr** | `safe` — stderr is not consulted for classification |
|
||||||
|
|
||||||
**Truthful total: 42 + 4 = 46 concrete cases.** Earlier drafts said
|
**Truthful totals:** the shared set is **45 cases** — the 42-case
|
||||||
twelve, then twenty-three, then thirty-four; each was a count of a set
|
cross-product plus X1, X3 and X4 — and both validators must agree on all
|
||||||
that had not actually been enumerated. Both validators are exercised
|
45. Rust additionally exercises X2, for **46 distinct cases overall**;
|
||||||
against all forty-six and must agree on every one, including the
|
the shell exercises 45 because an `exec` failure there necessarily
|
||||||
distinction between validated and boundary error.
|
becomes a shell status. Earlier drafts said twelve, then twenty-three,
|
||||||
|
then thirty-four; each was a count of a set that had not actually been
|
||||||
|
enumerated.
|
||||||
|
|
||||||
The two consumers:
|
The two consumers:
|
||||||
|
|
||||||
|
|
@ -1068,7 +1119,7 @@ show:
|
||||||
| helper prints the token to **stderr** instead of stdout | **A1 and A3** — see below |
|
| helper prints the token to **stderr** instead of stdout | **A1 and A3** — see below |
|
||||||
| consumer surfaces child stderr as the diagnosis on a **boundary** failure | **A6b** |
|
| consumer surfaces child stderr as the diagnosis on a **boundary** failure | **A6b** |
|
||||||
| consumer accepts any status 2 regardless of token | **A6c** |
|
| consumer accepts any status 2 regardless of token | **A6c** |
|
||||||
| consumer trims whitespace before comparing | **A6c** via rows 18–19 |
|
| consumer trims whitespace before comparing | the **S/C classes** in the shared 42-case cross-product |
|
||||||
|
|
||||||
**Why that last one maps to A1/A3 and not A2.** With the token on
|
**Why that last one maps to A1/A3 and not A2.** With the token on
|
||||||
stderr, stdout is empty, so *every* outcome becomes boundary `error`.
|
stderr, stdout is empty, so *every* outcome becomes boundary `error`.
|
||||||
|
|
@ -1104,14 +1155,15 @@ show:
|
||||||
`SIGINT is ignored` text on stderr** classifies as boundary `error`
|
`SIGINT is ignored` text on stderr** classifies as boundary `error`
|
||||||
in both consumers, **and neither presents "SIGINT is ignored" as the
|
in both consumers, **and neither presents "SIGINT is ignored" as the
|
||||||
diagnosis** — the child's stderr is omitted or explicitly labelled
|
diagnosis** — the child's stderr is omitted or explicitly labelled
|
||||||
untrusted. Conformance row 23. Without this, A6 is satisfiable in the
|
untrusted. Shared case X3. Without this, A6 is satisfiable in the
|
||||||
classification while being violated in the message the operator
|
classification while being violated in the message the operator
|
||||||
actually reads.
|
actually reads.
|
||||||
- **A6c — exact-pair validation at status 2.** `(2, …:error)` is
|
- **A6c — exact-pair validation at status 2.** `(2, …:error)` is
|
||||||
`error (validated)`; `(2, missing)`, `(2, …:safe)`, `(2, …:ignored)`
|
`error (validated)`; `(2, missing)`, `(2, …:safe)`, `(2, …:ignored)`
|
||||||
and `(2, unknown-version)` are each **boundary** errors. Conformance
|
and `(2, unknown-version)` are each **boundary** errors. The status-2
|
||||||
rows 3, 6, 11, 12, 14. A validator that accepts any status 2
|
slice of the shared cross-product exercises both permitted encodings,
|
||||||
regardless of token must fail this row.
|
every mismatched valid token and every malformed class. A validator
|
||||||
|
that accepts any status 2 regardless of token must fail this row.
|
||||||
- **A6a — the macOS row, SCOPED TO THE GATE.** An unexecutable helper
|
- **A6a — the macOS row, SCOPED TO THE GATE.** An unexecutable helper
|
||||||
on macOS makes `/bin/sh` exit **1 with no token**; the gate must
|
on macOS makes `/bin/sh` exit **1 with no token**; the gate must
|
||||||
classify that as **boundary error → 2**, never `ignored`. Not
|
classify that as **boundary error → 2**, never `ignored`. Not
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue