← Files Selective IntelligenceARCHIVED FILE
skills/selective-intelligence/scripts/policy_guard.py
31.8 KB · Sep 30, 2026 · 23:14 UTC
#!/usr/bin/env python3
"""Pre-adapter authorization and evidence capture for SI tool execution."""
from __future__ import annotations
import hashlib
import os
import re
import subprocess
import tempfile
import time
import uuid
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Callable, Sequence
class PolicyDenied(PermissionError):
def __init__(self, decision: dict[str, Any]):
super().__init__(decision["reason"])
self.decision = decision
def _now() -> str:
return datetime.now(UTC).isoformat()
def _id(prefix: str) -> str:
return f"{prefix}-{uuid.uuid4().hex[:12]}"
def _resolved(path: str | os.PathLike[str]) -> Path:
return Path(path).expanduser().resolve(strict=False)
def _inside(path: Path, root: Path) -> bool:
try:
path.relative_to(root)
return True
except ValueError:
return False
def _digest(text: str) -> str:
return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
_MAX_CAPTURE_BYTES = 1024 * 1024
def _captured_stream(handle: Any) -> tuple[str, str, int, bool]:
"""Return a bounded preview plus a digest of the complete process stream."""
handle.flush()
handle.seek(0, os.SEEK_END)
size = handle.tell()
handle.seek(0)
digest = hashlib.sha256()
preview = bytearray()
while True:
chunk = handle.read(65536)
if not chunk:
break
digest.update(chunk)
if len(preview) < _MAX_CAPTURE_BYTES:
preview.extend(chunk[: _MAX_CAPTURE_BYTES - len(preview)])
truncated = size > _MAX_CAPTURE_BYTES
text = bytes(preview).decode("utf-8", errors="replace")
if truncated:
text += f"\n[output truncated after {_MAX_CAPTURE_BYTES} of {size} bytes]"
return text, digest.hexdigest(), size, truncated
_MUTATING_GIT = {
"add", "am", "apply", "branch", "checkout", "cherry-pick", "clean", "commit",
"fetch", "merge", "mv", "pull", "push", "rebase", "reset", "restore", "revert",
"rm", "stash", "switch", "tag", "worktree",
}
_INSTALLERS = {
("npm", "install"), ("npm", "i"), ("pnpm", "install"), ("pnpm", "add"),
("yarn", "add"), ("yarn", "install"), ("pip", "install"), ("pip3", "install"),
("poetry", "add"), ("uv", "add"), ("cargo", "install"),
}
_DEPLOY_WORDS = {"deploy", "publish", "release"}
_WINDOWS_EXECUTABLE_SUFFIXES = (".exe", ".cmd", ".bat", ".com", ".ps1")
_TRANSPARENT_WRAPPERS = {"env", "command", "nice", "nohup", "timeout", "stdbuf", "sudo", "doas"}
_READ_ONLY_GIT = {
"describe", "diff", "grep", "log", "ls-files", "ls-tree", "rev-parse",
"shortlog", "show", "status", "version",
}
_ENV_ASSIGNMENT = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$", re.DOTALL)
def _command_basename(token: str) -> str:
"""Return a platform-normalized command basename without Windows suffixes."""
name = str(token).strip()
while len(name) >= 2 and name[0] == name[-1] and name[0] in {"'", '"'}:
name = name[1:-1].strip()
name = re.split(r"[/\\]", name)[-1].lower()
stripped = True
while stripped:
stripped = False
for suffix in _WINDOWS_EXECUTABLE_SUFFIXES:
if name.endswith(suffix):
name = name[: -len(suffix)]
stripped = True
break
return name
def _redact_argv(argv: Sequence[str]) -> list[str]:
redacted: list[str] = []
for value in argv:
token = str(value)
unquoted = token
while len(unquoted) >= 2 and unquoted[0] == unquoted[-1] and unquoted[0] in {"'", '"'}:
unquoted = unquoted[1:-1]
match = _ENV_ASSIGNMENT.fullmatch(unquoted)
redacted.append(f"{match.group(1)}=<redacted>" if match else token)
return redacted
def _skip_wrapper_prefix(base: str, argv: list[str]) -> tuple[list[str] | None, str | None]:
"""Remove one wrapper, returning an ambiguity reason on malformed input."""
if not argv or _command_basename(argv[0]) != base:
return None, "wrapper executable did not match resolver state"
rest = argv[1:]
if base == "env":
index = 0
while index < len(rest):
token = rest[index]
if token == "--":
return rest[index + 1 :], None
if (
token in {"-S", "--split-string"}
or token.startswith("-S")
or token.startswith("--split-string=")
):
return None, "env -S command splitting is ambiguous"
if token in {"-i", "--ignore-environment", "-0", "--null", "-v", "--debug"}:
index += 1
continue
if token in {"-C", "--chdir", "-P"} or token.startswith("--chdir="):
return None, f"env option {token.split('=', 1)[0]} changes command resolution context"
if token in {"-u", "--unset"}:
if index + 1 >= len(rest):
return None, f"env option {token} is missing its value"
index += 2
continue
if token.startswith("--unset="):
if token.endswith("="):
return None, f"env option {token.split('=', 1)[0]} is missing its value"
index += 1
continue
if token.startswith("-"):
return None, f"unsupported env option: {token}"
if _ENV_ASSIGNMENT.fullmatch(token):
index += 1
continue
return rest[index:], None
return [], None
if base == "command":
index = 0
while index < len(rest):
token = rest[index]
if token in {"-p", "-v", "-V"}:
index += 1
continue
if token == "--":
return rest[index + 1 :], None
if token.startswith("-"):
return None, f"unsupported command option: {token}"
return rest[index:], None
return [], None
if base == "nice":
index = 0
if index < len(rest) and rest[index] in {"-n", "--adjustment"}:
if index + 1 >= len(rest):
return None, f"nice option {rest[index]} is missing its value"
if not re.fullmatch(r"-?\d+", rest[index + 1]):
return None, f"nice option {rest[index]} has an invalid value"
index += 2
elif index < len(rest) and rest[index].startswith("--adjustment="):
if not re.fullmatch(r"-?\d+", rest[index].split("=", 1)[1]):
return None, "nice option --adjustment has an invalid value"
index += 1
elif index < len(rest) and re.fullmatch(r"-?\d+", rest[index] or ""):
index += 1
elif index < len(rest) and rest[index].startswith("-"):
return None, f"unsupported nice option: {rest[index]}"
return rest[index:], None
if base == "nohup":
if rest[:1] == ["--"]:
rest = rest[1:]
elif rest[:1] and rest[0].startswith("-"):
return None, f"unsupported nohup option: {rest[0]}"
return rest, None
if base == "timeout":
index = 0
while index < len(rest):
token = rest[index]
if token in {"-s", "--signal", "-k", "--kill-after"}:
if index + 1 >= len(rest):
return None, f"timeout option {token} is missing its value"
index += 2
continue
if any(token.startswith(prefix) for prefix in ("--signal=", "--kill-after=")):
if token.endswith("="):
return None, f"timeout option {token.split('=', 1)[0]} is missing its value"
index += 1
continue
if token in {"--preserve-status", "--foreground", "-v", "--verbose"}:
index += 1
continue
if token == "--":
index += 1
break
if token.startswith("-"):
return None, f"unsupported timeout option: {token}"
break
if index >= len(rest):
return None, "timeout is missing its duration"
if not re.fullmatch(r"(?:\d+(?:\.\d*)?|\.\d+)[smhd]?", rest[index], re.IGNORECASE):
return None, f"timeout has an invalid duration: {rest[index]}"
if index + 1 >= len(rest):
return [], None
return rest[index + 1 :], None
if base == "stdbuf":
index = 0
while index < len(rest):
token = rest[index]
if token in {"-i", "-o", "-e", "--input", "--output", "--error"}:
if index + 1 >= len(rest):
return None, f"stdbuf option {token} is missing its value"
index += 2
continue
if re.fullmatch(r"-[ioe].+", token) or any(
token.startswith(prefix) and not token.endswith("=")
for prefix in ("--input=", "--output=", "--error=")
):
index += 1
continue
if token == "--":
return rest[index + 1 :], None
if token.startswith("-"):
return None, f"unsupported stdbuf option: {token}"
return rest[index:], None
return [], None
if base in {"sudo", "doas"}:
index = 0
value_options = {
"-u", "-g", "-h", "-C", "-p", "-b",
"--user", "--group", "--host", "--prompt",
}
context_options = {"-D", "-R", "--chdir", "--chroot"}
flag_options = {
"-A", "-E", "-H", "-K", "-S", "-V", "-n", "-k", "-l", "-s",
"--askpass", "--preserve-env", "--set-home", "--stdin", "--non-interactive",
}
while index < len(rest):
token = rest[index]
if token == "--":
return rest[index + 1 :], None
if token in context_options or any(
token.startswith(option + "=") for option in context_options if option.startswith("--")
):
return None, f"{base} option {token.split('=', 1)[0]} changes execution context"
if token in value_options:
if index + 1 >= len(rest):
return None, f"{base} option {token} is missing its value"
index += 2
continue
if any(token.startswith(option + "=") for option in value_options if option.startswith("--")):
if token.endswith("="):
return None, f"{base} option {token.split('=', 1)[0]} is missing its value"
index += 1
continue
if token in flag_options:
index += 1
continue
if token.startswith("-"):
return None, f"unsupported {base} option: {token}"
return rest[index:], None
return [], None
return None, f"unsupported wrapper: {base}"
def _resolve_command_argv(argv: Sequence[str] | str) -> dict[str, Any]:
"""Resolve structured argv without invoking a shell.
This resolver is intentionally lexical. It narrows execution to commands
the policy can identify, but it is not an OS sandbox and cannot prove that
an allowed test suite has no filesystem side effects.
"""
if isinstance(argv, str):
original = [argv]
return {
"originalArgv": original,
"redactedOriginalArgv": _redact_argv(original),
"effectiveArgv": [],
"normalizedExecutable": "",
"wrapperChain": [],
"status": "ambiguous",
"reason": "shell-form string action argv is not allowed",
}
if not isinstance(argv, Sequence) or isinstance(argv, (bytes, bytearray)):
original = []
return {
"originalArgv": original,
"redactedOriginalArgv": original,
"effectiveArgv": [],
"normalizedExecutable": "",
"wrapperChain": [],
"status": "ambiguous",
"reason": "action argv is not a sequence",
}
if any(not isinstance(token, str) for token in argv):
original = [str(token) for token in argv]
return {
"originalArgv": original,
"redactedOriginalArgv": _redact_argv(original),
"effectiveArgv": [],
"normalizedExecutable": "",
"wrapperChain": [],
"status": "ambiguous",
"reason": "action argv contains a non-string token",
}
original = list(argv)
current = list(original)
wrappers: list[str] = []
for _ in range(32):
if not current:
return {
"originalArgv": original,
"redactedOriginalArgv": _redact_argv(original),
"effectiveArgv": [],
"normalizedExecutable": "",
"wrapperChain": wrappers,
"status": "no_command",
"reason": "no effective command remains after wrapper resolution",
}
base = _command_basename(current[0])
if not base:
return {
"originalArgv": original,
"redactedOriginalArgv": _redact_argv(original),
"effectiveArgv": [],
"normalizedExecutable": "",
"wrapperChain": wrappers,
"status": "no_command",
"reason": "effective command has an empty executable",
}
if base not in _TRANSPARENT_WRAPPERS:
return {
"originalArgv": original,
"redactedOriginalArgv": _redact_argv(original),
"effectiveArgv": _redact_argv(current),
"normalizedExecutable": base,
"wrapperChain": wrappers,
"status": "resolved",
"reason": "effective executable resolved from structured argv",
}
wrappers.append(base)
unwrapped, error = _skip_wrapper_prefix(base, current)
if error is not None or unwrapped is None:
return {
"originalArgv": original,
"redactedOriginalArgv": _redact_argv(original),
"effectiveArgv": [],
"normalizedExecutable": "",
"wrapperChain": wrappers,
"status": "ambiguous",
"reason": error or "wrapper resolution failed",
}
current = list(unwrapped)
return {
"originalArgv": original,
"redactedOriginalArgv": _redact_argv(original),
"effectiveArgv": _redact_argv(current),
"normalizedExecutable": _command_basename(current[0]) if current else "",
"wrapperChain": wrappers,
"status": "ambiguous",
"reason": "transparent wrapper depth limit reached",
}
def _git_subcommand(argv: list[str]) -> str | None:
"""Return the Git subcommand after global options such as ``-C``/``-c``."""
index = 1
options_with_value = {"-C", "-c", "--git-dir", "--work-tree", "--namespace", "--exec-path", "--config-env"}
while index < len(argv):
token = argv[index]
if token in options_with_value:
index += 2
continue
if any(token.startswith(prefix + "=") for prefix in options_with_value if prefix.startswith("--")):
index += 1
continue
if token.startswith("-"):
index += 1
continue
return token.lower()
return None
def _install_requested(base: str, argv: list[str]) -> bool:
tokens = [token.lower() for token in argv[1:]]
if base in {"npm", "pnpm", "yarn", "pip", "pip3", "poetry", "uv", "cargo"}:
return any(token in {"install", "add", "i"} for token in tokens)
if base in {"python", "python3", "py"} or base.startswith("python"):
try:
module_index = tokens.index("-m")
except ValueError:
return False
return (
module_index + 1 < len(tokens)
and tokens[module_index + 1] in {"pip", "pip3"}
and any(token == "install" for token in tokens[module_index + 2 :])
)
return False
def _inline_execution_violation(base: str, argv: list[str]) -> str | None:
"""Reject arbitrary inline programs without inspecting or approving payload text."""
lowered = [token.lower() for token in argv]
if base in {"sh", "bash", "zsh", "fish"} and any(
token == "-c" or (token.startswith("-c") and len(token) > 2) for token in lowered
):
return "arbitrary shell inline execution prohibited"
if base == "cmd" and any(token == "/c" or token.startswith("/c:") for token in lowered):
return "arbitrary cmd inline execution prohibited"
if base in {"powershell", "pwsh"} and any(
token in {"-command", "-c"} or token.startswith("-command:")
for token in lowered
):
return "arbitrary PowerShell inline execution prohibited"
if _is_python(base) and any(
token == "-c" or (token.startswith("-c") and len(token) > 2) for token in lowered
):
return "arbitrary Python inline execution prohibited"
if base in {"node", "nodejs"} and any(
token in {"-e", "--eval"}
or (token.startswith("-e") and not token.startswith("--"))
or token.startswith("--eval=")
for token in lowered
):
return "arbitrary Node inline execution prohibited"
return None
def _is_python(base: str) -> bool:
return base in {"python", "python3", "py"} or bool(re.fullmatch(r"python\d+(?:\.\d+)*", base))
def _structured_command_allowed(base: str, argv: list[str]) -> tuple[bool, str]:
"""Allow only the smallest structured read/verification surface in use."""
lowered = [token.lower() for token in argv]
if base == "git":
subcommand = _git_subcommand(argv)
unsafe_git_options = {
"-c", "-C", "--config-env", "--exec-path", "--git-dir", "--namespace",
"--output", "--work-tree", "--ext-diff", "--textconv",
}
if any(
token in unsafe_git_options
or any(token.startswith(option + "=") for option in unsafe_git_options if option.startswith("--"))
or token.startswith("--open-files-in-pager")
for token in argv[1:]
):
return False, "Git option may change context, write output, or invoke an external helper"
if subcommand in _READ_ONLY_GIT or lowered[1:] in (["--version"], ["-v"]):
return True, "read-only Git command"
return False, "Git command is not on the read-only allowlist"
if _is_python(base):
if lowered[1:] in (["--version"], ["-v"]):
return True, "Python version query"
if len(lowered) >= 3 and lowered[1:3] == ["-m", "unittest"]:
return True, "structured Python unittest verification"
return False, "Python command is not on the structured verification allowlist"
if base in {"node", "nodejs"} and lowered[1:] in (["--version"], ["-v"]):
return True, "Node version query"
return False, "executable is not on the structured command allowlist"
class PolicyGuard:
"""Authorizes an action before an adapter is called.
``canonical_roots`` are protected. Writes are allowed only inside
``writable_roots``. Command execution can occur in a writable root, but
explicit constraints still deny Git mutation, installs, deploys, or other
configured operations.
"""
def __init__(
self,
*,
canonical_roots: Sequence[str | os.PathLike[str]],
writable_roots: Sequence[str | os.PathLike[str]],
prohibit_git_mutation: bool = True,
prohibit_dependency_install: bool = True,
prohibit_deploy: bool = True,
) -> None:
self.canonical_roots = tuple(_resolved(p) for p in canonical_roots)
self.writable_roots = tuple(_resolved(p) for p in writable_roots)
self.prohibit_git_mutation = prohibit_git_mutation
self.prohibit_dependency_install = prohibit_dependency_install
self.prohibit_deploy = prohibit_deploy
def _decision(
self,
*,
session_id: str,
task_id: str,
action: dict[str, Any],
allowed: bool,
reason: str,
constraint: str,
resolution: dict[str, Any] | None = None,
) -> dict[str, Any]:
decision = {
"decisionId": _id("policy"),
"timestamp": _now(),
"sessionId": session_id,
"taskId": task_id,
"requestedOperation": action,
"relevantConstraint": constraint,
"matchedConstraint": constraint,
"decision": "ALLOW" if allowed else "DENY",
"allowed": allowed,
"reason": reason,
"adapterInvocationStatus": "PENDING" if allowed else "NOT_INVOKED",
}
if resolution is not None:
decision.update(
{
"effectiveArgv": resolution["effectiveArgv"],
"normalizedExecutable": resolution["normalizedExecutable"],
"wrapperChain": resolution["wrapperChain"],
"resolutionStatus": resolution["status"],
"resolutionReason": resolution["reason"],
}
)
return decision
def authorize(
self,
*,
session_id: str,
task_id: str,
action: dict[str, Any],
) -> dict[str, Any]:
kind = str(action.get("kind", ""))
if kind == "filesystem.write":
target = _resolved(str(action.get("path", "")))
for root in self.canonical_roots:
if _inside(target, root):
return self._decision(
session_id=session_id,
task_id=task_id,
action=action,
allowed=False,
reason=f"canonical repository write prohibited: {target}",
constraint="no canonical repository modifications",
)
if not any(_inside(target, root) for root in self.writable_roots):
return self._decision(
session_id=session_id,
task_id=task_id,
action=action,
allowed=False,
reason=f"write target is outside authorized roots: {target}",
constraint="writes restricted to declared disposable scope",
)
return self._decision(
session_id=session_id,
task_id=task_id,
action=action,
allowed=True,
reason="write is within authorized disposable scope",
constraint="writes restricted to declared disposable scope",
)
if kind == "process.run":
raw_argv = action.get("argv")
resolution = _resolve_command_argv(raw_argv if raw_argv is not None else [])
safe_action = dict(action)
safe_action["argv"] = resolution["redactedOriginalArgv"]
cwd = _resolved(str(action.get("cwd") or os.getcwd()))
safe_action["cwd"] = str(cwd)
effective_argv = list(resolution["effectiveArgv"])
base = str(resolution["normalizedExecutable"])
lowered = [v.lower() for v in effective_argv]
if resolution["status"] != "resolved":
return self._decision(
session_id=session_id,
task_id=task_id,
action=safe_action,
allowed=False,
reason=f"command resolution failed closed: {resolution['reason']}",
constraint="only unambiguous structured command argv may execute",
resolution=resolution,
)
# Never execute commands from inside a protected repo in this vertical.
if any(_inside(cwd, root) for root in self.canonical_roots):
return self._decision(
session_id=session_id,
task_id=task_id,
action=safe_action,
allowed=False,
reason=f"process working directory is a protected canonical repository: {cwd}",
constraint="no state-changing actions in canonical repositories",
resolution=resolution,
)
git_subcommand = _git_subcommand(effective_argv) if base == "git" else None
if self.prohibit_git_mutation and git_subcommand in _MUTATING_GIT:
return self._decision(
session_id=session_id,
task_id=task_id,
action=safe_action,
allowed=False,
reason=f"Git mutation prohibited: git {git_subcommand}",
constraint="no commit, push, branch, reset, clean, stash, or other Git mutation",
resolution=resolution,
)
if self.prohibit_dependency_install and _install_requested(base, effective_argv):
return self._decision(
session_id=session_id,
task_id=task_id,
action=safe_action,
allowed=False,
reason="dependency installation prohibited",
constraint="no dependency installation",
resolution=resolution,
)
inline_violation = _inline_execution_violation(base, effective_argv)
if inline_violation and (
self.prohibit_git_mutation
or self.prohibit_dependency_install
or self.prohibit_deploy
):
return self._decision(
session_id=session_id,
task_id=task_id,
action=safe_action,
allowed=False,
reason=inline_violation,
constraint="arbitrary inline execution is denied while prohibitions are active",
resolution=resolution,
)
if self.prohibit_deploy and (
any(word in _DEPLOY_WORDS for word in lowered[1:])
or (base in {"vercel", "wrangler", "render", "netlify", "firebase", "fly"} and len(lowered) > 1)
):
return self._decision(
session_id=session_id,
task_id=task_id,
action=safe_action,
allowed=False,
reason="deployment or publishing prohibited",
constraint="no deploy or publish",
resolution=resolution,
)
if not any(_inside(cwd, root) or cwd == root for root in self.writable_roots):
return self._decision(
session_id=session_id,
task_id=task_id,
action=safe_action,
allowed=False,
reason=f"process cwd outside authorized disposable scope: {cwd}",
constraint="process execution restricted to declared disposable scope",
resolution=resolution,
)
command_allowed, command_reason = _structured_command_allowed(base, effective_argv)
if not command_allowed:
return self._decision(
session_id=session_id,
task_id=task_id,
action=safe_action,
allowed=False,
reason=command_reason,
constraint="commands restricted to explicit read-only and verification allowlist",
resolution=resolution,
)
return self._decision(
session_id=session_id,
task_id=task_id,
action=safe_action,
allowed=True,
reason=command_reason,
constraint="commands restricted to explicit read-only and verification allowlist",
resolution=resolution,
)
return self._decision(
session_id=session_id,
task_id=task_id,
action=action,
allowed=False,
reason=f"unknown operation kind: {kind or '<empty>'}",
constraint="deny unknown operations",
)
def guarded_write_text(
path: str | os.PathLike[str],
content: str,
*,
guard: PolicyGuard,
session_id: str,
task_id: str,
) -> tuple[dict[str, Any], dict[str, Any]]:
target = _resolved(path)
decision = guard.authorize(
session_id=session_id,
task_id=task_id,
action={"kind": "filesystem.write", "path": str(target)},
)
if not decision["allowed"]:
raise PolicyDenied(decision)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
decision["adapterInvocationStatus"] = "INVOKED"
evidence = {
"evidenceId": _id("write"),
"timestamp": _now(),
"sessionId": session_id,
"taskId": task_id,
"path": str(target),
"bytes": len(content.encode("utf-8")),
"sha256": _digest(content),
"result": "written",
}
return decision, evidence
def guarded_run(
argv: Sequence[str],
*,
cwd: str | os.PathLike[str],
guard: PolicyGuard,
session_id: str,
task_id: str,
timeout: int = 120,
cancel_check: Callable[[], bool] | None = None,
) -> tuple[dict[str, Any], dict[str, Any]]:
command = [str(v) for v in argv]
workdir = _resolved(cwd)
decision = guard.authorize(
session_id=session_id,
task_id=task_id,
action={"kind": "process.run", "argv": command, "cwd": str(workdir)},
)
if not decision["allowed"]:
raise PolicyDenied(decision)
started = _now()
with tempfile.TemporaryFile(mode="w+b") as stdout_file, tempfile.TemporaryFile(mode="w+b") as stderr_file:
proc = subprocess.Popen(
command,
cwd=str(workdir),
stdout=stdout_file,
stderr=stderr_file,
)
deadline = time.monotonic() + timeout
cancelled = False
while proc.poll() is None:
if cancel_check is not None and cancel_check():
cancelled = True
proc.terminate()
try:
proc.wait(timeout=2)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
break
if time.monotonic() >= deadline:
proc.kill()
proc.wait()
stdout, _, _, _ = _captured_stream(stdout_file)
stderr, _, _, _ = _captured_stream(stderr_file)
raise subprocess.TimeoutExpired(command, timeout, output=stdout, stderr=stderr)
time.sleep(0.05)
proc.wait()
stdout, stdout_digest, stdout_bytes, stdout_truncated = _captured_stream(stdout_file)
stderr, stderr_digest, stderr_bytes, stderr_truncated = _captured_stream(stderr_file)
ended = _now()
decision["adapterInvocationStatus"] = "INVOKED"
evidence = {
"evidenceId": _id("command"),
"sessionId": session_id,
"taskId": task_id,
"argv": _redact_argv(command),
"effectiveArgv": decision["effectiveArgv"],
"normalizedExecutable": decision["normalizedExecutable"],
"wrapperChain": decision["wrapperChain"],
"resolutionStatus": decision["resolutionStatus"],
"matchedConstraint": decision["matchedConstraint"],
"adapterInvocationStatus": decision["adapterInvocationStatus"],
"cwd": str(workdir),
"startedAt": started,
"endedAt": ended,
"exitCode": proc.returncode,
"cancelled": cancelled,
"stdout": stdout,
"stderr": stderr,
"stdoutSha256": stdout_digest,
"stderrSha256": stderr_digest,
"stdoutBytes": stdout_bytes,
"stderrBytes": stderr_bytes,
"stdoutTruncated": stdout_truncated,
"stderrTruncated": stderr_truncated,
}
return decision, evidence
SHA-256: e38b72964360580bd4c7dd956b5d05e3db5cd946867f0422591a45a51a398054