@arbiterforge/ca-pi 0.14.12 → 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.
@@ -1,18 +1,26 @@
1
1
  #!/usr/bin/env python3
2
2
  # codeArbiter — durable closed authority producers for structured artifacts.
3
- """Supervise verification and correlate independent Codex review evidence.
3
+ """Supervise verification and correlate independent Codex or Claude review evidence.
4
4
 
5
5
  arm_request(...) -> dict
6
6
  run_verification(...) -> dict
7
7
  observe_codex_hook(...) -> dict
8
+ observe_claude_hook(...) -> dict
8
9
  publish_request(...) -> dict
9
10
 
10
11
  The artifact engine owns every target binding. Callers select only an activity
11
12
  and record; they cannot supply an authority kind, verdict, evidence payload, or
12
13
  reviewer identity. External work is journaled before launch, observations are
13
14
  immutable and content-addressed, and replay never silently reruns a completed
14
- attempt. Codex review correlation uses stable hook fields and deliberately
15
- ignores transcript bytes whose format is not a public contract.
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.
16
24
  """
17
25
 
18
26
  from __future__ import annotations
@@ -57,6 +65,23 @@ MAX_STATE = 1 << 20
57
65
  MAX_OUTPUT = 8 << 20
58
66
  MAX_DECISION = 65536
59
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
+ ]
60
85
 
61
86
 
62
87
  class AuthorityError(RuntimeError):
@@ -230,9 +255,12 @@ def _spool_root(root: Path) -> Path:
230
255
  def _save(root: Path, value: dict[str, Any]) -> None:
231
256
  value = dict(value)
232
257
  value["integrity_sha256"] = _integrity(value)
233
- _atomic_replace(_spool_root(root), _request_path(value["request_id"]), _canonical(value))
234
- if value["state"] in TERMINAL_STATES:
235
- (_registry_root() / f"{value['request_id']}.json").unlink(missing_ok=True)
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
236
264
 
237
265
 
238
266
  def _load(root: Path, request_id: str) -> dict[str, Any]:
@@ -253,7 +281,7 @@ def _load(root: Path, request_id: str) -> dict[str, Any]:
253
281
  "observation_sha256", "receipt", "integrity_sha256", "payload",
254
282
  "authority_source", "dispatch_prompt", "review_contract_sha256",
255
283
  "required_coverage", "wrapper", "command_bindings", "recovery",
256
- "launch_envelope", "workspace_roots",
284
+ "launch_envelope", "workspace_roots", "host",
257
285
  }
258
286
  if (
259
287
  not isinstance(value, dict)
@@ -262,6 +290,7 @@ def _load(root: Path, request_id: str) -> dict[str, Any]:
262
290
  or value.get("request_id") != request_id
263
291
  or value.get("activity") not in ACTIVITIES
264
292
  or value.get("state") not in STATES
293
+ or value.get("host", "codex") not in HOSTS
265
294
  or not isinstance(value.get("context"), dict)
266
295
  or not isinstance(value.get("repository"), dict)
267
296
  or not isinstance(value.get("command_bindings", []), list)
@@ -431,7 +460,9 @@ def _register_request(root: Path, request: dict[str, Any]) -> None:
431
460
  raise AuthorityError("AUTHORITY_COLLISION", "request registry entry differs")
432
461
 
433
462
 
434
- def _registered_requests() -> list[tuple[Path, dict[str, Any]]]:
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."""
435
466
  results = []
436
467
  for path in _registry_root().glob("*.json"):
437
468
  try:
@@ -456,7 +487,17 @@ def _registered_requests() -> list[tuple[Path, dict[str, Any]]]:
456
487
  path.unlink(missing_ok=True)
457
488
  continue
458
489
  results.append((root, request))
459
- except (AuthorityError, OSError, UnicodeError, ValueError):
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
460
501
  continue
461
502
  return results
462
503
 
@@ -468,6 +509,85 @@ def _registered_request(request_id: str) -> tuple[Path, dict[str, Any]]:
468
509
  return matches[0]
469
510
 
