@arbiterforge/ca-pi 0.17.5 → 0.17.6

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 CHANGED
@@ -10,7 +10,7 @@ and durable project context. You decide. codeArbiter enforces.
10
10
  <img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
11
11
  <img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
12
12
  <img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
13
- <img alt="version 2.25.4" src="https://img.shields.io/badge/version-2.25.4-2b7489">
13
+ <img alt="version 2.25.5" src="https://img.shields.io/badge/version-2.25.5-2b7489">
14
14
  <img alt="core lanes" src="https://img.shields.io/badge/core_lanes-18-555">
15
15
  <img alt="skills" src="https://img.shields.io/badge/skills-22-555">
16
16
  <img alt="agents" src="https://img.shields.io/badge/agents-20-555">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.17.5",
3
+ "version": "0.17.6",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,14 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.17.6] - 2026-10-10
8
+
9
+ ### Fixed
10
+
11
+ - Preserve the shared engine's Codex native V2 review bindings and bounded
12
+ verification context storage. Pi verification and review authority remain
13
+ unsupported; this shared payload update does not enable those host seams.
14
+
7
15
  ## [0.17.5] - 2026-10-10
8
16
 
9
17
  ### Fixed
@@ -6,43 +6,43 @@
6
6
  "binaries": {
7
7
  "darwin/amd64": {
8
8
  "file": "ca-artifact-darwin-amd64",
9
- "sha256": "12a8a4b23da84cd672d03abe0c748310225e8bce47c5e8c8cbb3753e5acdbe57",
9
+ "sha256": "7b9e95dba78bda2962d1b879c373a835466fe59aa082a3afb6efbc1fb3de454d",
10
10
  "native_tested": true
11
11
  },
12
12
  "darwin/arm64": {
13
13
  "file": "ca-artifact-darwin-arm64",
14
- "sha256": "2107c247473c8a9cc7c422dd754d98fa6211cf7bec5b7ecdd85ba5d7db0676b0",
14
+ "sha256": "2469bd38df4e241f19bb443c4388283aaadc0638951a77331b25be81929d4f4c",
15
15
  "native_tested": true
16
16
  },
17
17
  "linux/amd64": {
18
18
  "file": "ca-artifact-linux-amd64",
19
- "sha256": "92e27501fd18ae805c6390b17b4f2335e97d69c3645c60b3267fb7869c624727",
19
+ "sha256": "cf4116c08afa66426c3b9d78853dfc09b608e05412054b6fe18817a65db96a76",
20
20
  "native_tested": true
21
21
  },
22
22
  "linux/arm64": {
23
23
  "file": "ca-artifact-linux-arm64",
24
- "sha256": "1961df0bdf37fd6a7b3a6214b925cf9f3b2efe0e797c05151a43ee96d7df07b3",
24
+ "sha256": "b1e92f0d279fe04b61dd287a4b20fc1db4abc3722c0325f5294a55bb1a972895",
25
25
  "native_tested": true
26
26
  },
27
27
  "windows/amd64": {
28
28
  "file": "ca-artifact-windows-amd64.exe",
29
- "sha256": "63def27bda36ed03c9f7e6898eaf2110badda2419950a8dcb91666c4e4bdd438",
29
+ "sha256": "d8fa783dc8d30c5d48a8c336bca8ee5fd85be4c72629d7ac7246842b89c5135c",
30
30
  "native_tested": true
31
31
  },
32
32
  "windows/arm64": {
33
33
  "file": "ca-artifact-windows-arm64.exe",
34
- "sha256": "107e1a8fe971fe24ce8a274ce1188b15c9785e4f94b835dadde1918b8fed5c4b",
34
+ "sha256": "2df30c81eb58d502aa9a146563b9dd725176cac9f907ebf36b73c157e5de2124",
35
35
  "native_tested": true
36
36
  }
37
37
  },
