# ═══════════════════════════════════════════════════════════════════ # ✦ T L A M A T I N I ✦ — "one who knows" # # Created by Angela López Mendoza · @angelahack1 # Developer · Architect · Creator of Tlamatini # # Every line of this file was written by Angela López Mendoza. # ═══════════════════════════════════════════════════════════════════ # Tlamatini Author Banner — do not remove (releases scrub the name automatically) """ regen_secrets.py — toggle Tlamatini config files between two modes: --mode push-able Replace every secret value with a generic placeholder like "" so the repo can be pushed to GitHub without tripping push-protection. --mode keyed Read real values from `data.keys` and write them back into the same files for local development. Targeted files (resolved relative to this script): Tlamatini/agent/config.json ANTHROPIC_API_KEY (top + acpx.agents.claude.env) GEMINI_API_KEY (top + acpx.agents.gemini.env) GOOGLE_API_KEY (acpx.agents.gemini.env, alias) OPENAI_API_KEY (acpx.agents.codex.env) ollama_token (top) whatsapp_access_token, whatsapp_phone_number_id (top — Whatsapper / Meta Cloud API) telegram_bot_token (top — Telegrammer / official Telegram Bot API) pdcp_api_key (top — Discoverer / ProjectDiscovery Cloud Platform key; OPTIONAL, secret) Tlamatini/agent/agents/telegrammer/config.yaml telegram.bot_token (official Telegram Bot API token from @BotFather) Tlamatini/agent/agents/whatsapper/config.yaml whatsapp.phone_number_id, whatsapp.access_token, whatsapp.verify_token (Meta WhatsApp Cloud API) Tlamatini/agent/agents/teletlamatini/config.yaml telegram.api_id, telegram.api_hash, telegram.bot_token, password, tlamatini.username, tlamatini.password Tlamatini/agent/agents/emailer/config.yaml smtp.username, smtp.password (Gmail app password from https://myaccount.google.com/apppasswords) Tlamatini/agent/agents/recmailer/config.yaml imap.username, imap.password (Gmail app password from https://myaccount.google.com/apppasswords) Tlamatini/agent/agents/discoverer/config.yaml pdcp_api_key (ProjectDiscovery Cloud Platform key from https://cloud.projectdiscovery.io; OPTIONAL — lifts cvemap/vulnx rate limits, enables nuclei -ai / cloud upload) YAML files are edited line-by-line so all comments survive intact. The JSON file is round-tripped through json.load/dump (its existing format already uses indent=2 and pseudo "_section_*" comment-keys that survive verbatim). All writes are atomic (write to .tmp, then os.replace). Usage: python regen_secrets.py --mode push-able python regen_secrets.py --mode keyed python regen_secrets.py --mode keyed --keys-file other.keys python regen_secrets.py --mode keyed --dry-run """ from __future__ import annotations import argparse import json import os import re import sys from pathlib import Path from typing import Any, Dict, List, Tuple REPO_ROOT = Path(__file__).resolve().parent CONFIG_JSON = REPO_ROOT / "Tlamatini" / "agent" / "config.json" TELEGRAMMER_YAML = REPO_ROOT / "Tlamatini" / "agent" / "agents" / "telegrammer" / "config.yaml" WHATSAPPER_YAML = REPO_ROOT / "Tlamatini" / "agent" / "agents" / "whatsapper" / "config.yaml" TELETLAMATINI_YAML = REPO_ROOT / "Tlamatini" / "agent" / "agents" / "teletlamatini" / "config.yaml" EMAILER_YAML = REPO_ROOT / "Tlamatini" / "agent" / "agents" / "emailer" / "config.yaml" RECMAILER_YAML = REPO_ROOT / "Tlamatini" / "agent" / "agents" / "recmailer" / "config.yaml" DISCOVERER_YAML = REPO_ROOT / "Tlamatini" / "agent" / "agents" / "discoverer" / "config.yaml" EXTERNAL_MCPS_JSON = REPO_ROOT / "Tlamatini" / "agent" / "external_mcps.json" #: Catalog secrets with a HAND-PICKED data.keys name (nicer than the derived one). #: Everything else secret-ish is handled generically — see ``patch_external_mcps_json``. #: (server key, env field, data.keys name) EXTERNAL_MCP_SECRET_RULES: List[Tuple[str, str, str]] = [ ("octocode", "GITHUB_TOKEN", "OCTOCODE_GITHUB_TOKEN"), ("Snyk Security Scanner", "API_KEY", "SNYK_API_KEY"), ] #: A field name containing any of these is treated as a secret. #: NOTE "pat" is deliberately ABSENT: it matches "MEMORY_FILE_PATH" (PATH!), which #: turned a harmless file path into a fake secret — it got scrubbed to a #: placeholder and vaulted, and `keyed` would then stamp the build machine's own #: path onto someone else's install. Real GitHub PATs are named *_TOKEN anyway. _SECRETISH_FIELD_PARTS = ( "token", "key", "secret", "password", "passwd", "auth", "bearer", "credential", "apikey", ) #: ...unless the field name also contains one of THESE, which always WINS. #: A location is not a credential: `KEY_FILE`, `SECRET_PATH` and `AUTH_URL` name #: where something lives, and redacting them breaks a working config while #: protecting nothing. _NEVER_SECRET_FIELD_PARTS = ( "path", "file", "dir", "folder", "url", "uri", "host", "port", "endpoint", "region", "profile", "timeout", "version", ) def placeholder(name: str) -> str: """Generic, identifiable, non-secret-looking placeholder string.""" return f"<{name} goes here>" def parse_keys_file(path: Path) -> Dict[str, str]: """Parse a `KEY=VALUE` file. Comments (`#...`) and blank lines ignored.""" if not path.exists(): raise FileNotFoundError( f"keys file not found: {path}\n" f"Create it (see data.keys.example) before running --mode keyed." ) out: Dict[str, str] = {} for raw_lineno, raw in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), 1): line = raw.strip() if not line or line.startswith("#"): continue if "=" not in line: print(f"WARN {path.name}:{raw_lineno} no '=' — skipped: {line!r}", file=sys.stderr) continue k, v = line.split("=", 1) out[k.strip()] = v.strip() return out def atomic_write(path: Path, text: str) -> None: tmp = path.with_suffix(path.suffix + ".tmp") tmp.write_text(text, encoding="utf-8") os.replace(tmp, path) # ──────────────────────────────────────────────────────────────────────── # JSON: config.json # ──────────────────────────────────────────────────────────────────────── def resolve_value(mode: str, keys: Dict[str, str], data_key: str, placeholder_label: str | None = None) -> str: if mode == "push-able": return placeholder(placeholder_label or data_key) # keyed return keys.get(data_key, "") def patch_config_json(mode: str, keys: Dict[str, str], dry_run: bool) -> List[str]: if not CONFIG_JSON.exists(): return [f"SKIP {CONFIG_JSON} (missing)"] data: Dict[str, Any] = json.loads(CONFIG_JSON.read_text(encoding="utf-8")) changes: List[str] = [] def set_top(json_key: str, data_key: str) -> None: new_val = resolve_value(mode, keys, data_key, placeholder_label=json_key) if data.get(json_key) != new_val: changes.append(f" config.json[{json_key!r}] <- {data_key}") data[json_key] = new_val def set_acpx_env(agent_id: str, env_key: str, data_key: str) -> None: acpx = data.setdefault("acpx", {}) agents = acpx.setdefault("agents", {}) agent = agents.get(agent_id) if not isinstance(agent, dict): return # leave alone if the agent block doesn't exist env = agent.setdefault("env", {}) new_val = resolve_value(mode, keys, data_key, placeholder_label=env_key) if env.get(env_key) != new_val: changes.append(f" config.json[acpx.agents.{agent_id}.env.{env_key}] <- {data_key}") env[env_key] = new_val set_top("ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY") set_top("GEMINI_API_KEY", "GEMINI_API_KEY") set_top("ollama_token", "OLLAMA_TOKEN") set_top("whatsapp_access_token", "WHATSAPP_ACCESS_TOKEN") set_top("whatsapp_phone_number_id", "WHATSAPP_PHONE_NUMBER_ID") set_top("telegram_bot_token", "TELEGRAM_BOT_TOKEN") set_top("pdcp_api_key", "PDCP_API_KEY") set_acpx_env("claude", "ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY") set_acpx_env("gemini", "GEMINI_API_KEY", "GEMINI_API_KEY") set_acpx_env("gemini", "GOOGLE_API_KEY", "GOOGLE_API_KEY") set_acpx_env("codex", "OPENAI_API_KEY", "OPENAI_API_KEY") serialized = json.dumps(data, indent=2, ensure_ascii=False) + "\n" if not dry_run: atomic_write(CONFIG_JSON, serialized) return [f"WROTE {CONFIG_JSON.relative_to(REPO_ROOT)}"] + changes # ──────────────────────────────────────────────────────────────────────── # YAML: surgical line-level edits (preserves every comment + ordering) # ──────────────────────────────────────────────────────────────────────── # Each rule: (yaml_path, data_key) where yaml_path is a list of nested keys. TELEGRAMMER_RULES: List[Tuple[List[str], str]] = [ (["telegram", "bot_token"], "TELEGRAM_BOT_TOKEN"), # api_id / api_hash power the Telegram USER session (the only way a @username # can be resolved at runtime). They reuse the same Telegram app credentials as # TeleTlamatini. With them set, Telegrammer auto-opens a one-time login window # on the first @username send, then sends. (["telegram", "api_id"], "TELETLAMATINI_API_ID"), (["telegram", "api_hash"], "TELETLAMATINI_API_HASH"), # session_string = a one-time-login Telegram USER session, BAKED into a keyed build # so a fresh install can message any @username on the FIRST send with NO login window. # push-able mode redacts it to a placeholder, so a distributed build ships no session. (["telegram", "session_string"], "TELEGRAM_SESSION_STRING"), ] WHATSAPPER_RULES: List[Tuple[List[str], str]] = [ (["whatsapp", "phone_number_id"], "WHATSAPP_PHONE_NUMBER_ID"), (["whatsapp", "access_token"], "WHATSAPP_ACCESS_TOKEN"), (["whatsapp", "verify_token"], "WHATSAPP_VERIFY_TOKEN"), ] TELETLAMATINI_RULES: List[Tuple[List[str], str]] = [ (["telegram", "api_id"], "TELETLAMATINI_API_ID"), (["telegram", "api_hash"], "TELETLAMATINI_API_HASH"), (["telegram", "bot_token"], "TELETLAMATINI_BOT_TOKEN"), (["password"], "TELETLAMATINI_PASSWORD"), (["tlamatini", "username"], "TLAMATINI_USERNAME"), (["tlamatini", "password"], "TLAMATINI_PASSWORD"), ] EMAILER_RULES: List[Tuple[List[str], str]] = [ (["smtp", "username"], "EMAILER_USERNAME"), (["smtp", "password"], "EMAILER_PASSWORD"), ] RECMAILER_RULES: List[Tuple[List[str], str]] = [ (["imap", "username"], "RECMAILER_USERNAME"), (["imap", "password"], "RECMAILER_PASSWORD"), ] DISCOVERER_RULES: List[Tuple[List[str], str]] = [ (["pdcp_api_key"], "PDCP_API_KEY"), ] _KEY_RE = re.compile(r"^(?P\s*)(?P[A-Za-z_][A-Za-z0-9_]*)\s*:(?P.*)$") def _force_quote(value: str) -> str: """Emit one lossless YAML double-quoted string, including control characters.""" # JSON string escaping is also valid in YAML. Keep non-BMP Unicode intact; # YAML additionally treats these Unicode separators as line breaks. return (json.dumps(value, ensure_ascii=False) .replace("\u0085", "\\u0085") .replace("\u2028", "\\u2028") .replace("\u2029", "\\u2029")) def _quote_value(value: str, *, force_quote: bool = False) -> str: """Keep credentials as strings; YAML implicit types must not change them. The keyword remains accepted for existing callers. All credential strings now receive the same lossless quoting, including numeric-looking values. The explicitly numeric Telegram api_id field is handled by its path below. """ return _force_quote(value) def _yaml_inline_comment(rest: str) -> str: """Return a real comment, never a hash inside a quoted credential.""" start = len(rest) - len(rest.lstrip()) quote = rest[start] if start < len(rest) and rest[start] in "\"'" else "" index = start + 1 if quote else start while index < len(rest): char = rest[index] if quote == '"' and char == "\\": index += 2 continue if quote and char == quote: if quote == "'" and index + 1 < len(rest) and rest[index + 1] == "'": index += 2 continue quote = "" elif not quote and char == "#" and (index == 0 or rest[index - 1].isspace()): begin = index while begin > 0 and rest[begin - 1].isspace(): begin -= 1 return rest[begin:] index += 1 return "" def _patch_yaml_text(text: str, rules: List[Tuple[List[str], str]], mode: str, keys: Dict[str, str], file_label: str, *, force_quote_passwords: bool = False) -> Tuple[str, List[str]]: """ Walk `text` line-by-line and rewrite the value of each `yaml_path`. Indent-based path tracking is good enough for our hand-written YAML files (no anchors, no flow-style nested mappings, no sequences-of-mappings). When `force_quote_passwords=True`, any rule whose YAML path ends in `password` is rendered as a YAML double-quoted scalar regardless of value shape. This is enabled for the Emailer and RecMailer rule sets so the on-disk password line always reads `password: "<...>"` — both for the push-able placeholder and for the keyed real value. All other credential strings are quoted too; only the explicitly numeric Telegram api_id field retains an integer value. """ lines = text.splitlines(keepends=False) # Pre-compute target last-key + parent-key chain by indent depth. # indent_stack: list of (indent, key) describing the current path. indent_stack: List[Tuple[int, str]] = [] rule_index_by_full_path: Dict[str, Tuple[List[str], str]] = { ".".join(path): (path, dk) for path, dk in rules } matched_paths: set[str] = set() changes: List[str] = [] out: List[str] = [] for line in lines: m = _KEY_RE.match(line) if not m: out.append(line) continue indent = len(m.group("indent")) key = m.group("key") rest = m.group("rest") # leading colon already consumed # Pop any deeper-or-equal-indent frames before recording this one. while indent_stack and indent_stack[-1][0] >= indent: indent_stack.pop() indent_stack.append((indent, key)) full_path = ".".join(k for _, k in indent_stack) rule = rule_index_by_full_path.get(full_path) if rule is None: out.append(line) continue path, data_key = rule new_val = resolve_value(mode, keys, data_key) # When `force_quote_passwords` is on (Emailer / RecMailer rule sets), # any path ending in `password` is rendered as a YAML double-quoted # scalar regardless of value shape. Push-able placeholder and keyed # real value both land as `password: "<...>"`. force_quote = ( force_quote_passwords and bool(path) and path[-1].lower() == "password" ) formatted = (new_val if path[-1] == "api_id" and re.fullmatch(r"[0-9]+", new_val) else _quote_value(new_val, force_quote=force_quote)) # Preserve any inline comment after the value. comment = _yaml_inline_comment(rest) new_line = f"{m.group('indent')}{key}: {formatted}{comment}" if new_line != line: changes.append(f" {file_label}:{full_path} <- {data_key}") out.append(new_line) matched_paths.add(full_path) # Note any rule that never fired — the caller sees this in the change log. for full_path in rule_index_by_full_path: if full_path not in matched_paths: changes.append(f" WARN {file_label}:{full_path} not present in file (skipped)") final = "\n".join(out) if text.endswith("\n") and not final.endswith("\n"): final += "\n" return final, changes def patch_yaml(path: Path, rules: List[Tuple[List[str], str]], mode: str, keys: Dict[str, str], dry_run: bool, *, force_quote_passwords: bool = False) -> List[str]: if not path.exists(): return [f"SKIP {path} (missing)"] original = path.read_text(encoding="utf-8") new_text, changes = _patch_yaml_text( original, rules, mode, keys, file_label=str(path.relative_to(REPO_ROOT)).replace("\\", "/"), force_quote_passwords=force_quote_passwords, ) if not dry_run: atomic_write(path, new_text) return [f"WROTE {path.relative_to(REPO_ROOT)}"] + changes # ──────────────────────────────────────────────────────────────────────── # CLI # ──────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────── # JSON: external_mcps.json (the External ▸ MCPs catalog) # ──────────────────────────────────────────────────────────────────────── # # The catalog is TRACKED in git (Angela, 2026-08-15) so the repo shows every # server Tlamatini knows about — but an MCP catalog's whole value is the # secrets in its `env` blocks, so it gets the same placeholder treatment # config.json already has. # # LOSSLESS CONTRACT: `push-able` AUTO-VAULTS any secret it is about to redact # into data.keys (which is gitignored) FIRST. That is what makes the round trip # safe for a server the rule table has never heard of: scrub → push → `keyed` # restores it exactly. A scrub that silently destroyed an unrecognised token # would be far worse than the leak it prevents. def _extmcp_clean(text: str) -> str: return "".join(ch if ch.isalnum() else "_" for ch in str(text)).strip("_").upper() def _extmcp_data_key(server: str, field: str) -> str: """Stable data.keys name for an UNDECLARED catalog secret.""" return f"EXTMCP_{_extmcp_clean(server)}_{_extmcp_clean(field)}" def _extmcp_is_secretish(field: str) -> bool: lowered = str(field).lower() if any(part in lowered for part in _NEVER_SECRET_FIELD_PARTS): return False # a LOCATION is not a credential — denylist wins return any(part in lowered for part in _SECRETISH_FIELD_PARTS) def _extmcp_is_placeholder(value: Any) -> bool: text = str(value or "").strip() return (not text) or (text.startswith("<") and text.endswith(">")) def patch_external_mcps_json(mode: str, keys: Dict[str, str], dry_run: bool, keys_path: Path) -> List[str]: if not EXTERNAL_MCPS_JSON.exists(): return [f"SKIP {EXTERNAL_MCPS_JSON} (missing)"] try: data: Dict[str, Any] = json.loads(EXTERNAL_MCPS_JSON.read_text(encoding="utf-8-sig")) except Exception as exc: return [f"SKIP {EXTERNAL_MCPS_JSON} (unreadable: {exc})"] servers = data.get("mcpServers") if not isinstance(servers, dict): return [f"SKIP {EXTERNAL_MCPS_JSON} (no mcpServers map)"] # The vault is read regardless of mode: `keyed` restores FROM it, and # `push-able` needs it to know which secrets are already stored. vault: Dict[str, str] = dict(keys) try: vault.update({k: v for k, v in parse_keys_file(keys_path).items() if v}) except Exception: pass declared = {(s, f): dk for s, f, dk in EXTERNAL_MCP_SECRET_RULES} changes: List[str] = [] additions: Dict[str, str] = {} touched = False for server_key in sorted(servers.keys()): spec = servers.get(server_key) if not isinstance(spec, dict): continue env = spec.get("env") if not isinstance(env, dict): continue for field in sorted(env.keys()): if not _extmcp_is_secretish(field): continue value = env.get(field) data_key = declared.get((server_key, field)) or _extmcp_data_key(server_key, field) if mode == "push-able": if not _extmcp_is_placeholder(value) and vault.get(data_key) != value: additions[data_key] = str(value) # vault BEFORE redacting new_val = placeholder(data_key) else: new_val = vault.get(data_key, "") if not new_val: changes.append( f" WARN external_mcps.json[{server_key!r}].env.{field}: no " f"{data_key} in {keys_path.name} — left as-is" ) continue if env.get(field) != new_val: env[field] = new_val touched = True changes.append( f" external_mcps.json[{server_key!r}].env.{field} <- {data_key}" ) if additions: changes.append( f" AUTO-VAULTED {len(additions)} catalog secret(s) into {keys_path.name}: " + ", ".join(sorted(additions)) ) if not dry_run: with open(keys_path, "a", encoding="utf-8") as fh: fh.write("\n# --- auto-vaulted from external_mcps.json by regen_secrets.py ---\n") for name in sorted(additions): fh.write(f"{name}={additions[name]}\n") if touched and not dry_run: atomic_write(EXTERNAL_MCPS_JSON, json.dumps(data, indent=2, ensure_ascii=False) + "\n") return changes or [f"OK {EXTERNAL_MCPS_JSON.name} (no secret changes needed)"] def main() -> int: parser = argparse.ArgumentParser( description="Toggle Tlamatini config files between push-able and keyed mode.", ) parser.add_argument( "--mode", required=True, choices=("push-able", "keyed"), help="push-able = generic placeholders; keyed = real values from data.keys", ) parser.add_argument( "--keys-file", default=str(REPO_ROOT / "data.keys"), help="Path to the KEY=VALUE secrets file (default: data.keys next to script).", ) parser.add_argument( "--dry-run", action="store_true", help="Print what would change without writing any files.", ) args = parser.parse_args() if args.mode == "keyed": keys = parse_keys_file(Path(args.keys_file)) else: keys = {} # push-able doesn't need any real values print(f"== Tlamatini regen_secrets.py — mode={args.mode}" f"{' (dry-run)' if args.dry_run else ''} ==") reports: List[List[str]] = [ patch_config_json(args.mode, keys, args.dry_run), patch_yaml(TELEGRAMMER_YAML, TELEGRAMMER_RULES, args.mode, keys, args.dry_run), patch_yaml(WHATSAPPER_YAML, WHATSAPPER_RULES, args.mode, keys, args.dry_run), patch_yaml(TELETLAMATINI_YAML, TELETLAMATINI_RULES, args.mode, keys, args.dry_run), patch_yaml(EMAILER_YAML, EMAILER_RULES, args.mode, keys, args.dry_run, force_quote_passwords=True), patch_yaml(RECMAILER_YAML, RECMAILER_RULES, args.mode, keys, args.dry_run, force_quote_passwords=True), patch_yaml(DISCOVERER_YAML, DISCOVERER_RULES, args.mode, keys, args.dry_run), patch_external_mcps_json(args.mode, keys, args.dry_run, Path(args.keys_file)), ] for block in reports: for line in block: print(line) print("== done ==") if args.mode == "push-able": print("Reminder: confirm with `git diff`, then commit + push.") print("After pushing, run `python regen_secrets.py --mode keyed` to restore real values.") return 0 if __name__ == "__main__": raise SystemExit(main())