Add local agent sandbox policy

This commit is contained in:
juvdiaz 2026-06-30 13:16:27 -06:00
parent c7956638b3
commit 0b886295c8
8 changed files with 153 additions and 1 deletions

View File

@ -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 actions. The implementation lives under `infra/safety-cases` and reuses the
same impact rules as `./jeannie impact`. 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 ## Deploying
From the Debian server: From the Debian server:

View File

@ -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 actions. The implementation lives under `infra/safety-cases` and reuses the
same impact rules as `./{{ main_script }} impact`. 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 ## Deploying
From the Debian server: From the Debian server:

View File

@ -96,6 +96,10 @@ explicitly approved.
: Generate a read-only safety case for a Git diff or explicit paths, including : Generate a read-only safety case for a Git diff or explicit paths, including
risk, blast radius, required validation, rollback, and forbidden actions. 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` `scorecard`
: Roll up backup readiness, GitOps health, DNS/RPi health, cluster health, edge : Roll up backup readiness, GitOps health, DNS/RPi health, cluster health, edge
health, capacity pressure, security posture, public certificates, inventory, and health, capacity pressure, security posture, public certificates, inventory, and

View File

@ -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`.

View File

@ -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.
1 pattern decision reason
2 ./jeannie status allow Read-only health report.
3 ./jeannie doctor- allow Read-only focused doctor command.
4 ./jeannie capacity allow Read-only capacity report.
5 ./jeannie validate allow Read-only repository validation.
6 ./jeannie safety-case allow Read-only change safety review.
7 ./jeannie incident allow Read-only incident triage/replay.
8 ./jeannie ai-evals allow Read-only eval harness.
9 ./jeannie prompt-injection-lab allow Read-only security lab harness.
10 ./jeannie up approval High-blast-radius deployment.
11 ./jeannie apps approval Application deployment can mutate cluster state.
12 ./jeannie rpi-services approval Mutates RPi Docker services.
13 ./jeannie nuke deny Destructive cluster state path.
14 tofu destroy deny Destructive infrastructure path.
15 terraform destroy deny Destructive infrastructure path.
16 rm -rf deny Destructive file deletion pattern.
17 kubectl delete approval Kubernetes deletion needs explicit scope and approval.
18 docker rm approval Docker deletion needs explicit scope and approval.

View File

@ -6334,6 +6334,10 @@ safety_case() {
"${REPO_ROOT}/scripts/safety-case" "${@:2}" "${REPO_ROOT}/scripts/safety-case" "${@:2}"
} }
agent_sandbox() {
"${REPO_ROOT}/scripts/agent-sandbox" "${@:2}"
}
validate_homelab() { validate_homelab() {
"${REPO_ROOT}/scripts/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. recover-power [--dry-run] Run or preview post-outage recovery.
incident {list|replay|triage} Classify incident text and suggest read-only next steps. 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. 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. impact [--since REF] [PATH...] Explain affected lab areas before changes.
review-last-change [REF] Summarize changed files, impact, validation. review-last-change [REF] Summarize changed files, impact, validation.
map [--dot] Print the homelab dependency map. map [--dot] Print the homelab dependency map.
@ -6700,6 +6705,9 @@ case "${1:-}" in
safety-case) safety-case)
safety_case "$@" safety_case "$@"
;; ;;
agent-sandbox)
agent_sandbox "$@"
;;
scorecard) scorecard)
scorecard scorecard
;; ;;

82
scripts/agent-sandbox Executable file
View File

@ -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())

View File

@ -34,7 +34,7 @@ EOF
is_read_only_command() { is_read_only_command() {
case "$1" in 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 return 0
;; ;;
*) *)