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))