From 0b886295c8c38b1669aa7807b0f5827235a55da3 Mon Sep 17 00:00:00 2001 From: juvdiaz Date: Tue, 30 Jun 2026 13:16:27 -0600 Subject: [PATCH] Add local agent sandbox policy --- README.md | 12 +++++ README.md.tmpl | 12 +++++ docs/jeannie.1.md | 4 ++ infra/agent-sandbox/README.md | 15 +++++++ infra/agent-sandbox/policy.tsv | 19 ++++++++ jeannie | 8 ++++ scripts/agent-sandbox | 82 ++++++++++++++++++++++++++++++++++ scripts/explain | 2 +- 8 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 infra/agent-sandbox/README.md create mode 100644 infra/agent-sandbox/policy.tsv create mode 100755 scripts/agent-sandbox diff --git a/README.md b/README.md index 22fbe7d..0dc53dd 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,18 @@ It summarizes risk, blast radius, required validation, rollback, and forbidden actions. The implementation lives under `infra/safety-cases` and reuses the same impact rules as `./jeannie impact`. +## Agent Sandbox + +Future Jeannie AI actions should be checked before execution. The local sandbox +policy starts with a read-only command classifier: + +```bash +./jeannie agent-sandbox check "./jeannie status" +./jeannie agent-sandbox check "./jeannie nuke" +``` + +Policy is stored in `infra/agent-sandbox/policy.tsv`. + ## Deploying From the Debian server: diff --git a/README.md.tmpl b/README.md.tmpl index 95632c2..e132615 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -206,6 +206,18 @@ It summarizes risk, blast radius, required validation, rollback, and forbidden actions. The implementation lives under `infra/safety-cases` and reuses the same impact rules as `./{{ main_script }} impact`. +## Agent Sandbox + +Future Jeannie AI actions should be checked before execution. The local sandbox +policy starts with a read-only command classifier: + +```bash +./{{ main_script }} agent-sandbox check "./{{ main_script }} status" +./{{ main_script }} agent-sandbox check "./{{ main_script }} nuke" +``` + +Policy is stored in `infra/agent-sandbox/policy.tsv`. + ## Deploying From the Debian server: diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index be3bcc6..e730db1 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -96,6 +96,10 @@ explicitly approved. : Generate a read-only safety case for a Git diff or explicit paths, including risk, blast radius, required validation, rollback, and forbidden actions. +`agent-sandbox {check|policy}` +: Classify proposed commands against repo-managed agent action policy. The first +version is read-only and reports `allow`, `approval`, or `deny`. + `scorecard` : Roll up backup readiness, GitOps health, DNS/RPi health, cluster health, edge health, capacity pressure, security posture, public certificates, inventory, and diff --git a/infra/agent-sandbox/README.md b/infra/agent-sandbox/README.md new file mode 100644 index 0000000..7847097 --- /dev/null +++ b/infra/agent-sandbox/README.md @@ -0,0 +1,15 @@ +# Local Agent Sandbox + +The agent sandbox is a command policy layer for future Jeannie AI actions. The +first implementation is read-only: it classifies proposed commands, explains why +they are allowed or blocked, and writes no state. + +Run: + +```bash +./jeannie agent-sandbox check "./jeannie status" +./jeannie agent-sandbox check "./jeannie nuke" +``` + +Policy lives in `infra/agent-sandbox/policy.tsv`. + diff --git a/infra/agent-sandbox/policy.tsv b/infra/agent-sandbox/policy.tsv new file mode 100644 index 0000000..2bc74bf --- /dev/null +++ b/infra/agent-sandbox/policy.tsv @@ -0,0 +1,19 @@ +pattern decision reason +./jeannie status allow Read-only health report. +./jeannie doctor- allow Read-only focused doctor command. +./jeannie capacity allow Read-only capacity report. +./jeannie validate allow Read-only repository validation. +./jeannie safety-case allow Read-only change safety review. +./jeannie incident allow Read-only incident triage/replay. +./jeannie ai-evals allow Read-only eval harness. +./jeannie prompt-injection-lab allow Read-only security lab harness. +./jeannie up approval High-blast-radius deployment. +./jeannie apps approval Application deployment can mutate cluster state. +./jeannie rpi-services approval Mutates RPi Docker services. +./jeannie nuke deny Destructive cluster state path. +tofu destroy deny Destructive infrastructure path. +terraform destroy deny Destructive infrastructure path. +rm -rf deny Destructive file deletion pattern. +kubectl delete approval Kubernetes deletion needs explicit scope and approval. +docker rm approval Docker deletion needs explicit scope and approval. + diff --git a/jeannie b/jeannie index 9838aeb..45f54ff 100755 --- a/jeannie +++ b/jeannie @@ -6334,6 +6334,10 @@ safety_case() { "${REPO_ROOT}/scripts/safety-case" "${@:2}" } +agent_sandbox() { + "${REPO_ROOT}/scripts/agent-sandbox" "${@:2}" +} + validate_homelab() { "${REPO_ROOT}/scripts/validate-homelab" } @@ -6529,6 +6533,7 @@ Operator View And Reports recover-power [--dry-run] Run or preview post-outage recovery. incident {list|replay|triage} Classify incident text and suggest read-only next steps. safety-case [--since REF] [PATH...] Generate risk, blast-radius, test, and rollback case. + agent-sandbox {check|policy} Classify proposed commands against agent policy. impact [--since REF] [PATH...] Explain affected lab areas before changes. review-last-change [REF] Summarize changed files, impact, validation. map [--dot] Print the homelab dependency map. @@ -6700,6 +6705,9 @@ case "${1:-}" in safety-case) safety_case "$@" ;; + agent-sandbox) + agent_sandbox "$@" + ;; scorecard) scorecard ;; diff --git a/scripts/agent-sandbox b/scripts/agent-sandbox new file mode 100755 index 0000000..ec02a1b --- /dev/null +++ b/scripts/agent-sandbox @@ -0,0 +1,82 @@ +#!/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()) diff --git a/scripts/explain b/scripts/explain index d9e93b7..ea50ca8 100755 --- a/scripts/explain +++ b/scripts/explain @@ -34,7 +34,7 @@ EOF is_read_only_command() { case "$1" in - status|capacity|capacity-advisor|capacity-limits|recover-plan|incident|safety-case|scorecard|gitops-status|cert-check|backup-status|synthetic-checks|resource-budget|route-inventory|golden-ledger|workers|change-journal|map|validate|access-audit|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|secrets-check|tailnet-policy-check|ai-check|ask|ai-evals|impact|review-last-change|security-scan|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-attack-path|prompt-injection-lab|control-plane) + status|capacity|capacity-advisor|capacity-limits|recover-plan|incident|safety-case|agent-sandbox|scorecard|gitops-status|cert-check|backup-status|synthetic-checks|resource-budget|route-inventory|golden-ledger|workers|change-journal|map|validate|access-audit|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|secrets-check|tailnet-policy-check|ai-check|ask|ai-evals|impact|review-last-change|security-scan|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-attack-path|prompt-injection-lab|control-plane) return 0 ;; *)