Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a101af-e240-7882-a1e3-e37720cbdbea
163 lines
9.2 KiB
Python
163 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Run one bounded, read-only provider knowledge probe in a disposable fixture.
|
|
|
|
Uses the installed provider's configured model/auth, never reads credentials.
|
|
Raw client diagnostics stay mode 0600 under /tmp, not in repository evidence.
|
|
This is a comprehension probe, not a production or harness-isolation proof.
|
|
"""
|
|
from pathlib import Path
|
|
import argparse
|
|
from datetime import datetime, timezone
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import uuid
|
|
|
|
import yaml
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
from emergent_security.knowledge import Knowledge
|
|
|
|
|
|
def extract_json(text):
|
|
text = text.strip()
|
|
if text.startswith("```"):
|
|
text = text.split("\n", 1)[1].rsplit("```", 1)[0].strip()
|
|
return json.loads(text)
|
|
|
|
|
|
def unwrap_response(response):
|
|
"""Provider envelopes differ; do not grade an envelope as the actual answer."""
|
|
if not isinstance(response, dict):
|
|
raise ValueError("expected response object")
|
|
metadata = {}
|
|
if "structured_output" in response or "structuredOutput" in response:
|
|
metadata = {"session_id": response.get("session_id", response.get("sessionId")),
|
|
"model_usage": list(response.get("modelUsage", {})),
|
|
"usage": response.get("usage")}
|
|
response = response.get("structured_output", response.get("structuredOutput"))
|
|
elif "result" in response and isinstance(response["result"], str):
|
|
metadata["session_id"] = response.get("session_id")
|
|
response = extract_json(response["result"])
|
|
if not isinstance(response, dict) or not isinstance(response.get("answers"), list):
|
|
raise ValueError("missing structured answers")
|
|
return response, metadata
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("provider", choices=["codex", "claude", "grok"])
|
|
parser.add_argument("--output", type=Path, required=True)
|
|
parser.add_argument("--timeout", type=int, default=180)
|
|
parser.add_argument("--case", action="append", help="Run only this scenario ID (repeatable)")
|
|
args = parser.parse_args()
|
|
package = Knowledge(ROOT / "knowledge")
|
|
corpus = yaml.safe_load((ROOT / "tests/fixtures/knowledge/scenarios.yaml").read_text())["cases"]
|
|
if args.case:
|
|
if not set(args.case) <= {case["id"] for case in corpus}:
|
|
raise SystemExit("Unknown scenario ID")
|
|
corpus = [case for case in corpus if case["id"] in args.case]
|
|
work = Path(tempfile.mkdtemp(prefix=f"emergent-{args.provider}-probe-"))
|
|
os.chmod(work, 0o700)
|
|
shutil.copytree(ROOT / "knowledge", work / "knowledge")
|
|
startup_marker, retrieval_marker = uuid.uuid4().hex, uuid.uuid4().hex
|
|
with (work / "knowledge/cards/start.md").open("a") as f:
|
|
f.write(f"\nFixture-only startup marker: {startup_marker}\n")
|
|
with (work / "knowledge/cards/access.md").open("a") as f:
|
|
f.write(f"\nFixture-only retrieval marker: {retrieval_marker}\n")
|
|
# Markers instrument the test copy, never the sealed release.
|
|
instruction = (
|
|
"# Disposable knowledge evaluation\nRead knowledge/cards/start.md before answering. "
|
|
"Read task-specific cards under knowledge/cards/ as needed, including access.md. "
|
|
"Use only these fixture files. No shell effects, network tools, edits, credentials, "
|
|
"other repositories, or subagents. Read-only evaluation, not actual operations. "
|
|
f"The fixture is instrumented from package {package.catalog['version']}; marker "
|
|
"additions invalidate its copy's seal intentionally. Do not modify the original.\n"
|
|
)
|
|
(work / "AGENTS.md").write_text(instruction)
|
|
(work / "CLAUDE.md").write_text(instruction)
|
|
(work / "questions.json").write_text(json.dumps([{k: c[k] for k in ("id", "question")} for c in corpus], indent=2))
|
|
schema = {"type": "object", "additionalProperties": False,
|
|
"properties": {"package_version": {"type": "string"},
|
|
"startup_marker": {"type": "string"},
|
|
"retrieval_marker": {"type": "string"},
|
|
"answers": {"type": "array", "items": {
|
|
"type": "object", "additionalProperties": False,
|
|
"properties": {k: {"type": "string"} for k in ("id", "route", "disposition", "sources", "reason")},
|
|
"required": ["id", "route", "disposition", "sources", "reason"]}}},
|
|
"required": ["package_version", "startup_marker", "retrieval_marker", "answers"]}
|
|
(work / "response-schema.json").write_text(json.dumps(schema))
|
|
prompt = (
|
|
"Perform this read-only knowledge evaluation. Follow the workspace orientation "
|
|
"instructions and answer every question in questions.json. Read the knowledge cards; "
|
|
"do not use outside sources or perform any actual operation. Return ONLY JSON with "
|
|
"package_version, startup_marker (from start.md), retrieval_marker (from access.md), "
|
|
"and answers: [{id, route, disposition, sources, reason}]. Route means the actual "
|
|
"next recipient (a human role such as delegator, platform, tenant-admin, root-steward "
|
|
"or pep-owner when appropriate, otherwise the owning repository), NOT the source "
|
|
"of documentation. Use a concise action for disposition, comma-separated exact source owner IDs "
|
|
"listed in the relevant card for sources, and a one-sentence explanation. Cite the "
|
|
"most specific source, including emergent-security for its generic envelope and "
|
|
"escalation semantics. Do not invent owner IDs or marker values."
|
|
)
|
|
if args.provider == "codex":
|
|
command = ["codex", "exec", "--ephemeral", "--sandbox", "read-only", "--skip-git-repo-check",
|
|
"--cd", str(work), "--json", "--output-schema", str(work / "response-schema.json"),
|
|
"--output-last-message", str(work / "answer.json"), prompt]
|
|
elif args.provider == "claude":
|
|
command = ["claude", "-p", "--tools", "Read", "--permission-mode", "default",
|
|
"--no-session-persistence", "--max-budget-usd", "3", "--output-format", "json",
|
|
"--json-schema", json.dumps(schema), prompt]
|
|
else:
|
|
command = ["grok", "--cwd", str(work), "--permission-mode", "plan", "--no-subagents",
|
|
"--disable-web-search", "--max-turns", "15", "--output-format", "json",
|
|
"--json-schema", json.dumps(schema), "-p", prompt]
|
|
report = {"schema": "emergent.provider-probe.v1", "provider": args.provider,
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"package_version": package.catalog["version"], "package_digest": package.package_digest,
|
|
"mode": "native-file-pointer-probe", "model_selection": "installed-config-default",
|
|
"scope": "disposable offline fixtures; no live operations",
|
|
"scenario_ids": [case["id"] for case in corpus],
|
|
"status": "unverified", "work_directory": str(work)}
|
|
try:
|
|
result = subprocess.run(command, cwd=work, capture_output=True, text=True, timeout=args.timeout)
|
|
for name, text in [("stdout.txt", result.stdout), ("stderr.txt", result.stderr)]:
|
|
path = work / name
|
|
path.write_text(text)
|
|
os.chmod(path, 0o600)
|
|
report["exit_code"] = result.returncode
|
|
combined = result.stdout + result.stderr
|
|
if result.returncode:
|
|
report["status"] = "client-failed"
|
|
report["failure_category"] = ("filesystem-permission" if any(s in combined.lower() for s in
|
|
["permission denied", "read-only file system", "eperm", "eacces"])
|
|
else "client-or-auth-or-network")
|
|
else:
|
|
raw = ((work / "answer.json").read_text() if args.provider == "codex" else result.stdout)
|
|
response, metadata = unwrap_response(extract_json(raw))
|
|
report.update(metadata)
|
|
report["startup_read_proven"] = response.get("startup_marker") == startup_marker
|
|
report["deeper_read_proven"] = response.get("retrieval_marker") == retrieval_marker
|
|
answers = response.get("answers", [])
|
|
expected_ids = {c["id"] for c in corpus}
|
|
report["all_cases_answered"] = (len(answers) == len(corpus) and {a.get("id") for a in answers} == expected_ids)
|
|
report["package_version_correct"] = response.get("package_version") == package.catalog["version"]
|
|
report["answers"] = answers
|
|
report["status"] = "awaiting-semantic-review" if all(report[k] for k in
|
|
("startup_read_proven", "deeper_read_proven", "all_cases_answered", "package_version_correct")) else "probe-failed"
|
|
except subprocess.TimeoutExpired:
|
|
report["status"] = "timeout"
|
|
except (OSError, ValueError, KeyError, TypeError):
|
|
report["status"] = "client-output-unusable"
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(json.dumps(report, indent=2) + "\n")
|
|
print(json.dumps({k: v for k, v in report.items() if k != "answers"}, indent=2))
|
|
return 0 if report["status"] == "awaiting-semantic-review" else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|