38
38
  "context_qualification": {
39
39
  "darwin/amd64": {
40
- "source_commit": "ee70aa8851f2b96500e1b27085e3466ab222445d",
40
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
41
41
  "workflow": ".github/workflows/ci.yml",
42
- "run_id": "38030208537",
43
- "binary_sha256": "12a8a4b23da84cd672d03abe0c748310225e8bce47c5e8c8cbb3753e5acdbe57",
44
- "observation_sha256": "cbfbaf744db2c79e4c71fd23903ff06b5284e7475b3f094a90711bb62eb6a39e",
45
- "module_sha256": "a7600f74a743626c4569da0da7803e057ed1df33d0fab8b0581bd25e45d402ed",
42
+ "run_id": "38042731230",
43
+ "binary_sha256": "7b9e95dba78bda2962d1b879c373a835466fe59aa082a3afb6efbc1fb3de454d",
44
+ "observation_sha256": "c359781d36118baf2ebe6b73bb0ecefb59f4ca500b31a6f7e5baf52474e1127c",
45
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
46
46
  "native_cases": [
47
47
  "TestContextBoundedRender",
48
48
  "TestContextDocumentContract",
@@ -53,12 +53,12 @@
53
53
  ]
54
54
  },
55
55
  "darwin/arm64": {
56
- "source_commit": "ee70aa8851f2b96500e1b27085e3466ab222445d",
56
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
57
57
  "workflow": ".github/workflows/ci.yml",
58
- "run_id": "38030208537",
59
- "binary_sha256": "2107c247473c8a9cc7c422dd754d98fa6211cf7bec5b7ecdd85ba5d7db0676b0",
60
- "observation_sha256": "426035459f63029ddd368a2a1a9dc542aeaebd920ae0ed993c6bbe6d5ecc1aa5",
61
- "module_sha256": "a7600f74a743626c4569da0da7803e057ed1df33d0fab8b0581bd25e45d402ed",
58
+ "run_id": "38042731230",
59
+ "binary_sha256": "2469bd38df4e241f19bb443c4388283aaadc0638951a77331b25be81929d4f4c",
60
+ "observation_sha256": "2d4c75ec50210c0bbf4b8c9a754a0fd89ee81dc60c9607066cf17c4e1af2050a",
61
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
62
62
  "native_cases": [
63
63
  "TestContextBoundedRender",
64
64
  "TestContextDocumentContract",
@@ -69,12 +69,12 @@
69
69
  ]
70
70
  },
71
71
  "linux/amd64": {
72
- "source_commit": "ee70aa8851f2b96500e1b27085e3466ab222445d",
72
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
73
73
  "workflow": ".github/workflows/ci.yml",
74
- "run_id": "38030208537",
75
- "binary_sha256": "92e27501fd18ae805c6390b17b4f2335e97d69c3645c60b3267fb7869c624727",
76
- "observation_sha256": "50f326cc374cd2914c907ef1367e75dc4ac49ef3526cebfc220a08e9e0d9bac8",
77
- "module_sha256": "a7600f74a743626c4569da0da7803e057ed1df33d0fab8b0581bd25e45d402ed",
74
+ "run_id": "38042731230",
75
+ "binary_sha256": "cf4116c08afa66426c3b9d78853dfc09b608e05412054b6fe18817a65db96a76",
76
+ "observation_sha256": "0a57cde8e4f251ee5faf2cd2dc256638622464030cda79988c11295dec6dfec9",
77
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
78
78
  "native_cases": [
79
79
  "TestContextBoundedRender",
80
80
  "TestContextDocumentContract",
@@ -85,12 +85,12 @@
85
85
  ]
86
86
  },
87
87
  "linux/arm64": {
88
- "source_commit": "ee70aa8851f2b96500e1b27085e3466ab222445d",
88
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
89
89
  "workflow": ".github/workflows/ci.yml",
90
- "run_id": "38030208537",
91
- "binary_sha256": "1961df0bdf37fd6a7b3a6214b925cf9f3b2efe0e797c05151a43ee96d7df07b3",
92
- "observation_sha256": "59a46860f93ed22094e9de20f7d2753cf361411d9e5230fcaabde95275cd7092",
93
- "module_sha256": "a7600f74a743626c4569da0da7803e057ed1df33d0fab8b0581bd25e45d402ed",
90
+ "run_id": "38042731230",
91
+ "binary_sha256": "b1e92f0d279fe04b61dd287a4b20fc1db4abc3722c0325f5294a55bb1a972895",
92
+ "observation_sha256": "926d97ef85d2cd9caa658db620c7f135ea5a07873a188f289181a37ae636921d",
93
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
94
94
  "native_cases": [
95
95
  "TestContextBoundedRender",
96
96
  "TestContextDocumentContract",
@@ -101,12 +101,12 @@
101
101
  ]
102
102
  },
103
103
  "windows/amd64": {
104
- "source_commit": "ee70aa8851f2b96500e1b27085e3466ab222445d",
104
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
105
105
  "workflow": ".github/workflows/ci.yml",
106
- "run_id": "38030208537",
107
- "binary_sha256": "63def27bda36ed03c9f7e6898eaf2110badda2419950a8dcb91666c4e4bdd438",
108
- "observation_sha256": "ff372ee0e5f5bacc316909afc37c6b9b5442143385180dd7eb235eae725f25f5",
109
- "module_sha256": "a7600f74a743626c4569da0da7803e057ed1df33d0fab8b0581bd25e45d402ed",
106
+ "run_id": "38042731230",
107
+ "binary_sha256": "d8fa783dc8d30c5d48a8c336bca8ee5fd85be4c72629d7ac7246842b89c5135c",
108
+ "observation_sha256": "70827706a7d231df640f9d4b27b4250f81311bfd89bee11b65ff48b985c1085c",
109
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
110
110
  "native_cases": [
111
111
  "TestContextBoundedRender",
112
112
  "TestContextDocumentContract",
@@ -117,12 +117,12 @@
117
117
  ]
118
118
  },
119
119
  "windows/arm64": {
120
- "source_commit": "ee70aa8851f2b96500e1b27085e3466ab222445d",
120
+ "source_commit": "7ef1feead876e08e9ad63d5e507a01650488719f",
121
121
  "workflow": ".github/workflows/ci.yml",
122
- "run_id": "38030208537",
123
- "binary_sha256": "107e1a8fe971fe24ce8a274ce1188b15c9785e4f94b835dadde1918b8fed5c4b",
124
- "observation_sha256": "c6761f9845ea9f51281ceb31118dc526a0496a57fa606807bf6118478f2906b4",
125
- "module_sha256": "a7600f74a743626c4569da0da7803e057ed1df33d0fab8b0581bd25e45d402ed",
122
+ "run_id": "38042731230",
123
+ "binary_sha256": "2df30c81eb58d502aa9a146563b9dd725176cac9f907ebf36b73c157e5de2124",
124
+ "observation_sha256": "f15d9d9894d0ae845e14c3bfba1cf1528f1debe166850e9fb0b46aee27d99dbd",
125
+ "module_sha256": "fb635481a9bf9a88dbe052c2d738c28c1e3d2a6d741514fc28ac51b6fc8a47b4",
126
126
  "native_cases": [
127
127
  "TestContextBoundedRender",
128
128
  "TestContextDocumentContract",
@@ -82,12 +82,14 @@ REQUEST_LOCK_WAIT_SECONDS = 5.0
82
82
  CLAUDE_JOIN_LOCK_WAIT_SECONDS = 30.0
83
83
  MAX_OUTPUT = 8 << 20
84
84
  MAX_DECISION = 65536
85
- # Current native completion reviews retain the complete immutable context by
85
+ # Verification and native completion reviews retain the complete immutable context by
86
86
  # exact reference instead of duplicating it in every mutable request state.
87
87
  CONTEXT_STORAGE = "frozen-reference/0.1.0"
88
88
  CONTEXT_REFERENCE = {"format": "codearbiter.evidence-context-reference/0.1.0"}
89
89
  # The native canonical decoder already bounds each complete context at 8 MiB.
90
90
  MAX_EVIDENCE_CONTEXT = 8 << 20
91
+ # Frozen evidence reads can outlast the ordinary client's 30-second budget.
92
+ EVIDENCE_ENGINE_TIMEOUT_SECONDS = 120
91
93
  # ASCII canonical JSON can expand each input UTF-8 byte to at most six bytes.
92
94
  # Reserve that closed decision budget plus 32 bounded 256-byte lifecycle slots
93
95
  # (host identities, content-addressed locators, their keys and delimiters).
@@ -114,9 +116,13 @@ CLAUDE_REVIEWER_MODELS = frozenset({"opus", "sonnet", "haiku"})
114
116
  CLAUDE_REVIEW_PROFILE = "claude-review/0.1.0"
115
117
  CODEX_REVIEW_PROFILE = "codex-review/0.1.0"
116
118
  CODEX_NATIVE_V1 = "codex-native-v1/0.145.0"
119
+ CODEX_NATIVE_V2 = "codex-native-v2/0.162.0-alpha.2"
120
+ CODEX_NATIVE_PROFILES = frozenset({CODEX_NATIVE_V1, CODEX_NATIVE_V2})
117
121
  CODEX_UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
122
+ CODEX_AGENT_PATH_RE = re.compile(r"/root(?:/[a-z0-9_]+)*")
118
123
  CODEX_STEERING_TOOLS = frozenset({
119
124
  "multi_agent_v1send_input", "multi_agent_v1resume_agent", "multi_agent_v1close_agent",
125
+ "collaborationsend_message", "collaborationfollowup_task", "collaborationinterrupt_agent",
120
126
  })
121
127
  CODEX_REVIEW_TOOLS = frozenset({"spawn_agent", "collaborationspawn_agent"}) | CODEX_STEERING_TOOLS
122
128
  CODEX_CHILD_FORMAT = "codearbiter.codex-native-child/0.1.0"
@@ -416,7 +422,7 @@ def _request_lock(spool: Path, relative: Path, wait: float | None = None):
416
422
 
417
423
  def _referenced_review_state(value: dict[str, Any]) -> dict[str, Any]:
418
424
  if (value.get("context_storage") != CONTEXT_STORAGE
419
- or value.get("codex_review_profile") != CODEX_NATIVE_V1
425
+ or value.get("codex_review_profile") not in CODEX_NATIVE_PROFILES
420
426
  or value.get("host", "codex") != "codex"
421
427
  or value.get("activity") not in REVIEW_ACTIVITIES
422
428
  or "completion" not in value["context"] or "handback" in value):
@@ -442,13 +448,101 @@ def _referenced_review_state(value: dict[str, Any]) -> dict[str, Any]:
442
448
  return dict(value, context=CONTEXT_REFERENCE)
443
449
 
444
450
 
451
+ def _valid_verification_binding_values(binding: dict[str, Any]) -> bool:
452
+ def path(value: Any) -> bool:
453
+ return isinstance(value, str) and bool(value) and "\0" not in value and Path(value).is_absolute()
454
+
455
+ def identity(value: Any) -> bool:
456
+ return isinstance(value, str) and re.fullmatch(r"[0-9]+:[0-9]+", value) is not None
457
+
458
+ def digest(value: Any) -> bool:
459
+ return isinstance(value, str) and SHA256_RE.fullmatch(value) is not None
460
+
461
+ argv, files, collector = binding["argv"], binding["launch_files"], binding["collector_profile"]
462
+ if (not isinstance(argv, list) or not argv
463
+ or any(not isinstance(item, str) or not item or "\0" in item for item in argv)
464
+ or not path(argv[0])
465
+ or not isinstance(collector, str) or collector not in COLLECTORS
466
+ or any(not path(binding[field]) for field in ("cwd", "workspace_root", "git_common_dir"))
467
+ or any(not identity(binding[field]) for field in (
468
+ "cwd_filesystem_id", "workspace_filesystem_id", "git_common_filesystem_id"))
469
+ or any(not digest(binding[field]) for field in ("definition_sha256", "executable_sha256"))
470
+ or not isinstance(files, list) or not files):
471
+ return False
472
+ # These are the roles emitted by _native_launch, _bind_commands and its
473
+ # nested npm qualifier. Validate serialization without replacing rebinding.
474
+ roles = {"declared-executable", "node-runtime", "npm-cli", "runner-entrypoint",
475
+ "npm-manifest", "npm-workspace-manifest", "npm-prefix"}
476
+ return all(isinstance(item, dict) and set(item) == {"path", "sha256", "filesystem_id", "role"}
477
+ and path(item["path"]) and digest(item["sha256"]) and identity(item["filesystem_id"])
478
+ and isinstance(item["role"], str) and item["role"] in roles for item in files)
479
+
480
+
481
+ def _referenced_verification_state(value: dict[str, Any]) -> dict[str, Any]:
482
+ context = value["context"]
483
+ subject = context.get("subject", {})
484
+ _validate_context(context, subject.get("artifact_id"), subject.get("record_id"), "verification")
485
+ binding_fields = {"definition_sha256", "argv", "collector_profile", "launch_files", "cwd",
486
+ "cwd_filesystem_id", "workspace_root", "workspace_filesystem_id", "git_common_dir",
487
+ "git_common_filesystem_id", "executable_sha256"}
488
+ if (value.get("context_storage") != CONTEXT_STORAGE
489
+ or value.get("activity") != "verification"
490
+ or value.get("host", "codex") not in HOSTS
491
+ or value.get("state") not in STATES
492
+ or value.get("launch") is not None
493
+ or any(field in value for field in (
494
+ "codex_review_profile", "handback", "dispatch_prompt", "review_contract_sha256",
495
+ "required_coverage", "launch_envelope"))
496
+ or not isinstance(value.get("command_bindings"), list)
497
+ or len(value["command_bindings"]) != len(context.get("commands", []))
498
+ or any(not isinstance(binding, dict)
499
+ or set(binding) != binding_fields
500
+ or not _valid_verification_binding_values(binding)
501
+ or binding.get("definition_sha256") != command["definition_sha256"]
502
+ for binding, command in zip(value["command_bindings"], context.get("commands", [])))):
503
+ raise AuthorityError("INVALID_AUTHORITY_STATE", "context reference is not closed verification")
504
+ # Reserve the exact closed result (including every required test name), a
505
+ # wrapper with the bound commands/workspaces, and bounded host/lifecycle IDs.
506
+ # Large manifests stay immutable; large mutable results must still refuse.
507
+ commands = [{
508
+ "definition_sha256": command["definition_sha256"], "exit": command["definition"]["expected_exit"],
509
+ "tests": [{"name": name, "status": "pass"} for name in command["definition"]["required_tests"]],
510
+ "stdout_sha256": "0" * 64, "stderr_sha256": "0" * 64,
511
+ } for command in context["commands"]]
512
+ workspaces = {binding["workspace_root"]: {
513
+ "root": binding["workspace_root"], "filesystem_id": binding["workspace_filesystem_id"],
514
+ "git_common_dir": binding["git_common_dir"], "git_common_filesystem_id": binding["git_common_filesystem_id"],
515
+ "head": "0" * 64, "status_sha256": "0" * 64, "content_sha256": "0" * 64,
516
+ } for binding in value["command_bindings"]}
517
+ base = dict(value, context=CONTEXT_REFERENCE, state="CAPTURED", attempt="0" * 32,
518
+ observation_ref=None, observation_sha256=None, receipt=None, recovery=None)
519
+ base.pop("authority_source", None)
520
+ base["integrity_sha256"] = "0" * 64
521
+ base["payload"] = {field: context[field] for field in ("input_sha256", "spec_sha256", "task_sha256")}
522
+ base["payload"]["commands"] = commands
523
+ base["wrapper"] = {
524
+ "state": "CORROBORATED", "session_id": "s" * 256, "tool_use_id": "t" * 256,
525
+ "prompt_id" if value.get("host", "codex") == "claude" else "turn_id": "p" * 256,
526
+ "command_bindings": value["command_bindings"], "workspace_before": list(workspaces.values()),
527
+ }
528
+ if len(_canonical(base)) + 32 * 256 > MAX_STATE:
529
+ raise AuthorityError("INVALID_AUTHORITY_STATE", "verification result has no bounded lifecycle reserve")
530
+ return dict(value, context=CONTEXT_REFERENCE)
531
+
532
+
533
+ def _referenced_state(value: dict[str, Any]) -> dict[str, Any]:
534
+ if value.get("activity") == "verification":
535
+ return _referenced_verification_state(value)
536
+ return _referenced_review_state(value)
537
+
538
+
445
539
  def _save(root: Path, value: dict[str, Any]) -> None:
446
540
  expected = value.get("integrity_sha256")
447
541
  updated = dict(value)
448
542
  if "context_storage" in updated:
449
543
  if _read_context(root, updated["context_ref"], updated["context_sha256"]) != updated["context"]:
450
- raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "stored review context changed")
451
- updated = _referenced_review_state(updated)
544
+ raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "stored context changed")
545
+ updated = _referenced_state(updated)
452
546
  updated["integrity_sha256"] = _integrity(updated)
453
547
  legacy_abandonment = (
454
548
  value["state"] == "ABANDONED"
@@ -510,7 +604,7 @@ def _load(
510
604
  "authority_source", "dispatch_prompt", "review_contract_sha256",
511
605
  "required_coverage", "wrapper", "command_bindings", "recovery",
512
606
  "launch_envelope", "workspace_roots", "host", "codex_review_profile",
513
- "handback", "context_storage",
607
+ "handback", "context_storage", "codex_native_trace",
514
608
  }
515
609
  if (
516
610
  not isinstance(value, dict)
@@ -533,7 +627,7 @@ def _load(
533
627
  ):
534
628
  raise AuthorityError("INVALID_AUTHORITY_STATE", "request state failed validation")
535
629
  if "codex_review_profile" in value and (
536
- value["codex_review_profile"] != CODEX_NATIVE_V1
630
+ value["codex_review_profile"] not in CODEX_NATIVE_PROFILES
537
631
  or value.get("host", "codex") != "codex"
538
632
  or value["activity"] not in REVIEW_ACTIVITIES
539
633
  ):
@@ -545,7 +639,7 @@ def _load(
545
639
  subject = value["context"].get("subject", {})
546
640
  _validate_context(value["context"], subject.get("artifact_id"),
547
641
  subject.get("record_id"), value["activity"])
548
- _referenced_review_state(value)
642
+ _referenced_state(value)
549
643
  elif value["context"] == CONTEXT_REFERENCE:
550
644
  raise AuthorityError("INVALID_AUTHORITY_STATE", "context reference lacks its closed storage profile")
551
645
  if len(raw) > MAX_STATE:
@@ -587,13 +681,16 @@ def _read_context(
587
681
  raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context locator is not content addressed")
588
682
  try:
589
683
  path = root / expected_ref
684
+ _safe_directory(root, Path(expected_ref).parent, create=False)
590
685
  _path_identity(path)
686
+ if not stat.S_ISREG(path.lstat().st_mode):
687
+ raise ValueError("context is not a regular file")
591
688
  with path.open("rb") as stream:
592
689
  raw = stream.read(MAX_EVIDENCE_CONTEXT + 1)
593
690
  if len(raw) > MAX_EVIDENCE_CONTEXT:
594
691
  raise ValueError("context exceeds the native canonical budget")
595
692
  context = json.loads(raw.decode("utf-8"))
596
- except (OSError, UnicodeError, ValueError) as exc:
693
+ except (OSError, UnicodeError, ValueError, AuthorityError) as exc:
597
694
  raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context bytes are unavailable") from exc
598
695
  if not isinstance(context, dict) or _digest(raw) != context_hash or raw != _native_context_canonical(context):
599
696
  raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context bytes changed or are not canonical")
@@ -626,7 +723,10 @@ def _completion_client(root: Path) -> Any:
626
723
  """Use the native engine shipped with this adapter, never PATH or a caller."""
627
724
  import _artifactlib
628
725
 
629
- return _artifactlib.ArtifactClient(root, _artifactlib.helper_installation(__file__))
726
+ return _artifactlib.ArtifactClient(
727
+ root, _artifactlib.helper_installation(__file__),
728
+ timeout=EVIDENCE_ENGINE_TIMEOUT_SECONDS,
729
+ )
630
730
 
631
731
 
632
732
  def _context(
@@ -1079,6 +1179,7 @@ def _reject_overlapping_authority(session_id: str, turn_id: str) -> None:
1079
1179
  launch = request.get("launch") or {}
1080
1180
  if (
1081
1181
  request["state"] in {"LAUNCHING", "RUNNING"}
1182
+ and request.get("codex_review_profile") != CODEX_NATIVE_V2
1082
1183
  and launch.get("parent_session_id") == session_id
1083
1184
  and launch.get("parent_turn_id") == turn_id
1084
1185
  ):
@@ -1399,6 +1500,10 @@ def _dispatch_prompt(request: dict[str, Any], *, legacy_inline: bool = False) ->
1399
1500
  "these settings do not provide an OS sandbox.\n"
1400
1501
  if request.get("codex_review_profile") == CODEX_NATIVE_V1 else ""
1401
1502
  )
1503
+ if request.get("codex_review_profile") == CODEX_NATIVE_V2:
1504
+ profile = (f"Native review profile: {CODEX_NATIVE_V2}. This review was dispatched through "
1505
+ "collaboration.spawn_agent with default role and fork_turns=none. Read-only conduct "
1506
+ "is cooperative; these settings do not provide an OS sandbox.\n")
1402
1507
  completion = ""
1403
1508
  completion_fields = ""
1404
1509
  if "completion" in context:
@@ -1426,7 +1531,8 @@ def _dispatch_prompt(request: dict[str, Any], *, legacy_inline: bool = False) ->
1426
1531
  "done_when:<1-based index>. Each object has task_id, obligation, status (substantiated or "
1427
1532
  "missing), assessment (nonempty reasons with concrete source locations, including file paths "
1428
1533
  "and lines), and evidence_refs (nonempty unique exact selected receipt/source/event/observation/"
1429
- "context refs, mapped workspace_root, or material source_path). Reference the selected facts "
1534
+ f"context refs, {'mapped workspace_root' if legacy_inline else 'raw workspace_root path'}, "
1535
+ "or material source_path). Reference the selected facts "
1430
1536
  "that support each conclusion. Missing or unreadable facts require status=missing and "
1431
1537
  "decision=changes_requested. A passing decision requires every completion obligation "
1432
1538
  "substantiated; a claimed coverage list is insufficient.\n"
@@ -1434,6 +1540,19 @@ def _dispatch_prompt(request: dict[str, Any], *, legacy_inline: bool = False) ->
1434
1540
  + json.dumps(sorted(_completion_obligations(context)), ensure_ascii=True) + ".\n"
1435
1541
  )
1436
1542
  completion_fields = ", completion_sha256 (exact selected packet hash), completion_assessment (separate obligation rows)"
1543
+ if not legacy_inline:
1544
+ references = {task_id: sorted(refs) for task_id, refs in _completion_references(context).items()}
1545
+ completion += (
1546
+ "Exact allowed evidence_refs by task_id (JSON):\n"
1547
+ + _canonical(references).decode("ascii") + "\n"
1548
+ "Copy decoded JSON string values exactly into evidence_refs; choose only entries for "
1549
+ "that row's task_id, never another task's references. Do not add labels or prefixes, "
1550
+ "line numbers, descriptions, or alter path spelling. A raw workspace_root path is the "
1551
+ "listed path itself, without a mapped workspace_root: prefix. The verification-context "
1552
+ "refs listed here belong to selected published verifications; the current review container "
1553
+ "shown as Frozen context above is not an allowed evidence ref unless listed for that task. "
1554
+ "Put concrete source locations and lines in assessment, not in evidence_refs.\n"
1555
+ )
1437
1556
  return (
1438
1557
  f"[CODEARBITER_AUTHORITY_REQUEST:{request['request_id']}]\n"
1439
1558
  f"Target repository: {request['repository']['path']}\n"
@@ -1481,7 +1600,7 @@ def arm_request(
1481
1600
  if host == "claude" and reviewer_model not in CLAUDE_REVIEWER_MODELS:
1482
1601
  raise AuthorityError("INVALID_AUTHORITY_REQUEST", "reviewer model is unsupported")
1483
1602
  if codex_review_profile is not None and (
1484
- codex_review_profile != "native-v1" or host != "codex" or activity not in REVIEW_ACTIVITIES
1603
+ codex_review_profile not in {"native-v1", "native-v2"} or host != "codex" or activity not in REVIEW_ACTIVITIES
1485
1604
  ):
1486
1605
  raise AuthorityError("INVALID_AUTHORITY_REQUEST", "Codex review profile is unsupported")
1487
1606
  completion_selection = None
@@ -1530,8 +1649,10 @@ def arm_request(
1530
1649
  }
1531
1650
  if host != "codex":
1532
1651
  seed_value["host"] = host
1652
+ if activity == "verification":
1653
+ seed_value["context_storage"] = CONTEXT_STORAGE
1533
1654
  if codex_review_profile is not None:
1534
- seed_value["codex_review_profile"] = CODEX_NATIVE_V1
1655
+ seed_value["codex_review_profile"] = CODEX_NATIVE_V1 if codex_review_profile == "native-v1" else CODEX_NATIVE_V2
1535
1656
  if completion_selection is not None:
1536
1657
  seed_value["context_storage"] = CONTEXT_STORAGE
1537
1658
  seed = _canonical(seed_value)
@@ -1557,8 +1678,10 @@ def arm_request(
1557
1678
  }
1558
1679
  if host != "codex":
1559
1680
  request["host"] = host
1681
+ if activity == "verification":
1682
+ request["context_storage"] = CONTEXT_STORAGE
1560
1683
  if codex_review_profile is not None:
1561
- request["codex_review_profile"] = CODEX_NATIVE_V1
1684
+ request["codex_review_profile"] = seed_value["codex_review_profile"]
1562
1685
  if completion_selection is not None:
1563
1686
  request["context_storage"] = CONTEXT_STORAGE
1564
1687
  if activity in REVIEW_ACTIVITIES:
@@ -1571,7 +1694,7 @@ def arm_request(
1571
1694
  "subagent_type": CLAUDE_REVIEWER,
1572
1695
  "model": reviewer_model,
1573
1696
  }
1574
- elif codex_review_profile is not None:
1697
+ elif codex_review_profile == "native-v1":
1575
1698
  request["launch_envelope"] = {
1576
1699
  "message": request["dispatch_prompt"], "fork_context": False,
1577
1700
  }
@@ -1594,7 +1717,7 @@ def arm_request(
1594
1717
  result["required_coverage"] = request["required_coverage"]
1595
1718
  result["launch_envelope"] = request["launch_envelope"]
1596
1719
  if codex_review_profile is not None:
1597
- result["codex_review_profile"] = CODEX_NATIVE_V1
1720
+ result["codex_review_profile"] = request["codex_review_profile"]
1598
1721
  if completion_selection is not None:
1599
1722
  result["completion_sha256"] = context["completion_sha256"]
1600
1723
  return result
@@ -2820,6 +2943,145 @@ def _codex_uuid(value: Any) -> str:
2820
2943
  return value
2821
2944
 
2822
2945
 
2946
+ @contextmanager
2947
+ def _codex_transcript(path: Any):
2948
+ """Open the hook-owned native file, never a caller-selected linked path."""
2949
+ if not isinstance(path, str) or not Path(path).is_absolute():
2950
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native transcript path must be absolute")
2951
+ target = Path(path)
2952
+ try:
2953
+ for component in (*reversed(target.parents), target):
2954
+ info = component.lstat()
2955
+ if stat.S_ISLNK(info.st_mode) or getattr(info, "st_file_attributes", 0) & 0x400:
2956
+ raise OSError("linked transcript component")
2957
+ if not stat.S_ISREG(info.st_mode):
2958
+ raise OSError("nonregular transcript")
2959
+ flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
2960
+ with os.fdopen(os.open(target, flags), "rb") as stream:
2961
+ opened = os.fstat(stream.fileno())
2962
+ if (opened.st_dev, opened.st_ino) != (info.st_dev, info.st_ino):
2963
+ raise OSError("changed transcript")
2964
+ yield stream, opened
2965
+ except OSError as exc:
2966
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native transcript is unavailable or unsafe") from exc
2967
+
2968
+
2969
+ def _codex_metadata(stream) -> tuple[bytes, dict[str, Any]]:
2970
+ stream.seek(0)
2971
+ raw = stream.readline(MAX_DECISION + 1)
2972
+ try:
2973
+ value = json.loads(raw.decode("utf-8"), object_pairs_hook=_unique_json_pairs,
2974
+ parse_constant=_invalid_json_constant)
2975
+ if (len(raw) > MAX_DECISION or not raw.endswith(b"\n") or not isinstance(value, dict)
2976
+ or value.get("type") != "session_meta" or not isinstance(value.get("payload"), dict)):
2977
+ raise ValueError("unsupported metadata")
2978
+ return raw, value["payload"]
2979
+ except (ValueError, UnicodeError, RecursionError) as exc:
2980
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native session metadata is malformed or oversized") from exc
2981
+
2982
+
2983
+ def _codex_v2_trace_anchor(event: dict[str, Any], task_name: str) -> dict[str, Any]:
2984
+ with _codex_transcript(event.get("transcript_path")) as (stream, info):
2985
+ raw, meta = _codex_metadata(stream)
2986
+ thread = _codex_uuid(meta.get("id"))
2987
+ session = meta.get("session_id", thread)
2988
+ parent_path = meta.get("agent_path") or "/root"
2989
+ if (session != event.get("session_id") or not isinstance(parent_path, str)
2990
+ or CODEX_AGENT_PATH_RE.fullmatch(parent_path) is None
2991
+ or not re.fullmatch(r"authority_[0-9a-f]{12}", task_name)):
2992
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native parent session or task path differs")
2993
+ offset = info.st_size
2994
+ tail_start = max(0, offset - MAX_DECISION)
2995
+ stream.seek(tail_start)
2996
+ tail = stream.read(offset - tail_start)
2997
+ if not tail.endswith(b"\n"):
2998
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native parent transcript is not materialized")
2999
+ return {"path": event["transcript_path"], "device": info.st_dev, "inode": info.st_ino,
3000
+ "offset": offset, "tail_start": tail_start, "tail_sha256": _digest(tail),
3001
+ "metadata_sha256": _digest(raw), "parent_thread_id": thread,
3002
+ "parent_agent_path": parent_path, "agent_path": parent_path + "/" + task_name}
3003
+
3004
+
3005
+ def _codex_v2_activity(event: dict[str, Any], request: dict[str, Any]) -> dict[str, Any]:
3006
+ anchor = request.get("codex_native_trace")
3007
+ launch = request["launch"]
3008
+ if not isinstance(anchor, dict) or event.get("transcript_path") != anchor.get("path"):
3009
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native parent transcript locator changed")
3010
+ with _codex_transcript(anchor["path"]) as (stream, info):
3011
+ raw, _ = _codex_metadata(stream)
3012
+ if ((info.st_dev, info.st_ino) != (anchor["device"], anchor["inode"])
3013
+ or info.st_size < anchor["offset"] or _digest(raw) != anchor["metadata_sha256"]):
3014
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native parent transcript identity changed")
3015
+ stream.seek(anchor["tail_start"])
3016
+ if _digest(stream.read(anchor["offset"] - anchor["tail_start"])) != anchor["tail_sha256"]:
3017
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native parent transcript prefix changed")
3018
+ stream.seek(anchor["offset"])
3019
+ suffix = stream.read(MAX_OUTPUT + 1)
3020
+ if len(suffix) > MAX_OUTPUT or not suffix.endswith(b"\n"):
3021
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native activity suffix is absent, incomplete or oversized")
3022
+ matches = []
3023
+ try:
3024
+ # JSONL records end at LF; Unicode line separators are valid string data.
3025
+ for line in suffix.decode("utf-8").split("\n")[:-1]:
3026
+ value = json.loads(line, object_pairs_hook=_unique_json_pairs, parse_constant=_invalid_json_constant)
3027
+ payload = value.get("payload") if isinstance(value, dict) else None
3028
+ item = payload.get("item") if isinstance(payload, dict) else None
3029
+ if (value.get("type") == "event_msg" and isinstance(item, dict)
3030
+ and item.get("type") == "SubAgentActivity" and payload.get("type") == "item_completed"
3031
+ and item.get("id") == launch["tool_use_id"]):
3032
+ matches.append(payload)
3033
+ except (ValueError, UnicodeError, AttributeError, RecursionError) as exc:
3034
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native activity suffix is not strict JSONL") from exc
3035
+ if len(matches) != 1:
3036
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native activity for exact call is missing or ambiguous")
3037
+ activity = matches[0]
3038
+ item = activity["item"]
3039
+ if (set(activity) != {"type", "thread_id", "turn_id", "item", "started_at_ms", "completed_at_ms"}
3040
+ or set(item) != {"type", "id", "kind", "agent_thread_id", "agent_path"}
3041
+ or activity["thread_id"] != anchor["parent_thread_id"] or activity["turn_id"] != launch["parent_turn_id"]
3042
+ or item["kind"] != "started" or item["agent_path"] != anchor["agent_path"]
3043
+ or any(type(activity[key]) is not int or activity[key] < 0 for key in ("started_at_ms", "completed_at_ms"))
3044
+ or activity["completed_at_ms"] < activity["started_at_ms"]):
3045
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native activity binding differs from the exact launch")
3046
+ _codex_uuid(item["agent_thread_id"])
3047
+ return activity
3048
+
3049
+
3050
+ def _codex_v2_child(event: dict[str, Any]) -> dict[str, Any] | None:
3051
+ # Historical V1 lifecycle fixtures/hosts need no native rollout metadata.
3052
+ if event.get("transcript_path") is None:
3053
+ return None
3054
+ try:
3055
+ with _codex_transcript(event["transcript_path"]) as (stream, _):
3056
+ raw, meta = _codex_metadata(stream)
3057
+ except AuthorityError:
3058
+ # A V1 Start did not promise native metadata. V2 binding below requires
3059
+ # qualified metadata; missing/unsafe bytes still cannot pass V2.
3060
+ return None
3061
+ if meta.get("cli_version") != "0.162.0-alpha.2":
3062
+ return None
3063
+ identity = {"cli_version": meta["cli_version"], "thread_id": _codex_uuid(meta.get("id")),
3064
+ "parent_thread_id": _codex_uuid(meta.get("parent_thread_id")),
3065
+ "session_id": _host_id(meta.get("session_id"), "session_id"),
3066
+ "agent_path": meta.get("agent_path"), "metadata_sha256": _digest(raw)}
3067
+ if (identity["thread_id"] != event["agent_id"] or identity["session_id"] != event.get("session_id")
3068
+ or not isinstance(identity["agent_path"], str)
3069
+ or CODEX_AGENT_PATH_RE.fullmatch(identity["agent_path"]) is None):
3070
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native child metadata differs from its lifecycle")
3071
+ return identity
3072
+
3073
+
3074
+ def _codex_v2_bind_child(request: dict[str, Any], child: dict[str, Any]) -> None:
3075
+ identity = child.get("native_v2")
3076
+ launch = request["launch"]
3077
+ if (not isinstance(identity, dict) or identity.get("parent_thread_id") != launch["parent_thread_id"]
3078
+ or identity.get("thread_id") != launch["agent_id"]
3079
+ or identity.get("session_id") != launch["parent_session_id"]
3080
+ or identity.get("agent_path") != launch["task_name"]):
3081
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native V2 child runtime or parent association is unqualified")
3082
+ launch["native_child"] = identity
3083
+
3084
+
2823
3085
  def _codex_native_marker(agent_id: str) -> Path:
2824
3086
  # UUID, not arrival order or session, is the direct host-observed join key.
2825
3087
  # Retaining this marker also prevents reuse across requests and sessions.
@@ -2884,8 +3146,10 @@ def _read_codex_child(path: Path, agent_id: str, session_id: str) -> dict[str, A
2884
3146
  start = value.get("start") if isinstance(value, dict) else None
2885
3147
  if (
2886
3148
  len(raw) > MAX_CHILD_MARKER or not isinstance(value, dict)
2887
- or set(value) != {"format", "agent_id", "session_id", "request_id", "start", "stop_seen",
2888
- "rejected", "integrity_sha256"}
3149
+ or set(value) - {"format", "agent_id", "session_id", "request_id", "start", "stop_seen",
3150
+ "rejected", "integrity_sha256", "native_v2"}
3151
+ or not {"format", "agent_id", "session_id", "request_id", "start", "stop_seen",
3152
+ "rejected", "integrity_sha256"} <= set(value)
2889
3153
  or value.get("format") != CODEX_CHILD_FORMAT or value.get("agent_id") != agent_id
2890
3154
  or not isinstance(value.get("session_id"), str) or not HOST_ID_RE.fullmatch(value["session_id"])
2891
3155
  or (value.get("request_id") is not None and (
@@ -2898,6 +3162,19 @@ def _read_codex_child(path: Path, agent_id: str, session_id: str) -> dict[str, A
2898
3162
  or not CODEX_UUID_RE.fullmatch(start["turn_id"])))
2899
3163
  ):
2900
3164
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native Codex child marker failed validation")
3165
+ if "native_v2" in value:
3166
+ identity = value["native_v2"]
3167
+ if (not isinstance(identity, dict) or set(identity) != {
3168
+ "cli_version", "thread_id", "parent_thread_id", "session_id", "agent_path", "metadata_sha256"}
3169
+ or identity.get("cli_version") != "0.162.0-alpha.2"
3170
+ or identity.get("thread_id") != agent_id or identity.get("session_id") != value["session_id"]
3171
+ or not isinstance(identity.get("parent_thread_id"), str)
3172
+ or CODEX_UUID_RE.fullmatch(identity["parent_thread_id"]) is None
3173
+ or not isinstance(identity.get("agent_path"), str)
3174
+ or CODEX_AGENT_PATH_RE.fullmatch(identity["agent_path"]) is None
3175
+ or not isinstance(identity.get("metadata_sha256"), str)
3176
+ or SHA256_RE.fullmatch(identity["metadata_sha256"]) is None):
3177
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native child runtime metadata failed validation")
2901
3178
  return value
2902
3179
 
2903
3180
 
@@ -2936,10 +3213,13 @@ def _codex_native_post(event: dict[str, Any], root: Path, request: dict[str, Any
2936
3213
  _reject(root, current, "invalid-first-native-review-post")
2937
3214
  if isinstance(exc, AuthorityError):
2938
3215
  raise
2939
- raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v1 spawn result is malformed") from exc
3216
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native spawn result is malformed") from exc
2940
3217
 
2941
3218
 
2942
3219
  def _codex_native_post_join(event: dict[str, Any], root: Path, request: dict[str, Any]) -> dict[str, Any]:
3220
+ expected_tool = "collaborationspawn_agent" if request.get("codex_review_profile") == CODEX_NATIVE_V2 else "spawn_agent"
3221
+ if event.get("tool_name") != expected_tool:
3222
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native review PostToolUse tool differs from its launch profile")
2943
3223
  if _canonical(event.get("tool_input")) != _canonical(request["launch_envelope"]):
2944
3224
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native review PostToolUse envelope was changed")
2945
3225
  _require_frozen_context(root, request)
@@ -2950,11 +3230,20 @@ def _codex_native_post_join(event: dict[str, Any], root: Path, request: dict[str
2950
3230
  response = json.loads(raw, object_pairs_hook=_unique_json_pairs, parse_constant=_invalid_json_constant)
2951
3231
  except ValueError as exc:
2952
3232
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v1 spawn result is not strict JSON") from exc
2953
- if (not isinstance(response, dict) or set(response) != {"agent_id", "nickname"}
3233
+ activity = None
3234
+ if request.get("codex_review_profile") == CODEX_NATIVE_V2:
3235
+ if (not isinstance(response, dict) or set(response) not in ({"task_name"}, {"task_name", "nickname"})
3236
+ or response.get("task_name") != request["codex_native_trace"]["agent_path"]
3237
+ or ("nickname" in response and (not isinstance(response["nickname"], str) or len(response["nickname"]) > 256))):
3238
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v2 spawn result fields or canonical task path differ")
3239
+ activity = _codex_v2_activity(event, request)
3240
+ agent_id = _codex_uuid(activity["item"]["agent_thread_id"])
3241
+ elif (not isinstance(response, dict) or set(response) != {"agent_id", "nickname"}
2954
3242
  or (response["nickname"] is not None and (
2955
3243
  not isinstance(response["nickname"], str) or len(response["nickname"]) > 256))):
2956
3244
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native-v1 spawn result fields are unsupported")
2957
- agent_id = _codex_uuid(response.get("agent_id"))
3245
+ else:
3246
+ agent_id = _codex_uuid(response.get("agent_id"))
2958
3247
  session_id = request["launch"]["parent_session_id"]
2959
3248
  with _codex_child_lock(agent_id, session_id) as (path, child):
2960
3249
  request = _load(root, request["request_id"])
@@ -2970,7 +3259,11 @@ def _codex_native_post_join(event: dict[str, Any], root: Path, request: dict[str
2970
3259
  _codex_native_refuse(root, request, "native reviewer stopped or conflicted before association")
2971
3260
  launch = request["launch"]
2972
3261
  launch.update(agent_id=agent_id, post_confirmed=True)
3262
+ if activity is not None:
3263
+ launch.update(native_activity=activity, native_activity_sha256=_engine_digest(activity))
2973
3264
  if child["start"] is not None:
3265
+ if request.get("codex_review_profile") == CODEX_NATIVE_V2:
3266
+ _codex_v2_bind_child(request, child)
2974
3267
  launch.update(child_turn_id=child["start"]["turn_id"], agent_type="default")
2975
3268
  request["state"] = "RUNNING"
2976
3269
  _save(root, request)
@@ -3011,7 +3304,7 @@ def _codex_native_lifecycle(event: dict[str, Any]) -> dict[str, Any] | None:
3011
3304
  except (AuthorityError, OSError) as exc:
3012
3305
  _claim_codex_event(agent_id, "failed")
3013
3306
  for root, request in _registered_requests():
3014
- if (request.get("codex_review_profile") == CODEX_NATIVE_V1
3307
+ if (request.get("codex_review_profile") in CODEX_NATIVE_PROFILES
3015
3308
  and request["state"] in {"LAUNCHING", "RUNNING"}
3016
3309
  and (request.get("launch") or {}).get("agent_id") == agent_id):
3017
3310
  _reject(root, request, "failed-native-review-lifecycle")
@@ -3039,6 +3332,9 @@ def _codex_native_lifecycle_join(event: dict[str, Any]) -> dict[str, Any] | None
3039
3332
  if name == "SubagentStart":
3040
3333
  valid = valid and start is None
3041
3334
  if valid:
3335
+ identity = _codex_v2_child(event)
3336
+ if identity is not None:
3337
+ child["native_v2"] = identity
3042
3338
  child["start"] = {"turn_id": event["turn_id"], "agent_type": "default"}
3043
3339
  else:
3044
3340
  valid = (valid and start is not None and start["turn_id"] == event.get("turn_id")
@@ -3052,7 +3348,7 @@ def _codex_native_lifecycle_join(event: dict[str, Any]) -> dict[str, Any] | None
3052
3348
  return None
3053
3349
  root, request = _registered_request(child["request_id"])
3054
3350
  launch = request.get("launch") or {}
3055
- if (not valid or request.get("codex_review_profile") != CODEX_NATIVE_V1
3351
+ if (not valid or request.get("codex_review_profile") not in CODEX_NATIVE_PROFILES
3056
3352
  or launch.get("parent_session_id") != child["session_id"]
3057
3353
  or launch.get("agent_id") != agent_id or launch.get("post_confirmed") is not True
3058
3354
  or event.get("turn_id") == launch.get("parent_turn_id")):
@@ -3060,6 +3356,8 @@ def _codex_native_lifecycle_join(event: dict[str, Any]) -> dict[str, Any] | None
3060
3356
  if name == "SubagentStart":
3061
3357
  if request["state"] != "LAUNCHING":
3062
3358
  _codex_native_refuse(root, request, "native reviewer start was repeated")
3359
+ if request.get("codex_review_profile") == CODEX_NATIVE_V2:
3360
+ _codex_v2_bind_child(request, child)
3063
3361
  launch.update(child_turn_id=event["turn_id"], agent_type="default")
3064
3362
  request["state"] = "RUNNING"
3065
3363
  _save(root, request)
@@ -3100,6 +3398,43 @@ def _codex_native_steering(event: dict[str, Any]) -> None:
3100
3398
  if not isinstance(tool_input, dict):
3101
3399
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native reviewer steering input is malformed")
3102
3400
  agent_id = _codex_steering_uuid(tool_input.get(field))
3401
+ if event["tool_name"].startswith("collaboration"):
3402
+ target = tool_input.get("target")
3403
+ canonical_path = None
3404
+ if isinstance(target, str) and CODEX_AGENT_PATH_RE.fullmatch(target):
3405
+ canonical_path = target
3406
+ child_identity = None
3407
+ if agent_id is not None:
3408
+ _claim_codex_event(agent_id, "failed")
3409
+ with _codex_child_lock(agent_id, _host_id(event.get("session_id"), "session_id")) as (path, child):
3410
+ child_identity = child.get("native_v2")
3411
+ child["rejected"] = True
3412
+ _write_codex_child(path, child)
3413
+ for root, request in _registered_requests(strict=True):
3414
+ launch = request.get("launch") or {}
3415
+ if (request.get("codex_review_profile") != CODEX_NATIVE_V2
3416
+ or request["state"] not in {"LAUNCHING", "RUNNING"}
3417
+ or launch.get("parent_session_id") != event.get("session_id")):
3418
+ continue
3419
+ # Caller-relative identity matters only to an active review here.
3420
+ if canonical_path is None and isinstance(target, str) and re.fullmatch(r"[a-z0-9_]+(?:/[a-z0-9_]+)*", target):
3421
+ with _codex_transcript(event.get("transcript_path")) as (stream, _):
3422
+ _, meta = _codex_metadata(stream)
3423
+ caller_path = meta.get("agent_path") or "/root"
3424
+ if isinstance(caller_path, str) and CODEX_AGENT_PATH_RE.fullmatch(caller_path):
3425
+ canonical_path = caller_path + "/" + target
3426
+ matches = canonical_path == launch.get("task_name") or (
3427
+ agent_id is not None and (launch.get("agent_id") == agent_id or
3428
+ isinstance(child_identity, dict) and child_identity.get("agent_path") == launch.get("task_name")
3429
+ and child_identity.get("parent_thread_id") == launch.get("parent_thread_id")))
3430
+ if not matches and agent_id is not None and launch.get("agent_id") is None:
3431
+ try:
3432
+ matches = _codex_v2_activity(event, request)["item"]["agent_thread_id"] == agent_id
3433
+ except AuthorityError:
3434
+ pass # Missing direct evidence never supplies an association.
3435
+ if matches:
3436
+ _codex_native_refuse(root, request, "native reviewer cannot be steered or interrupted")
3437
+ return None
3103
3438
  if agent_id is None:
3104
3439
  return None
3105
3440
  _claim_codex_event(agent_id, "failed")
@@ -3123,25 +3458,28 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3123
3458
  return native
3124
3459
  session_id = _host_id(event.get("session_id"), "session_id")
3125
3460
 
3126
- if name in {"PreToolUse", "PostToolUse"} and event.get("tool_name") == "collaborationspawn_agent":
3461
+ if name == "PreToolUse" and event.get("tool_name") == "collaborationspawn_agent":
3127
3462
  tool_input = event.get("tool_input")
3128
3463
  message = tool_input.get("message") if isinstance(tool_input, dict) else None
3129
3464
  if isinstance(message, str) and message.startswith("[CODEARBITER_AUTHORITY_REQUEST:"):
3130
- raise AuthorityError(
3131
- "UNSUPPORTED_HOST_SEAM",
3132
- "default Codex v2 has no direct child UUID binding; qualify the native-v1 interface and arm a fresh request",
3133
- )
3134
- if name == "PreToolUse":
3465
+ _, candidate = _registered_request(_request_id_from_prompt(message))
3466
+ if candidate.get("codex_review_profile") != CODEX_NATIVE_V2:
3467
+ raise AuthorityError(
3468
+ "UNSUPPORTED_HOST_SEAM",
3469
+ "Codex v2 requires a fresh qualified native-v2 request with direct activity UUID binding; "
3470
+ "otherwise qualify native-v1 and arm a fresh request",
3471
+ )
3472
+ else:
3135
3473
  turn_id = _host_id(event.get("turn_id"), "turn_id")
3136
3474
  _record_ordinary_spawn(session_id, turn_id)
3137
3475
  _reject_overlapping_authority(session_id, turn_id)
3138
- return None
3476
+ return None
3139
3477
 
3140
3478
  if name in {"PreToolUse", "PostToolUse"} and event.get("tool_name") in CODEX_STEERING_TOOLS:
3141
3479
  return _codex_native_steering(event)
3142
3480
 
3143
3481
  if name == "PreToolUse":
3144
- if event.get("tool_name") != "spawn_agent":
3482
+ if event.get("tool_name") not in {"spawn_agent", "collaborationspawn_agent"}:
3145
3483
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "only spawn_agent can launch a reviewer")
3146
3484
  tool_use_id = _host_id(event.get("tool_use_id"), "tool_use_id")
3147
3485
  turn_id = _host_id(event.get("turn_id"), "turn_id")
@@ -3163,8 +3501,11 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3163
3501
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review request is not launchable")
3164
3502
  if _canonical(tool_input) != _canonical(request.get("launch_envelope")):
3165
3503
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch envelope was changed")
3504
+ is_v2 = request.get("codex_review_profile") == CODEX_NATIVE_V2
3505
+ if is_v2 != (event["tool_name"] == "collaborationspawn_agent"):
3506
+ raise AuthorityError("UNSUPPORTED_HOST_SEAM", "native review profile differs from launch tool")
3166
3507
  _require_frozen_context(root, request)
3167
- if _ordinary_spawn_marker(session_id, turn_id).exists():
3508
+ if not is_v2 and _ordinary_spawn_marker(session_id, turn_id).exists():
3168
3509
  request["state"] = "REJECTED"
3169
3510
  request["recovery"] = {
3170
3511
  "mode": "overlapping-subagent-launch",
@@ -3185,6 +3526,11 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3185
3526
  codex_review_profile=CODEX_NATIVE_V1, child_turn_id=None,
3186
3527
  fork_context=False,
3187
3528
  )
3529
+ elif is_v2:
3530
+ anchor = _codex_v2_trace_anchor(event, tool_input["task_name"])
3531
+ request["codex_native_trace"] = anchor
3532
+ request["launch"].update(codex_review_profile=CODEX_NATIVE_V2, child_turn_id=None,
3533
+ parent_thread_id=anchor["parent_thread_id"], task_name=anchor["agent_path"], fork_turns="none")
3188
3534
  else:
3189
3535
  request["launch"].update(task_name=tool_input["task_name"], fork_turns=tool_input["fork_turns"])
3190
3536
  request["state"] = "LAUNCHING"
@@ -3192,7 +3538,7 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3192
3538
  return {"request_id": request_id, "state": "LAUNCHING"}
3193
3539
 
3194
3540
  if name == "PostToolUse":
3195
- if event.get("tool_name") != "spawn_agent":
3541
+ if event.get("tool_name") not in {"spawn_agent", "collaborationspawn_agent"}:
3196
3542
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "post-tool event is not reviewer launch")
3197
3543
  tool_use_id = _host_id(event.get("tool_use_id"), "tool_use_id")
3198
3544
  turn_id = _host_id(event.get("turn_id"), "turn_id")
@@ -3211,7 +3557,7 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3211
3557
  if len(matches) != 1:
3212
3558
  raise AuthorityError("UNSUPPORTED_HOST_SEAM", "review launch correlation is ambiguous")
3213
3559
  root, request = matches[0]
3214
- if request.get("codex_review_profile") == CODEX_NATIVE_V1:
3560
+ if request.get("codex_review_profile") in CODEX_NATIVE_PROFILES:
3215
3561
  return _codex_native_post(event, root, request)
3216
3562
  response = event.get("tool_response")
3217
3563
  if not isinstance(response, dict):
@@ -3229,7 +3575,7 @@ def observe_codex_hook(root: str | Path, event: dict[str, Any]) -> dict[str, Any
3229
3575
  matches = []
3230
3576
  same_turn = []
3231
3577
  for candidate_root, candidate in _registered_requests():
3232
- if candidate.get("codex_review_profile") == CODEX_NATIVE_V1:
3578
+ if candidate.get("codex_review_profile") in CODEX_NATIVE_PROFILES:
3233
3579
  continue
3234
3580
  launch = candidate.get("launch") or {}
3235
3581
  if (
@@ -447,7 +447,8 @@ def _installed_workflow_preflight(installation: Path) -> dict[str, object]:
447
447
  tools = ("Bash", "Agent") if host == "claude" else (
448
448
  "Bash", "shell_command", "exec_command", "unified_exec", "spawn_agent",
449
449
  "collaborationspawn_agent", "multi_agent_v1send_input", "multi_agent_v1resume_agent",
450
- "multi_agent_v1close_agent")
450
+ "multi_agent_v1close_agent", "collaborationsend_message", "collaborationfollowup_task",
451
+ "collaborationinterrupt_agent")
451
452
  required += [(event, tool, "artifact-authority-hook.py")
452
453
  for event in ("PreToolUse", "PostToolUse") for tool in tools]
453
454
  required += [(event, None, "artifact-authority-hook.py")
@@ -11,7 +11,7 @@ import hostapi # noqa: E402
11
11
  class PiHost(hostapi.Host):
12
12
  name = "pi"
13
13
  adapter_name = "@arbiterforge/ca-pi"
14
- adapter_version = "0.17.5"
14
+ adapter_version = "0.17.6"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -52,8 +52,8 @@ def main(argv=None) -> int:
52
52
  default="opus", help="Claude reviewer model pinned into the launch envelope",
53
53
  )
54
54
  arm.add_argument(
55
- "--codex-review-profile", choices=("native-v1",), default=None,
56
- help="select only after qualifying the actual registered native V1 UUID interface",
55
+ "--codex-review-profile", choices=("native-v1", "native-v2"), default=None,
56
+ help="select only after qualifying the actual registered native interface and lifecycle",
57
57
  )
58
58
  context_arm = sub.add_parser("arm-context")
59
59
  context_arm.add_argument("--root", required=True)
@@ -80,7 +80,8 @@ def main(argv=None) -> int:
80
80
  print(json.dumps(_artifactauthoritylib.cancel_context_preview(root, args.document_id), sort_keys=True))
81
81
  return 0
82
82
  client = _artifactlib.ArtifactClient(
83
- root, _artifactlib.helper_installation(__file__)
83
+ root, _artifactlib.helper_installation(__file__),
84
+ timeout=_artifactauthoritylib.EVIDENCE_ENGINE_TIMEOUT_SECONDS,
84
85
  )
85
86
  if args.command == "arm":
86
87
  workspaces = _workspace_map(args.workspace)
@@ -93,7 +94,7 @@ def main(argv=None) -> int:
93
94
  and args.codex_review_profile is None):
94
95
  raise _artifactauthoritylib.AuthorityError(
95
96
  "UNSUPPORTED_HOST_SEAM",
96
- "Codex review requires explicit --codex-review-profile native-v1 after live interface qualification",
97
+ "Codex review requires an explicit native-v1 or native-v2 profile after live interface qualification",
97
98
  )
98
99
  result = _artifactauthoritylib.arm_request(
99
100
  root, client, args.artifact_id, args.record_id, args.activity,
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "2.25.4"
316
+ adapter_version = "2.25.5"
317
317
 
318
318
  # Update-notifier descriptor. Each independently versioned host overrides
319
319
  # these three values in its per-plugin _host.py. Keeping the target,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.17.5",
3
+ "version": "0.17.6",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",