@arbiterforge/ca-pi 0.14.10 → 0.14.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -9
- package/package.json +2 -1
- package/plugins/ca-pi/CHANGELOG.md +67 -0
- package/plugins/ca-pi/LICENSE +661 -0
- package/plugins/ca-pi/SKILLS.md +2 -2
- package/plugins/ca-pi/SPRINT.md +47 -0
- package/plugins/ca-pi/THIRD_PARTY_NOTICES.md +40 -0
- package/plugins/ca-pi/generated/command-catalog.json +2 -2
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/release.json +6 -6
- package/plugins/ca-pi/hooks/_approvallib.py +58 -22
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +2135 -0
- package/plugins/ca-pi/hooks/_artifactlib.py +164 -7
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +177 -0
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_overrideappendlib.py +5 -1
- package/plugins/ca-pi/hooks/_prerequisitelib.py +44 -22
- package/plugins/ca-pi/hooks/_reconciliationlib.py +472 -0
- package/plugins/ca-pi/hooks/_releaselib.py +345 -111
- package/plugins/ca-pi/hooks/_sprintapprovallib.py +302 -0
- package/plugins/ca-pi/hooks/artifact-authority-hook.py +127 -0
- package/plugins/ca-pi/hooks/artifact-authority.py +102 -0
- package/plugins/ca-pi/hooks/artifact-reconcile.py +57 -0
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/prompt-submit.py +20 -3
- package/plugins/ca-pi/hooks/releasehash.py +36 -23
- package/plugins/ca-pi/includes/artifacts.md +120 -13
- package/plugins/ca-pi/includes/sprint-authority.md +69 -0
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +7 -4
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +97 -29
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +7 -1
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +32 -15
- package/plugins/ca-pi/routines/release/SKILL.md +220 -70
- package/plugins/ca-pi/routines/skill-author/SKILL.md +26 -5
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +22 -10
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +170 -16
- package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +116 -21
- package/plugins/ca-pi/skills/ca-release/SKILL.md +6 -4
|
@@ -0,0 +1,2135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter — durable closed authority producers for structured artifacts.
|
|
3
|
+
"""Supervise verification and correlate independent Codex or Claude review evidence.
|
|
4
|
+
|
|
5
|
+
arm_request(...) -> dict
|
|
6
|
+
run_verification(...) -> dict
|
|
7
|
+
observe_codex_hook(...) -> dict
|
|
8
|
+
observe_claude_hook(...) -> dict
|
|
9
|
+
publish_request(...) -> dict
|
|
10
|
+
|
|
11
|
+
The artifact engine owns every target binding. Callers select only an activity
|
|
12
|
+
and record; they cannot supply an authority kind, verdict, evidence payload, or
|
|
13
|
+
reviewer identity. External work is journaled before launch, observations are
|
|
14
|
+
immutable and content-addressed, and replay never silently reruns a completed
|
|
15
|
+
attempt. Codex and Claude review correlation use stable hook fields and
|
|
16
|
+
deliberately ignore transcript bytes whose format is not a public contract.
|
|
17
|
+
|
|
18
|
+
Claude Code correlation (recorded Claude Code 2.1.281 payloads): a Bash or
|
|
19
|
+
Agent PreToolUse/PostToolUse pair shares session_id, prompt_id and tool_use_id;
|
|
20
|
+
the Agent PostToolUse tool_response carries the child's agentId; SubagentStart
|
|
21
|
+
and SubagentStop carry only agent_id and the parent's *current* prompt_id, so
|
|
22
|
+
child lifecycle events are bound by agent_id alone. One agent can emit more
|
|
23
|
+
than one SubagentStop; only the first is a review result.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
import re
|
|
33
|
+
import secrets
|
|
34
|
+
import shlex
|
|
35
|
+
import shutil
|
|
36
|
+
import stat
|
|
37
|
+
import subprocess
|
|
38
|
+
import tempfile
|
|
39
|
+
import threading
|
|
40
|
+
import time
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
from _gitexec import git_executable, root_bound_git_env
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
OBSERVATION_DIR = Path(".codearbiter/.artifacts/observations")
|
|
47
|
+
SOURCE_DIR = Path(".codearbiter/.artifacts/authority-sources")
|
|
48
|
+
STATE_FORMAT = "codearbiter.authority-run/0.1.0"
|
|
49
|
+
OBSERVATION_FORMAT = "codearbiter.observation/0.2.0"
|
|
50
|
+
EVENT_FORMAT = "codearbiter.workflow-event/0.2.0"
|
|
51
|
+
DECISION_FORMAT = "codearbiter.review-decision/0.1.0"
|
|
52
|
+
ACTIVITIES = frozenset({"verification", "spec_review", "quality_review"})
|
|
53
|
+
REVIEW_ACTIVITIES = frozenset({"spec_review", "quality_review"})
|
|
54
|
+
STATES = frozenset({
|
|
55
|
+
"ARMED", "LAUNCHING", "LAUNCHED", "RUNNING", "COMPLETED", "REJECTED",
|
|
56
|
+
"FAILED", "ABANDONED", "CAPTURED",
|
|
57
|
+
})
|
|
58
|
+
TERMINAL_STATES = frozenset({"REJECTED", "FAILED", "ABANDONED", "CAPTURED"})
|
|
59
|
+
SHA256_RE = re.compile(r"[0-9a-f]{64}")
|
|
60
|
+
ID_RE = re.compile(r"[A-Z][A-Z0-9_-]{0,127}")
|
|
61
|
+
REQUEST_RE = re.compile(r"[0-9a-f]{64}")
|
|
62
|
+
TOKEN_RE = re.compile(r"[A-Za-z0-9_-]{12,128}")
|
|
63
|
+
HOST_ID_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}")
|
|
64
|
+
MAX_STATE = 1 << 20
|
|
65
|
+
MAX_OUTPUT = 8 << 20
|
|
66
|
+
MAX_DECISION = 65536
|
|
67
|
+
REGISTRY_PARENT = Path(tempfile.gettempdir())
|
|
68
|
+
HOSTS = frozenset({"codex", "claude"})
|
|
69
|
+
CLAUDE_REVIEWER = "ca:authority-reviewer"
|
|
70
|
+
CLAUDE_REVIEWER_MODELS = frozenset({"opus", "sonnet", "haiku"})
|
|
71
|
+
CLAUDE_REVIEW_PROFILE = "claude-review/0.1.0"
|
|
72
|
+
CODEX_REVIEW_PROFILE = "codex-review/0.1.0"
|
|
73
|
+
# None means the real ~/.claude/agents (plus CLAUDE_CONFIG_DIR/agents); tests
|
|
74
|
+
# point it at a fixture directory.
|
|
75
|
+
CLAUDE_USER_AGENT_DIR: Path | None = None
|
|
76
|
+
# Managed (organization policy) agent locations; their definitions outrank
|
|
77
|
+
# every other source.
|
|
78
|
+
CLAUDE_MANAGED_AGENT_DIRS: list[Path] = [
|
|
79
|
+
Path(os.environ.get("ProgramData", r"C:\ProgramData")) / "ClaudeCode" / "agents",
|
|
80
|
+
Path(os.environ.get("ProgramFiles", r"C:\Program Files")) / "ClaudeCode" / "agents",
|
|
81
|
+
] if os.name == "nt" else [
|
|
82
|
+
Path("/Library/Application Support/ClaudeCode/agents"),
|
|
83
|
+
Path("/etc/claude-code/agents"),
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AuthorityError(RuntimeError):
|
|
88
|
+
def __init__(self, code: str, message: str):
|
|
89
|
+
self.code = code
|
|
90
|
+
super().__init__(f"{code}: {message}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _canonical(value: Any) -> bytes:
|
|
94
|
+
try:
|
|
95
|
+
return json.dumps(
|
|
96
|
+
value, ensure_ascii=True, allow_nan=False, sort_keys=True,
|
|
97
|
+
separators=(",", ":"),
|
|
98
|
+
).encode("utf-8")
|
|
99
|
+
except (TypeError, ValueError) as exc:
|
|
100
|
+
raise AuthorityError("INVALID_AUTHORITY_STATE", "state is not finite JSON") from exc
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _digest(data: bytes) -> str:
|
|
104
|
+
return hashlib.sha256(data).hexdigest()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _integrity(value: dict[str, Any]) -> str:
|
|
108
|
+
unsigned = dict(value)
|
|
109
|
+
unsigned.pop("integrity_sha256", None)
|
|
110
|
+
return _digest(_canonical(unsigned))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _real_root(root: str | Path) -> Path:
|
|
114
|
+
try:
|
|
115
|
+
resolved = Path(root).resolve(strict=True)
|
|
116
|
+
info = resolved.lstat()
|
|
117
|
+
except OSError as exc:
|
|
118
|
+
raise AuthorityError("UNSAFE_REPOSITORY", "repository root is unavailable") from exc
|
|
119
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
120
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISDIR(info.st_mode):
|
|
121
|
+
raise AuthorityError("UNSAFE_REPOSITORY", "repository root must be a real directory")
|
|
122
|
+
return resolved
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _safe_directory(root: Path, relative: Path, *, create: bool) -> Path:
|
|
126
|
+
current = root
|
|
127
|
+
for part in relative.parts:
|
|
128
|
+
current = current / part
|
|
129
|
+
try:
|
|
130
|
+
info = current.lstat()
|
|
131
|
+
except FileNotFoundError:
|
|
132
|
+
if not create:
|
|
133
|
+
raise
|
|
134
|
+
current.mkdir()
|
|
135
|
+
info = current.lstat()
|
|
136
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
137
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISDIR(info.st_mode):
|
|
138
|
+
raise AuthorityError("UNSAFE_AUTHORITY_PATH", "authority directories must be real")
|
|
139
|
+
return current
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _atomic_replace(root: Path, relative: Path, data: bytes) -> None:
|
|
143
|
+
parent = _safe_directory(root, relative.parent, create=True)
|
|
144
|
+
temporary = parent / f".{relative.name}.{secrets.token_hex(8)}.tmp"
|
|
145
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
146
|
+
if hasattr(os, "O_BINARY"):
|
|
147
|
+
flags |= os.O_BINARY
|
|
148
|
+
fd = os.open(temporary, flags, 0o600)
|
|
149
|
+
try:
|
|
150
|
+
view = memoryview(data)
|
|
151
|
+
while view:
|
|
152
|
+
written = os.write(fd, view)
|
|
153
|
+
if written <= 0:
|
|
154
|
+
raise OSError("short authority-state write")
|
|
155
|
+
view = view[written:]
|
|
156
|
+
os.fsync(fd)
|
|
157
|
+
finally:
|
|
158
|
+
os.close(fd)
|
|
159
|
+
try:
|
|
160
|
+
os.replace(temporary, root / relative)
|
|
161
|
+
finally:
|
|
162
|
+
try:
|
|
163
|
+
temporary.unlink()
|
|
164
|
+
except FileNotFoundError:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _publish_immutable(root: Path, relative: Path, data: bytes) -> None:
|
|
169
|
+
parent = _safe_directory(root, relative.parent, create=True)
|
|
170
|
+
target = parent / relative.name
|
|
171
|
+
temporary = parent / f".{relative.name}.{secrets.token_hex(8)}.tmp"
|
|
172
|
+
try:
|
|
173
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
174
|
+
if hasattr(os, "O_BINARY"):
|
|
175
|
+
flags |= os.O_BINARY
|
|
176
|
+
fd = os.open(temporary, flags, 0o600)
|
|
177
|
+
view = memoryview(data)
|
|
178
|
+
while view:
|
|
179
|
+
written = os.write(fd, view)
|
|
180
|
+
if written <= 0:
|
|
181
|
+
raise OSError("short immutable observation write")
|
|
182
|
+
view = view[written:]
|
|
183
|
+
os.fsync(fd)
|
|
184
|
+
finally:
|
|
185
|
+
if 'fd' in locals():
|
|
186
|
+
os.close(fd)
|
|
187
|
+
try:
|
|
188
|
+
try:
|
|
189
|
+
os.link(temporary, target)
|
|
190
|
+
except FileExistsError:
|
|
191
|
+
try:
|
|
192
|
+
info = target.lstat()
|
|
193
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
194
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISREG(info.st_mode) or info.st_size != len(data):
|
|
195
|
+
raise OSError("unsafe immutable authority target")
|
|
196
|
+
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
197
|
+
with os.fdopen(os.open(target, flags), "rb") as existing_file:
|
|
198
|
+
opened = os.fstat(existing_file.fileno())
|
|
199
|
+
if (
|
|
200
|
+
not stat.S_ISREG(opened.st_mode) or opened.st_size != len(data)
|
|
201
|
+
or (opened.st_dev, opened.st_ino) != (info.st_dev, info.st_ino)
|
|
202
|
+
):
|
|
203
|
+
raise OSError("immutable authority target changed during read")
|
|
204
|
+
existing = existing_file.read(len(data) + 1)
|
|
205
|
+
after = target.lstat()
|
|
206
|
+
if (after.st_dev, after.st_ino) != (info.st_dev, info.st_ino):
|
|
207
|
+
raise OSError("immutable authority target changed during read")
|
|
208
|
+
except OSError as exc:
|
|
209
|
+
raise AuthorityError("UNSAFE_AUTHORITY_PATH", "immutable authority target is unsafe") from exc
|
|
210
|
+
if existing != data:
|
|
211
|
+
raise AuthorityError("AUTHORITY_COLLISION", "content-addressed bytes differ")
|
|
212
|
+
finally:
|
|
213
|
+
try:
|
|
214
|
+
temporary.unlink()
|
|
215
|
+
except FileNotFoundError:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _request_path(request_id: str) -> Path:
|
|
220
|
+
if not isinstance(request_id, str) or REQUEST_RE.fullmatch(request_id) is None:
|
|
221
|
+
raise AuthorityError("INVALID_REQUEST_ID", "request identity is malformed")
|
|
222
|
+
return Path(f"{request_id}.json")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _spool_root(root: Path) -> Path:
|
|
226
|
+
try:
|
|
227
|
+
# Callers may supply an alias for the same repository (notably /var on
|
|
228
|
+
# macOS). Use the canonical root for both Git and the spool identity.
|
|
229
|
+
root = _real_root(root)
|
|
230
|
+
result = subprocess.run(
|
|
231
|
+
[git_executable(), "-C", str(root), "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
232
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8",
|
|
233
|
+
errors="replace", timeout=5, check=False, env=root_bound_git_env(),
|
|
234
|
+
)
|
|
235
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
236
|
+
raise OSError("git common directory unavailable")
|
|
237
|
+
common = Path(result.stdout.strip()).resolve(strict=True)
|
|
238
|
+
info = common.lstat()
|
|
239
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
240
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISDIR(info.st_mode):
|
|
241
|
+
raise OSError("git common directory is unsafe")
|
|
242
|
+
worktree_id = _digest(_canonical(_repository_identity(root)))[:32]
|
|
243
|
+
spool = _safe_directory(
|
|
244
|
+
common, Path("codearbiter") / "authority-runs" / worktree_id, create=True
|
|
245
|
+
)
|
|
246
|
+
spool_info = spool.lstat()
|
|
247
|
+
spool_reparse = getattr(spool_info, "st_file_attributes", 0) & 0x400
|
|
248
|
+
if stat.S_ISLNK(spool_info.st_mode) or spool_reparse or not stat.S_ISDIR(spool_info.st_mode):
|
|
249
|
+
raise OSError("authority spool is unsafe")
|
|
250
|
+
return spool
|
|
251
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
252
|
+
raise AuthorityError("AUTHORITY_BUSY", "resolved git-common authority spool is unavailable") from exc
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _save(root: Path, value: dict[str, Any]) -> None:
|
|
256
|
+
value = dict(value)
|
|
257
|
+
value["integrity_sha256"] = _integrity(value)
|
|
258
|
+
try:
|
|
259
|
+
_atomic_replace(_spool_root(root), _request_path(value["request_id"]), _canonical(value))
|
|
260
|
+
if value["state"] in TERMINAL_STATES:
|
|
261
|
+
(_registry_root() / f"{value['request_id']}.json").unlink(missing_ok=True)
|
|
262
|
+
except OSError as exc:
|
|
263
|
+
raise AuthorityError("AUTHORITY_BUSY", "request state could not be written") from exc
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _load(root: Path, request_id: str) -> dict[str, Any]:
|
|
267
|
+
path = _spool_root(root) / _request_path(request_id)
|
|
268
|
+
try:
|
|
269
|
+
info = path.lstat()
|
|
270
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
271
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISREG(info.st_mode):
|
|
272
|
+
raise OSError("not regular")
|
|
273
|
+
if info.st_size > MAX_STATE:
|
|
274
|
+
raise OSError("oversized")
|
|
275
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
276
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
277
|
+
raise AuthorityError("INVALID_AUTHORITY_STATE", "request state is unreadable") from exc
|
|
278
|
+
allowed = {
|
|
279
|
+
"format", "request_id", "state", "activity", "repository", "context",
|
|
280
|
+
"context_ref", "context_sha256", "attempt", "launch", "observation_ref",
|
|
281
|
+
"observation_sha256", "receipt", "integrity_sha256", "payload",
|
|
282
|
+
"authority_source", "dispatch_prompt", "review_contract_sha256",
|
|
283
|
+
"required_coverage", "wrapper", "command_bindings", "recovery",
|
|
284
|
+
"launch_envelope", "workspace_roots", "host",
|
|
285
|
+
}
|
|
286
|
+
if (
|
|
287
|
+
not isinstance(value, dict)
|
|
288
|
+
or set(value) - allowed
|
|
289
|
+
or value.get("format") != STATE_FORMAT
|
|
290
|
+
or value.get("request_id") != request_id
|
|
291
|
+
or value.get("activity") not in ACTIVITIES
|
|
292
|
+
or value.get("state") not in STATES
|
|
293
|
+
or value.get("host", "codex") not in HOSTS
|
|
294
|
+
or not isinstance(value.get("context"), dict)
|
|
295
|
+
or not isinstance(value.get("repository"), dict)
|
|
296
|
+
or not isinstance(value.get("command_bindings", []), list)
|
|
297
|
+
or not isinstance(value.get("workspace_roots", {}), dict)
|
|
298
|
+
or any(not isinstance(key, str) or not isinstance(path, str)
|
|
299
|
+
for key, path in value.get("workspace_roots", {}).items())
|
|
300
|
+
or value.get("integrity_sha256") != _integrity(value)
|
|
301
|
+
or not isinstance(value.get("context_ref"), str)
|
|
302
|
+
or SHA256_RE.fullmatch(value.get("context_sha256", "")) is None
|
|
303
|
+
):
|
|
304
|
+
raise AuthorityError("INVALID_AUTHORITY_STATE", "request state failed validation")
|
|
305
|
+
return value
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _validate_hash(value: Any, field: str) -> str:
|
|
309
|
+
if not isinstance(value, str) or SHA256_RE.fullmatch(value) is None:
|
|
310
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", f"{field} is malformed")
|
|
311
|
+
return value
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _context(
|
|
315
|
+
root: Path, client: Any, artifact_id: str, record_id: str, activity: str
|
|
316
|
+
) -> tuple[dict[str, Any], str, str]:
|
|
317
|
+
result = client.call("evidence-context", {
|
|
318
|
+
"artifact_id": artifact_id, "record_id": record_id, "activity": activity,
|
|
319
|
+
})
|
|
320
|
+
if not isinstance(result, dict):
|
|
321
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "engine context result is malformed")
|
|
322
|
+
context_hash = _validate_hash(result.get("context_sha256"), "context_sha256")
|
|
323
|
+
context_ref = result.get("context_ref")
|
|
324
|
+
expected_ref = f".codearbiter/.artifacts/evidence-contexts/{context_hash}.json"
|
|
325
|
+
if context_ref != expected_ref:
|
|
326
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context locator is not content addressed")
|
|
327
|
+
try:
|
|
328
|
+
raw = (root / expected_ref).read_bytes()
|
|
329
|
+
context = json.loads(raw.decode("utf-8"))
|
|
330
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
331
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context bytes are unavailable") from exc
|
|
332
|
+
if _digest(raw) != context_hash or raw != _canonical(context):
|
|
333
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context bytes changed or are not canonical")
|
|
334
|
+
subject = context.get("subject") if isinstance(context, dict) else None
|
|
335
|
+
if (
|
|
336
|
+
not isinstance(context, dict)
|
|
337
|
+
or context.get("format") != "codearbiter.evidence-context/0.1.0"
|
|
338
|
+
or not isinstance(subject, dict)
|
|
339
|
+
or subject.get("artifact_id") != artifact_id
|
|
340
|
+
or subject.get("record_id") != record_id
|
|
341
|
+
or context["activity"] != activity
|
|
342
|
+
or not ID_RE.fullmatch(artifact_id)
|
|
343
|
+
or not ID_RE.fullmatch(record_id)
|
|
344
|
+
):
|
|
345
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "engine context changed selection")
|
|
346
|
+
for field in ("input_sha256", "spec_sha256", "plan_sha256"):
|
|
347
|
+
_validate_hash(context.get(field), field)
|
|
348
|
+
_validate_hash(subject.get("normative_sha256"), "normative_sha256")
|
|
349
|
+
if activity in {"verification", "spec_review"}:
|
|
350
|
+
_validate_hash(context.get("task_sha256"), "task_sha256")
|
|
351
|
+
if activity == "verification":
|
|
352
|
+
commands = context.get("commands")
|
|
353
|
+
if not isinstance(commands, list) or not commands:
|
|
354
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "verification has no commands")
|
|
355
|
+
for command in commands:
|
|
356
|
+
definition = command.get("definition") if isinstance(command, dict) else None
|
|
357
|
+
if (
|
|
358
|
+
not isinstance(command, dict)
|
|
359
|
+
or set(command) != {"definition_sha256", "definition"}
|
|
360
|
+
or not isinstance(definition, dict)
|
|
361
|
+
or not {"argv", "cwd", "expected_exit", "required_tests"} <= set(definition)
|
|
362
|
+
or not isinstance(definition.get("argv"), list)
|
|
363
|
+
or not definition.get("argv")
|
|
364
|
+
or any(not isinstance(item, str) or not item for item in definition.get("argv", []))
|
|
365
|
+
or not isinstance(definition.get("cwd"), str)
|
|
366
|
+
or not definition.get("cwd")
|
|
367
|
+
or type(definition.get("expected_exit")) is not int
|
|
368
|
+
or not 0 <= definition.get("expected_exit", -1) <= 255
|
|
369
|
+
or not isinstance(definition.get("required_tests"), list)
|
|
370
|
+
or any(not isinstance(item, str) or not item for item in definition.get("required_tests", []))
|
|
371
|
+
or len(set(definition.get("required_tests", []))) != len(definition.get("required_tests", []))
|
|
372
|
+
):
|
|
373
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "command definition is malformed")
|
|
374
|
+
_validate_hash(command["definition_sha256"], "definition_sha256")
|
|
375
|
+
elif activity == "spec_review":
|
|
376
|
+
if not isinstance(context.get("task"), dict):
|
|
377
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "review task is absent")
|
|
378
|
+
else:
|
|
379
|
+
_validate_hash(context.get("base_input_sha256"), "base_input_sha256")
|
|
380
|
+
if not isinstance(context.get("task_hashes"), dict) or not isinstance(context.get("tasks"), list):
|
|
381
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "quality-review scope is incomplete")
|
|
382
|
+
return context, context_ref, context_hash
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _review_binding(context: dict[str, Any]) -> tuple[str, list[str]]:
|
|
386
|
+
contract = {
|
|
387
|
+
"format": DECISION_FORMAT,
|
|
388
|
+
"decision": ["pass", "changes_requested"],
|
|
389
|
+
"finding_severity": ["BLOCK", "WARN", "INFO"],
|
|
390
|
+
"required_fields": [
|
|
391
|
+
"format", "request_id", "target_sha256", "contract_sha256",
|
|
392
|
+
"decision", "coverage", "findings", "assessment",
|
|
393
|
+
],
|
|
394
|
+
}
|
|
395
|
+
records = [context["task"]] if context["activity"] == "spec_review" else context["tasks"]
|
|
396
|
+
coverage = sorted({
|
|
397
|
+
ref for record in records for ref in record.get("criterion_refs", [])
|
|
398
|
+
if isinstance(ref, str) and ref
|
|
399
|
+
})
|
|
400
|
+
if not coverage:
|
|
401
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "review target has no criterion coverage")
|
|
402
|
+
return _digest(_canonical(contract)), coverage
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _repository_identity(root: Path) -> dict[str, str]:
|
|
406
|
+
info = os.stat(root)
|
|
407
|
+
return {
|
|
408
|
+
"path": str(root),
|
|
409
|
+
"filesystem_id": f"{int(getattr(info, 'st_dev', 0))}:{int(getattr(info, 'st_ino', 0))}",
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _registry_root() -> Path:
|
|
414
|
+
namespace = f"uid-{os.getuid()}" if hasattr(os, "getuid") else "current-user"
|
|
415
|
+
root = REGISTRY_PARENT.resolve(strict=True) / f"codearbiter-authority-{namespace}"
|
|
416
|
+
try:
|
|
417
|
+
root.mkdir(mode=0o700, exist_ok=True)
|
|
418
|
+
info = root.lstat()
|
|
419
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
420
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISDIR(info.st_mode):
|
|
421
|
+
raise OSError("unsafe registry")
|
|
422
|
+
if hasattr(os, "getuid"):
|
|
423
|
+
if info.st_uid != os.getuid():
|
|
424
|
+
raise OSError("foreign registry")
|
|
425
|
+
os.chmod(root, 0o700)
|
|
426
|
+
except OSError as exc:
|
|
427
|
+
raise AuthorityError("AUTHORITY_BUSY", "authority registry is unavailable") from exc
|
|
428
|
+
return root
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _register_request(root: Path, request: dict[str, Any]) -> None:
|
|
432
|
+
pointer = {
|
|
433
|
+
"format": "codearbiter.authority-pointer/0.1.0",
|
|
434
|
+
"request_id": request["request_id"],
|
|
435
|
+
"repository": request["repository"],
|
|
436
|
+
}
|
|
437
|
+
pointer["integrity_sha256"] = _integrity(pointer)
|
|
438
|
+
registry = _registry_root()
|
|
439
|
+
target = registry / f"{request['request_id']}.json"
|
|
440
|
+
data = _canonical(pointer)
|
|
441
|
+
try:
|
|
442
|
+
existing = target.read_bytes()
|
|
443
|
+
except FileNotFoundError:
|
|
444
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
445
|
+
if hasattr(os, "O_BINARY"):
|
|
446
|
+
flags |= os.O_BINARY
|
|
447
|
+
fd = os.open(target, flags, 0o600)
|
|
448
|
+
try:
|
|
449
|
+
view = memoryview(data)
|
|
450
|
+
while view:
|
|
451
|
+
written = os.write(fd, view)
|
|
452
|
+
if written <= 0:
|
|
453
|
+
raise OSError("short authority-pointer write")
|
|
454
|
+
view = view[written:]
|
|
455
|
+
os.fsync(fd)
|
|
456
|
+
finally:
|
|
457
|
+
os.close(fd)
|
|
458
|
+
else:
|
|
459
|
+
if existing != data:
|
|
460
|
+
raise AuthorityError("AUTHORITY_COLLISION", "request registry entry differs")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _registered_requests(strict: bool = False) -> list[tuple[Path, dict[str, Any]]]:
|
|
464
|
+
"""Live registered requests. Strict callers refuse, rather than skip, a
|
|
465
|
+
request whose pointer or state exists but cannot be read."""
|
|
466
|
+
results = []
|
|
467
|
+
for path in _registry_root().glob("*.json"):
|
|
468
|
+
try:
|
|
469
|
+
raw = path.read_bytes()
|
|
470
|
+
if len(raw) > 16384:
|
|
471
|
+
continue
|
|
472
|
+
pointer = json.loads(raw.decode("utf-8"))
|
|
473
|
+
repository = pointer.get("repository") if isinstance(pointer, dict) else None
|
|
474
|
+
if (
|
|
475
|
+
not isinstance(pointer, dict)
|
|
476
|
+
or pointer.get("format") != "codearbiter.authority-pointer/0.1.0"
|
|
477
|
+
or pointer.get("request_id") != path.stem
|
|
478
|
+
or pointer.get("integrity_sha256") != _integrity(pointer)
|
|
479
|
+
or not isinstance(repository, dict)
|
|
480
|
+
):
|
|
481
|
+
continue
|
|
482
|
+
root = _real_root(repository.get("path"))
|
|
483
|
+
if _repository_identity(root) != repository:
|
|
484
|
+
continue
|
|
485
|
+
request = _load(root, path.stem)
|
|
486
|
+
if request["state"] in TERMINAL_STATES:
|
|
487
|
+
path.unlink(missing_ok=True)
|
|
488
|
+
continue
|
|
489
|
+
results.append((root, request))
|
|
490
|
+
except (AuthorityError, OSError, UnicodeError, ValueError) as exc:
|
|
491
|
+
unreadable = (
|
|
492
|
+
exc.code == "INVALID_AUTHORITY_STATE"
|
|
493
|
+
if isinstance(exc, AuthorityError) else REQUEST_RE.fullmatch(path.stem) is not None
|
|
494
|
+
)
|
|
495
|
+
cause = exc.__cause__ if isinstance(exc, AuthorityError) else exc
|
|
496
|
+
if strict and unreadable and not isinstance(cause, FileNotFoundError):
|
|
497
|
+
raise AuthorityError(
|
|
498
|
+
"AUTHORITY_BUSY",
|
|
499
|
+
f"authority request {path} is unreadable; remove it if no review is running",
|
|
500
|
+
) from exc
|
|
501
|
+
continue
|
|
502
|
+
return results
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _registered_request(request_id: str) -> tuple[Path, dict[str, Any]]:
|
|
506
|
+
matches = [item for item in _registered_requests() if item[1]["request_id"] == request_id]
|
|
507
|
+
if len(matches) != 1:
|
|
508
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "authority request routing is ambiguous")
|
|
509
|
+
return matches[0]
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _claude_start_marker(agent_id: str) -> Path:
|
|
513
|
+
key = _digest(_canonical({"host": "claude", "agent_id": agent_id}))
|
|
514
|
+
return _registry_root() / f"claude-start-{key}.json"
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _claude_agent_dirs(root: Path, sessions: tuple[Path, ...] = ()) -> list[Path]:
|
|
518
|
+
"""Every agent directory whose definitions outrank a plugin agent.
|
|
519
|
+
|
|
520
|
+
A session opened outside the artifact repository loads project agents
|
|
521
|
+
from its own directory, so the observing session roots are scanned too.
|
|
522
|
+
"""
|
|
523
|
+
directories = [root / ".claude" / "agents"]
|
|
524
|
+
directories.extend(Path(session) / ".claude" / "agents" for session in sessions)
|
|
525
|
+
try:
|
|
526
|
+
common = Path(_git_text(root, "rev-parse", "--path-format=absolute", "--git-common-dir"))
|
|
527
|
+
directories.append(common.parent / ".claude" / "agents")
|
|
528
|
+
except AuthorityError:
|
|
529
|
+
pass
|
|
530
|
+
if CLAUDE_USER_AGENT_DIR is not None:
|
|
531
|
+
directories.append(CLAUDE_USER_AGENT_DIR)
|
|
532
|
+
else:
|
|
533
|
+
directories.append(Path.home() / ".claude" / "agents")
|
|
534
|
+
configured = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
535
|
+
if configured:
|
|
536
|
+
directories.append(Path(configured) / "agents")
|
|
537
|
+
directories.extend(CLAUDE_MANAGED_AGENT_DIRS)
|
|
538
|
+
return directories
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _raise_walk_error(exc: OSError) -> None:
|
|
542
|
+
raise exc
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _refuse_shadowed_reviewer(root: Path, *sessions: Path) -> None:
|
|
546
|
+
"""Project, user and managed agents outrank plugin agents; refuse any shadow.
|
|
547
|
+
|
|
548
|
+
Fail closed rather than parse YAML: a definition shadows the reviewer when
|
|
549
|
+
its file name, or anything in its front matter, names the reviewer in any
|
|
550
|
+
case. A description merely mentioning the name is a false positive the
|
|
551
|
+
error message explains; a missed shadow would let a parent-authored agent
|
|
552
|
+
grade its own work.
|
|
553
|
+
"""
|
|
554
|
+
short = CLAUDE_REVIEWER.split(":", 1)[1].casefold()
|
|
555
|
+
for directory in _claude_agent_dirs(root, sessions):
|
|
556
|
+
candidates = []
|
|
557
|
+
try:
|
|
558
|
+
if directory.is_dir():
|
|
559
|
+
for base, dirs, files in os.walk(directory, onerror=_raise_walk_error, followlinks=False):
|
|
560
|
+
for name in dirs:
|
|
561
|
+
# A linked subdirectory is not walked, so it cannot be cleared.
|
|
562
|
+
info = (Path(base) / name).lstat()
|
|
563
|
+
if stat.S_ISLNK(info.st_mode) or getattr(info, "st_file_attributes", 0) & 0x400:
|
|
564
|
+
raise AuthorityError(
|
|
565
|
+
"SHADOWED_REVIEWER", f"{Path(base) / name} is a link that cannot be inspected",
|
|
566
|
+
)
|
|
567
|
+
candidates.extend(Path(base) / name for name in files if name.casefold().endswith(".md"))
|
|
568
|
+
except OSError as exc:
|
|
569
|
+
raise AuthorityError("SHADOWED_REVIEWER", f"agent definitions under {directory} cannot be inspected") from exc
|
|
570
|
+
for candidate in candidates:
|
|
571
|
+
if short in candidate.stem.casefold():
|
|
572
|
+
raise AuthorityError("SHADOWED_REVIEWER", f"{candidate} shadows {CLAUDE_REVIEWER}")
|
|
573
|
+
try:
|
|
574
|
+
# A linked definition is followed, but only to a regular file:
|
|
575
|
+
# a pipe or device would hang the hook.
|
|
576
|
+
if not stat.S_ISREG(os.stat(candidate).st_mode):
|
|
577
|
+
raise AuthorityError("SHADOWED_REVIEWER", f"{candidate} is not a regular file")
|
|
578
|
+
with candidate.open("rb") as stream:
|
|
579
|
+
raw = stream.read(1 << 20)
|
|
580
|
+
except OSError as exc:
|
|
581
|
+
raise AuthorityError("SHADOWED_REVIEWER", "agent definition is unreadable") from exc
|
|
582
|
+
text = raw.decode("utf-8", errors="replace")
|
|
583
|
+
front = text
|
|
584
|
+
match = re.match(r"\A?\s*---[^\n]*\n(.*?)^---", text, re.S | re.M)
|
|
585
|
+
if match:
|
|
586
|
+
front = match.group(1)
|
|
587
|
+
if short in front.casefold():
|
|
588
|
+
raise AuthorityError("SHADOWED_REVIEWER", f"{candidate} shadows {CLAUDE_REVIEWER}")
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _ordinary_spawn_marker(session_id: str, turn_id: str) -> Path:
|
|
592
|
+
key = _digest(_canonical({"session_id": session_id, "turn_id": turn_id}))
|
|
593
|
+
return _registry_root() / f"ordinary-spawn-{key}.marker"
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _record_ordinary_spawn(session_id: str, turn_id: str) -> None:
|
|
597
|
+
marker = _ordinary_spawn_marker(session_id, turn_id)
|
|
598
|
+
try:
|
|
599
|
+
fd = os.open(marker, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
600
|
+
except FileExistsError:
|
|
601
|
+
return
|
|
602
|
+
try:
|
|
603
|
+
os.write(fd, b"ordinary spawn observed\n")
|
|
604
|
+
os.fsync(fd)
|
|
605
|
+
finally:
|
|
606
|
+
os.close(fd)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _reject_overlapping_authority(session_id: str, turn_id: str) -> None:
|
|
610
|
+
for root, request in _registered_requests():
|
|
611
|
+
launch = request.get("launch") or {}
|
|
612
|
+
if (
|
|
613
|
+
request["state"] in {"LAUNCHING", "RUNNING"}
|
|
614
|
+
and launch.get("parent_session_id") == session_id
|
|
615
|
+
and launch.get("parent_turn_id") == turn_id
|
|
616
|
+
):
|
|
617
|
+
request["state"] = "REJECTED"
|
|
618
|
+
request["recovery"] = {
|
|
619
|
+
"mode": "overlapping-subagent-launch",
|
|
620
|
+
"rerun_permitted": False,
|
|
621
|
+
}
|
|
622
|
+
_save(root, request)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _git_text(directory: Path, *args: str) -> str:
|
|
626
|
+
try:
|
|
627
|
+
result = subprocess.run(
|
|
628
|
+
[git_executable(), "-C", str(directory), *args],
|
|
629
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False,
|
|
630
|
+
timeout=15, env=root_bound_git_env(),
|
|
631
|
+
)
|
|
632
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
633
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "git workspace identity is unavailable") from exc
|
|
634
|
+
if result.returncode != 0:
|
|
635
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "git workspace identity is unavailable")
|
|
636
|
+
decoded = result.stdout.decode("utf-8", "strict")
|
|
637
|
+
return decoded if "-z" in args else decoded.strip()
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _path_identity(path: Path) -> str:
|
|
641
|
+
info = path.lstat()
|
|
642
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
643
|
+
if stat.S_ISLNK(info.st_mode) or reparse:
|
|
644
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "workspace path is replaceable indirection")
|
|
645
|
+
return f"{int(getattr(info, 'st_dev', 0))}:{int(getattr(info, 'st_ino', 0))}"
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _file_sha256(path: Path) -> str:
|
|
649
|
+
digest = hashlib.sha256()
|
|
650
|
+
try:
|
|
651
|
+
with path.open("rb") as stream:
|
|
652
|
+
while True:
|
|
653
|
+
block = stream.read(1 << 20)
|
|
654
|
+
if not block:
|
|
655
|
+
break
|
|
656
|
+
digest.update(block)
|
|
657
|
+
except OSError as exc:
|
|
658
|
+
raise AuthorityError("UNSUPPORTED_EXECUTABLE", "declared executable is unreadable") from exc
|
|
659
|
+
return digest.hexdigest()
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _workspace_for(
|
|
663
|
+
root: Path, label: str, workspace_roots: dict[str, str]
|
|
664
|
+
) -> tuple[Path, Path, Path]:
|
|
665
|
+
if label in workspace_roots:
|
|
666
|
+
cwd = _real_root(workspace_roots[label])
|
|
667
|
+
declared_top = _real_root(_git_text(cwd, "rev-parse", "--show-toplevel"))
|
|
668
|
+
if declared_top != cwd:
|
|
669
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "mapped workspace is not an exact Git worktree root")
|
|
670
|
+
elif label in {".", "current worktree", "candidate worktree"}:
|
|
671
|
+
cwd = root
|
|
672
|
+
else:
|
|
673
|
+
candidate = Path(label)
|
|
674
|
+
if candidate.is_absolute() or ".." in candidate.parts:
|
|
675
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "declared cwd escapes the artifact worktree")
|
|
676
|
+
cwd = _resolved_directory(root / candidate)
|
|
677
|
+
worktree = _real_root(_git_text(cwd, "rev-parse", "--show-toplevel"))
|
|
678
|
+
if label not in workspace_roots and worktree != root:
|
|
679
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "declared cwd is not in the artifact worktree")
|
|
680
|
+
common = _real_root(_git_text(worktree, "rev-parse", "--path-format=absolute", "--git-common-dir"))
|
|
681
|
+
artifact_common = _real_root(_git_text(root, "rev-parse", "--path-format=absolute", "--git-common-dir"))
|
|
682
|
+
if common != artifact_common:
|
|
683
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "workspace does not share the artifact git-common-dir")
|
|
684
|
+
return cwd, worktree, common
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _resolve_executable(name: str, cwd: Path) -> Path:
|
|
688
|
+
candidate = Path(name)
|
|
689
|
+
if candidate.is_absolute() or candidate.parent != Path("."):
|
|
690
|
+
path = (cwd / candidate).resolve(strict=True) if not candidate.is_absolute() else candidate.resolve(strict=True)
|
|
691
|
+
else:
|
|
692
|
+
located = shutil.which(name, path=os.environ.get("PATH", ""))
|
|
693
|
+
if not located:
|
|
694
|
+
raise AuthorityError("UNSUPPORTED_EXECUTABLE", "declared executable is not resolvable")
|
|
695
|
+
path = Path(located).resolve(strict=True)
|
|
696
|
+
if not path.is_file():
|
|
697
|
+
raise AuthorityError("UNSUPPORTED_EXECUTABLE", "declared executable is not a regular file")
|
|
698
|
+
_path_identity(path)
|
|
699
|
+
return path
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _bind_commands(
|
|
703
|
+
root: Path, context: dict[str, Any], workspace_roots: dict[str, str]
|
|
704
|
+
) -> list[dict[str, Any]]:
|
|
705
|
+
bindings = []
|
|
706
|
+
for command in context["commands"]:
|
|
707
|
+
definition = command["definition"]
|
|
708
|
+
cwd, worktree, common = _workspace_for(root, definition["cwd"], workspace_roots)
|
|
709
|
+
executable = _resolve_executable(definition["argv"][0], cwd)
|
|
710
|
+
bindings.append({
|
|
711
|
+
"definition_sha256": command["definition_sha256"],
|
|
712
|
+
"argv": [str(executable), *definition["argv"][1:]],
|
|
713
|
+
"cwd": str(cwd),
|
|
714
|
+
"cwd_filesystem_id": _path_identity(cwd),
|
|
715
|
+
"workspace_root": str(worktree),
|
|
716
|
+
"workspace_filesystem_id": _path_identity(worktree),
|
|
717
|
+
"git_common_dir": str(common),
|
|
718
|
+
"git_common_filesystem_id": _path_identity(common),
|
|
719
|
+
"executable_sha256": _file_sha256(executable),
|
|
720
|
+
})
|
|
721
|
+
return bindings
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _workspace_snapshots(bindings: list[dict[str, Any]]) -> list[dict[str, str]]:
|
|
725
|
+
unique: dict[str, dict[str, str]] = {}
|
|
726
|
+
for binding in bindings:
|
|
727
|
+
cwd = _resolved_directory(binding["cwd"])
|
|
728
|
+
root = _real_root(binding["workspace_root"])
|
|
729
|
+
common = _real_root(binding["git_common_dir"])
|
|
730
|
+
if (
|
|
731
|
+
_path_identity(cwd) != binding["cwd_filesystem_id"]
|
|
732
|
+
or _path_identity(root) != binding["workspace_filesystem_id"]
|
|
733
|
+
or _path_identity(common) != binding["git_common_filesystem_id"]
|
|
734
|
+
or _file_sha256(Path(binding["argv"][0])) != binding["executable_sha256"]
|
|
735
|
+
):
|
|
736
|
+
raise AuthorityError("WORKSPACE_DRIFT", "workspace or executable identity changed")
|
|
737
|
+
status = _git_text(root, "status", "--porcelain=v2", "-z", "--untracked-files=all")
|
|
738
|
+
index = _git_text(root, "ls-files", "--stage", "-z")
|
|
739
|
+
untracked = _git_text(root, "ls-files", "--others", "--exclude-standard", "-z")
|
|
740
|
+
entries: dict[str, str] = {}
|
|
741
|
+
for record in (part for part in index.split("\0") if part):
|
|
742
|
+
try:
|
|
743
|
+
metadata, relative = record.split("\t", 1)
|
|
744
|
+
mode, _object_id, stage = metadata.split(" ")
|
|
745
|
+
except ValueError as exc:
|
|
746
|
+
raise AuthorityError("WORKSPACE_DRIFT", "git index entry cannot be frozen") from exc
|
|
747
|
+
if stage != "0" or mode not in {"100644", "100755", "120000", "160000"} or relative in entries:
|
|
748
|
+
raise AuthorityError("WORKSPACE_DRIFT", "git index entry cannot be frozen")
|
|
749
|
+
entries[relative] = mode
|
|
750
|
+
for relative in (part for part in untracked.split("\0") if part):
|
|
751
|
+
if relative in entries:
|
|
752
|
+
raise AuthorityError("WORKSPACE_DRIFT", "git workspace entry cannot be frozen")
|
|
753
|
+
entries[relative] = "untracked"
|
|
754
|
+
content = hashlib.sha256()
|
|
755
|
+
for relative in sorted(entries):
|
|
756
|
+
encoded = relative.encode("utf-8")
|
|
757
|
+
candidate = root / relative
|
|
758
|
+
try:
|
|
759
|
+
if not candidate.parent.resolve(strict=False).is_relative_to(root):
|
|
760
|
+
raise OSError("workspace member escapes the worktree")
|
|
761
|
+
mode = entries[relative]
|
|
762
|
+
try:
|
|
763
|
+
info = candidate.lstat()
|
|
764
|
+
except FileNotFoundError:
|
|
765
|
+
if mode == "untracked":
|
|
766
|
+
raise
|
|
767
|
+
kind, data = "deleted", b""
|
|
768
|
+
else:
|
|
769
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
770
|
+
if stat.S_ISLNK(info.st_mode):
|
|
771
|
+
if mode not in {"120000", "untracked"}:
|
|
772
|
+
raise OSError("regular workspace member became a symlink")
|
|
773
|
+
resolved = candidate.resolve(strict=False)
|
|
774
|
+
if not resolved.is_relative_to(root):
|
|
775
|
+
raise OSError("workspace symlink escapes the worktree")
|
|
776
|
+
target = os.fsencode(os.readlink(candidate))
|
|
777
|
+
resolved_path = resolved.relative_to(root).as_posix().encode("utf-8")
|
|
778
|
+
try:
|
|
779
|
+
target_info = resolved.lstat()
|
|
780
|
+
except FileNotFoundError:
|
|
781
|
+
kind, data = "dangling-symlink", target + b"\0" + resolved_path
|
|
782
|
+
else:
|
|
783
|
+
target_reparse = getattr(target_info, "st_file_attributes", 0) & 0x400
|
|
784
|
+
if target_reparse or not stat.S_ISREG(target_info.st_mode):
|
|
785
|
+
raise OSError("workspace symlink target has unsupported type")
|
|
786
|
+
kind, data = "symlink", target + b"\0" + resolved_path + b"\0" + resolved.read_bytes()
|
|
787
|
+
elif reparse:
|
|
788
|
+
raise OSError("workspace member is replaceable indirection")
|
|
789
|
+
elif stat.S_ISREG(info.st_mode) and mode != "160000":
|
|
790
|
+
kind = "materialized-symlink" if mode == "120000" else "file"
|
|
791
|
+
data = candidate.read_bytes()
|
|
792
|
+
elif stat.S_ISDIR(info.st_mode) and mode == "160000":
|
|
793
|
+
nested_top = _git_text(candidate, "rev-parse", "--show-toplevel")
|
|
794
|
+
if _real_root(nested_top) != candidate:
|
|
795
|
+
if any(candidate.iterdir()):
|
|
796
|
+
raise OSError("uninitialized gitlink has unexpected content")
|
|
797
|
+
kind, data = "uninitialized-gitlink", b""
|
|
798
|
+
else:
|
|
799
|
+
if _git_text(candidate, "status", "--porcelain", "--untracked-files=all"):
|
|
800
|
+
raise OSError("dirty gitlink cannot be frozen")
|
|
801
|
+
kind, data = "gitlink", _git_text(candidate, "rev-parse", "HEAD").encode("ascii")
|
|
802
|
+
else:
|
|
803
|
+
raise OSError("workspace member has unsupported type")
|
|
804
|
+
except (OSError, RuntimeError, AuthorityError) as exc:
|
|
805
|
+
raise AuthorityError("WORKSPACE_DRIFT", "workspace content cannot be frozen") from exc
|
|
806
|
+
content.update(len(encoded).to_bytes(8, "big"))
|
|
807
|
+
content.update(encoded)
|
|
808
|
+
typed = f"{mode}:{kind}".encode("ascii")
|
|
809
|
+
content.update(len(typed).to_bytes(8, "big"))
|
|
810
|
+
content.update(typed)
|
|
811
|
+
content.update(len(data).to_bytes(8, "big"))
|
|
812
|
+
content.update(data)
|
|
813
|
+
unique[str(root)] = {
|
|
814
|
+
"root": str(root),
|
|
815
|
+
"filesystem_id": binding["workspace_filesystem_id"],
|
|
816
|
+
"git_common_dir": str(common),
|
|
817
|
+
"git_common_filesystem_id": binding["git_common_filesystem_id"],
|
|
818
|
+
"head": _git_text(root, "rev-parse", "HEAD"),
|
|
819
|
+
"status_sha256": _digest(status.encode("utf-8")),
|
|
820
|
+
"content_sha256": content.hexdigest(),
|
|
821
|
+
}
|
|
822
|
+
return [unique[key] for key in sorted(unique)]
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _dispatch_prompt(request: dict[str, Any]) -> str:
|
|
826
|
+
context = request["context"]
|
|
827
|
+
return (
|
|
828
|
+
f"[CODEARBITER_AUTHORITY_REQUEST:{request['request_id']}]\n"
|
|
829
|
+
f"Target repository: {request['repository']['path']}\n"
|
|
830
|
+
f"Frozen context: {request['context_ref']} sha256={request['context_sha256']}\n"
|
|
831
|
+
"Frozen target data follows as canonical JSON:\n"
|
|
832
|
+
+ _canonical(context).decode("ascii") + "\n"
|
|
833
|
+
"Act as a fresh read-only independent codeArbiter reviewer. Do not edit files or "
|
|
834
|
+
"delegate. Review only the frozen target and return exactly one JSON object using "
|
|
835
|
+
f"format {DECISION_FORMAT}. Bind request_id={request['request_id']}, "
|
|
836
|
+
f"target_sha256={context['input_sha256']}, "
|
|
837
|
+
f"contract_sha256={request['review_contract_sha256']}. Required coverage: "
|
|
838
|
+
+ json.dumps(request["required_coverage"], ensure_ascii=True)
|
|
839
|
+
+ ". Fields: format, request_id, target_sha256, contract_sha256, decision "
|
|
840
|
+
"(pass or changes_requested), coverage (unique strings), findings (objects with "
|
|
841
|
+
"severity, code, message), assessment (non-empty string). Reply with the JSON "
|
|
842
|
+
"object only: no code fence and no other text."
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def arm_request(
|
|
847
|
+
root: str | Path,
|
|
848
|
+
client: Any,
|
|
849
|
+
artifact_id: str,
|
|
850
|
+
record_id: str,
|
|
851
|
+
activity: str,
|
|
852
|
+
*,
|
|
853
|
+
request_nonce: str | None = None,
|
|
854
|
+
workspace_roots: dict[str, str | Path] | None = None,
|
|
855
|
+
host: str = "codex",
|
|
856
|
+
reviewer_model: str = "opus",
|
|
857
|
+
**unexpected: Any,
|
|
858
|
+
) -> dict[str, Any]:
|
|
859
|
+
if unexpected:
|
|
860
|
+
raise AuthorityError(
|
|
861
|
+
"INVALID_AUTHORITY_REQUEST",
|
|
862
|
+
"authority, verdict, payload, and observations are not caller inputs",
|
|
863
|
+
)
|
|
864
|
+
if activity not in ACTIVITIES:
|
|
865
|
+
raise AuthorityError("INVALID_AUTHORITY_REQUEST", "activity is unsupported")
|
|
866
|
+
if host not in HOSTS:
|
|
867
|
+
raise AuthorityError("INVALID_AUTHORITY_REQUEST", "host is unsupported")
|
|
868
|
+
if host == "claude" and reviewer_model not in CLAUDE_REVIEWER_MODELS:
|
|
869
|
+
raise AuthorityError("INVALID_AUTHORITY_REQUEST", "reviewer model is unsupported")
|
|
870
|
+
root = _real_root(root)
|
|
871
|
+
if host == "claude" and activity in REVIEW_ACTIVITIES:
|
|
872
|
+
_refuse_shadowed_reviewer(root)
|
|
873
|
+
nonce = request_nonce or secrets.token_urlsafe(24)
|
|
874
|
+
if not isinstance(nonce, str) or TOKEN_RE.fullmatch(nonce) is None:
|
|
875
|
+
raise AuthorityError("INVALID_AUTHORITY_REQUEST", "request nonce is malformed")
|
|
876
|
+
context, context_ref, context_sha256 = _context(
|
|
877
|
+
root, client, artifact_id, record_id, activity
|
|
878
|
+
)
|
|
879
|
+
frozen_workspaces: dict[str, str] = {}
|
|
880
|
+
if workspace_roots is not None:
|
|
881
|
+
if activity != "verification" or not isinstance(workspace_roots, dict):
|
|
882
|
+
raise AuthorityError("INVALID_AUTHORITY_REQUEST", "workspace roots apply only to verification")
|
|
883
|
+
labels = {command["definition"]["cwd"] for command in context["commands"]}
|
|
884
|
+
if set(workspace_roots) != labels:
|
|
885
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "workspace map must exactly cover declared cwd labels")
|
|
886
|
+
frozen_workspaces = {label: str(_real_root(path)) for label, path in workspace_roots.items()}
|
|
887
|
+
command_bindings = _bind_commands(root, context, frozen_workspaces) if activity == "verification" else []
|
|
888
|
+
seed_value = {
|
|
889
|
+
"repository": _repository_identity(root), "context": context, "nonce": nonce,
|
|
890
|
+
"command_bindings": command_bindings,
|
|
891
|
+
"workspace_roots": frozen_workspaces,
|
|
892
|
+
}
|
|
893
|
+
if host != "codex":
|
|
894
|
+
seed_value["host"] = host
|
|
895
|
+
seed = _canonical(seed_value)
|
|
896
|
+
request_id = _digest(seed)
|
|
897
|
+
request = {
|
|
898
|
+
"format": STATE_FORMAT,
|
|
899
|
+
"request_id": request_id,
|
|
900
|
+
"state": "ARMED",
|
|
901
|
+
"activity": activity,
|
|
902
|
+
"repository": _repository_identity(root),
|
|
903
|
+
"context": context,
|
|
904
|
+
"context_ref": context_ref,
|
|
905
|
+
"context_sha256": context_sha256,
|
|
906
|
+
"attempt": None,
|
|
907
|
+
"launch": None,
|
|
908
|
+
"observation_ref": None,
|
|
909
|
+
"observation_sha256": None,
|
|
910
|
+
"receipt": None,
|
|
911
|
+
"wrapper": None,
|
|
912
|
+
"recovery": None,
|
|
913
|
+
"command_bindings": command_bindings,
|
|
914
|
+
"workspace_roots": frozen_workspaces,
|
|
915
|
+
}
|
|
916
|
+
if host != "codex":
|
|
917
|
+
request["host"] = host
|
|
918
|
+
if activity in REVIEW_ACTIVITIES:
|
|
919
|
+
request["review_contract_sha256"], request["required_coverage"] = _review_binding(context)
|
|
920
|
+
request["dispatch_prompt"] = _dispatch_prompt(request)
|
|
921
|
+
if host == "claude":
|
|
922
|
+
request["launch_envelope"] = {
|
|
923
|
+
"description": f"codeArbiter {activity} {request_id[:12]}",
|
|
924
|
+
"prompt": request["dispatch_prompt"],
|
|
925
|
+
"subagent_type": CLAUDE_REVIEWER,
|
|
926
|
+
"model": reviewer_model,
|
|
927
|
+
}
|
|
928
|
+
else:
|
|
929
|
+
request["launch_envelope"] = {
|
|
930
|
+
"message": request["dispatch_prompt"],
|
|
931
|
+
"task_name": f"authority_{request_id[:12]}",
|
|
932
|
+
"fork_turns": "none",
|
|
933
|
+
}
|
|
934
|
+
_save(root, request)
|
|
935
|
+
_register_request(root, request)
|
|
936
|
+
result = {
|
|
937
|
+
"request_id": request_id,
|
|
938
|
+
"request_path": str(_spool_root(root) / _request_path(request_id)),
|
|
939
|
+
"activity": activity,
|
|
940
|
+
}
|
|
941
|
+
if activity in REVIEW_ACTIVITIES:
|
|
942
|
+
result["dispatch_prompt"] = request["dispatch_prompt"]
|
|
943
|
+
result["review_contract_sha256"] = request["review_contract_sha256"]
|
|
944
|
+
result["required_coverage"] = request["required_coverage"]
|
|
945
|
+
result["launch_envelope"] = request["launch_envelope"]
|
|
946
|
+
return result
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def _bounded_output(value: Any) -> bytes:
|
|
950
|
+
if not isinstance(value, (bytes, bytearray)):
|
|
951
|
+
raise AuthorityError("INVALID_PROCESS_RESULT", "process output must be bytes")
|
|
952
|
+
if len(value) > MAX_OUTPUT:
|
|
953
|
+
raise AuthorityError("PROCESS_OUTPUT_TOO_LARGE", "process output exceeds 8 MiB")
|
|
954
|
+
return bytes(value)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _unittest_collector(stdout: bytes, stderr: bytes, required: list[str]) -> list[dict[str, str]]:
|
|
958
|
+
text = (stdout + b"\n" + stderr).decode("utf-8", "replace")
|
|
959
|
+
results = []
|
|
960
|
+
for name in required:
|
|
961
|
+
pattern = re.compile(r"(?m)^" + re.escape(name) + r"(?:\s+\([^\r\n]*\))?\s+\.\.\.\s+ok\s*$")
|
|
962
|
+
matches = pattern.findall(text)
|
|
963
|
+
if not matches:
|
|
964
|
+
raise AuthorityError("MISSING_TEST_RESULT", f"required test did not pass: {name}")
|
|
965
|
+
if len(matches) != 1:
|
|
966
|
+
raise AuthorityError("DUPLICATE_TEST_RESULT", f"required test appeared more than once: {name}")
|
|
967
|
+
results.append({"name": name, "status": "pass"})
|
|
968
|
+
return results
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _named_line_collector(stdout: bytes, stderr: bytes, required: list[str]) -> list[dict[str, str]]:
|
|
972
|
+
# Intentional producer gap: current SingedTerra movement, walls, and
|
|
973
|
+
# fall-damage harnesses emit umbrella summaries for some plan aggregates.
|
|
974
|
+
# Those summaries are not semantic evidence; only exact PASS names qualify.
|
|
975
|
+
text = (stdout + b"\n" + stderr).decode("utf-8", "replace")
|
|
976
|
+
passed: dict[str, int] = {}
|
|
977
|
+
for line in text.splitlines():
|
|
978
|
+
stripped = line.strip()
|
|
979
|
+
if stripped.startswith("PASS: "):
|
|
980
|
+
name = stripped[6:].rstrip(".")
|
|
981
|
+
passed[name] = passed.get(name, 0) + 1
|
|
982
|
+
duplicates = [name for name in required if passed.get(name, 0) > 1]
|
|
983
|
+
if duplicates:
|
|
984
|
+
raise AuthorityError("DUPLICATE_TEST_RESULT", "required PASS lines are duplicated: " + ", ".join(duplicates))
|
|
985
|
+
missing = [name for name in required if passed.get(name, 0) != 1]
|
|
986
|
+
if missing:
|
|
987
|
+
raise AuthorityError(
|
|
988
|
+
"MISSING_TEST_RESULT", "required explicit PASS lines are absent: " + ", ".join(missing)
|
|
989
|
+
)
|
|
990
|
+
return [{"name": name, "status": "pass"} for name in required]
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def _vitest_verbose_collector(stdout: bytes, stderr: bytes, required: list[str]) -> list[dict[str, str]]:
|
|
994
|
+
text = (stdout + b"\n" + stderr).decode("utf-8", "replace")
|
|
995
|
+
passed: dict[str, int] = {}
|
|
996
|
+
for line in text.splitlines():
|
|
997
|
+
stripped = line.strip()
|
|
998
|
+
if stripped.startswith(("✓ ", "√ ")):
|
|
999
|
+
name = stripped[2:].rsplit(" > ", 1)[-1]
|
|
1000
|
+
name = re.sub(r"\s+\d+(?:\.\d+)?m?s$", "", name)
|
|
1001
|
+
passed[name] = passed.get(name, 0) + 1
|
|
1002
|
+
duplicates = [name for name in required if passed.get(name, 0) > 1]
|
|
1003
|
+
if duplicates:
|
|
1004
|
+
raise AuthorityError("DUPLICATE_TEST_RESULT", "required Vitest cases are duplicated: " + ", ".join(duplicates))
|
|
1005
|
+
missing = [name for name in required if passed.get(name, 0) != 1]
|
|
1006
|
+
if missing:
|
|
1007
|
+
raise AuthorityError(
|
|
1008
|
+
"MISSING_TEST_RESULT", "required exact Vitest case names are absent: " + ", ".join(missing)
|
|
1009
|
+
)
|
|
1010
|
+
return [{"name": name, "status": "pass"} for name in required]
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _exit_only_collector(_stdout: bytes, _stderr: bytes, required: list[str]) -> list[dict[str, str]]:
|
|
1014
|
+
if required:
|
|
1015
|
+
raise AuthorityError("UNSUPPORTED_COLLECTOR", "exit-only profile cannot attest named tests")
|
|
1016
|
+
return []
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
COLLECTORS: dict[str, Callable[[bytes, bytes, list[str]], list[dict[str, str]]]] = {
|
|
1020
|
+
"python-unittest-text/0.1.0": _unittest_collector,
|
|
1021
|
+
"codearbiter-named-lines/0.1.0": _named_line_collector,
|
|
1022
|
+
"vitest-verbose/0.1.0": _vitest_verbose_collector,
|
|
1023
|
+
"exit-only/0.1.0": _exit_only_collector,
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def _collector_profile(argv: list[str], required: list[str]) -> str:
|
|
1028
|
+
if not required:
|
|
1029
|
+
return "exit-only/0.1.0"
|
|
1030
|
+
lowered = [part.casefold() for part in argv]
|
|
1031
|
+
if len(lowered) >= 3 and lowered[1:3] == ["-m", "unittest"]:
|
|
1032
|
+
return "python-unittest-text/0.1.0"
|
|
1033
|
+
if "vitest" in lowered and "--reporter=verbose" in lowered:
|
|
1034
|
+
return "vitest-verbose/0.1.0"
|
|
1035
|
+
if "tsx" in lowered or lowered[:3] == ["npm", "run", "test:client"] or lowered[:3] == ["npm", "run", "check"]:
|
|
1036
|
+
return "codearbiter-named-lines/0.1.0"
|
|
1037
|
+
raise AuthorityError("UNSUPPORTED_COLLECTOR", "declared runner has no qualified collector")
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def _minimal_environment() -> tuple[dict[str, str], str]:
|
|
1041
|
+
names = (
|
|
1042
|
+
"PATH", "PATHEXT", "SystemRoot", "WINDIR", "COMSPEC", "TEMP", "TMP",
|
|
1043
|
+
"HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "LANG", "LC_ALL",
|
|
1044
|
+
)
|
|
1045
|
+
environment = {name: os.environ[name] for name in names if name in os.environ}
|
|
1046
|
+
return environment, _digest(_canonical(environment))
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def _resolved_directory(value: Any) -> Path:
|
|
1050
|
+
if not isinstance(value, (str, Path)):
|
|
1051
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "workspace resolver returned no directory")
|
|
1052
|
+
try:
|
|
1053
|
+
path = Path(value).resolve(strict=True)
|
|
1054
|
+
info = path.lstat()
|
|
1055
|
+
except OSError as exc:
|
|
1056
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "workspace directory is unavailable") from exc
|
|
1057
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
1058
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISDIR(info.st_mode):
|
|
1059
|
+
raise AuthorityError("UNSUPPORTED_WORKSPACE", "workspace must be a real directory")
|
|
1060
|
+
return path
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _run_contained(
|
|
1064
|
+
argv: list[str], *, cwd: str, env: dict[str, str], timeout_seconds: float = 1800.0
|
|
1065
|
+
) -> subprocess.CompletedProcess:
|
|
1066
|
+
"""Bound output and descendants with POSIX sessions or a Windows Job Object."""
|
|
1067
|
+
if not isinstance(timeout_seconds, (int, float)) or timeout_seconds <= 0:
|
|
1068
|
+
raise AuthorityError("INVALID_PROCESS_TIMEOUT", "process timeout must be positive")
|
|
1069
|
+
job = None
|
|
1070
|
+
if os.name == "nt":
|
|
1071
|
+
import ctypes
|
|
1072
|
+
from ctypes import wintypes
|
|
1073
|
+
|
|
1074
|
+
class BasicLimits(ctypes.Structure):
|
|
1075
|
+
_fields_ = [
|
|
1076
|
+
("PerProcessUserTimeLimit", ctypes.c_longlong),
|
|
1077
|
+
("PerJobUserTimeLimit", ctypes.c_longlong),
|
|
1078
|
+
("LimitFlags", wintypes.DWORD),
|
|
1079
|
+
("MinimumWorkingSetSize", ctypes.c_size_t),
|
|
1080
|
+
("MaximumWorkingSetSize", ctypes.c_size_t),
|
|
1081
|
+
("ActiveProcessLimit", wintypes.DWORD),
|
|
1082
|
+
("Affinity", ctypes.c_size_t),
|
|
1083
|
+
("PriorityClass", wintypes.DWORD),
|
|
1084
|
+
("SchedulingClass", wintypes.DWORD),
|
|
1085
|
+
]
|
|
1086
|
+
|
|
1087
|
+
class IoCounters(ctypes.Structure):
|
|
1088
|
+
_fields_ = [(name, ctypes.c_ulonglong) for name in (
|
|
1089
|
+
"ReadOperationCount", "WriteOperationCount", "OtherOperationCount",
|
|
1090
|
+
"ReadTransferCount", "WriteTransferCount", "OtherTransferCount",
|
|
1091
|
+
)]
|
|
1092
|
+
|
|
1093
|
+
class ExtendedLimits(ctypes.Structure):
|
|
1094
|
+
_fields_ = [
|
|
1095
|
+
("BasicLimitInformation", BasicLimits), ("IoInfo", IoCounters),
|
|
1096
|
+
("ProcessMemoryLimit", ctypes.c_size_t), ("JobMemoryLimit", ctypes.c_size_t),
|
|
1097
|
+
("PeakProcessMemoryUsed", ctypes.c_size_t), ("PeakJobMemoryUsed", ctypes.c_size_t),
|
|
1098
|
+
]
|
|
1099
|
+
|
|
1100
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
1101
|
+
ntdll = ctypes.WinDLL("ntdll")
|
|
1102
|
+
kernel32.CreateJobObjectW.restype = wintypes.HANDLE
|
|
1103
|
+
kernel32.CreateJobObjectW.argtypes = [ctypes.c_void_p, wintypes.LPCWSTR]
|
|
1104
|
+
kernel32.SetInformationJobObject.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD]
|
|
1105
|
+
kernel32.AssignProcessToJobObject.argtypes = [wintypes.HANDLE, wintypes.HANDLE]
|
|
1106
|
+
kernel32.TerminateJobObject.argtypes = [wintypes.HANDLE, wintypes.UINT]
|
|
1107
|
+
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
1108
|
+
ntdll.NtResumeProcess.argtypes = [wintypes.HANDLE]
|
|
1109
|
+
job = kernel32.CreateJobObjectW(None, None)
|
|
1110
|
+
if not job:
|
|
1111
|
+
raise AuthorityError("UNSUPPORTED_PROCESS_CONTAINMENT", "CreateJobObjectW failed")
|
|
1112
|
+
limits = ExtendedLimits()
|
|
1113
|
+
limits.BasicLimitInformation.LimitFlags = 0x00002000 # KILL_ON_JOB_CLOSE
|
|
1114
|
+
if not kernel32.SetInformationJobObject(job, 9, ctypes.byref(limits), ctypes.sizeof(limits)):
|
|
1115
|
+
kernel32.CloseHandle(job)
|
|
1116
|
+
raise AuthorityError("UNSUPPORTED_PROCESS_CONTAINMENT", "job limits could not be installed")
|
|
1117
|
+
try:
|
|
1118
|
+
process = subprocess.Popen(
|
|
1119
|
+
argv, cwd=cwd, env=env, stdin=subprocess.DEVNULL,
|
|
1120
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False,
|
|
1121
|
+
creationflags=0x00000200 | 0x00000004, # NEW_PROCESS_GROUP | SUSPENDED
|
|
1122
|
+
)
|
|
1123
|
+
if not kernel32.AssignProcessToJobObject(job, wintypes.HANDLE(int(process._handle))):
|
|
1124
|
+
process.kill()
|
|
1125
|
+
process.wait()
|
|
1126
|
+
raise AuthorityError("UNSUPPORTED_PROCESS_CONTAINMENT", "child could not enter Job Object")
|
|
1127
|
+
if ntdll.NtResumeProcess(wintypes.HANDLE(int(process._handle))) != 0:
|
|
1128
|
+
kernel32.TerminateJobObject(job, 1)
|
|
1129
|
+
process.wait()
|
|
1130
|
+
raise AuthorityError("UNSUPPORTED_PROCESS_CONTAINMENT", "suspended child could not resume")
|
|
1131
|
+
except Exception:
|
|
1132
|
+
kernel32.CloseHandle(job)
|
|
1133
|
+
raise
|
|
1134
|
+
else:
|
|
1135
|
+
process = subprocess.Popen(
|
|
1136
|
+
argv, cwd=cwd, env=env, stdin=subprocess.DEVNULL,
|
|
1137
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False,
|
|
1138
|
+
start_new_session=True,
|
|
1139
|
+
)
|
|
1140
|
+
buffers = {"stdout": bytearray(), "stderr": bytearray()}
|
|
1141
|
+
overflow = threading.Event()
|
|
1142
|
+
|
|
1143
|
+
def drain(name: str, stream: Any) -> None:
|
|
1144
|
+
while True:
|
|
1145
|
+
chunk = stream.read(65536)
|
|
1146
|
+
if not chunk:
|
|
1147
|
+
return
|
|
1148
|
+
if len(buffers[name]) + len(chunk) > MAX_OUTPUT:
|
|
1149
|
+
overflow.set()
|
|
1150
|
+
else:
|
|
1151
|
+
buffers[name].extend(chunk)
|
|
1152
|
+
|
|
1153
|
+
threads = [
|
|
1154
|
+
threading.Thread(target=drain, args=("stdout", process.stdout), daemon=True),
|
|
1155
|
+
threading.Thread(target=drain, args=("stderr", process.stderr), daemon=True),
|
|
1156
|
+
]
|
|
1157
|
+
for thread in threads:
|
|
1158
|
+
thread.start()
|
|
1159
|
+
deadline = time.monotonic() + timeout_seconds
|
|
1160
|
+
try:
|
|
1161
|
+
while process.poll() is None:
|
|
1162
|
+
if overflow.is_set() or time.monotonic() >= deadline:
|
|
1163
|
+
if os.name == "nt":
|
|
1164
|
+
kernel32.TerminateJobObject(job, 1)
|
|
1165
|
+
else:
|
|
1166
|
+
import signal
|
|
1167
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
1168
|
+
process.wait(timeout=10)
|
|
1169
|
+
if overflow.is_set():
|
|
1170
|
+
raise AuthorityError("PROCESS_OUTPUT_TOO_LARGE", "process output exceeds 8 MiB")
|
|
1171
|
+
raise AuthorityError("VERIFICATION_PROCESS_FAILED", "verification process timed out")
|
|
1172
|
+
time.sleep(0.02)
|
|
1173
|
+
finally:
|
|
1174
|
+
if job is not None:
|
|
1175
|
+
kernel32.CloseHandle(job) # KILL_ON_JOB_CLOSE ends surviving descendants.
|
|
1176
|
+
job = None
|
|
1177
|
+
elif process.poll() is not None:
|
|
1178
|
+
try:
|
|
1179
|
+
import signal
|
|
1180
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
1181
|
+
except (OSError, ProcessLookupError):
|
|
1182
|
+
pass
|
|
1183
|
+
for thread in threads:
|
|
1184
|
+
thread.join(timeout=10)
|
|
1185
|
+
process.stdout.close()
|
|
1186
|
+
process.stderr.close()
|
|
1187
|
+
if overflow.is_set():
|
|
1188
|
+
raise AuthorityError("PROCESS_OUTPUT_TOO_LARGE", "process output exceeds 8 MiB")
|
|
1189
|
+
return subprocess.CompletedProcess(argv, process.returncode, bytes(buffers["stdout"]), bytes(buffers["stderr"]))
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def _observation(root: Path, request: dict[str, Any], value: dict[str, Any]) -> None:
|
|
1193
|
+
raw = _canonical(value)
|
|
1194
|
+
digest = _digest(raw)
|
|
1195
|
+
relative = OBSERVATION_DIR / f"{digest}.json"
|
|
1196
|
+
_publish_immutable(root, relative, raw)
|
|
1197
|
+
request["observation_ref"] = relative.as_posix()
|
|
1198
|
+
request["observation_sha256"] = digest
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _closed_observation(
|
|
1202
|
+
request: dict[str, Any], payload: dict[str, Any], producer_profile: str,
|
|
1203
|
+
producer_run_id: str, producer_result: dict[str, Any],
|
|
1204
|
+
) -> dict[str, Any]:
|
|
1205
|
+
return {
|
|
1206
|
+
"format": OBSERVATION_FORMAT,
|
|
1207
|
+
"kind": request["activity"],
|
|
1208
|
+
"subject": request["context"]["subject"],
|
|
1209
|
+
"context_ref": request["context_ref"],
|
|
1210
|
+
"context_sha256": request["context_sha256"],
|
|
1211
|
+
"payload_sha256": _digest(_canonical(payload)),
|
|
1212
|
+
"producer_profile": producer_profile,
|
|
1213
|
+
"producer_run_id": producer_run_id,
|
|
1214
|
+
"producer_result": producer_result,
|
|
1215
|
+
"producer_result_sha256": _digest(_canonical(producer_result)),
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def capture_user_prompt(
|
|
1220
|
+
root: str | Path, client: Any, event: dict[str, Any], *, host: str, session_id: str
|
|
1221
|
+
) -> dict[str, Any]:
|
|
1222
|
+
"""Capture one exact host UserPromptSubmit event through a closed observation."""
|
|
1223
|
+
root = _real_root(root)
|
|
1224
|
+
if (
|
|
1225
|
+
not isinstance(event, dict)
|
|
1226
|
+
or event.get("kind") not in {"approval", "prerequisite", "reconciliation", "farm_authorization"}
|
|
1227
|
+
or event.get("authority_kind") != "user_workflow"
|
|
1228
|
+
or not isinstance(event.get("subject"), dict)
|
|
1229
|
+
or not isinstance(event.get("source_text"), str)
|
|
1230
|
+
or not event["source_text"]
|
|
1231
|
+
):
|
|
1232
|
+
raise AuthorityError("INVALID_PROMPT_EVENT", "user prompt event is malformed")
|
|
1233
|
+
producer_run_id = f"{_host_id(host, 'host')}:{_host_id(session_id, 'session_id')}"
|
|
1234
|
+
prompt_sha256 = _digest(event["source_text"].encode("utf-8"))
|
|
1235
|
+
subject = event["subject"]
|
|
1236
|
+
result = client.call("evidence-context", {
|
|
1237
|
+
"artifact_id": subject.get("artifact_id"),
|
|
1238
|
+
"record_id": subject.get("record_id"),
|
|
1239
|
+
"activity": event["kind"],
|
|
1240
|
+
"prompt_sha256": prompt_sha256,
|
|
1241
|
+
})
|
|
1242
|
+
if not isinstance(result, dict):
|
|
1243
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "prompt context result is malformed")
|
|
1244
|
+
context_ref = result.get("context_ref")
|
|
1245
|
+
context_sha256 = _validate_hash(result.get("context_sha256"), "context_sha256")
|
|
1246
|
+
if context_ref != f".codearbiter/.artifacts/evidence-contexts/{context_sha256}.json":
|
|
1247
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "prompt context is not content addressed")
|
|
1248
|
+
try:
|
|
1249
|
+
context_raw = (root / context_ref).read_bytes()
|
|
1250
|
+
context = json.loads(context_raw.decode("utf-8"))
|
|
1251
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
1252
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "prompt context is unavailable") from exc
|
|
1253
|
+
if (
|
|
1254
|
+
_digest(context_raw) != context_sha256
|
|
1255
|
+
or context_raw != _canonical(context)
|
|
1256
|
+
or context.get("activity") != event["kind"]
|
|
1257
|
+
or context.get("subject") != subject
|
|
1258
|
+
or context.get("prompt_sha256") != prompt_sha256
|
|
1259
|
+
):
|
|
1260
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "prompt context changed its authority binding")
|
|
1261
|
+
producer_result = {"host": host, "session_id": session_id, "prompt_sha256": prompt_sha256}
|
|
1262
|
+
observation = {
|
|
1263
|
+
"format": OBSERVATION_FORMAT,
|
|
1264
|
+
"kind": event["kind"],
|
|
1265
|
+
"subject": subject,
|
|
1266
|
+
"context_ref": context_ref,
|
|
1267
|
+
"context_sha256": context_sha256,
|
|
1268
|
+
"payload_sha256": _digest(_canonical(event.get("payload"))),
|
|
1269
|
+
"producer_profile": "host-user-prompt/0.1.0",
|
|
1270
|
+
"producer_run_id": producer_run_id,
|
|
1271
|
+
"producer_result": producer_result,
|
|
1272
|
+
"producer_result_sha256": _digest(_canonical(producer_result)),
|
|
1273
|
+
}
|
|
1274
|
+
observation_raw = _canonical(observation)
|
|
1275
|
+
observation_sha256 = _digest(observation_raw)
|
|
1276
|
+
observation_ref = OBSERVATION_DIR / f"{observation_sha256}.json"
|
|
1277
|
+
_publish_immutable(root, observation_ref, observation_raw)
|
|
1278
|
+
current_event = dict(event)
|
|
1279
|
+
current_event["format"] = EVENT_FORMAT
|
|
1280
|
+
current_event["observation_ref"] = observation_ref.as_posix()
|
|
1281
|
+
current_event["observation_sha256"] = observation_sha256
|
|
1282
|
+
event_raw = _canonical(current_event)
|
|
1283
|
+
event_sha256 = _digest(event_raw)
|
|
1284
|
+
source_ref = SOURCE_DIR / f"{event_sha256}.json"
|
|
1285
|
+
_publish_immutable(root, source_ref, event_raw)
|
|
1286
|
+
captured = client.call("capture-observation", {
|
|
1287
|
+
"source_ref": source_ref.as_posix(), "source_sha256": event_sha256,
|
|
1288
|
+
})
|
|
1289
|
+
if not isinstance(captured, dict) or not isinstance(captured.get("receipt"), str):
|
|
1290
|
+
raise AuthorityError("INVALID_RECEIPT", "observed prompt capture returned no receipt")
|
|
1291
|
+
return {
|
|
1292
|
+
**captured, "authority_source": source_ref.as_posix(),
|
|
1293
|
+
"observation_ref": observation_ref.as_posix(),
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def run_verification(
|
|
1298
|
+
root: str | Path,
|
|
1299
|
+
client: Any,
|
|
1300
|
+
request_id: str,
|
|
1301
|
+
) -> dict[str, Any]:
|
|
1302
|
+
root = _real_root(root)
|
|
1303
|
+
request = _load(root, request_id)
|
|
1304
|
+
if request["activity"] != "verification":
|
|
1305
|
+
raise AuthorityError("INVALID_AUTHORITY_REQUEST", "request is not verification")
|
|
1306
|
+
if request["state"] == "COMPLETED":
|
|
1307
|
+
# Marked so Claude corroboration, which binds evidence to the
|
|
1308
|
+
# observed call, can tell a replay from the run it authorized.
|
|
1309
|
+
return {
|
|
1310
|
+
"request_id": request_id, "state": "COMPLETED",
|
|
1311
|
+
"commands": request["payload"]["commands"], "replayed": True,
|
|
1312
|
+
}
|
|
1313
|
+
if request["state"] != "ARMED":
|
|
1314
|
+
raise AuthorityError("INTERRUPTED_ATTEMPT", "verification attempt cannot be rerun")
|
|
1315
|
+
wrapper = request.get("wrapper")
|
|
1316
|
+
if not isinstance(wrapper, dict) or wrapper.get("state") != "AUTHORIZED":
|
|
1317
|
+
raise AuthorityError(
|
|
1318
|
+
"UNAUTHORIZED_VERIFIER",
|
|
1319
|
+
"verification requires a correlated PreToolUse authorization",
|
|
1320
|
+
)
|
|
1321
|
+
if _bind_commands(root, request["context"], request["workspace_roots"]) != request["command_bindings"]:
|
|
1322
|
+
raise AuthorityError("WORKSPACE_DRIFT", "effective argv or workspace changed after arm")
|
|
1323
|
+
fresh_context, fresh_ref, fresh_hash = _context(
|
|
1324
|
+
root, client, request["context"]["subject"]["artifact_id"],
|
|
1325
|
+
request["context"]["subject"]["record_id"], "verification",
|
|
1326
|
+
)
|
|
1327
|
+
if (fresh_context, fresh_ref, fresh_hash) != (
|
|
1328
|
+
request["context"], request["context_ref"], request["context_sha256"],
|
|
1329
|
+
):
|
|
1330
|
+
raise AuthorityError("WORKSPACE_DRIFT", "repository content changed after arm")
|
|
1331
|
+
workspace_before = _workspace_snapshots(request["command_bindings"])
|
|
1332
|
+
if workspace_before != wrapper.get("workspace_before"):
|
|
1333
|
+
raise AuthorityError("WORKSPACE_DRIFT", "workspace changed after wrapper authorization")
|
|
1334
|
+
|
|
1335
|
+
environment, environment_sha256 = _minimal_environment()
|
|
1336
|
+
request["state"] = "RUNNING"
|
|
1337
|
+
request["attempt"] = secrets.token_hex(16)
|
|
1338
|
+
_save(root, request)
|
|
1339
|
+
commands = []
|
|
1340
|
+
for command, binding in zip(request["context"]["commands"], request["command_bindings"]):
|
|
1341
|
+
definition = command["definition"]
|
|
1342
|
+
collector = COLLECTORS[_collector_profile(definition["argv"], definition["required_tests"])]
|
|
1343
|
+
cwd = _resolved_directory(binding["cwd"])
|
|
1344
|
+
try:
|
|
1345
|
+
completed = _run_contained(list(binding["argv"]), cwd=str(cwd), env=environment)
|
|
1346
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
1347
|
+
raise AuthorityError("VERIFICATION_PROCESS_FAILED", "verification process did not complete") from exc
|
|
1348
|
+
stdout = _bounded_output(completed.stdout)
|
|
1349
|
+
stderr = _bounded_output(completed.stderr)
|
|
1350
|
+
if type(completed.returncode) is not int or not 0 <= completed.returncode <= 255:
|
|
1351
|
+
raise AuthorityError("INVALID_PROCESS_RESULT", "process exit is invalid")
|
|
1352
|
+
if completed.returncode != definition["expected_exit"]:
|
|
1353
|
+
raise AuthorityError("FAILED_VERIFICATION", "process exit differs from approved definition")
|
|
1354
|
+
tests = collector(stdout, stderr, list(definition["required_tests"]))
|
|
1355
|
+
commands.append({
|
|
1356
|
+
"definition_sha256": command["definition_sha256"],
|
|
1357
|
+
"exit": completed.returncode,
|
|
1358
|
+
"tests": tests,
|
|
1359
|
+
"stdout_sha256": _digest(stdout),
|
|
1360
|
+
"stderr_sha256": _digest(stderr),
|
|
1361
|
+
})
|
|
1362
|
+
workspace_after = _workspace_snapshots(request["command_bindings"])
|
|
1363
|
+
if workspace_after != workspace_before:
|
|
1364
|
+
raise AuthorityError("WORKSPACE_DRIFT", "verification changed the frozen workspace")
|
|
1365
|
+
final_context, final_ref, final_hash = _context(
|
|
1366
|
+
root, client, request["context"]["subject"]["artifact_id"],
|
|
1367
|
+
request["context"]["subject"]["record_id"], "verification",
|
|
1368
|
+
)
|
|
1369
|
+
if (final_context, final_ref, final_hash) != (
|
|
1370
|
+
request["context"], request["context_ref"], request["context_sha256"],
|
|
1371
|
+
):
|
|
1372
|
+
raise AuthorityError("WORKSPACE_DRIFT", "verification changed repository content")
|
|
1373
|
+
producer_result = {
|
|
1374
|
+
"environment_sha256": environment_sha256,
|
|
1375
|
+
"command_bindings": request["command_bindings"],
|
|
1376
|
+
"workspace_before": workspace_before,
|
|
1377
|
+
"workspace_after": workspace_after,
|
|
1378
|
+
"commands": commands,
|
|
1379
|
+
}
|
|
1380
|
+
payload = {
|
|
1381
|
+
"input_sha256": request["context"]["input_sha256"],
|
|
1382
|
+
"spec_sha256": request["context"]["spec_sha256"],
|
|
1383
|
+
"task_sha256": request["context"]["task_sha256"],
|
|
1384
|
+
"commands": commands,
|
|
1385
|
+
}
|
|
1386
|
+
observation = _closed_observation(
|
|
1387
|
+
request, payload, "declared-command/0.1.0", request["attempt"],
|
|
1388
|
+
producer_result,
|
|
1389
|
+
)
|
|
1390
|
+
_observation(root, request, observation)
|
|
1391
|
+
request["payload"] = payload
|
|
1392
|
+
request["state"] = "COMPLETED"
|
|
1393
|
+
_save(root, request)
|
|
1394
|
+
return {"request_id": request_id, "state": "COMPLETED", "commands": commands}
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
def _request_id_from_prompt(message: Any) -> str:
|
|
1398
|
+
if not isinstance(message, str):
|
|
1399
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "spawn message is absent")
|
|
1400
|
+
match = re.match(r"\[CODEARBITER_AUTHORITY_REQUEST:([0-9a-f]{64})\]\n", message)
|
|
1401
|
+
if match is None:
|
|
1402
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "spawn message has no authority request")
|
|
1403
|
+
return match.group(1)
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def _host_id(value: Any, field: str) -> str:
|
|
1407
|
+
if not isinstance(value, str) or HOST_ID_RE.fullmatch(value) is None:
|
|
1408
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", f"{field} is malformed")
|
|
1409
|
+
return value
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def _verification_selector(event: dict[str, Any]) -> tuple[str, Path, str] | None:
|
|
1413
|
+
"""Recognize only the closed wrapper selector, never caller-authored child argv."""
|
|
1414
|
+
if event.get("tool_name") not in {
|
|
1415
|
+
"Bash", "shell_command", "exec_command", "unified_exec",
|
|
1416
|
+
}:
|
|
1417
|
+
return None
|
|
1418
|
+
tool_input = event.get("tool_input")
|
|
1419
|
+
if not isinstance(tool_input, dict):
|
|
1420
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "exec input is malformed")
|
|
1421
|
+
command = tool_input.get("command", tool_input.get("cmd"))
|
|
1422
|
+
if not isinstance(command, str):
|
|
1423
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "exec command is absent")
|
|
1424
|
+
compound = any(marker in command for marker in ("\n", "\r", ";", "&", "|", ">", "<", "`", "$"))
|
|
1425
|
+
try:
|
|
1426
|
+
tokens = [token.strip('"') for token in shlex.split(command, posix=False)]
|
|
1427
|
+
except ValueError as exc:
|
|
1428
|
+
# An ordinary command with unbalanced quoting is not ours to deny.
|
|
1429
|
+
if "artifact-authority" not in command.casefold():
|
|
1430
|
+
return None
|
|
1431
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier wrapper command is malformed") from exc
|
|
1432
|
+
wrapper_start = (
|
|
1433
|
+
len(tokens) >= 3
|
|
1434
|
+
and Path(tokens[0]).name.lower() in {
|
|
1435
|
+
"python", "python.exe", "python3", "python3.exe", "py", "py.exe",
|
|
1436
|
+
}
|
|
1437
|
+
and Path(tokens[1]).name.lower() == "artifact-authority.py"
|
|
1438
|
+
and tokens[2] == "verify"
|
|
1439
|
+
)
|
|
1440
|
+
if compound:
|
|
1441
|
+
if wrapper_start:
|
|
1442
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier wrapper command is compound")
|
|
1443
|
+
return None
|
|
1444
|
+
if len(tokens) != 7:
|
|
1445
|
+
return None
|
|
1446
|
+
if (
|
|
1447
|
+
Path(tokens[0]).name.lower() not in {
|
|
1448
|
+
"python", "python.exe", "python3", "python3.exe", "py", "py.exe",
|
|
1449
|
+
}
|
|
1450
|
+
or Path(tokens[1]).name.lower() != "artifact-authority.py"
|
|
1451
|
+
or tokens[2] != "verify"
|
|
1452
|
+
or tokens[3] != "--root"
|
|
1453
|
+
or tokens[5] != "--request-id"
|
|
1454
|
+
or REQUEST_RE.fullmatch(tokens[6]) is None
|
|
1455
|
+
):
|
|
1456
|
+
return None
|
|
1457
|
+
return tokens[6], _real_root(tokens[4]), tokens[1]
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
def observe_verifier_hook(
|
|
1461
|
+
invocation_root: str | Path, event: dict[str, Any], host: str = "codex",
|
|
1462
|
+
) -> dict[str, Any] | None:
|
|
1463
|
+
"""Authorize/corroborate the wrapper; native exit/output remain result evidence."""
|
|
1464
|
+
if host not in HOSTS:
|
|
1465
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier host is unsupported")
|
|
1466
|
+
events = {"PreToolUse", "PostToolUse"} | ({"PostToolUseFailure"} if host == "claude" else set())
|
|
1467
|
+
if not isinstance(event, dict) or event.get("hook_event_name") not in events:
|
|
1468
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier hook event is unsupported")
|
|
1469
|
+
selected = _verification_selector(event)
|
|
1470
|
+
if selected is None:
|
|
1471
|
+
return None
|
|
1472
|
+
# Only an authority call is ever refused for an unavailable invocation root.
|
|
1473
|
+
_real_root(invocation_root)
|
|
1474
|
+
request_id, selected_root, script = selected
|
|
1475
|
+
root, request = _registered_request(request_id)
|
|
1476
|
+
if host == "claude":
|
|
1477
|
+
shipped = (Path(__file__).resolve().parent / "artifact-authority.py").resolve()
|
|
1478
|
+
try:
|
|
1479
|
+
named = Path(script).resolve(strict=True)
|
|
1480
|
+
except OSError:
|
|
1481
|
+
named = None
|
|
1482
|
+
if named != shipped:
|
|
1483
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier wrapper is not the shipped artifact-authority.py")
|
|
1484
|
+
if (
|
|
1485
|
+
root != selected_root
|
|
1486
|
+
or request["activity"] != "verification"
|
|
1487
|
+
or request.get("host", "codex") != host
|
|
1488
|
+
):
|
|
1489
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier target does not match its request")
|
|
1490
|
+
session_id = _host_id(event.get("session_id"), "session_id")
|
|
1491
|
+
# Codex names the parent turn turn_id; Claude Code names it prompt_id.
|
|
1492
|
+
turn_field = "prompt_id" if host == "claude" else "turn_id"
|
|
1493
|
+
turn_id = _host_id(event.get(turn_field), turn_field)
|
|
1494
|
+
tool_use_id = _host_id(event.get("tool_use_id"), "tool_use_id")
|
|
1495
|
+
if host == "claude" and event["hook_event_name"] == "PreToolUse":
|
|
1496
|
+
tool_input = event["tool_input"]
|
|
1497
|
+
# A background run returns no stdout to corroborate; an unsandboxed
|
|
1498
|
+
# run escapes the host boundary the evidence assumes.
|
|
1499
|
+
if tool_input.get("run_in_background") or tool_input.get("dangerouslyDisableSandbox"):
|
|
1500
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier wrapper must run in the foreground and sandbox")
|
|
1501
|
+
if set(tool_input) - {"command", "description", "timeout"}:
|
|
1502
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier wrapper input carries unexpected options")
|
|
1503
|
+
if event["hook_event_name"] == "PreToolUse":
|
|
1504
|
+
if request["state"] != "ARMED" or request.get("wrapper") is not None:
|
|
1505
|
+
raise AuthorityError("INTERRUPTED_ATTEMPT", "verifier wrapper is not armable")
|
|
1506
|
+
current_bindings = _bind_commands(root, request["context"], request["workspace_roots"])
|
|
1507
|
+
if current_bindings != request["command_bindings"]:
|
|
1508
|
+
raise AuthorityError("WORKSPACE_DRIFT", "effective child argv or cwd changed")
|
|
1509
|
+
for command in request["context"]["commands"]:
|
|
1510
|
+
definition = command["definition"]
|
|
1511
|
+
_collector_profile(definition["argv"], definition["required_tests"])
|
|
1512
|
+
workspace_before = _workspace_snapshots(current_bindings)
|
|
1513
|
+
request["wrapper"] = {
|
|
1514
|
+
"state": "AUTHORIZED", "session_id": session_id,
|
|
1515
|
+
turn_field: turn_id, "tool_use_id": tool_use_id,
|
|
1516
|
+
"command_bindings": current_bindings,
|
|
1517
|
+
"workspace_before": workspace_before,
|
|
1518
|
+
}
|
|
1519
|
+
_save(root, request)
|
|
1520
|
+
return {
|
|
1521
|
+
"request_id": request_id, "state": "AUTHORIZED",
|
|
1522
|
+
"command_bindings": current_bindings, "workspace_before": workspace_before,
|
|
1523
|
+
}
|
|
1524
|
+
wrapper = request.get("wrapper")
|
|
1525
|
+
if (
|
|
1526
|
+
not isinstance(wrapper, dict)
|
|
1527
|
+
or wrapper.get("session_id") != session_id
|
|
1528
|
+
or wrapper.get(turn_field) != turn_id
|
|
1529
|
+
or wrapper.get("tool_use_id") != tool_use_id
|
|
1530
|
+
):
|
|
1531
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier completion is not correlated")
|
|
1532
|
+
if host == "claude":
|
|
1533
|
+
return _claude_verifier_result(root, request, wrapper, event)
|
|
1534
|
+
if request["state"] != "COMPLETED":
|
|
1535
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier completion is not correlated")
|
|
1536
|
+
response = event.get("tool_response")
|
|
1537
|
+
if isinstance(response, dict):
|
|
1538
|
+
if response.get("exit_code") != 0:
|
|
1539
|
+
raise AuthorityError("FAILED_VERIFICATION", "verifier wrapper did not report exit 0")
|
|
1540
|
+
elif isinstance(response, str):
|
|
1541
|
+
try:
|
|
1542
|
+
completed = json.loads(response) if len(response.encode("utf-8")) <= MAX_OUTPUT else None
|
|
1543
|
+
except ValueError:
|
|
1544
|
+
completed = None
|
|
1545
|
+
if (
|
|
1546
|
+
not isinstance(completed, dict)
|
|
1547
|
+
or completed.get("request_id") != request_id
|
|
1548
|
+
or completed.get("state") != "COMPLETED"
|
|
1549
|
+
):
|
|
1550
|
+
raise AuthorityError("FAILED_VERIFICATION", "verifier wrapper output did not corroborate completion")
|
|
1551
|
+
else:
|
|
1552
|
+
raise AuthorityError("FAILED_VERIFICATION", "verifier wrapper result is unavailable")
|
|
1553
|
+
wrapper["state"] = "CORROBORATED"
|
|
1554
|
+
_save(root, request)
|
|
1555
|
+
return {"request_id": request_id, "state": "CORROBORATED"}
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
def _claude_verifier_result(
|
|
1559
|
+
root: Path, request: dict[str, Any], wrapper: dict[str, Any], event: dict[str, Any]
|
|
1560
|
+
) -> dict[str, Any]:
|
|
1561
|
+
"""Claude reports no exit code: success and failure are distinct events."""
|
|
1562
|
+
request_id = request["request_id"]
|
|
1563
|
+
if wrapper.get("state") != "AUTHORIZED":
|
|
1564
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier completion was already observed")
|
|
1565
|
+
if event["hook_event_name"] == "PostToolUseFailure":
|
|
1566
|
+
wrapper["state"] = "FAILED"
|
|
1567
|
+
_save(root, request)
|
|
1568
|
+
return {"request_id": request_id, "state": "FAILED"}
|
|
1569
|
+
response = event.get("tool_response")
|
|
1570
|
+
if request["state"] != "COMPLETED":
|
|
1571
|
+
raise AuthorityError("FAILED_VERIFICATION", "verifier request did not complete")
|
|
1572
|
+
if not isinstance(response, dict) or response.get("interrupted") is not False:
|
|
1573
|
+
raise AuthorityError("FAILED_VERIFICATION", "verifier wrapper was interrupted or has no result")
|
|
1574
|
+
stdout = response.get("stdout")
|
|
1575
|
+
if not isinstance(stdout, str) or len(stdout.encode("utf-8")) > MAX_OUTPUT:
|
|
1576
|
+
raise AuthorityError("FAILED_VERIFICATION", "verifier wrapper output is unavailable")
|
|
1577
|
+
try:
|
|
1578
|
+
completed = json.loads(stdout.strip())
|
|
1579
|
+
except ValueError:
|
|
1580
|
+
completed = None
|
|
1581
|
+
if (
|
|
1582
|
+
not isinstance(completed, dict)
|
|
1583
|
+
or set(completed) != {"request_id", "state", "commands"}
|
|
1584
|
+
or completed.get("request_id") != request_id
|
|
1585
|
+
or completed.get("state") != "COMPLETED"
|
|
1586
|
+
):
|
|
1587
|
+
raise AuthorityError("FAILED_VERIFICATION", "verifier wrapper output did not corroborate completion")
|
|
1588
|
+
wrapper["state"] = "CORROBORATED"
|
|
1589
|
+
_save(root, request)
|
|
1590
|
+
return {"request_id": request_id, "state": "CORROBORATED"}
|
|
1591
|
+
|
|
1592
|
+
|
|
1593
|
+
def _parse_decision(raw: Any, request: dict[str, Any]) -> dict[str, Any]:
|
|
1594
|
+
if not isinstance(raw, str) or len(raw.encode("utf-8")) > MAX_DECISION:
|
|
1595
|
+
raise AuthorityError("INVALID_REVIEW_DECISION", "review decision is absent or oversized")
|
|
1596
|
+
try:
|
|
1597
|
+
value = json.loads(raw)
|
|
1598
|
+
except ValueError as exc:
|
|
1599
|
+
raise AuthorityError("INVALID_REVIEW_DECISION", "review decision is not JSON") from exc
|
|
1600
|
+
expected = {
|
|
1601
|
+
"format", "request_id", "target_sha256", "contract_sha256", "decision",
|
|
1602
|
+
"coverage", "findings", "assessment",
|
|
1603
|
+
}
|
|
1604
|
+
context = request["context"]
|
|
1605
|
+
if (
|
|
1606
|
+
not isinstance(value, dict)
|
|
1607
|
+
or set(value) != expected
|
|
1608
|
+
or value.get("format") != DECISION_FORMAT
|
|
1609
|
+
or value.get("request_id") != request["request_id"]
|
|
1610
|
+
or value.get("target_sha256") != context["input_sha256"]
|
|
1611
|
+
or value.get("contract_sha256") != request["review_contract_sha256"]
|
|
1612
|
+
or value.get("decision") not in {"pass", "changes_requested"}
|
|
1613
|
+
or not isinstance(value.get("assessment"), str)
|
|
1614
|
+
or not value["assessment"].strip()
|
|
1615
|
+
or not isinstance(value.get("coverage"), list)
|
|
1616
|
+
or len(set(value["coverage"])) != len(value["coverage"])
|
|
1617
|
+
or not isinstance(value.get("findings"), list)
|
|
1618
|
+
):
|
|
1619
|
+
raise AuthorityError("INVALID_REVIEW_DECISION", "review decision failed its closed schema")
|
|
1620
|
+
if set(value["coverage"]) != set(request["required_coverage"]):
|
|
1621
|
+
raise AuthorityError("INCOMPLETE_REVIEW", "review coverage is incomplete")
|
|
1622
|
+
for finding in value["findings"]:
|
|
1623
|
+
if (
|
|
1624
|
+
not isinstance(finding, dict)
|
|
1625
|
+
or set(finding) != {"severity", "code", "message"}
|
|
1626
|
+
or finding["severity"] not in {"BLOCK", "WARN", "INFO"}
|
|
1627
|
+
or not all(isinstance(finding[key], str) and finding[key] for key in ("code", "message"))
|
|
1628
|
+
):
|
|
1629
|
+
raise AuthorityError("INVALID_REVIEW_DECISION", "review finding is malformed")
|
|
1630
|
+
if value["decision"] != "pass" or any(
|
|
1631
|
+
finding["severity"] == "BLOCK" for finding in value["findings"]
|
|
1632
|
+
):
|
|
1633
|
+
raise AuthorityError("REVIEW_REJECTED", "independent reviewer did not pass the target")
|
|
1634
|
+
return value
|
|
1635
|
+
|
|
1636
|
+
|
|
1637
|
+
def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any]:
|
|
1638
|
+
_real_root(root) # Invocation repository is observed but cannot select authority.
|
|
1639
|
+
if not isinstance(event, dict):
|
|
1640
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "hook event is malformed")
|
|
1641
|
+
name = event.get("hook_event_name")
|
|
1642
|
+
session_id = _host_id(event.get("session_id"), "session_id")
|
|
1643
|
+
|
|
1644
|
+
if name == "PreToolUse":
|
|
1645
|
+
if event.get("tool_name") != "spawn_agent":
|
|
1646
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "only spawn_agent can launch a reviewer")
|
|
1647
|
+
tool_use_id = _host_id(event.get("tool_use_id"), "tool_use_id")
|
|
1648
|
+
turn_id = _host_id(event.get("turn_id"), "turn_id")
|
|
1649
|
+
tool_input = event.get("tool_input")
|
|
1650
|
+
if not isinstance(tool_input, dict):
|
|
1651
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "spawn input is malformed")
|
|
1652
|
+
message = tool_input.get("message")
|
|
1653
|
+
if not isinstance(message, str) or not message.startswith("[CODEARBITER_AUTHORITY_REQUEST:"):
|
|
1654
|
+
_record_ordinary_spawn(session_id, turn_id)
|
|
1655
|
+
_reject_overlapping_authority(session_id, turn_id)
|
|
1656
|
+
return None
|
|
1657
|
+
request_id = _request_id_from_prompt(message)
|
|
1658
|
+
root, request = _registered_request(request_id)
|
|
1659
|
+
if (
|
|
1660
|
+
request.get("host", "codex") != "codex"
|
|
1661
|
+
or request["activity"] not in REVIEW_ACTIVITIES
|
|
1662
|
+
or request["state"] != "ARMED"
|
|
1663
|
+
):
|
|
1664
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review request is not launchable")
|
|
1665
|
+
if tool_input != request.get("launch_envelope"):
|
|
1666
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch envelope was changed")
|
|
1667
|
+
if _ordinary_spawn_marker(session_id, turn_id).exists():
|
|
1668
|
+
request["state"] = "REJECTED"
|
|
1669
|
+
request["recovery"] = {
|
|
1670
|
+
"mode": "overlapping-subagent-launch",
|
|
1671
|
+
"rerun_permitted": False,
|
|
1672
|
+
}
|
|
1673
|
+
_save(root, request)
|
|
1674
|
+
raise AuthorityError(
|
|
1675
|
+
"UNSUPPORTED_HOST_SEAM",
|
|
1676
|
+
"authority review cannot share a parent turn with another subagent launch",
|
|
1677
|
+
)
|
|
1678
|
+
request["launch"] = {
|
|
1679
|
+
"parent_session_id": session_id, "parent_turn_id": turn_id,
|
|
1680
|
+
"tool_use_id": tool_use_id, "post_confirmed": False,
|
|
1681
|
+
"agent_id": None, "agent_type": None,
|
|
1682
|
+
"task_name": tool_input["task_name"],
|
|
1683
|
+
"fork_turns": tool_input["fork_turns"],
|
|
1684
|
+
}
|
|
1685
|
+
request["state"] = "LAUNCHING"
|
|
1686
|
+
_save(root, request)
|
|
1687
|
+
return {"request_id": request_id, "state": "LAUNCHING"}
|
|
1688
|
+
|
|
1689
|
+
if name == "PostToolUse":
|
|
1690
|
+
if event.get("tool_name") != "spawn_agent":
|
|
1691
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "post-tool event is not reviewer launch")
|
|
1692
|
+
tool_use_id = _host_id(event.get("tool_use_id"), "tool_use_id")
|
|
1693
|
+
turn_id = _host_id(event.get("turn_id"), "turn_id")
|
|
1694
|
+
response = event.get("tool_response")
|
|
1695
|
+
if not isinstance(response, dict):
|
|
1696
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "spawn result is malformed")
|
|
1697
|
+
task_name = _host_id(response.get("task_name"), "task_name")
|
|
1698
|
+
matches = []
|
|
1699
|
+
for candidate_root, candidate in _registered_requests():
|
|
1700
|
+
launch = candidate.get("launch") or {}
|
|
1701
|
+
if (
|
|
1702
|
+
candidate["state"] in {"LAUNCHING", "RUNNING"}
|
|
1703
|
+
and launch.get("parent_session_id") == session_id
|
|
1704
|
+
and launch.get("parent_turn_id") == turn_id
|
|
1705
|
+
and launch.get("tool_use_id") == tool_use_id
|
|
1706
|
+
):
|
|
1707
|
+
matches.append((candidate_root, candidate))
|
|
1708
|
+
if not matches:
|
|
1709
|
+
return None
|
|
1710
|
+
if len(matches) != 1:
|
|
1711
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch correlation is ambiguous")
|
|
1712
|
+
root, request = matches[0]
|
|
1713
|
+
if task_name != request["launch"].get("task_name"):
|
|
1714
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "spawn result task_name differs")
|
|
1715
|
+
request["launch"]["post_confirmed"] = True
|
|
1716
|
+
_save(root, request)
|
|
1717
|
+
return {"request_id": request["request_id"], "state": request["state"]}
|
|
1718
|
+
|
|
1719
|
+
if name in {"SubagentStart", "SubagentStop"}:
|
|
1720
|
+
agent_id = _host_id(event.get("agent_id"), "agent_id")
|
|
1721
|
+
turn_id = _host_id(event.get("turn_id"), "turn_id")
|
|
1722
|
+
matches = []
|
|
1723
|
+
same_turn = []
|
|
1724
|
+
for candidate_root, candidate in _registered_requests():
|
|
1725
|
+
launch = candidate.get("launch") or {}
|
|
1726
|
+
if (
|
|
1727
|
+
launch.get("parent_session_id") == session_id
|
|
1728
|
+
and launch.get("parent_turn_id") == turn_id
|
|
1729
|
+
and candidate["state"] in {"LAUNCHING", "RUNNING"}
|
|
1730
|
+
):
|
|
1731
|
+
same_turn.append((candidate_root, candidate))
|
|
1732
|
+
if name == "SubagentStart" and (
|
|
1733
|
+
candidate["state"] == "LAUNCHING"
|
|
1734
|
+
and launch.get("parent_session_id") == session_id
|
|
1735
|
+
and launch.get("parent_turn_id") == turn_id
|
|
1736
|
+
):
|
|
1737
|
+
matches.append((candidate_root, candidate))
|
|
1738
|
+
elif name == "SubagentStop" and (
|
|
1739
|
+
launch.get("parent_session_id") == session_id
|
|
1740
|
+
and launch.get("parent_turn_id") == turn_id
|
|
1741
|
+
and launch.get("agent_id") == agent_id
|
|
1742
|
+
):
|
|
1743
|
+
matches.append((candidate_root, candidate))
|
|
1744
|
+
if not matches and not same_turn:
|
|
1745
|
+
return None
|
|
1746
|
+
if not matches and same_turn:
|
|
1747
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "subagent does not match the active authority launch")
|
|
1748
|
+
if len(matches) != 1:
|
|
1749
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "subagent correlation is ambiguous")
|
|
1750
|
+
root, request = matches[0]
|
|
1751
|
+
agent_type = _host_id(event.get("agent_type"), "agent_type")
|
|
1752
|
+
if name == "SubagentStart":
|
|
1753
|
+
request["launch"]["agent_id"] = agent_id
|
|
1754
|
+
request["launch"]["agent_type"] = agent_type
|
|
1755
|
+
request["state"] = "RUNNING"
|
|
1756
|
+
_save(root, request)
|
|
1757
|
+
return {"request_id": request["request_id"], "state": "RUNNING"}
|
|
1758
|
+
if (
|
|
1759
|
+
request["state"] != "RUNNING"
|
|
1760
|
+
or request["launch"].get("agent_type") != agent_type
|
|
1761
|
+
or request["launch"].get("post_confirmed") is not True
|
|
1762
|
+
):
|
|
1763
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "subagent stop is out of order")
|
|
1764
|
+
try:
|
|
1765
|
+
decision = _parse_decision(event.get("last_assistant_message"), request)
|
|
1766
|
+
except AuthorityError:
|
|
1767
|
+
request["state"] = "REJECTED"
|
|
1768
|
+
_save(root, request)
|
|
1769
|
+
raise
|
|
1770
|
+
payload = {
|
|
1771
|
+
"input_sha256": request["context"]["input_sha256"],
|
|
1772
|
+
"spec_sha256": request["context"]["spec_sha256"],
|
|
1773
|
+
"assessment": decision["assessment"],
|
|
1774
|
+
}
|
|
1775
|
+
if request["activity"] == "spec_review":
|
|
1776
|
+
payload["task_sha256"] = request["context"]["task_sha256"]
|
|
1777
|
+
else:
|
|
1778
|
+
payload["base_input_sha256"] = request["context"]["base_input_sha256"]
|
|
1779
|
+
payload["task_hashes"] = request["context"]["task_hashes"]
|
|
1780
|
+
producer_result = {"launch": request["launch"], "decision": decision}
|
|
1781
|
+
observation = _closed_observation(
|
|
1782
|
+
request, payload, CODEX_REVIEW_PROFILE, agent_id,
|
|
1783
|
+
producer_result,
|
|
1784
|
+
)
|
|
1785
|
+
_observation(root, request, observation)
|
|
1786
|
+
request["payload"] = payload
|
|
1787
|
+
request["state"] = "COMPLETED"
|
|
1788
|
+
_save(root, request)
|
|
1789
|
+
return {"request_id": request["request_id"], "state": "COMPLETED"}
|
|
1790
|
+
|
|
1791
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "hook event is not supported")
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
def _claude_requests(state: set[str], strict: bool = False) -> list[tuple[Path, dict[str, Any]]]:
|
|
1795
|
+
return [
|
|
1796
|
+
(root, request) for root, request in _registered_requests(strict)
|
|
1797
|
+
if request.get("host") == "claude" and request["activity"] in REVIEW_ACTIVITIES
|
|
1798
|
+
and request["state"] in state
|
|
1799
|
+
]
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
def _reject(root: Path, request: dict[str, Any], mode: str) -> None:
|
|
1803
|
+
request["state"] = "REJECTED"
|
|
1804
|
+
request["recovery"] = {"mode": mode, "rerun_permitted": False}
|
|
1805
|
+
_save(root, request)
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
def _claude_bind_child(root: Path, request: dict[str, Any]) -> dict[str, Any]:
|
|
1809
|
+
"""Complete the order-independent launch/child join when both sides exist."""
|
|
1810
|
+
launch = request["launch"]
|
|
1811
|
+
marker = _claude_start_marker(launch["agent_id"])
|
|
1812
|
+
try:
|
|
1813
|
+
started = json.loads(marker.read_text(encoding="utf-8"))
|
|
1814
|
+
except FileNotFoundError:
|
|
1815
|
+
_save(root, request)
|
|
1816
|
+
return {"request_id": request["request_id"], "state": request["state"]}
|
|
1817
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
1818
|
+
_reject(root, request, "unreadable-subagent-start")
|
|
1819
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "subagent start record is unreadable") from exc
|
|
1820
|
+
if not isinstance(started, dict) or started.get("agent_type") != CLAUDE_REVIEWER:
|
|
1821
|
+
_reject(root, request, "unpinned-reviewer-agent")
|
|
1822
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "subagent is not the pinned reviewer")
|
|
1823
|
+
launch["agent_type"] = CLAUDE_REVIEWER
|
|
1824
|
+
request["state"] = "RUNNING"
|
|
1825
|
+
_save(root, request)
|
|
1826
|
+
marker.unlink(missing_ok=True)
|
|
1827
|
+
return {"request_id": request["request_id"], "state": "RUNNING"}
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
def observe_claude_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any] | None:
|
|
1831
|
+
"""Correlate a Claude Code reviewer launch, child lifecycle, and first result.
|
|
1832
|
+
|
|
1833
|
+
The invocation repository is observed but never selects authority, and an
|
|
1834
|
+
ordinary (non-authority) event is never refused.
|
|
1835
|
+
"""
|
|
1836
|
+
if not isinstance(event, dict):
|
|
1837
|
+
return None
|
|
1838
|
+
name = event.get("hook_event_name")
|
|
1839
|
+
tool = event.get("tool_name")
|
|
1840
|
+
|
|
1841
|
+
if name == "PreToolUse" and tool == "SendMessage":
|
|
1842
|
+
# The message shape is not a recorded contract, so fail closed: any
|
|
1843
|
+
# message from the reviewer's parent session while it runs could steer it.
|
|
1844
|
+
session = event.get("session_id")
|
|
1845
|
+
rejected = None
|
|
1846
|
+
for candidate_root, candidate in _claude_requests({"LAUNCHING", "RUNNING"}, strict=True):
|
|
1847
|
+
if session is not None and session == (candidate.get("launch") or {}).get("parent_session_id"):
|
|
1848
|
+
_reject(candidate_root, candidate, "reviewer-steered-by-message")
|
|
1849
|
+
rejected = {"request_id": candidate["request_id"], "state": "REJECTED"}
|
|
1850
|
+
return rejected
|
|
1851
|
+
|
|
1852
|
+
if name == "PreToolUse" and tool == "Agent":
|
|
1853
|
+
tool_input = event.get("tool_input")
|
|
1854
|
+
prompt = tool_input.get("prompt") if isinstance(tool_input, dict) else None
|
|
1855
|
+
if not isinstance(prompt, str) or not prompt.startswith("[CODEARBITER_AUTHORITY_REQUEST:"):
|
|
1856
|
+
return None
|
|
1857
|
+
session_root = _real_root(root)
|
|
1858
|
+
session_id = _host_id(event.get("session_id"), "session_id")
|
|
1859
|
+
prompt_id = _host_id(event.get("prompt_id"), "prompt_id")
|
|
1860
|
+
tool_use_id = _host_id(event.get("tool_use_id"), "tool_use_id")
|
|
1861
|
+
root, request = _registered_request(_request_id_from_prompt(prompt))
|
|
1862
|
+
if (
|
|
1863
|
+
request.get("host") != "claude"
|
|
1864
|
+
or request["activity"] not in REVIEW_ACTIVITIES
|
|
1865
|
+
or request["state"] != "ARMED"
|
|
1866
|
+
):
|
|
1867
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review request is not launchable")
|
|
1868
|
+
if tool_input != request.get("launch_envelope"):
|
|
1869
|
+
_reject(root, request, "changed-launch-envelope")
|
|
1870
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch envelope was changed")
|
|
1871
|
+
try:
|
|
1872
|
+
_refuse_shadowed_reviewer(root, session_root)
|
|
1873
|
+
except AuthorityError:
|
|
1874
|
+
_reject(root, request, "shadowed-reviewer")
|
|
1875
|
+
raise
|
|
1876
|
+
request["launch"] = {
|
|
1877
|
+
"parent_session_id": session_id, "parent_prompt_id": prompt_id,
|
|
1878
|
+
"tool_use_id": tool_use_id, "post_confirmed": False,
|
|
1879
|
+
"agent_id": None, "agent_type": None,
|
|
1880
|
+
"subagent_type": tool_input["subagent_type"], "model": tool_input["model"],
|
|
1881
|
+
"resolved_model": None, "first_stop": False,
|
|
1882
|
+
}
|
|
1883
|
+
request["state"] = "LAUNCHING"
|
|
1884
|
+
_save(root, request)
|
|
1885
|
+
return {"request_id": request["request_id"], "state": "LAUNCHING"}
|
|
1886
|
+
|
|
1887
|
+
if name == "PostToolUse" and tool == "Agent":
|
|
1888
|
+
session_id = event.get("session_id")
|
|
1889
|
+
tool_use_id = event.get("tool_use_id")
|
|
1890
|
+
matches = [
|
|
1891
|
+
(candidate_root, candidate)
|
|
1892
|
+
for candidate_root, candidate in _claude_requests({"LAUNCHING"})
|
|
1893
|
+
if (candidate.get("launch") or {}).get("tool_use_id") == tool_use_id
|
|
1894
|
+
and candidate["launch"].get("parent_session_id") == session_id
|
|
1895
|
+
]
|
|
1896
|
+
if not matches:
|
|
1897
|
+
return None
|
|
1898
|
+
if len(matches) != 1:
|
|
1899
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch correlation is ambiguous")
|
|
1900
|
+
root, request = matches[0]
|
|
1901
|
+
response = event.get("tool_response")
|
|
1902
|
+
try:
|
|
1903
|
+
prompt_id = _host_id(event.get("prompt_id"), "prompt_id")
|
|
1904
|
+
if request["launch"].get("parent_prompt_id") != prompt_id or request["launch"]["post_confirmed"]:
|
|
1905
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch result is out of order")
|
|
1906
|
+
if not isinstance(response, dict):
|
|
1907
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "agent result is malformed")
|
|
1908
|
+
agent_id = _host_id(response.get("agentId"), "agentId")
|
|
1909
|
+
resolved = _host_id(response.get("resolvedModel"), "resolvedModel")
|
|
1910
|
+
except AuthorityError:
|
|
1911
|
+
_reject(root, request, "uncorrelated-launch-result")
|
|
1912
|
+
raise
|
|
1913
|
+
request["launch"].update(post_confirmed=True, agent_id=agent_id, resolved_model=resolved)
|
|
1914
|
+
return _claude_bind_child(root, request)
|
|
1915
|
+
|
|
1916
|
+
if name == "SubagentStart":
|
|
1917
|
+
if not _claude_requests({"LAUNCHING"}):
|
|
1918
|
+
return None
|
|
1919
|
+
agent_id = _host_id(event.get("agent_id"), "agent_id")
|
|
1920
|
+
agent_type = _host_id(event.get("agent_type"), "agent_type")
|
|
1921
|
+
for candidate_root, candidate in _claude_requests({"LAUNCHING"}):
|
|
1922
|
+
launch = candidate.get("launch") or {}
|
|
1923
|
+
if launch.get("agent_id") == agent_id:
|
|
1924
|
+
if agent_type != CLAUDE_REVIEWER:
|
|
1925
|
+
_reject(candidate_root, candidate, "unpinned-reviewer-agent")
|
|
1926
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "subagent is not the pinned reviewer")
|
|
1927
|
+
launch["agent_type"] = agent_type
|
|
1928
|
+
candidate["state"] = "RUNNING"
|
|
1929
|
+
_save(candidate_root, candidate)
|
|
1930
|
+
return {"request_id": candidate["request_id"], "state": "RUNNING"}
|
|
1931
|
+
if any(
|
|
1932
|
+
(candidate.get("launch") or {}).get("post_confirmed") is False
|
|
1933
|
+
for _root, candidate in _claude_requests({"LAUNCHING"})
|
|
1934
|
+
):
|
|
1935
|
+
# The launch result may not have arrived yet; remember this start.
|
|
1936
|
+
marker = _claude_start_marker(agent_id)
|
|
1937
|
+
data = _canonical({"agent_id": agent_id, "agent_type": agent_type})
|
|
1938
|
+
# Written aside and linked into place: a concurrent launch result
|
|
1939
|
+
# joining this agent must never read an empty or partial marker.
|
|
1940
|
+
temporary = marker.with_name(f".{marker.name}.{secrets.token_hex(8)}.tmp")
|
|
1941
|
+
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
1942
|
+
try:
|
|
1943
|
+
os.write(fd, data)
|
|
1944
|
+
os.fsync(fd)
|
|
1945
|
+
finally:
|
|
1946
|
+
os.close(fd)
|
|
1947
|
+
try:
|
|
1948
|
+
os.link(temporary, marker)
|
|
1949
|
+
except FileExistsError:
|
|
1950
|
+
return None
|
|
1951
|
+
finally:
|
|
1952
|
+
temporary.unlink(missing_ok=True)
|
|
1953
|
+
# The launch result may have bound this agent while the marker was
|
|
1954
|
+
# being written; finish the join now rather than strand it.
|
|
1955
|
+
for candidate_root, candidate in _claude_requests({"LAUNCHING"}):
|
|
1956
|
+
if (candidate.get("launch") or {}).get("agent_id") == agent_id:
|
|
1957
|
+
return _claude_bind_child(candidate_root, candidate)
|
|
1958
|
+
return None
|
|
1959
|
+
|
|
1960
|
+
if name == "SubagentStop":
|
|
1961
|
+
agent_id = event.get("agent_id")
|
|
1962
|
+
matches = [
|
|
1963
|
+
(candidate_root, candidate)
|
|
1964
|
+
for candidate_root, candidate in _claude_requests({"LAUNCHING", "RUNNING"})
|
|
1965
|
+
if (candidate.get("launch") or {}).get("agent_id") == agent_id
|
|
1966
|
+
]
|
|
1967
|
+
if not matches:
|
|
1968
|
+
# Includes every later Stop for an already-completed reviewer.
|
|
1969
|
+
return None
|
|
1970
|
+
if len(matches) != 1:
|
|
1971
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "subagent correlation is ambiguous")
|
|
1972
|
+
session_root = root
|
|
1973
|
+
root, request = matches[0]
|
|
1974
|
+
launch = request["launch"]
|
|
1975
|
+
if request["state"] == "LAUNCHING" and launch.get("post_confirmed") is True:
|
|
1976
|
+
# A start marker written late in the launch/start race.
|
|
1977
|
+
_claude_bind_child(root, request)
|
|
1978
|
+
request = _load(root, request["request_id"])
|
|
1979
|
+
launch = request["launch"]
|
|
1980
|
+
try:
|
|
1981
|
+
# background_tasks is the parent session's list and includes this
|
|
1982
|
+
# reviewer itself while it runs; it can only corroborate identity.
|
|
1983
|
+
tasks = event.get("background_tasks")
|
|
1984
|
+
own = [
|
|
1985
|
+
task for task in tasks if isinstance(task, dict) and task.get("id") == agent_id
|
|
1986
|
+
] if isinstance(tasks, list) else None
|
|
1987
|
+
if (
|
|
1988
|
+
request["state"] != "RUNNING"
|
|
1989
|
+
or launch.get("post_confirmed") is not True
|
|
1990
|
+
or event.get("agent_type") != CLAUDE_REVIEWER
|
|
1991
|
+
or event.get("stop_hook_active") is not False
|
|
1992
|
+
or own is None
|
|
1993
|
+
or len(own) != 1
|
|
1994
|
+
or own[0].get("agent_type") != CLAUDE_REVIEWER
|
|
1995
|
+
):
|
|
1996
|
+
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "reviewer stop is not a first clean stop")
|
|
1997
|
+
_refuse_shadowed_reviewer(root, _real_root(session_root))
|
|
1998
|
+
decision = _parse_decision(event.get("last_assistant_message"), request)
|
|
1999
|
+
except AuthorityError:
|
|
2000
|
+
_reject(root, request, "rejected-first-stop")
|
|
2001
|
+
raise
|
|
2002
|
+
launch["first_stop"] = True
|
|
2003
|
+
payload = {
|
|
2004
|
+
"input_sha256": request["context"]["input_sha256"],
|
|
2005
|
+
"spec_sha256": request["context"]["spec_sha256"],
|
|
2006
|
+
"assessment": decision["assessment"],
|
|
2007
|
+
}
|
|
2008
|
+
if request["activity"] == "spec_review":
|
|
2009
|
+
payload["task_sha256"] = request["context"]["task_sha256"]
|
|
2010
|
+
else:
|
|
2011
|
+
payload["base_input_sha256"] = request["context"]["base_input_sha256"]
|
|
2012
|
+
payload["task_hashes"] = request["context"]["task_hashes"]
|
|
2013
|
+
observation = _closed_observation(
|
|
2014
|
+
request, payload, CLAUDE_REVIEW_PROFILE, agent_id,
|
|
2015
|
+
{"launch": launch, "decision": decision},
|
|
2016
|
+
)
|
|
2017
|
+
_observation(root, request, observation)
|
|
2018
|
+
request["payload"] = payload
|
|
2019
|
+
request["state"] = "COMPLETED"
|
|
2020
|
+
_save(root, request)
|
|
2021
|
+
return {"request_id": request["request_id"], "state": "COMPLETED"}
|
|
2022
|
+
|
|
2023
|
+
return None
|
|
2024
|
+
|
|
2025
|
+
|
|
2026
|
+
def _event(request: dict[str, Any]) -> dict[str, Any]:
|
|
2027
|
+
context = request["context"]
|
|
2028
|
+
activity = request["activity"]
|
|
2029
|
+
host = request.get("host", "codex")
|
|
2030
|
+
host_label = "Claude" if host == "claude" else "Codex"
|
|
2031
|
+
if activity == "verification":
|
|
2032
|
+
authority_kind = "verification_runner"
|
|
2033
|
+
actor = "codeArbiter supervised verifier"
|
|
2034
|
+
origin = f"{host}:verification-run:{request['request_id']}"
|
|
2035
|
+
source_text = "Observed exact declared-command completion and qualified named-test results."
|
|
2036
|
+
else:
|
|
2037
|
+
authority_kind = "review_workflow"
|
|
2038
|
+
actor = f"independent {host} reviewer"
|
|
2039
|
+
origin = f"{host}:subagent:{request['launch']['agent_id']}"
|
|
2040
|
+
source_text = f"Observed a correlated fresh {host_label} reviewer completion with a closed passing decision."
|
|
2041
|
+
return {
|
|
2042
|
+
"format": EVENT_FORMAT,
|
|
2043
|
+
"kind": activity,
|
|
2044
|
+
"authority_kind": authority_kind,
|
|
2045
|
+
"subject": context["subject"],
|
|
2046
|
+
"actor": actor,
|
|
2047
|
+
"origin": origin,
|
|
2048
|
+
"verdict": "passed",
|
|
2049
|
+
"payload": request["payload"],
|
|
2050
|
+
"source_text": source_text,
|
|
2051
|
+
"observation_ref": request["observation_ref"],
|
|
2052
|
+
"observation_sha256": request["observation_sha256"],
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
|
|
2056
|
+
def publish_request(
|
|
2057
|
+
root: str | Path, client: Any, request_id: str
|
|
2058
|
+
) -> dict[str, Any]:
|
|
2059
|
+
root = _real_root(root)
|
|
2060
|
+
request = _load(root, request_id)
|
|
2061
|
+
if request["state"] == "CAPTURED":
|
|
2062
|
+
return {
|
|
2063
|
+
"request_id": request_id, "receipt": request["receipt"],
|
|
2064
|
+
"authority_source": request["authority_source"],
|
|
2065
|
+
}
|
|
2066
|
+
if request["state"] != "COMPLETED":
|
|
2067
|
+
raise AuthorityError("AUTHORITY_NOT_COMPLETE", "successful observation is absent")
|
|
2068
|
+
if request["activity"] == "verification":
|
|
2069
|
+
wrapper = request.get("wrapper")
|
|
2070
|
+
if not isinstance(wrapper, dict) or wrapper.get("state") != "CORROBORATED":
|
|
2071
|
+
raise AuthorityError("AUTHORITY_NOT_COMPLETE", "verifier wrapper completion is uncorroborated")
|
|
2072
|
+
current, current_ref, current_hash = _context(
|
|
2073
|
+
root, client, request["context"]["subject"]["artifact_id"],
|
|
2074
|
+
request["context"]["subject"]["record_id"],
|
|
2075
|
+
request["activity"],
|
|
2076
|
+
)
|
|
2077
|
+
if (
|
|
2078
|
+
current != request["context"]
|
|
2079
|
+
or current_ref != request["context_ref"]
|
|
2080
|
+
or current_hash != request["context_sha256"]
|
|
2081
|
+
):
|
|
2082
|
+
raise AuthorityError("STALE_AUTHORITY_REQUEST", "engine evidence context changed")
|
|
2083
|
+
observation_path = root / request["observation_ref"]
|
|
2084
|
+
try:
|
|
2085
|
+
observation = observation_path.read_bytes()
|
|
2086
|
+
except OSError as exc:
|
|
2087
|
+
raise AuthorityError("INVALID_OBSERVATION", "observation is unavailable") from exc
|
|
2088
|
+
if _digest(observation) != request["observation_sha256"]:
|
|
2089
|
+
raise AuthorityError("INVALID_OBSERVATION", "observation digest differs")
|
|
2090
|
+
event = _event(request)
|
|
2091
|
+
raw = _canonical(event)
|
|
2092
|
+
digest = _digest(raw)
|
|
2093
|
+
relative = SOURCE_DIR / f"{digest}.json"
|
|
2094
|
+
_publish_immutable(root, relative, raw)
|
|
2095
|
+
captured = client.call("capture-observation", {
|
|
2096
|
+
"source_ref": relative.as_posix(), "source_sha256": digest,
|
|
2097
|
+
})
|
|
2098
|
+
receipt = captured.get("receipt")
|
|
2099
|
+
if not isinstance(receipt, str) or not receipt:
|
|
2100
|
+
raise AuthorityError("INVALID_RECEIPT", "capture returned no receipt")
|
|
2101
|
+
request["state"] = "CAPTURED"
|
|
2102
|
+
request["receipt"] = receipt
|
|
2103
|
+
request["authority_source"] = relative.as_posix()
|
|
2104
|
+
_save(root, request)
|
|
2105
|
+
return {"request_id": request_id, "receipt": receipt, "authority_source": relative.as_posix()}
|
|
2106
|
+
|
|
2107
|
+
|
|
2108
|
+
def recover_request(root: str | Path, request_id: str, disposition: str) -> dict[str, Any]:
|
|
2109
|
+
"""Retain an uncertain attempt and close it without ever rerunning commands."""
|
|
2110
|
+
root = _real_root(root)
|
|
2111
|
+
if disposition not in {"failed", "abandoned"}:
|
|
2112
|
+
raise AuthorityError("INVALID_RECOVERY", "recovery disposition must be failed or abandoned")
|
|
2113
|
+
request = _load(root, request_id)
|
|
2114
|
+
recoverable_completed = (
|
|
2115
|
+
request["state"] == "COMPLETED"
|
|
2116
|
+
and request["activity"] == "verification"
|
|
2117
|
+
and isinstance(request.get("wrapper"), dict)
|
|
2118
|
+
and request["wrapper"].get("state") != "CORROBORATED"
|
|
2119
|
+
)
|
|
2120
|
+
recoverable_authorized = (
|
|
2121
|
+
request["state"] == "ARMED"
|
|
2122
|
+
and request["activity"] == "verification"
|
|
2123
|
+
and isinstance(request.get("wrapper"), dict)
|
|
2124
|
+
and request["wrapper"].get("state") in {"AUTHORIZED", "FAILED"}
|
|
2125
|
+
)
|
|
2126
|
+
if request["state"] not in {"RUNNING", "LAUNCHING"} and not recoverable_completed and not recoverable_authorized:
|
|
2127
|
+
raise AuthorityError("INVALID_RECOVERY", "only an interrupted active attempt can be recovered")
|
|
2128
|
+
request["state"] = disposition.upper()
|
|
2129
|
+
request["recovery"] = {
|
|
2130
|
+
"disposition": disposition,
|
|
2131
|
+
"previous_attempt": request.get("attempt"),
|
|
2132
|
+
"rerun_permitted": False,
|
|
2133
|
+
}
|
|
2134
|
+
_save(root, request)
|
|
2135
|
+
return {"request_id": request_id, "state": request["state"], "rerun_permitted": False}
|