2026-07-17 20:14:20 +01:00
|
|
|
"""Audit contract for Hermes' arbitrary ``execute_code`` tool.
|
|
|
|
|
|
|
|
|
|
``execute_code`` can run arbitrary Python outside Violin's typed executor. It
|
|
|
|
|
therefore remains available only with explicit engagement metadata and produces
|
|
|
|
|
an engagement-local source receipt plus a command-history record.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-08-10 08:54:56 +01:00
|
|
|
import ast
|
2026-07-17 20:14:20 +01:00
|
|
|
import hashlib
|
|
|
|
|
import json
|
|
|
|
|
import re
|
2026-08-10 08:54:56 +01:00
|
|
|
import uuid
|
2026-07-17 20:14:20 +01:00
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from . import command, history, state
|
2026-08-10 08:54:56 +01:00
|
|
|
from .targets import extract_target_candidates, normalise_target
|
2026-07-17 20:14:20 +01:00
|
|
|
|
|
|
|
|
_HEADER = re.compile(r"^\s*#\s*violin:\s*(\{.*\})\s*$")
|
|
|
|
|
_REQUIRED_FIELDS = frozenset({"eng_dir", "phase", "target", "session_id"})
|
2026-08-10 08:54:56 +01:00
|
|
|
_LOCAL_ANALYSIS_IMPORTS = frozenset(
|
|
|
|
|
{
|
|
|
|
|
"ast",
|
|
|
|
|
"base64",
|
|
|
|
|
"binascii",
|
|
|
|
|
"collections",
|
|
|
|
|
"csv",
|
|
|
|
|
"dataclasses",
|
|
|
|
|
"datetime",
|
|
|
|
|
"decimal",
|
|
|
|
|
"difflib",
|
|
|
|
|
"enum",
|
|
|
|
|
"functools",
|
|
|
|
|
"hashlib",
|
|
|
|
|
"heapq",
|
|
|
|
|
"html",
|
|
|
|
|
"io",
|
|
|
|
|
"itertools",
|
|
|
|
|
"json",
|
|
|
|
|
"math",
|
|
|
|
|
"operator",
|
|
|
|
|
"pathlib",
|
|
|
|
|
"pprint",
|
|
|
|
|
"re",
|
|
|
|
|
"statistics",
|
|
|
|
|
"string",
|
|
|
|
|
"textwrap",
|
|
|
|
|
"time",
|
|
|
|
|
"typing",
|
|
|
|
|
"unicodedata",
|
|
|
|
|
"uuid",
|
|
|
|
|
"xml",
|
|
|
|
|
"yaml",
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
_TARGET_CAPABLE_NAMES = frozenset(
|
|
|
|
|
{
|
|
|
|
|
"aiohttp",
|
|
|
|
|
"asyncssh",
|
|
|
|
|
"ctypes",
|
|
|
|
|
"ftplib",
|
|
|
|
|
"httpx",
|
|
|
|
|
"importlib",
|
|
|
|
|
"os",
|
|
|
|
|
"paramiko",
|
|
|
|
|
"playwright",
|
|
|
|
|
"pwn",
|
|
|
|
|
"requests",
|
|
|
|
|
"scapy",
|
|
|
|
|
"selenium",
|
|
|
|
|
"socket",
|
|
|
|
|
"subprocess",
|
|
|
|
|
"sys",
|
|
|
|
|
"telnetlib",
|
|
|
|
|
"urllib",
|
|
|
|
|
"webbrowser",
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
_DYNAMIC_EXECUTION_CALLS = frozenset(
|
|
|
|
|
{
|
|
|
|
|
"__import__",
|
|
|
|
|
"compile",
|
|
|
|
|
"connect",
|
|
|
|
|
"connect_ex",
|
|
|
|
|
"create_connection",
|
|
|
|
|
"eval",
|
|
|
|
|
"exec",
|
|
|
|
|
"execv",
|
|
|
|
|
"execve",
|
|
|
|
|
"fork",
|
|
|
|
|
"popen",
|
|
|
|
|
"spawn",
|
|
|
|
|
"system",
|
|
|
|
|
"urlopen",
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-07-17 20:14:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_metadata(source: object) -> tuple[dict[str, str] | None, str | None]:
|
|
|
|
|
"""Parse the required first-line Violin JSON header from Python source."""
|
|
|
|
|
if not isinstance(source, str) or not source.strip():
|
|
|
|
|
return None, "execute_code requires a non-empty `code` string"
|
|
|
|
|
first_line = source.splitlines()[0] if source.splitlines() else ""
|
|
|
|
|
match = _HEADER.fullmatch(first_line)
|
|
|
|
|
if not match:
|
|
|
|
|
return None, (
|
|
|
|
|
"execute_code requires first-line metadata: "
|
|
|
|
|
'# violin: {"eng_dir":"...","phase":"...","target":"...","session_id":"..."}'
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
raw = json.loads(match.group(1))
|
|
|
|
|
except json.JSONDecodeError as exc:
|
|
|
|
|
return None, f"execute_code metadata must be valid JSON: {exc.msg}"
|
|
|
|
|
if not isinstance(raw, dict) or set(raw) != _REQUIRED_FIELDS:
|
|
|
|
|
return (
|
|
|
|
|
None,
|
2026-08-07 17:06:49 +01:00
|
|
|
"execute_code metadata must contain exactly eng_dir, phase, target, and session_id. "
|
|
|
|
|
'Header format (line 1 of code): # violin: {"eng_dir":"<path>","phase":"<phase>","target":"<target>","session_id":"<session_id>"} '
|
|
|
|
|
"(obtain session_id via violin_status)",
|
2026-07-17 20:14:20 +01:00
|
|
|
)
|
|
|
|
|
if not all(isinstance(raw[name], str) and raw[name].strip() for name in _REQUIRED_FIELDS):
|
|
|
|
|
return None, "execute_code metadata values must be non-empty strings"
|
|
|
|
|
return {name: raw[name].strip() for name in _REQUIRED_FIELDS}, None
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 08:54:56 +01:00
|
|
|
def execution_class(source: object) -> str:
|
|
|
|
|
"""Classify whether Python can touch a target or launch another process.
|
|
|
|
|
|
|
|
|
|
This changes review accounting, never authorization. Unknown imports and
|
|
|
|
|
dynamic execution primitives fail closed as target-capable.
|
|
|
|
|
"""
|
|
|
|
|
tree = ast.parse(str(source))
|
|
|
|
|
for node in ast.walk(tree):
|
|
|
|
|
if isinstance(node, ast.Import):
|
|
|
|
|
if any(
|
|
|
|
|
alias.name.split(".", 1)[0] not in _LOCAL_ANALYSIS_IMPORTS for alias in node.names
|
|
|
|
|
):
|
|
|
|
|
return "target_touching"
|
|
|
|
|
elif isinstance(node, ast.ImportFrom):
|
|
|
|
|
root = (node.module or "").split(".", 1)[0]
|
|
|
|
|
if not root or root not in _LOCAL_ANALYSIS_IMPORTS:
|
|
|
|
|
return "target_touching"
|
|
|
|
|
elif isinstance(node, ast.Name) and node.id in _TARGET_CAPABLE_NAMES:
|
|
|
|
|
return "target_touching"
|
|
|
|
|
elif isinstance(node, ast.Call):
|
|
|
|
|
name = (
|
|
|
|
|
node.func.id
|
|
|
|
|
if isinstance(node.func, ast.Name)
|
|
|
|
|
else node.func.attr
|
|
|
|
|
if isinstance(node.func, ast.Attribute)
|
|
|
|
|
else ""
|
|
|
|
|
)
|
|
|
|
|
if name in _DYNAMIC_EXECUTION_CALLS:
|
|
|
|
|
return "target_touching"
|
|
|
|
|
return "local_analysis"
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 20:14:20 +01:00
|
|
|
def validate_source(source: object) -> tuple[dict[str, str] | None, str | None]:
|
|
|
|
|
"""Validate metadata against the same engagement gates as command execution."""
|
|
|
|
|
metadata, error = parse_metadata(source)
|
|
|
|
|
if error or metadata is None:
|
|
|
|
|
return None, error
|
2026-08-10 08:54:56 +01:00
|
|
|
try:
|
|
|
|
|
tree = ast.parse(str(source))
|
|
|
|
|
except SyntaxError as exc:
|
|
|
|
|
return None, f"execute_code source must parse as Python: {exc.msg}"
|
|
|
|
|
classification = execution_class(source)
|
2026-07-17 20:14:20 +01:00
|
|
|
eng_dir = state.resolve_eng_dir(metadata["eng_dir"])
|
|
|
|
|
gate = command.check_command(
|
|
|
|
|
command.CheckCommandArgs(
|
2026-08-10 08:54:56 +01:00
|
|
|
command=f"execute_code class={classification} sha256={source_digest(source)}",
|
2026-07-17 20:14:20 +01:00
|
|
|
phase=metadata["phase"],
|
|
|
|
|
eng_dir=str(eng_dir),
|
|
|
|
|
scope=str(eng_dir / "scope" / "scope.yaml"),
|
|
|
|
|
target=metadata["target"],
|
|
|
|
|
session_id=metadata["session_id"],
|
2026-08-10 08:54:56 +01:00
|
|
|
account_sync=classification == "target_touching",
|
2026-07-17 20:14:20 +01:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if gate.errors:
|
|
|
|
|
return None, "execute_code blocked by Violin guard: " + "; ".join(gate.errors)
|
2026-08-10 08:54:56 +01:00
|
|
|
declared = normalise_target(metadata["target"])
|
|
|
|
|
foreign: set[str] = set()
|
|
|
|
|
for node in ast.walk(tree):
|
|
|
|
|
if not isinstance(node, ast.Constant) or not isinstance(node.value, str):
|
|
|
|
|
continue
|
|
|
|
|
for candidate in extract_target_candidates(f"probe {node.value}"):
|
|
|
|
|
normalized = normalise_target(candidate)
|
|
|
|
|
if normalized not in {declared, "localhost", "127.0.0.1", "0.0.0.0", "::1"}:
|
|
|
|
|
foreign.add(normalized)
|
|
|
|
|
if foreign:
|
|
|
|
|
return None, (
|
|
|
|
|
"execute_code contains non-local target literals that differ from declared target: "
|
|
|
|
|
+ ", ".join(sorted(foreign))
|
|
|
|
|
)
|
2026-07-17 20:14:20 +01:00
|
|
|
return metadata, None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def source_digest(source: object) -> str:
|
|
|
|
|
return hashlib.sha256(str(source).encode("utf-8")).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 08:54:56 +01:00
|
|
|
def prepare_execution(source: object) -> tuple[dict[str, str], Path]:
|
|
|
|
|
"""Persist intent and account only for target-capable dispatches."""
|
|
|
|
|
metadata, error = validate_source(source)
|
2026-07-17 20:14:20 +01:00
|
|
|
if error or metadata is None:
|
2026-08-10 08:54:56 +01:00
|
|
|
raise ValueError(error or "execute_code validation failed")
|
2026-07-17 20:14:20 +01:00
|
|
|
eng_dir = state.resolve_eng_dir(metadata["eng_dir"])
|
|
|
|
|
digest = source_digest(source)
|
2026-08-10 08:54:56 +01:00
|
|
|
classification = execution_class(source)
|
|
|
|
|
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%S.%fZ")
|
|
|
|
|
audit_id = str(uuid.uuid4())
|
|
|
|
|
evidence_dir = eng_dir / "evidence" / "executions"
|
|
|
|
|
source_path = evidence_dir / f"{stamp}-{audit_id[:8]}-execute-code.py"
|
|
|
|
|
receipt_path = evidence_dir / f"{stamp}-{audit_id[:8]}-execute-code.json"
|
|
|
|
|
state.ensure_dir(evidence_dir)
|
|
|
|
|
source_path.write_text(str(source), encoding="utf-8")
|
|
|
|
|
command_text = (
|
|
|
|
|
f"execute_code class={classification} sha256={digest} target={metadata['target']}"
|
|
|
|
|
)
|
|
|
|
|
receipt = {
|
|
|
|
|
"schema_version": 1,
|
|
|
|
|
"audit_id": audit_id,
|
|
|
|
|
"status": "starting",
|
|
|
|
|
"command": command_text,
|
|
|
|
|
"execution_class": classification,
|
|
|
|
|
"sync_accounted": classification == "target_touching",
|
|
|
|
|
"source_digest": digest,
|
|
|
|
|
"phase": metadata["phase"],
|
|
|
|
|
"target": metadata["target"],
|
|
|
|
|
"session_id": metadata["session_id"],
|
|
|
|
|
"started_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
|
|
|
"evidence_paths": {
|
|
|
|
|
"manifest": receipt_path.relative_to(eng_dir).as_posix(),
|
|
|
|
|
"source": source_path.relative_to(eng_dir).as_posix(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
state.atomic_json(receipt_path, receipt)
|
|
|
|
|
if classification == "target_touching":
|
|
|
|
|
try:
|
|
|
|
|
from .execution import _commit_guard_state
|
|
|
|
|
from .ptt import find_active_task, parse_ptt
|
|
|
|
|
|
|
|
|
|
active = find_active_task(parse_ptt(eng_dir / "state" / "ptt.md"))
|
|
|
|
|
remaining = _commit_guard_state(
|
|
|
|
|
eng_dir, command_text, metadata["phase"], active.id if active else ""
|
|
|
|
|
)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
receipt.update(status="failed_to_dispatch", error=str(exc))
|
|
|
|
|
state.atomic_json(receipt_path, receipt)
|
|
|
|
|
raise
|
|
|
|
|
else:
|
|
|
|
|
remaining = state.sync_credit_remaining(eng_dir, metadata["phase"])
|
|
|
|
|
receipt["sync_credit_remaining"] = remaining
|
|
|
|
|
state.atomic_json(receipt_path, receipt)
|
|
|
|
|
return metadata, receipt_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def record_completion(
|
|
|
|
|
source: object,
|
|
|
|
|
result: object,
|
|
|
|
|
duration_ms: object = 0,
|
|
|
|
|
*,
|
|
|
|
|
receipt_path: str | Path,
|
|
|
|
|
) -> Path:
|
|
|
|
|
"""Finalize the pre-dispatch receipt and append one explicit history record."""
|
|
|
|
|
metadata, error = parse_metadata(source)
|
|
|
|
|
if error or metadata is None:
|
|
|
|
|
raise ValueError(error or "execute_code metadata is missing")
|
|
|
|
|
eng_dir = state.resolve_eng_dir(metadata["eng_dir"])
|
|
|
|
|
digest = source_digest(source)
|
|
|
|
|
receipt_file = Path(receipt_path)
|
|
|
|
|
receipt = state.read_json(receipt_file)
|
|
|
|
|
if receipt.get("source_digest") != digest:
|
|
|
|
|
raise ValueError("execute_code completion does not match its intent receipt")
|
2026-07-17 20:14:20 +01:00
|
|
|
|
|
|
|
|
summary = _result_summary(result, duration_ms)
|
2026-08-10 08:54:56 +01:00
|
|
|
# Command identity is created before dispatch and is also stored in the
|
|
|
|
|
# pending sync batch. Keep it byte-for-byte stable so review/rebind can
|
|
|
|
|
# reconcile the completed execution against that batch. Outcome metadata
|
|
|
|
|
# belongs in the receipt and dedicated history fields, not in command=.
|
|
|
|
|
command_text = str(receipt.get("command") or "").strip()
|
|
|
|
|
if not command_text:
|
|
|
|
|
raise ValueError("execute_code intent receipt has no command identity")
|
|
|
|
|
completed_receipt = {
|
|
|
|
|
**receipt,
|
|
|
|
|
"status": "completed" if summary["status"] == "ok" else "completed_with_error",
|
|
|
|
|
"completed_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
|
|
|
"duration_ms": summary["duration_ms"],
|
|
|
|
|
"exit_code": summary["exit_code"],
|
|
|
|
|
}
|
|
|
|
|
if not history.history_contains(eng_dir, command_text):
|
|
|
|
|
history.append_history(
|
|
|
|
|
eng_dir,
|
|
|
|
|
command_text,
|
|
|
|
|
metadata["phase"],
|
|
|
|
|
summary["exit_code"],
|
|
|
|
|
receipt_file.relative_to(eng_dir).as_posix(),
|
|
|
|
|
status=str(completed_receipt["status"]),
|
|
|
|
|
)
|
|
|
|
|
state.atomic_json(receipt_file, completed_receipt)
|
|
|
|
|
return receipt_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def abandon_execution(receipt_path: str | Path, reason: str) -> None:
|
|
|
|
|
"""Close a prepared intent that cannot receive a post-tool completion."""
|
|
|
|
|
receipt_file = Path(receipt_path)
|
|
|
|
|
receipt = state.read_json(receipt_file)
|
|
|
|
|
if receipt.get("status") != "starting":
|
|
|
|
|
return
|
|
|
|
|
eng_dir = receipt_file.parents[2]
|
|
|
|
|
command_text = str(receipt.get("command") or "").strip()
|
|
|
|
|
abandoned = {
|
|
|
|
|
**receipt,
|
|
|
|
|
"status": "abandoned",
|
|
|
|
|
"completed_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
|
|
|
"exit_code": -1,
|
|
|
|
|
"error": reason,
|
|
|
|
|
}
|
|
|
|
|
if command_text and not history.history_contains(eng_dir, command_text):
|
|
|
|
|
history.append_history(
|
|
|
|
|
eng_dir,
|
|
|
|
|
command_text,
|
|
|
|
|
str(receipt.get("phase") or "RECON"),
|
|
|
|
|
-1,
|
|
|
|
|
receipt_file.relative_to(eng_dir).as_posix(),
|
|
|
|
|
status="abandoned",
|
|
|
|
|
)
|
|
|
|
|
state.atomic_json(receipt_file, abandoned)
|
2026-07-17 20:14:20 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _result_summary(result: object, duration_ms: object) -> dict[str, int | str]:
|
|
|
|
|
try:
|
|
|
|
|
parsed: Any = json.loads(result) if isinstance(result, str) else result
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
parsed = {"error": "non-JSON tool result"}
|
|
|
|
|
failed = isinstance(parsed, dict) and bool(parsed.get("error"))
|
|
|
|
|
try:
|
|
|
|
|
elapsed = max(0, int(duration_ms))
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
elapsed = 0
|
|
|
|
|
return {"status": "error" if failed else "ok", "exit_code": int(failed), "duration_ms": elapsed}
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 08:54:56 +01:00
|
|
|
__all__ = [
|
|
|
|
|
"abandon_execution",
|
|
|
|
|
"execution_class",
|
|
|
|
|
"parse_metadata",
|
|
|
|
|
"prepare_execution",
|
|
|
|
|
"record_completion",
|
|
|
|
|
"source_digest",
|
|
|
|
|
"validate_source",
|
|
|
|
|
]
|