470
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
+
471
591
  def _ordinary_spawn_marker(session_id: str, turn_id: str) -> Path:
472
592
  key = _digest(_canonical({"session_id": session_id, "turn_id": turn_id}))
473
593
  return _registry_root() / f"ordinary-spawn-{key}.marker"
@@ -718,7 +838,8 @@ def _dispatch_prompt(request: dict[str, Any]) -> str:
718
838
  + json.dumps(request["required_coverage"], ensure_ascii=True)
719
839
  + ". Fields: format, request_id, target_sha256, contract_sha256, decision "
720
840
  "(pass or changes_requested), coverage (unique strings), findings (objects with "
721
- "severity, code, message), assessment (non-empty string)."
841
+ "severity, code, message), assessment (non-empty string). Reply with the JSON "
842
+ "object only: no code fence and no other text."
722
843
  )
723
844
 
724
845
 
@@ -731,6 +852,8 @@ def arm_request(
731
852
  *,
732
853
  request_nonce: str | None = None,
733
854
  workspace_roots: dict[str, str | Path] | None = None,
855
+ host: str = "codex",
856
+ reviewer_model: str = "opus",
734
857
  **unexpected: Any,
735
858
  ) -> dict[str, Any]:
736
859
  if unexpected:
@@ -740,7 +863,13 @@ def arm_request(
740
863
  )
741
864
  if activity not in ACTIVITIES:
742
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")
743
870
  root = _real_root(root)
871
+ if host == "claude" and activity in REVIEW_ACTIVITIES:
872
+ _refuse_shadowed_reviewer(root)
744
873
  nonce = request_nonce or secrets.token_urlsafe(24)
745
874
  if not isinstance(nonce, str) or TOKEN_RE.fullmatch(nonce) is None:
746
875
  raise AuthorityError("INVALID_AUTHORITY_REQUEST", "request nonce is malformed")
