app guides night: R-1089..R-1092 (found writing the guides), R-1088 extended; read_credential.py refuses to overwrite the credentials file (red-proved); STATUS; batch evidence
gates / gates (push) Successful in 6m57s

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-15 23:25:53 +02:00
parent 3eca98f135
commit 08cf15d8cd
129 changed files with 9032 additions and 2 deletions
+11
View File
@@ -1,3 +1,14 @@
## 2026-10-15 (night) — read_credential.py refuses to write over the credentials file (R-1088)
- `read_credential.py KEY ~/.config/credentials` — the credentials path given as the OUTFILE — truncated the operator's
credentials file to one value (CC, the app-guides night). The reader now refuses, before reading or writing, an OUTFILE
that is the credentials file by path, by symlink or by hard link (exit 2, the reason named). The docstring says what
OUTFILE is.
- `test_read_credential.py`: `test_the_outfile_can_never_be_the_credentials_file` (same path, symlink, hard link, and a
separate outfile still accepted). Red-proved: with the guard off, the file is overwritten and 5 checks fail
(`documentation/audits/app-guides-2026-10-15/credential-guard-redproof.txt`). The file itself is NOT restored — that is
the operator's (R-1088).
## 2026-10-15 — golden_currency_gate: the golden is judged by its AGE (`09` §3 decision 250)
- A controller release needs no golden (operator ruling 2026-10-15 11:26). The gate no longer convicts because the newest
+12
View File
@@ -2,6 +2,7 @@
"""Read ONE value out of ~/.config/credentials, correctly, and refuse loudly when it is not.
Run: python3 scripts/read_credential.py <KEY> <OUTFILE> [--credentials PATH]
(OUTFILE is where the value GOES — a temp file. It is refused if it is the credentials file itself.)
WHY THIS FILE EXISTS — it is the third occurrence that earned it.
@@ -88,6 +89,17 @@ def main(argv=None):
ap.add_argument("--expect-length", type=int, default=None,
help="refuse unless the value is exactly this long (a caller-side second opinion)")
args = ap.parse_args(argv)
# THE OUTFILE MUST NOT BE THE CREDENTIALS FILE (2026-10-15 night). `read_credential.py KEY ~/.config/credentials`
# reads like „read KEY from the credentials file" and is the argument order a hurried caller types — and the
# O_TRUNC write below then replaced the whole credentials file with one value: every other line was lost, on the
# operator's DooPlex, by CC. Refused before anything is read or written; the same file through a symlink or a
# second path is the same file. Pinned by test_the_outfile_can_never_be_the_credentials_file.
if os.path.realpath(args.outfile) == os.path.realpath(args.credentials) or (
os.path.exists(args.outfile) and os.path.exists(args.credentials)
and os.path.samefile(args.outfile, args.credentials)):
print("CREDENTIAL READ REFUSED [%s]: the output file IS the credentials file (%s) — writing the value there "
"would erase every other line. Give a temp file as OUTFILE." % (args.key, args.outfile), file=sys.stderr)
return 2
try:
value = read(args.credentials, args.key)
except (CredentialError, OSError) as exc:
+37
View File
@@ -125,9 +125,46 @@ def test_the_value_is_never_printed():
os.unlink(out)
# --- the outfile can never be the credentials file -------------------------------------------------
#
# THE INCIDENT THIS PINS (2026-10-15 night, CC on DooPlex): `read_credential.py HUB_TOKEN ~/.config/credentials`
# — the credentials path given as the OUTFILE — truncated the operator's credentials file to one value.
# RED-PROOF (recorded in the app-guides night's evidence): delete the samefile guard in `main` and the three
# „unchanged" checks below fail.
def test_the_outfile_can_never_be_the_credentials_file():
print("TestCredentialsFileIsNeverTheOutfile")
body = "PASSWORD='abcdefghijklm'\nOTHER='kept'\n"
p = writefile(body)
rcode = rc.main(["PASSWORD", p, "--credentials", p])
with open(p, encoding="utf-8") as fh:
after = fh.read()
check("same path is REFUSED", rcode == 2, "rc=%s" % rcode)
check("same path: the credentials file is unchanged", after == body, repr(after))
link = p + ".link"
os.symlink(p, link)
rcode = rc.main(["PASSWORD", link, "--credentials", p])
with open(p, encoding="utf-8") as fh:
after = fh.read()
check("a symlink to it is REFUSED", rcode == 2, "rc=%s" % rcode)
check("symlink: the credentials file is unchanged", after == body, repr(after))
hard = p + ".hard"
os.link(p, hard)
rcode = rc.main(["PASSWORD", hard, "--credentials", p])
with open(p, encoding="utf-8") as fh:
after = fh.read()
check("a hard link to it is REFUSED", rcode == 2, "rc=%s" % rcode)
check("hard link: the credentials file is unchanged", after == body, repr(after))
# Negative control: a real temp outfile still works.
out = tempfile.mkstemp()[1]
check("a separate outfile is still accepted", rc.main(["PASSWORD", out, "--credentials", p]) == 0)
for f in (p, link, hard, out):
os.unlink(f)
if __name__ == "__main__":
test_r404_credential_length_mismatch_fails_loudly()
test_the_value_is_never_printed()
test_the_outfile_can_never_be_the_credentials_file()
if FAILURES:
print("\nFAILED: %d" % len(FAILURES))
sys.exit(1)