#!/usr/bin/env python3 """Safety scan for Streamliner config where Node is not available (e.g. the Claude API's code execution container). python3 quickcheck.py [--dev] A small subset of check.mjs: every JSON file parses; no secrets (provider keys, private keys, JWT-shaped strings); no card fields outside demo accounts; no plain http:// (localhost allowed with --dev); instructions never in device-visible agents/ files; credentialRef names a key rather than being one. Standard library only. It is NOT the full check: run configcheck (or check.mjs) where Node exists before anything is published. """ import json, os, re, sys SECRETS = [(r"sk-ant-[A-Za-z0-9_-]{8,}", "an Anthropic API key"), (r"\bsk-[A-Za-z0-9]{20,}", "an OpenAI-style key"), (r"\bgsk_[A-Za-z0-9]{20,}", "a Groq key"), (r"\bAKIA[0-9A-Z]{16}\b", "an AWS access key"), (r"-----BEGIN [A-Z ]*PRIVATE KEY-----", "a private key"), (r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}", "a JWT (a bearer token)")] LOCAL = re.compile(r"^http://(localhost|127\.0\.0\.1|\[::1\])(:|/|$)") errors = [] def scan(f, v, path, dev): if isinstance(v, str): for rx, what in SECRETS: if re.search(rx, v): errors.append(f"{f} {path}: looks like {what}; config never holds keys (use credentialRef)") if v.startswith("http://") and not (dev and LOCAL.match(v)): errors.append(f"{f} {path}: plain http:// URL; use https://") elif isinstance(v, list): for i, x in enumerate(v): scan(f, x, f"{path}/{i}", dev) elif isinstance(v, dict): for k, x in v.items(): scan(f, x, f"{path}/{k}", dev) def load(p, name): try: with open(p, encoding="utf-8") as fh: return json.load(fh) except Exception as e: # noqa: BLE001 errors.append(f"{name}: not valid JSON ({e})") return None def main(): args = [a for a in sys.argv[1:] if not a.startswith("--")] dev = "--dev" in sys.argv if not args or not os.path.exists(args[0]): print(__doc__) sys.exit(2) target = args[0] if os.path.isfile(target): s = load(target, os.path.basename(target)) if s is not None: scan(os.path.basename(target), s, "", dev) if not s.get("name"): errors.append("name is required") ref = (s.get("model") or {}).get("credentialRef") if ref and not ref.startswith("vault:"): errors.append("/model/credentialRef: personal streamlines name only vault:default or vault:") else: files = {} for d, _, names in os.walk(target): for n in names: if n.endswith(".json"): rel = os.path.relpath(os.path.join(d, n), target).replace(os.sep, "/") j = load(os.path.join(d, n), rel) if j is not None: files[rel] = j scan(rel, j, "", dev) tenant = files.get("tenant.json") or {} demo = tenant.get("mode") == "demo" for rel, j in files.items(): if rel.startswith("agents/") and "instructions" in j: errors.append(f"{rel}: instructions belong in private/agents/, never in a device-visible file") if rel.startswith("private/agents/"): ref = (j.get("model") or {}).get("credentialRef", "") if re.match(r"^(sk-|gsk_)", ref): errors.append(f"{rel} /model/credentialRef: must name a key, not be one") if rel.startswith("forms/") and not demo: for i, fld in enumerate(j.get("fields") or []): if fld.get("type") in ("cardNumber", "expiry", "cvc"): errors.append(f"{rel} /fields/{i}: card fields exist only for demo accounts (use a hosted page)") print("quickcheck: SAFETY SUBSET only; run the full configcheck before publishing") for e in errors: print("error ", e) print("ok" if not errors else f"{len(errors)} error(s)") sys.exit(1 if errors else 0) main()