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).
315 lines
14 KiB
Python
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()
|