Finish coding-agent high-risk boundary coverage
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
This commit is contained in:
parent
7a1dcb8a52
commit
429cc912ed
12 changed files with 720 additions and 54 deletions
146
scripts/agent_high_risk_boundary.py
Normal file
146
scripts/agent_high_risk_boundary.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Check the platform-owned agent deny policy against ops-warden's risk input."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_ARTIFACT = (
|
||||
REPO_DIR / "openbao/policies/inputs/ops-warden-high-risk-data-paths.yaml"
|
||||
)
|
||||
DEFAULT_POLICY = REPO_DIR / "openbao/policies/agent-high-risk-boundary.hcl"
|
||||
|
||||
|
||||
def load_artifact(path: Path) -> dict[str, Any]:
|
||||
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"artifact must be a mapping: {path}")
|
||||
return data
|
||||
|
||||
|
||||
def parse_policy(text: str) -> dict[str, set[str]]:
|
||||
paths: dict[str, set[str]] = {}
|
||||
for match in re.finditer(
|
||||
r'path\s+"([^"]+)"\s*\{[^}]*?capabilities\s*=\s*\[([^\]]*)\]',
|
||||
text,
|
||||
re.DOTALL,
|
||||
):
|
||||
paths[match.group(1)] = {
|
||||
value.strip().strip('"\'')
|
||||
for value in match.group(2).split(",")
|
||||
if value.strip()
|
||||
}
|
||||
return paths
|
||||
|
||||
|
||||
def check_boundary(
|
||||
artifact: dict[str, Any], policy: dict[str, set[str]]
|
||||
) -> dict[str, Any]:
|
||||
errors: list[str] = []
|
||||
rows = artifact.get("paths")
|
||||
no_concrete = artifact.get("no_concrete_path")
|
||||
if not isinstance(rows, list):
|
||||
rows = []
|
||||
errors.append("artifact paths must be a list")
|
||||
if not isinstance(no_concrete, list):
|
||||
no_concrete = []
|
||||
errors.append("artifact no_concrete_path must be a list")
|
||||
if artifact.get("catalog_dirty") is not False:
|
||||
errors.append("artifact catalog_dirty must be false")
|
||||
if artifact.get("concrete_path_count") != len(rows):
|
||||
errors.append("artifact concrete_path_count does not match paths")
|
||||
if artifact.get("high_risk_lane_count") != len(rows) + len(no_concrete):
|
||||
errors.append("artifact high_risk_lane_count does not match its entries")
|
||||
|
||||
seen_ids: set[str] = set()
|
||||
unique_paths: set[str] = set()
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
errors.append("artifact path entry must be a mapping")
|
||||
continue
|
||||
lane_id = row.get("id")
|
||||
data_path = row.get("data_path")
|
||||
metadata_path = row.get("metadata_path")
|
||||
if not isinstance(lane_id, str) or not lane_id:
|
||||
errors.append("artifact path entry has no id")
|
||||
continue
|
||||
if lane_id in seen_ids:
|
||||
errors.append(f"duplicate lane id: {lane_id}")
|
||||
seen_ids.add(lane_id)
|
||||
if not isinstance(data_path, str) or "/data/" not in data_path:
|
||||
errors.append(f"{lane_id}: invalid data_path")
|
||||
continue
|
||||
expected_metadata = data_path.replace("/data/", "/metadata/", 1)
|
||||
if metadata_path != expected_metadata:
|
||||
errors.append(f"{lane_id}: metadata_path does not match data_path")
|
||||
continue
|
||||
unique_paths.add(data_path)
|
||||
if "deny" not in policy.get(data_path, set()):
|
||||
errors.append(f"{lane_id}: data path is not denied: {data_path}")
|
||||
if "read" not in policy.get(metadata_path, set()):
|
||||
errors.append(f"{lane_id}: metadata path is not readable: {metadata_path}")
|
||||
|
||||
return {
|
||||
"catalog_revision": artifact.get("catalog_revision"),
|
||||
"high_risk_lanes": artifact.get("high_risk_lane_count"),
|
||||
"concrete_entries": len(rows),
|
||||
"unique_concrete_paths": len(unique_paths),
|
||||
"no_concrete_paths": len(no_concrete),
|
||||
"errors": errors,
|
||||
"ok": not errors,
|
||||
}
|
||||
|
||||
|
||||
def comparable_artifact(data: dict[str, Any]) -> dict[str, Any]:
|
||||
return {key: value for key, value in data.items() if key != "generated_at"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--artifact", type=Path, default=DEFAULT_ARTIFACT)
|
||||
parser.add_argument("--policy", type=Path, default=DEFAULT_POLICY)
|
||||
parser.add_argument(
|
||||
"--upstream",
|
||||
type=Path,
|
||||
help="optionally require the vendored artifact to match this upstream copy",
|
||||
)
|
||||
parser.add_argument("--json", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
artifact = load_artifact(args.artifact)
|
||||
policy = parse_policy(args.policy.read_text(encoding="utf-8"))
|
||||
report = check_boundary(artifact, policy)
|
||||
if args.upstream:
|
||||
upstream = load_artifact(args.upstream)
|
||||
if comparable_artifact(artifact) != comparable_artifact(upstream):
|
||||
report["errors"].append("vendored artifact differs from upstream")
|
||||
report["ok"] = False
|
||||
except (OSError, ValueError, yaml.YAMLError) as exc:
|
||||
report = {"ok": False, "errors": [str(exc)]}
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
elif report["ok"]:
|
||||
print(
|
||||
"PASS: agent boundary covers "
|
||||
f"{report['concrete_entries']} high-risk catalog entries at "
|
||||
f"{report['catalog_revision']}"
|
||||
)
|
||||
else:
|
||||
for error in report["errors"]:
|
||||
print(f"FAIL: {error}", file=sys.stderr)
|
||||
return 0 if report["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
140
scripts/verify_coding_agent_approle.py
Normal file
140
scripts/verify_coding_agent_approle.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Mint, verify, and revoke a coding-agent AppRole token without printing it."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
|
||||
DEFAULT_ADDR = "https://bao.coulomb.social"
|
||||
DATA_PATH = "platform/data/workloads/issue-core/issue-core/issue-core-runtime"
|
||||
METADATA_PATH = (
|
||||
"platform/metadata/workloads/issue-core/issue-core/issue-core-runtime"
|
||||
)
|
||||
EXPECTED_POLICIES = {
|
||||
"agent-high-risk-boundary",
|
||||
"workload-kv-read-issue-core-runtime",
|
||||
}
|
||||
|
||||
|
||||
def bao_json(args: list[str]) -> dict[str, Any]:
|
||||
completed = subprocess.run(
|
||||
["bao", *args],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def api_request(
|
||||
addr: str,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
token: str | None = None,
|
||||
payload: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if token:
|
||||
headers["X-Vault-Token"] = token
|
||||
body = json.dumps(payload).encode() if payload is not None else None
|
||||
request = urllib.request.Request(
|
||||
f"{addr.rstrip('/')}/v1/{path}", data=body, headers=headers, method=method
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
content = response.read()
|
||||
return json.loads(content) if content else {}
|
||||
|
||||
|
||||
def verify(role: str, addr: str) -> dict[str, Any]:
|
||||
role_payload = bao_json(
|
||||
["read", "-format=json", f"auth/approle/role/{role}/role-id"]
|
||||
)
|
||||
role_id = role_payload["data"]["role_id"]
|
||||
secret_payload = bao_json(
|
||||
["write", "-format=json", "-f", f"auth/approle/role/{role}/secret-id"]
|
||||
)
|
||||
secret_id = secret_payload["data"]["secret_id"]
|
||||
|
||||
agent_token: str | None = None
|
||||
report: dict[str, Any] = {}
|
||||
try:
|
||||
login = api_request(
|
||||
addr,
|
||||
"POST",
|
||||
"auth/approle/login",
|
||||
payload={"role_id": role_id, "secret_id": secret_id},
|
||||
)
|
||||
agent_token = login["auth"]["client_token"]
|
||||
lookup = api_request(addr, "GET", "auth/token/lookup-self", token=agent_token)
|
||||
capabilities = api_request(
|
||||
addr,
|
||||
"POST",
|
||||
"sys/capabilities-self",
|
||||
token=agent_token,
|
||||
payload={"paths": [DATA_PATH, METADATA_PATH]},
|
||||
)
|
||||
policies = set(lookup["data"].get("policies", []))
|
||||
ttl = int(lookup["data"].get("ttl", 0))
|
||||
data_caps = capabilities.get(DATA_PATH, [])
|
||||
metadata_caps = capabilities.get(METADATA_PATH, [])
|
||||
errors: list[str] = []
|
||||
if policies != EXPECTED_POLICIES:
|
||||
errors.append(f"unexpected policies: {sorted(policies)}")
|
||||
if ttl <= 0 or ttl > 900:
|
||||
errors.append(f"TTL outside 1..900 seconds: {ttl}")
|
||||
if data_caps != ["deny"]:
|
||||
errors.append(f"data capabilities are not deny: {data_caps}")
|
||||
if "read" not in metadata_caps or "deny" in metadata_caps:
|
||||
errors.append(f"metadata capabilities are not read-only: {metadata_caps}")
|
||||
report = {
|
||||
"role": role,
|
||||
"policies": sorted(policies),
|
||||
"ttl": ttl,
|
||||
"data_capabilities": data_caps,
|
||||
"metadata_capabilities": metadata_caps,
|
||||
"deny_wins": data_caps == ["deny"],
|
||||
"errors": errors,
|
||||
"ok": not errors,
|
||||
}
|
||||
finally:
|
||||
if agent_token:
|
||||
try:
|
||||
api_request(addr, "POST", "auth/token/revoke-self", token=agent_token)
|
||||
except (OSError, urllib.error.HTTPError):
|
||||
report.setdefault("errors", []).append("test token self-revocation failed")
|
||||
report["ok"] = False
|
||||
return report
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--role", default="coding-agent-railiance-platform")
|
||||
parser.add_argument("--addr", default=os.environ.get("BAO_ADDR", DEFAULT_ADDR))
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
report = verify(args.role, args.addr)
|
||||
except (
|
||||
KeyError,
|
||||
json.JSONDecodeError,
|
||||
OSError,
|
||||
subprocess.CalledProcessError,
|
||||
subprocess.TimeoutExpired,
|
||||
urllib.error.HTTPError,
|
||||
) as exc:
|
||||
report = {"role": args.role, "ok": False, "errors": [str(exc)]}
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
return 0 if report["ok"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue