136 lines
4.6 KiB
Python
Executable File
136 lines
4.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Extract the real `host_exec` command corpus from session transcripts (docs/NASH.md §10.6).
|
|
|
|
Sweeps a directory tree of transcript captures (JSON or JSONL — the Claude stream-json capture
|
|
files and session transcript stores both work) for `host_exec` tool calls, dedupes the command
|
|
strings, auto-classifies each as replayable or `excluded:<reason>` (network / host-mutating /
|
|
signing / toolchain — exclusions are explicit, never silent), and appends NEW entries to the
|
|
host corpus, preserving anything already there (including the self-test entries the harness
|
|
requires).
|
|
|
|
Usage: extract_host_corpus.py TRANSCRIPTS_DIR [--corpus PATH] [--dry-run]
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
EXCLUSION_PATTERNS = [
|
|
# (reason, regex over the whole command) — first hit wins; order: most-specific first.
|
|
("network", r"\b(curl|wget|git\s+(push|pull|fetch|clone)|gh\s|ssh\s|scp\s|rsync\s.*:)"),
|
|
("mutates-host", r"\b(brew\s+(install|upgrade|uninstall)|sudo\s|defaults\s+write|killall\s"
|
|
r"|launchctl\s|xcode-select\s+--install)"),
|
|
("signing-identity", r"\b(codesign|notarytool|productsign|security\s+import)"),
|
|
# Toolchain builds replay only against a real tree (--worktree), not the synthetic fixtures.
|
|
("toolchain — replay with --worktree against a real package",
|
|
r"\b(xcodebuild|swift\s+(build|test|run)|xcrun\s|make\b)"),
|
|
]
|
|
|
|
|
|
def classify(cmd):
|
|
for reason, pattern in EXCLUSION_PATTERNS:
|
|
if re.search(pattern, cmd):
|
|
return f"excluded:{reason}"
|
|
return "replayable"
|
|
|
|
|
|
def walk_json(node, found):
|
|
"""Recursively collect host_exec tool-call commands from any JSON shape."""
|
|
if isinstance(node, dict):
|
|
name = node.get("name", "")
|
|
if isinstance(name, str) and name.endswith("host_exec"):
|
|
command = (node.get("input") or {}).get("command")
|
|
if isinstance(command, str) and command.strip():
|
|
found.append(command.strip())
|
|
for value in node.values():
|
|
walk_json(value, found)
|
|
elif isinstance(node, list):
|
|
for value in node:
|
|
walk_json(value, found)
|
|
|
|
|
|
def commands_in_file(path):
|
|
found = []
|
|
try:
|
|
with open(path, encoding="utf-8", errors="replace") as f:
|
|
text = f.read()
|
|
except OSError:
|
|
return found
|
|
# Whole-file JSON first; else JSONL line by line.
|
|
try:
|
|
walk_json(json.loads(text), found)
|
|
return found
|
|
except ValueError:
|
|
pass
|
|
for line in text.splitlines():
|
|
line = line.strip()
|
|
if not line or "host_exec" not in line:
|
|
continue
|
|
try:
|
|
walk_json(json.loads(line), found)
|
|
except ValueError:
|
|
continue
|
|
return found
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("transcripts_dir")
|
|
ap.add_argument(
|
|
"--corpus",
|
|
default=os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)), "host-corpus.jsonl"),
|
|
)
|
|
ap.add_argument("--dry-run", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
commands = []
|
|
for root, _dirs, files in os.walk(args.transcripts_dir):
|
|
for name in files:
|
|
if name.endswith((".json", ".jsonl")):
|
|
commands.extend(commands_in_file(os.path.join(root, name)))
|
|
|
|
existing_cmds = set()
|
|
next_index = 1
|
|
if os.path.exists(args.corpus):
|
|
with open(args.corpus) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line:
|
|
entry = json.loads(line)
|
|
existing_cmds.add(entry["cmd"])
|
|
match = re.match(r"hx-(\d+)$", entry["id"])
|
|
if match:
|
|
next_index = max(next_index, int(match.group(1)) + 1)
|
|
|
|
new_entries = []
|
|
seen = set()
|
|
for cmd in commands:
|
|
if cmd in existing_cmds or cmd in seen:
|
|
continue
|
|
seen.add(cmd)
|
|
entry = {"id": f"hx-{next_index}", "cmd": cmd}
|
|
next_index += 1
|
|
klass = classify(cmd)
|
|
if klass != "replayable":
|
|
entry["class"] = klass
|
|
new_entries.append(entry)
|
|
|
|
print(f"transcripts: {args.transcripts_dir}")
|
|
print(f"host_exec commands found: {len(commands)} unique-new: {len(new_entries)}")
|
|
for entry in new_entries:
|
|
tag = entry.get("class", "replayable")
|
|
print(f" [{tag}] {entry['id']}: {entry['cmd']!r}")
|
|
if args.dry_run or not new_entries:
|
|
return
|
|
with open(args.corpus, "a") as f:
|
|
for entry in new_entries:
|
|
f.write(json.dumps(entry) + "\n")
|
|
print(f"appended {len(new_entries)} entries -> {args.corpus}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|