Files
rune/scripts/gen_tool_executor_lifecycle_truth.py
T
Nick 07c08c341e feat(tool-executor): lifecycle family gap-close — _execute_tool_now/serialize/deserialize/execute_tool/confirm_write/_find_capability to Go
Dichotomy wave #2479, contract factory-heavy-te-life. Name-preserving
ports of the six audit-missing lifecycle fns into gaps_lifecycle.go with
a 52-row frozen oracle battery (truth_lifecycle.json, hermetic generator,
byte-identical regen) and 49/49 live parity (per-side isolated roots).
Repairs en route: write-denial message now carries Python str(list) bytes
(pyListRepr); ReadFile numbers with the RAW offset (offset=0 renders
'0: line 1' — old clamp rewrote the footer). str(e)-law TypeError
envelopes, present-but-null vs absent args, broker seam for
_find_capability. Sibling-gap arms pinned as notPorted sentinels
(documented divergences, verification/te-life-summary.md).
2026-10-15 00:39:03 +01:00

315 lines
14 KiB
Python

#!/usr/bin/env python3
"""gen_tool_executor_lifecycle_truth.py — freeze the PREPARE/APPROVE/
SERIALIZE family gap truth for tool-executor (dichotomy wave #2479,
contract factory-heavy-te-life).
Covers the six audit-missing lifecycle functions against the LIVE oracle
(src/rune/tool_executor.py):
_execute_tool_now / serialize_prepared_tool /
deserialize_prepared_tool / execute_tool (== prepare_tool kernel) /
confirm_write / _find_capability
Hermetic laws (gen_tool_executor_truth.py precedent):
- fixtures live under roots the oracle itself allows; every occurrence of
those roots — inputs AND expectations — becomes {{TMP}} / {{WROOT}};
- the runner home becomes {{HOME}} (the write-denial message embeds
str(ALLOWED_WRITE_DIRS));
- uuid tokens become {{TOKEN}} (process-random; the battery normalizes
the Go side the same way);
- `now` is always an explicit argument (no-clock law);
- the broker is a frozen stub for _find_capability rows (the seam
contract: the battery rebuilds the same matches from the row JSON,
property ORDER included);
- write previews summarize the diff (head 2 lines + plus/minus counts) —
the diff body is the module's pre-existing kernel surface
(truth_fileops), not this family's contract.
Deliberately EXCLUDED rows (documented divergences, not bugs — see
verification/te-life-summary.md): dispatch arms whose handlers are
sibling-family gaps (edit_file/glob_files/repo_map/run_command/
read_persona/query_memory/check_growth/diagnose_behavior return a
notPorted envelope in Go), non-string optional fields through
deserialize (the unvalidated-Python-dataclass class), git/lint/format
EXECUTION rows (environment-dependent binaries).
Byte-identical regeneration: no timestamps embedded; re-run and diff.
Usage: python3 scripts/gen_tool_executor_lifecycle_truth.py [--out PATH]
"""
import copy
import json
import sys
import tempfile
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import tool_executor as te # noqa: E402 — the live oracle
def mk_root(dirs):
for d in dirs:
try:
return tempfile.mkdtemp(prefix="rune-life-", dir=d)
except OSError:
continue
raise RuntimeError(f"no creatable root among {dirs}")
W20 = "".join(f"line {i}\n" for i in range(1, 21))
STUB_NONE_QUERY = "run the whole test suite"
STUB_TWO_QUERY = "commit my changes"
STUB_MATCHES = [
{
"name": "git_commit",
"description": "Commit staged changes",
"risk": "irreversible",
"parameters": {"properties": {"message": {"type": "string"}, "files": {"type": "array"}}},
},
{
"name": "snapshot_tree",
"description": "Snapshot a directory tree",
"parameters": {"properties": {}},
},
]
def stub_broker(query):
# Deterministic frozen stub — the Go seam replays these from the row.
if query == STUB_TWO_QUERY:
return copy.deepcopy(STUB_MATCHES)
return []
def summarize_diff(diff):
lines = diff.split("\n")
return {
"head": lines[:2],
"plus": sum(1 for l in lines if l.startswith("+") and not l.startswith("+++")),
"minus": sum(1 for l in lines if l.startswith("-") and not l.startswith("---")),
}
def norm_envelope(env):
"""Normalize an oracle envelope for truth: diff summary + no pending obj."""
out = copy.deepcopy(env)
if "diff" in out:
out["diff"] = summarize_diff(out["diff"])
return out
def norm_prepare(env):
"""Normalize a prepare envelope: pending → serialized + {{TOKEN}}.
operation_digest becomes {{DIGEST}}: the digest binds the REAL root
path, which is per-generation random — the battery re-derives it
structurally (CanonicalDigest(tool, substituted-args) must equal the
pending's digest on BOTH sides; that invariant is the contract).
"""
out = norm_envelope(env)
if isinstance(out.get("pending"), te.PreparedTool):
ser = te.serialize_prepared_tool(out["pending"])
ser["token"] = "{{TOKEN}}"
ser["operation_digest"] = "{{DIGEST}}"
out["pending"] = ser
return out
def main():
tmp = mk_root(te.ALLOWED_READ_DIRS)
wroot = mk_root(te.ALLOWED_WRITE_DIRS)
home = str(Path.home())
(Path(tmp) / "w.txt").write_text(W20, encoding="utf-8")
(Path(tmp) / "a.txt").write_text("the spine rebuild parity\n", encoding="utf-8")
(Path(tmp) / "adir").mkdir()
fixtures = {
"files": [
{"path": "{{TMP}}/w.txt", "content": W20},
{"path": "{{TMP}}/a.txt", "content": "the spine rebuild parity\n"},
],
"dirs": ["{{TMP}}/adir"],
}
rows = []
def add(op, name, inp, expected=None, raises=None):
row = {"op": op, "name": name, **inp}
if expected is not None:
row["want"] = expected
if raises is not None:
row["raises"] = raises
rows.append(row)
# ── _execute_tool_now ────────────────────────────────────────────────
def exec_row(name, tool, args, **kw):
r = te._execute_tool_now(tool, copy.deepcopy(args))
add("exec_now", name, {"tool": tool, "args": args}, norm_envelope(r), **kw)
exec_row("read_window", "read_file", {"path": f"{tmp}/w.txt", "offset": 2, "limit": 3})
exec_row("read_default", "read_file", {"path": f"{tmp}/a.txt"})
exec_row("read_missing", "read_file", {"path": f"{tmp}/missing.txt"})
exec_row("read_blocked", "read_file", {"path": "/etc/hostname"})
exec_row("read_offset_none", "read_file", {"path": f"{tmp}/w.txt", "offset": None})
exec_row("read_limit_str", "read_file", {"path": f"{tmp}/w.txt", "limit": "x"})
exec_row("read_offset_frac", "read_file", {"path": f"{tmp}/w.txt", "offset": 1.5})
# offset=True (bool is an int subclass) renders as the literal "True"
# inside the footer f-string — excluded, documented divergence.
exec_row("unknown", "no_such_tool", {"x": 1})
exec_row("list_hit", "list_directory", {"path": tmp})
exec_row("list_not_dir", "list_directory", {"path": f"{tmp}/nodir"})
exec_row("grep_no_pattern", "grep_files", {"path": tmp})
exec_row("grep_no_match", "grep_files", {"pattern": "zzz_never", "path": tmp})
exec_row("write_preview", "write_file",
{"path": f"{wroot}/exec-new.txt", "content": "alpha\nbeta\n"})
exec_row("write_denied_outside", "write_file", {"path": "/usr/share/x.txt", "content": "x"})
exec_row("write_denied_protected", "write_file", {"path": "/etc/passwd", "content": "x"})
exec_row("git_commit_no_message", "git_commit", {})
exec_row("run_tests_no_runner", "run_tests", {"path": f"{tmp}/a.txt"})
exec_row("lint_missing", "lint_file", {"path": f"{tmp}/missing.py"})
exec_row("format_denied", "format_code", {"path": "/etc/f.py"})
# ── serialize_prepared_tool ──────────────────────────────────────────
full = te.PreparedTool(
token="ser-full-token", name="write_file",
arguments={"path": "/tmp/x.txt", "content": "abc"},
operation_digest="d" * 64, created_at=10.0, expires_at=310.0,
requested_path="/tmp/x.txt", resolved_path="/tmp/x.txt", path_digest="p" * 64,
)
minimal = te.PreparedTool(
token="ser-min-token", name="run_command",
arguments={"command": "ls"}, operation_digest="e" * 64,
created_at=0.0, expires_at=5.0,
)
ser_full = te.serialize_prepared_tool(full)
ser_min = te.serialize_prepared_tool(minimal)
add("serialize", "full", {"pending": ser_full}, ser_full)
add("serialize", "minimal", {"pending": ser_min}, ser_min)
# ── deserialize_prepared_tool ────────────────────────────────────────
def deser_row(name, value):
try:
p = te.deserialize_prepared_tool(copy.deepcopy(value))
add("deserialize", name, {"value": value}, te.serialize_prepared_tool(p))
except ValueError as exc:
add("deserialize", name, {"value": value}, raises=str(exc))
ok_full = te.serialize_prepared_tool(full)
ok_min = te.serialize_prepared_tool(minimal)
ok_int_ts = dict(ok_min, created_at=100, expires_at=400)
deser_row("ok_full", ok_full)
deser_row("ok_minimal", ok_min)
deser_row("ok_int_timestamps", ok_int_ts)
deser_row("not_object_str", "nope")
deser_row("not_object_null", None)
deser_row("extra_field", dict(ok_min, extra=1))
deser_row("missing_field", {k: v for k, v in ok_min.items() if k != "path_digest"})
deser_row("token_empty", dict(ok_min, token=""))
deser_row("token_number", dict(ok_min, token=5))
deser_row("name_empty", dict(ok_min, name=""))
deser_row("arguments_list", dict(ok_min, arguments=[]))
deser_row("digest_number", dict(ok_min, operation_digest=3.5))
deser_row("created_bool", dict(ok_min, created_at=True))
deser_row("created_str", dict(ok_min, created_at="1"))
deser_row("expires_null", dict(ok_min, expires_at=None))
# ── confirm_write ────────────────────────────────────────────────────
def confirm_row(name, path, content):
r = te.confirm_write(path, content)
add("confirm_write", name, {"path": path, "content": content}, norm_envelope(r))
confirm_row("new_file", f"{wroot}/confirmed-new.txt", "abc")
confirm_row("protected", "/etc/x.txt", "x")
confirm_row("outside", "/usr/share/x.txt", "x")
# Deterministic read-only-root denial: the {{TMP}} mkdtemp can land
# under $HOME (write-allowed there), so pin an explicit /tmp path —
# /tmp is read-allowed but never write-allowed, on every surface.
confirm_row("readonly_root", "/tmp/te-life-readonly-confirm/nope.txt", "x")
# ── execute_tool / prepare_tool ─────────────────────────────────────
def exec_tool_row(name, tool, args, now=None, ttl=None):
kwargs = {}
if now is not None:
kwargs["now"] = now
if ttl is not None:
kwargs["ttl"] = ttl
if kwargs:
r = te.prepare_tool(tool, copy.deepcopy(args), **kwargs)
else:
r = te.execute_tool(tool, copy.deepcopy(args))
add("execute_tool", name, {"tool": tool, "args": args, **kwargs}, norm_prepare(r))
exec_tool_row("readonly_passthrough", "read_file", {"path": f"{tmp}/a.txt"})
exec_tool_row("write_prepare", "write_file",
{"path": f"{wroot}/exec-prep.txt", "content": "one\ntwo\nthree\n"}, now=1000.0)
exec_tool_row("format_prepare", "format_code", {"path": f"{wroot}/fmt-prep.go"}, now=2000.0)
exec_tool_row("write_ttl", "write_file",
{"path": f"{wroot}/prep-ttl.txt", "content": "x"}, now=5.0, ttl=60.0)
exec_tool_row("write_denied", "write_file", {"path": "/usr/share/x.txt", "content": "x"})
exec_tool_row("edit_no_path", "edit_file", {"search": "a", "replace": "b"})
exec_tool_row("args_null", "write_file", None)
# /tmp is read-allowed but NOT write-allowed on every surface — the
# {{TMP}} mkdtemp can land under $HOME (write-allowed), so the denial
# row pins an explicit /tmp path instead of the fixture root.
exec_tool_row("format_denied_readonly_root", "format_code",
{"path": "/tmp/te-life-readonly-denial/x.go"}, now=3000.0)
# ── _find_capability (frozen broker stub) ───────────────────────────
def find_cap_row(name, args, matches):
import rune.capability_broker as broker
original = broker.find_capability
broker.find_capability = stub_broker
try:
r = te._find_capability(copy.deepcopy(args))
finally:
broker.find_capability = original
add("find_cap", name,
{"args": args, "matches": matches}, norm_envelope(r))
find_cap_row("query_missing", {}, [])
find_cap_row("query_blank", {"query": " "}, [])
find_cap_row("no_matches", {"query": STUB_NONE_QUERY}, [])
find_cap_row("two_matches", {"query": STUB_TWO_QUERY}, copy.deepcopy(STUB_MATCHES))
doc = {
"_doc": (
"FROZEN ORACLE (tool_executor lifecycle family — "
"factory-heavy-te-life). Placeholders: {{TMP}} read root, "
"{{WROOT}} write root, {{HOME}} runner home, {{TOKEN}} uuid. "
"Broker stub matches ride the find_cap rows verbatim "
"(property ORDER is contract). Regenerate: "
"scripts/gen_tool_executor_lifecycle_truth.py"
),
"fixtures": fixtures,
"stub_queries": {"none": STUB_NONE_QUERY, "two": STUB_TWO_QUERY},
"rows": rows,
}
def subst(obj):
if isinstance(obj, str):
# TMP/WROOT first — the roots may live UNDER home.
s = obj.replace(tmp, "{{TMP}}").replace(wroot, "{{WROOT}}")
s = s.replace(home, "{{HOME}}")
return s
if isinstance(obj, list):
return [subst(x) for x in obj]
if isinstance(obj, dict):
return {k: subst(v) for k, v in obj.items()}
return obj
doc = subst(doc)
out = Path(sys.argv[sys.argv.index("--out") + 1]) if "--out" in sys.argv else (
REPO / "src" / "go" / "tool-executor" / "truth_lifecycle.json"
)
out.write_text(json.dumps(doc, indent=1, ensure_ascii=False) + "\n", encoding="utf-8")
print(f"wrote {out} ({len(rows)} rows)")
if __name__ == "__main__":
main()