CC secrets build: consumers switched to sec, S19 age key in the recovery runbooks, evidence (R-1118, R-1122)
gates / gates (push) Successful in 6m44s

Build skill (hub token, ISO publish), fb_probe (token from `sec run` env, tests red-proved), secrets.md (Resend:
pipe inside sec run), bench README, read_credential docstring, 12-agent-tooling §5, workspace CLAUDE secrets line.
Evidence: audits/cc-creds-build-2026-10-17/ (tests, red-proofs, 36-row child-session proof both launch modes,
install, fresh-session proof, import names+lengths, history sweep names).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-17 13:33:07 +02:00
parent 498b9139cf
commit ba40c0a25b
39 changed files with 681 additions and 39 deletions
+14
View File
@@ -1,3 +1,17 @@
## 2026-10-17 — the CC secret broker `sec` replaces `~/.config/credentials` for CC (R-1118, R-1122)
The tool itself lives in the private repo `admin/cc-secrets` (its own CHANGELOG). Here: the consumers.
- `facebook/fb_probe.py`: the token comes from the environment when run as `sec run FACEBOOK_API -- python3 fb_probe.py …`,
with the same shape checks; the old file stays a fallback until it retires. Tests `TokenFrom` (3), red-proved (the
environment ignored → 2 errors).
- `read_credential.py`: docstring points at `sec`; behaviour unchanged.
- `bench/README.md`: the Tuya credentials belong in the store (they are not in it yet, nor in the old file).
- Docs, not scripts: `skills/felhom-build-deploy/SKILL.md` (hub token and ISO publish through `sec run`, values never on a
command line — both recipes proven 2026-10-17: a local server saw the token's hash, a read-only `lsf R2:felhom-iso`
listed the bucket), `runbooks/secrets.md` (Resend recipes; the pipe into `kubectl apply` must be INSIDE `sec run`),
`runbooks/total-loss-of-dooplex.md` + `break-glass-sheet.md` (S19, the age key — where, never what),
`architecture/12-agent-tooling.md` §5, `runbooks/workspace-CLAUDE.md` (one line: secrets through `sec`).
## 2026-10-17 — CI workflow: a manual proof that the failure mail can be sent (R-1129)
`.gitea/workflows/gates.yml` gains a manual `workflow_dispatch` trigger with one input, `force_alarm` (default false), and
+4 -2
View File
@@ -29,8 +29,10 @@ Setup (done 2026-10-13, `documentation/audits/plugmeter-2026-10-13/`): a user ve
lifetime total. Use watts × time for energy.
When the plug moves address or is paired again: re-run the tinytuya wizard in `~/.config/felhom-bench/`
(credentials `TUYA_ID`, `TUYA_SECRET`, `TUYA_DEVICE_ID` from `~/.config/credentials`, region `eu`) and
update `plug.json`. Its output prints the key — send it to a file, never to a terminal or a log.
(credentials `TUYA_ID`, `TUYA_SECRET`, `TUYA_DEVICE_ID`, region `eu`) and update `plug.json`. Its output prints the
key — send it to a file, never to a terminal or a log. The credentials belong in the CC secret store and are used
through `sec run TUYA_ID,TUYA_SECRET,TUYA_DEVICE_ID -- …` (`admin/cc-secrets`); **they are not in the store yet** —
the old `~/.config/credentials` did not hold them either on 2026-10-17 (R-1118 build).
## plugswitch.py — switch the N100's plug (`on`, `off`, `status`)
+24 -11
View File
@@ -14,7 +14,8 @@ Usage: python3 scripts/facebook/fb_probe.py [-v] [--version v26.0] [--evidence
There is deliberately NO "post for real" sub-command: real posting belongs to the skill written
from this spike's findings (audits/SPIKE-facebook-page-api-2026-10-08.md).
SECRETS. The token comes ONLY from ~/.config/credentials (key FACEBOOK_API); no flag takes one.
SECRETS. The token comes from the CC secret store — run `sec run FACEBOOK_API -- python3 fb_probe.py …` — or,
until the old file retires, from ~/.config/credentials (key FACEBOOK_API); no flag takes one.
It travels as `Authorization: Bearer`, never in a logged URL (the one query-string carrier,
debug_token's input_token, is never printed: -v prints the path without its query). Every saved
response passes redact(): every `access_token` key is dropped and any string carrying a held token
@@ -78,6 +79,17 @@ def vlog(msg):
# ---------------------------------------------------------------- the key (R-453)
def check_value(value):
"""Assert the shape of a Meta token: no quote, no whitespace, starts with EAA."""
if any(q in value for q in ("'", '"')):
raise CredentialError("value still contains a quote character")
if any(c.isspace() for c in value):
raise CredentialError("value contains whitespace")
if not value.startswith("EAA"):
raise CredentialError("value does not start with EAA (starts %r)" % value[:3])
return value
def load_key(path, key=KEY):
"""Return the value of `key`. Accepts an optional `export ` prefix; ONE matching quote pair is
stripped by read_credential.unwrap. Then asserts: no quote, no whitespace, starts with EAA."""
@@ -87,17 +99,18 @@ def load_key(path, key=KEY):
if s.startswith("export "):
s = s[len("export "):].lstrip()
if s.startswith(key + "="):
value = unwrap(s[len(key) + 1:].strip())
if any(q in value for q in ("'", '"')):
raise CredentialError("value still contains a quote character")
if any(c.isspace() for c in value):
raise CredentialError("value contains whitespace")
if not value.startswith("EAA"):
raise CredentialError("value does not start with EAA (starts %r)" % value[:3])
return value
return check_value(unwrap(s[len(key) + 1:].strip()))
raise CredentialError("key %r not present in %s" % (key, path))
def token_from(env, path, key=KEY):
"""The token: from the environment when `sec run FACEBOOK_API -- python3 fb_probe.py …` put it there (the
CC secret store, cc-secrets ARCHITECTURE.md); otherwise from the old credentials file, until that file retires."""
if key in env:
return check_value(env[key]), "environment (sec run)"
return load_key(path, key), path
# ---------------------------------------------------------------- redaction
def redact(obj, secrets=None):
@@ -569,12 +582,12 @@ def main(argv=None):
a = build_parser().parse_args(argv)
VERBOSE = a.v
try:
token = load_key(a.credentials)
token, source = token_from(os.environ, a.credentials)
except (CredentialError, OSError) as e:
log("KEY REFUSED [%s]: %s" % (KEY, e))
return 2
SECRETS.append(token)
log("key %s: %d chars, starts %s" % (KEY, len(token), token[:3]))
log("key %s: %d chars, starts %s, from %s" % (KEY, len(token), token[:3], source))
# write-test re-derives A and B into its own sub-directory, so the read run's files stay as they were
sub = {"read": "", "write-test": "write-test"}.get(a.cmd, "first-post")
g = Graph(a.version, a.evidence if not sub else os.path.join(a.evidence, sub))
+21
View File
@@ -54,6 +54,27 @@ class LoadKey(unittest.TestCase):
self.load("FACEBOOK_APIX='%s'\n" % FAKE)
class TokenFrom(unittest.TestCase):
"""`sec run FACEBOOK_API -- …` puts the token in the environment; the environment wins over the file."""
def test_environment_wins_and_is_checked(self):
tok, src = fb_probe.token_from({"FACEBOOK_API": "EAAfromenv"}, "/nonexistent")
self.assertEqual((tok, src), ("EAAfromenv", "environment (sec run)"))
def test_environment_value_gets_the_same_shape_checks(self):
for bad in ("'EAAx'", "EAA x", "abc"):
with self.assertRaises(CredentialError):
fb_probe.token_from({"FACEBOOK_API": bad}, "/nonexistent")
def test_without_the_environment_the_file_is_read(self):
with tempfile.NamedTemporaryFile("w", delete=False) as fh:
fh.write("FACEBOOK_API='EAAfromfile'\n")
try:
self.assertEqual(fb_probe.token_from({}, fh.name)[0], "EAAfromfile")
finally:
os.unlink(fh.name)
class Redact(unittest.TestCase):
def test_access_token_key_dropped_everywhere(self):
out = fb_probe.redact({"data": [{"id": "1", "access_token": "zzz"}], "access_token": "y"}, secrets=[])
+3
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
"""Read ONE value out of ~/.config/credentials, correctly, and refuse loudly when it is not.
SUPERSEDED 2026-10-17 by `sec` (the CC secret store, `admin/cc-secrets` ARCHITECTURE.md): use
`sec run NAME -- <command>`. This reader keeps working until the old file retires (R-1118 follow-up).
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.)