Files
steipete--codexbar/.agents/skills/codexbar/scripts/codexbar
T
2026-07-13 12:22:33 +08:00

322 lines
11 KiB
Python
Executable File

#!/usr/bin/env python3
"""Read-only, bounded CodexBar CLI bridge."""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import signal
import subprocess
import sys
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Sequence
MAX_CAPTURE_BYTES = 1024 * 1024
DEFAULT_TIMEOUT = 120.0
BIN_ENV = "CODEXBAR_BIN"
TIMEOUT_ENV = "CODEXBAR_TIMEOUT"
SKIP_DISCOVERY_ENV = "CODEXBAR_SKIP_DISCOVERY"
SECRET = "<redacted:secret>"
IDENTITY = "<redacted:identity>"
EMAIL = "<redacted:email>"
EMAIL_RE = re.compile(r"\b[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}\b", re.I)
BEARER_RE = re.compile(r"(?i)\bbearer\s+[A-Z0-9._~+/=\-]+")
JWT_RE = re.compile(r"\beyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\b")
LABELED_SECRET_RE = re.compile(
r"(?i)\b(authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|"
r"session(?:id)?|secret|password|passwd|cookie|set-cookie)(\s*[:=]\s*)([^\s,;]+)"
)
WORD_SECRET_RE = re.compile(
r"(?i)\b(token|secret|password|passwd|cookie)\s+([A-Z0-9._~+/=\-]{6,})\b"
)
SECRET_KEY_RE = re.compile(
r"(?i)(authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|"
r"session(?:id)?|secret|password|passwd|cookie)"
)
IDENTITY_KEYS = {
"accountemail",
"accountorganization",
"accountid",
"accountname",
"email",
"organization",
"userid",
"username",
}
# ProviderIdentitySnapshot.providerID is UsageProvider, not an account identifier.
@dataclass(frozen=True)
class Binary:
path: str
source: str
@dataclass(frozen=True)
class Result:
returncode: int
stdout: bytes
stderr: bytes
timed_out: bool
def normalized_key(value: str) -> str:
return re.sub(r"[^a-z0-9]", "", value.lower())
def timeout_seconds() -> float:
try:
return max(1.0, float(os.environ.get(TIMEOUT_ENV, DEFAULT_TIMEOUT)))
except ValueError:
return DEFAULT_TIMEOUT
def safe_path(path: str) -> str:
home = str(Path.home())
if path == home:
return "~"
if path.startswith(home + os.sep):
return "~" + path[len(home) :]
return re.sub(r"^/Users/[^/]+", "/Users/<redacted>", path)
def candidates() -> list[tuple[str, Path]]:
found: list[tuple[str, Path]] = []
override = os.environ.get(BIN_ENV)
if override:
found.append(("env", Path(override).expanduser()))
path_hit = shutil.which("codexbar")
if path_hit:
found.append(("path", Path(path_hit)))
if os.environ.get(SKIP_DISCOVERY_ENV) == "1":
return found
home = Path.home()
found.extend(
[
("homebrew", Path("/opt/homebrew/bin/codexbar")),
("homebrew", Path("/usr/local/bin/codexbar")),
("app", Path("/Applications/CodexBar.app/Contents/Helpers/CodexBarCLI")),
("app", home / "Applications/CodexBar.app/Contents/Helpers/CodexBarCLI"),
]
)
for root in (Path("/opt/homebrew/Caskroom/codexbar"), Path("/usr/local/Caskroom/codexbar")):
found.extend(("cask", app / "Contents/Helpers/CodexBarCLI") for app in sorted(root.glob("*/CodexBar.app")))
return found
def resolve_binary() -> Binary | None:
self_path = Path(__file__).resolve()
seen: set[Path] = set()
for source, candidate in candidates():
try:
resolved = candidate.resolve()
if resolved in seen or resolved == self_path:
continue
seen.add(resolved)
if resolved.is_file() and os.access(resolved, os.X_OK):
return Binary(str(resolved), source)
except OSError:
continue
return None
def drain(pipe: Any, target: bytearray) -> None:
try:
while True:
chunk = pipe.read(65536)
if not chunk:
break
room = MAX_CAPTURE_BYTES - len(target)
if room > 0:
target.extend(chunk[:room])
finally:
pipe.close()
def stop_group(process: subprocess.Popen[bytes], sig: signal.Signals) -> None:
try:
os.killpg(process.pid, sig)
except ProcessLookupError:
pass
def run_process(argv: Sequence[str]) -> Result:
process = subprocess.Popen(
list(argv),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True,
)
assert process.stdout is not None
assert process.stderr is not None
stdout = bytearray()
stderr = bytearray()
readers = [
threading.Thread(target=drain, args=(process.stdout, stdout), daemon=True),
threading.Thread(target=drain, args=(process.stderr, stderr), daemon=True),
]
for reader in readers:
reader.start()
timed_out = False
try:
process.wait(timeout=timeout_seconds())
except subprocess.TimeoutExpired:
timed_out = True
stop_group(process, signal.SIGTERM)
try:
process.wait(timeout=2)
except subprocess.TimeoutExpired:
stop_group(process, signal.SIGKILL)
process.wait()
for reader in readers:
reader.join(timeout=3)
return Result(124 if timed_out else process.returncode, bytes(stdout), bytes(stderr), timed_out)
def decode(value: bytes) -> str:
return value.decode("utf-8", errors="replace")
def sanitize_text(value: str, include_identities: bool) -> str:
value = JWT_RE.sub(SECRET, value)
value = BEARER_RE.sub("Bearer " + SECRET, value)
value = LABELED_SECRET_RE.sub(lambda match: match.group(1) + match.group(2) + SECRET, value)
value = WORD_SECRET_RE.sub(lambda match: match.group(1) + " " + SECRET, value)
if not include_identities:
value = EMAIL_RE.sub(EMAIL, value)
return value
def sanitize(value: Any, include_identities: bool, key: str | None = None) -> Any:
if key and SECRET_KEY_RE.search(key):
return SECRET
if key and normalized_key(key) in IDENTITY_KEYS and not include_identities and value is not None:
return EMAIL if "email" in normalized_key(key) else IDENTITY
if isinstance(value, dict):
return {str(child_key): sanitize(child, include_identities, str(child_key)) for child_key, child in value.items()}
if isinstance(value, list):
return [sanitize(child, include_identities) for child in value]
if isinstance(value, str):
return sanitize_text(value, include_identities)
return value
def print_json(value: Any) -> None:
print(json.dumps(value, indent=2, sort_keys=True))
def print_error(kind: str, message: str, *, binary: Binary | None = None) -> None:
payload: dict[str, Any] = {"error": {"kind": kind, "message": sanitize_text(message, False)}}
if binary:
payload["binary"] = {"path": safe_path(binary.path), "source": binary.source}
print_json(payload)
def emit_stderr(result: Result, include_identities: bool) -> None:
text = sanitize_text(decode(result.stderr), include_identities).strip()
if text:
print(text, file=sys.stderr)
def read_json(binary: Binary, argv: Sequence[str], include_identities: bool) -> int:
result = run_process([binary.path, *argv])
emit_stderr(result, include_identities)
if result.timed_out:
print_error("timeout", f"CodexBar exceeded {timeout_seconds():g}s and was stopped.", binary=binary)
return 124
try:
payload = json.loads(decode(result.stdout))
except json.JSONDecodeError as error:
preview = sanitize_text(decode(result.stdout[:400]), False)
print_error("invalid_json", f"CodexBar JSON failed: {error.msg}. stdout={preview!r}", binary=binary)
return result.returncode or 1
print_json(sanitize(payload, include_identities))
return result.returncode
def doctor(binary: Binary, include_identities: bool) -> int:
version = run_process([binary.path, "--version"])
if version.timed_out:
print_error("timeout", f"CodexBar version exceeded {timeout_seconds():g}s.", binary=binary)
return 124
validation = run_process([binary.path, "config", "validate", "--format", "json", "--json-only"])
emit_stderr(version, include_identities)
emit_stderr(validation, include_identities)
if validation.timed_out:
print_error("timeout", f"CodexBar config validation exceeded {timeout_seconds():g}s.", binary=binary)
return 124
try:
issues = json.loads(decode(validation.stdout))
except json.JSONDecodeError as error:
print_error("invalid_json", f"CodexBar config validation failed: {error.msg}.", binary=binary)
return validation.returncode or 1
print_json(
{
"binary": {"path": safe_path(binary.path), "source": binary.source},
"configIssues": sanitize(issues, include_identities),
"version": sanitize_text((decode(version.stdout) or decode(version.stderr)).strip(), include_identities),
}
)
return version.returncode or validation.returncode
def parser() -> argparse.ArgumentParser:
root = argparse.ArgumentParser(prog="codexbar", description="CodexBar read. JSON out. No writes.")
commands = root.add_subparsers(dest="command", required=True)
for name in ("doctor", "providers"):
command = commands.add_parser(name)
command.add_argument("--include-identities", action="store_true")
usage = commands.add_parser("usage")
scope = usage.add_mutually_exclusive_group()
scope.add_argument("--all", action="store_true")
scope.add_argument("--provider")
usage.add_argument("--include-identities", action="store_true")
return root
def main(argv: Sequence[str] | None = None) -> int:
args = parser().parse_args(argv)
binary = resolve_binary()
if not binary:
print_error("missing", "CodexBar CLI not found. Install CLI in CodexBar Advanced settings or set CODEXBAR_BIN.")
return 1
try:
if args.command == "doctor":
return doctor(binary, args.include_identities)
if args.command == "providers":
return read_json(
binary,
["config", "providers", "--format", "json", "--json-only"],
args.include_identities,
)
usage_args = ["usage", "--format", "json", "--json-only"]
if args.all:
usage_args.extend(["--provider", "all"])
elif args.provider:
usage_args.extend(["--provider", args.provider])
return read_json(binary, usage_args, args.include_identities)
except OSError as error:
print_error("launch", str(error), binary=binary)
return 1
if __name__ == "__main__":
try:
raise SystemExit(main())
except KeyboardInterrupt:
print_error("interrupted", "Interrupted.")
raise SystemExit(130)