83 lines
2.5 KiB
Python
Executable File
83 lines
2.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Classify proposed agent commands against the homelab sandbox policy."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
POLICY_FILE = REPO_ROOT / "infra" / "agent-sandbox" / "policy.tsv"
|
|
|
|
|
|
def load_policy() -> list[dict[str, str]]:
|
|
with POLICY_FILE.open(encoding="utf-8", newline="") as handle:
|
|
return list(csv.DictReader(handle, delimiter="\t"))
|
|
|
|
|
|
def classify(command: str) -> dict[str, str]:
|
|
normalized = " ".join(command.strip().split())
|
|
best: dict[str, str] | None = None
|
|
for rule in load_policy():
|
|
pattern = rule["pattern"]
|
|
if pattern in normalized:
|
|
if best is None or len(pattern) > len(best["pattern"]):
|
|
best = rule
|
|
if best is None:
|
|
return {
|
|
"command": normalized,
|
|
"decision": "approval",
|
|
"pattern": "default",
|
|
"reason": "Unknown command; require explicit approval before execution.",
|
|
}
|
|
return {
|
|
"command": normalized,
|
|
"decision": best["decision"],
|
|
"pattern": best["pattern"],
|
|
"reason": best["reason"],
|
|
}
|
|
|
|
|
|
def print_result(result: dict[str, str]) -> int:
|
|
print("Agent Sandbox")
|
|
print("=============")
|
|
print(f"command: {result['command']}")
|
|
print(f"decision: {result['decision']}")
|
|
print(f"matched: {result['pattern']}")
|
|
print(f"reason: {result['reason']}")
|
|
if result["decision"] == "allow":
|
|
return 0
|
|
if result["decision"] == "approval":
|
|
return 2
|
|
return 3
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
check_parser = subparsers.add_parser("check")
|
|
check_parser.add_argument("candidate", nargs="+")
|
|
check_parser.add_argument("--json", action="store_true")
|
|
subparsers.add_parser("policy")
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "policy":
|
|
for rule in load_policy():
|
|
print(f"{rule['decision']:8} {rule['pattern']} - {rule['reason']}")
|
|
return 0
|
|
if args.command == "check":
|
|
result = classify(" ".join(args.candidate))
|
|
if args.json:
|
|
print(json.dumps(result, indent=2, sort_keys=True))
|
|
return 0 if result["decision"] == "allow" else 2
|
|
return print_result(result)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|