@@ -756,11 +885,14 @@ def arm_request(
756
885
  raise AuthorityError("UNSUPPORTED_WORKSPACE", "workspace map must exactly cover declared cwd labels")
757
886
  frozen_workspaces = {label: str(_real_root(path)) for label, path in workspace_roots.items()}
758
887
  command_bindings = _bind_commands(root, context, frozen_workspaces) if activity == "verification" else []
759
- seed = _canonical({
888
+ seed_value = {
760
889
  "repository": _repository_identity(root), "context": context, "nonce": nonce,
761
890
  "command_bindings": command_bindings,
762
891
  "workspace_roots": frozen_workspaces,
763
- })
892
+ }
893
+ if host != "codex":
894
+ seed_value["host"] = host
895
+ seed = _canonical(seed_value)
764
896
  request_id = _digest(seed)
765
897
  request = {
766
898
  "format": STATE_FORMAT,
@@ -781,14 +913,24 @@ def arm_request(
781
913
  "command_bindings": command_bindings,
782
914
  "workspace_roots": frozen_workspaces,
783
915
  }
916
+ if host != "codex":
917
+ request["host"] = host
784
918
  if activity in REVIEW_ACTIVITIES:
785
919
  request["review_contract_sha256"], request["required_coverage"] = _review_binding(context)
786
920
  request["dispatch_prompt"] = _dispatch_prompt(request)
787
- request["launch_envelope"] = {
788
- "message": request["dispatch_prompt"],
789
- "task_name": f"authority_{request_id[:12]}",
790
- "fork_turns": "none",
791
- }
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
+ }
792
934
  _save(root, request)
793
935
  _register_request(root, request)
794
936
  result = {
@@ -1162,9 +1304,11 @@ def run_verification(
1162
1304
  if request["activity"] != "verification":
1163
1305
  raise AuthorityError("INVALID_AUTHORITY_REQUEST", "request is not verification")
1164
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.
1165
1309
  return {
1166
1310
  "request_id": request_id, "state": "COMPLETED",
1167
- "commands": request["payload"]["commands"],
1311
+ "commands": request["payload"]["commands"], "replayed": True,
1168
1312
  }
1169
1313
  if request["state"] != "ARMED":
1170
1314
  raise AuthorityError("INTERRUPTED_ATTEMPT", "verification attempt cannot be rerun")
@@ -1265,7 +1409,7 @@ def _host_id(value: Any, field: str) -> str:
1265
1409
  return value
1266
1410
 
1267
1411
 
1268
- def _verification_selector(event: dict[str, Any]) -> tuple[str, Path] | None:
1412
+ def _verification_selector(event: dict[str, Any]) -> tuple[str, Path, str] | None:
1269
1413
  """Recognize only the closed wrapper selector, never caller-authored child argv."""
1270
1414
  if event.get("tool_name") not in {
1271
1415
  "Bash", "shell_command", "exec_command", "unified_exec",
@@ -1277,10 +1421,13 @@ def _verification_selector(event: dict[str, Any]) -> tuple[str, Path] | None:
1277
1421
  command = tool_input.get("command", tool_input.get("cmd"))
1278
1422
  if not isinstance(command, str):
1279
1423
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "exec command is absent")
1280
- compound = any(marker in command for marker in ("\n", "\r", ";", "&&", "||", "|", ">", "<", "`"))
1424
+ compound = any(marker in command for marker in ("\n", "\r", ";", "&", "|", ">", "<", "`", "$"))
1281
1425
  try:
1282
1426
  tokens = [token.strip('"') for token in shlex.split(command, posix=False)]
1283
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
1284
1431
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier wrapper command is malformed") from exc
1285
1432
  wrapper_start = (
1286
1433
  len(tokens) >= 3
@@ -1307,26 +1454,52 @@ def _verification_selector(event: dict[str, Any]) -> tuple[str, Path] | None:
1307
1454
  or REQUEST_RE.fullmatch(tokens[6]) is None
1308
1455
  ):
1309
1456
  return None
1310
- return tokens[6], _real_root(tokens[4])
1457
+ return tokens[6], _real_root(tokens[4]), tokens[1]
1311
1458
 
1312
1459
 
1313
1460
  def observe_verifier_hook(
1314
- invocation_root: str | Path, event: dict[str, Any]
1461
+ invocation_root: str | Path, event: dict[str, Any], host: str = "codex",
1315
1462
  ) -> dict[str, Any] | None:
1316
1463
  """Authorize/corroborate the wrapper; native exit/output remain result evidence."""
1317
- _real_root(invocation_root)
1318
- if not isinstance(event, dict) or event.get("hook_event_name") not in {"PreToolUse", "PostToolUse"}:
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:
1319
1468
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier hook event is unsupported")
1320
1469
  selected = _verification_selector(event)
1321
1470
  if selected is None:
1322
1471
  return None
1323
- request_id, selected_root = selected
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
1324
1475
  root, request = _registered_request(request_id)
1325
- if root != selected_root or request["activity"] != "verification":
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
+ ):
1326
1489
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier target does not match its request")
1327
1490
  session_id = _host_id(event.get("session_id"), "session_id")
1328
- turn_id = _host_id(event.get("turn_id"), "turn_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)
1329
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")
1330
1503
  if event["hook_event_name"] == "PreToolUse":
1331
1504
  if request["state"] != "ARMED" or request.get("wrapper") is not None:
1332
1505
  raise AuthorityError("INTERRUPTED_ATTEMPT", "verifier wrapper is not armable")
@@ -1339,7 +1512,7 @@ def observe_verifier_hook(
1339
1512
  workspace_before = _workspace_snapshots(current_bindings)
1340
1513
  request["wrapper"] = {
1341
1514
  "state": "AUTHORIZED", "session_id": session_id,
1342
- "turn_id": turn_id, "tool_use_id": tool_use_id,
1515
+ turn_field: turn_id, "tool_use_id": tool_use_id,
1343
1516
  "command_bindings": current_bindings,
1344
1517
  "workspace_before": workspace_before,
1345
1518
  }
@@ -1352,11 +1525,14 @@ def observe_verifier_hook(
1352
1525
  if (
1353
1526
  not isinstance(wrapper, dict)
1354
1527
  or wrapper.get("session_id") != session_id
1355
- or wrapper.get("turn_id") != turn_id
1528
+ or wrapper.get(turn_field) != turn_id
1356
1529
  or wrapper.get("tool_use_id") != tool_use_id
1357
- or request["state"] != "COMPLETED"
1358
1530
  ):
1359
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")
1360
1536
  response = event.get("tool_response")
1361
1537
  if isinstance(response, dict):
1362
1538
  if response.get("exit_code") != 0:
@@ -1379,6 +1555,41 @@ def observe_verifier_hook(
1379
1555
  return {"request_id": request_id, "state": "CORROBORATED"}
1380
1556
 
1381
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
+
1382
1593
  def _parse_decision(raw: Any, request: dict[str, Any]) -> dict[str, Any]:
1383
1594
  if not isinstance(raw, str) or len(raw.encode("utf-8")) > MAX_DECISION:
1384
1595
  raise AuthorityError("INVALID_REVIEW_DECISION", "review decision is absent or oversized")
@@ -1445,7 +1656,11 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
1445
1656
  return None
1446
1657
  request_id = _request_id_from_prompt(message)
1447
1658
  root, request = _registered_request(request_id)
1448
- if request["activity"] not in REVIEW_ACTIVITIES or request["state"] != "ARMED":
1659
+ if (
1660
+ request.get("host", "codex") != "codex"
1661
+ or request["activity"] not in REVIEW_ACTIVITIES
1662
+ or request["state"] != "ARMED"
1663
+ ):
1449
1664
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review request is not launchable")
1450
1665
  if tool_input != request.get("launch_envelope"):
1451
1666
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch envelope was changed")
@@ -1564,7 +1779,7 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
1564
1779
  payload["task_hashes"] = request["context"]["task_hashes"]
1565
1780
  producer_result = {"launch": request["launch"], "decision": decision}
1566
1781
  observation = _closed_observation(
1567
- request, payload, "codex-review/0.1.0", agent_id,
1782
+ request, payload, CODEX_REVIEW_PROFILE, agent_id,
1568
1783
  producer_result,
1569
1784
  )
1570
1785
  _observation(root, request, observation)
@@ -1576,19 +1791,253 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
1576
1791
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "hook event is not supported")
1577
1792
 
1578
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
+
1579
2026
  def _event(request: dict[str, Any]) -> dict[str, Any]:
1580
2027
  context = request["context"]
1581
2028
  activity = request["activity"]
2029
+ host = request.get("host", "codex")
2030
+ host_label = "Claude" if host == "claude" else "Codex"
1582
2031
  if activity == "verification":
1583
2032
  authority_kind = "verification_runner"
1584
2033
  actor = "codeArbiter supervised verifier"
1585
- origin = f"codex:verification-run:{request['request_id']}"
2034
+ origin = f"{host}:verification-run:{request['request_id']}"
1586
2035
  source_text = "Observed exact declared-command completion and qualified named-test results."
1587
2036
  else:
1588
2037
  authority_kind = "review_workflow"
1589
- actor = "independent codex reviewer"
1590
- origin = f"codex:subagent:{request['launch']['agent_id']}"
1591
- source_text = "Observed a correlated fresh Codex reviewer completion with a closed passing decision."
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."
1592
2041
  return {
1593
2042
  "format": EVENT_FORMAT,
1594
2043
  "kind": activity,
@@ -1672,7 +2121,7 @@ def recover_request(root: str | Path, request_id: str, disposition: str) -> dict
1672
2121
  request["state"] == "ARMED"
1673
2122
  and request["activity"] == "verification"
1674
2123
  and isinstance(request.get("wrapper"), dict)
1675
- and request["wrapper"].get("state") == "AUTHORIZED"
2124
+ and request["wrapper"].get("state") in {"AUTHORIZED", "FAILED"}
1676
2125
  )
1677
2126
  if request["state"] not in {"RUNNING", "LAUNCHING"} and not recoverable_completed and not recoverable_authorized:
1678
2127
  raise AuthorityError("INVALID_RECOVERY", "only an interrupted active attempt can be recovered")