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
gates / gates (push) Successful in 6m57s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user