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
+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=[])