@arbiterforge/ca-pi 0.14.9 → 0.14.12
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 +7 -2
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +39 -0
- package/plugins/ca-pi/SKILLS.md +2 -2
- package/plugins/ca-pi/SPRINT.md +47 -0
- package/plugins/ca-pi/generated/command-catalog.json +2 -2
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/release.json +6 -6
- package/plugins/ca-pi/hooks/_approvallib.py +58 -22
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +1686 -0
- package/plugins/ca-pi/hooks/_artifactlib.py +14 -4
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +177 -0
- package/plugins/ca-pi/hooks/_githooks.py +368 -39
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_overrideappendlib.py +5 -1
- package/plugins/ca-pi/hooks/_prerequisitelib.py +44 -22
- package/plugins/ca-pi/hooks/_reconciliationlib.py +472 -0
- package/plugins/ca-pi/hooks/_sprintapprovallib.py +302 -0
- package/plugins/ca-pi/hooks/artifact-authority-hook.py +72 -0
- package/plugins/ca-pi/hooks/artifact-authority.py +80 -0
- package/plugins/ca-pi/hooks/artifact-reconcile.py +57 -0
- package/plugins/ca-pi/hooks/doctor.py +3 -1
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/prompt-submit.py +20 -3
- package/plugins/ca-pi/includes/artifacts.md +99 -12
- package/plugins/ca-pi/includes/sprint-authority.md +69 -0
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +7 -4
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +97 -29
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +7 -1
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +32 -15
- package/plugins/ca-pi/routines/skill-author/SKILL.md +26 -5
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +22 -10
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +170 -16
- package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +116 -21
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Private one-reply sprint approval adapter; no public registration.
|
|
3
|
+
|
|
4
|
+
The existing host hook records an actual reply. The native engine validates
|
|
5
|
+
both frozen definitions and commits their approvals in one journal transaction.
|
|
6
|
+
A retained submission can resume after interruption without another user reply;
|
|
7
|
+
merely arming a request is never approval or delegation.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
from functools import wraps
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import secrets
|
|
17
|
+
import stat
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import _approvallib as approval
|
|
21
|
+
import _artifactlib
|
|
22
|
+
import _artifactauthoritylib
|
|
23
|
+
import _artifactpromptlib
|
|
24
|
+
|
|
25
|
+
FORMAT = "codearbiter.pending-user-approval/0.2.0"
|
|
26
|
+
SUBMISSION_FORMAT = "codearbiter.sprint-submission/0.1.0"
|
|
27
|
+
IDENTITY_KEYS = {"artifact_id", "kind", "revision", "model_sha256", "normative_sha256"}
|
|
28
|
+
PAIR_KEYS = {"format", "spec", "plan", "approved_plan_normative_sha256", "scope_sha256", "delegate_methods"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _hash(value: Any) -> str:
|
|
32
|
+
return hashlib.sha256(approval._canonical(value)).hexdigest()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _digest(value: Any) -> bool:
|
|
36
|
+
return isinstance(value, str) and approval.SHA256_RE.fullmatch(value) is not None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _serialized(function):
|
|
40
|
+
"""Reuse the existing private OS lock; no lock file enters the input snapshot."""
|
|
41
|
+
@wraps(function)
|
|
42
|
+
def run(root, *args, **kwargs):
|
|
43
|
+
import _prerequisitelib
|
|
44
|
+
root = _artifactlib._trusted_directory(root, "UNSAFE_ROOT")
|
|
45
|
+
try:
|
|
46
|
+
# One pending approval marker is shared by all pairs in this root.
|
|
47
|
+
with _prerequisitelib._pending_transition_lock(root, "sprint-pair-approval"):
|
|
48
|
+
return function(root, *args, **kwargs)
|
|
49
|
+
except _prerequisitelib.PrerequisiteError as exc:
|
|
50
|
+
raise approval.ApprovalError("APPROVAL_BUSY", "another session is completing this repository's paired approval") from exc
|
|
51
|
+
return run
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_pending(value: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
expected = {"format", "artifact_id", "kind", "pair", "spec_context", "plan_context", "prompt_sha256", "token_sha256", "request_key"}
|
|
56
|
+
if set(value) != expected or value.get("format") != FORMAT or value.get("kind") != "sprint-pair":
|
|
57
|
+
raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "paired pending shape is invalid")
|
|
58
|
+
pair = value.get("pair")
|
|
59
|
+
if not isinstance(pair, dict) or set(pair) != PAIR_KEYS or pair.get("format") != "codearbiter.sprint-pair/0.1.0" or type(pair.get("delegate_methods")) is not bool:
|
|
60
|
+
raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "paired scope is malformed")
|
|
61
|
+
for name in ("spec", "plan"):
|
|
62
|
+
identity = pair.get(name)
|
|
63
|
+
if (not isinstance(identity, dict) or set(identity) != IDENTITY_KEYS
|
|
64
|
+
or identity.get("kind") != name or not isinstance(identity.get("artifact_id"), str)
|
|
65
|
+
or not identity["artifact_id"] or type(identity.get("revision")) is not int
|
|
66
|
+
or identity["revision"] < 1
|
|
67
|
+
or not all(_digest(identity.get(k)) for k in ("model_sha256", "normative_sha256"))):
|
|
68
|
+
raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "paired identity is malformed")
|
|
69
|
+
context = value.get(name + "_context")
|
|
70
|
+
if (not isinstance(context, dict) or set(context) != {"context_ref", "context_sha256", "subject"}
|
|
71
|
+
or not _digest(context.get("context_sha256"))
|
|
72
|
+
or context.get("context_ref") != ".codearbiter/.artifacts/evidence-contexts/" + context["context_sha256"] + ".json"):
|
|
73
|
+
raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "context locator is malformed")
|
|
74
|
+
expected_norm = identity["normative_sha256"] if name == "spec" else pair["approved_plan_normative_sha256"]
|
|
75
|
+
if context.get("subject") != {"artifact_id": identity["artifact_id"], "record_id": identity["artifact_id"], "normative_sha256": expected_norm}:
|
|
76
|
+
raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "context belongs to a different pair")
|
|
77
|
+
if (value.get("artifact_id") != pair["plan"]["artifact_id"]
|
|
78
|
+
or not all(_digest(value.get(k)) for k in ("prompt_sha256", "token_sha256", "request_key"))
|
|
79
|
+
or not all(_digest(pair.get(k)) for k in ("approved_plan_normative_sha256", "scope_sha256"))
|
|
80
|
+
or value["request_key"] != _hash({"pair": pair, "prompt_sha256": value["prompt_sha256"]})):
|
|
81
|
+
raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "paired request identity is invalid")
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _read(root: Path, relative: Path, limit: int = 65536) -> dict[str, Any]:
|
|
86
|
+
parent = approval._safe_directory(root, relative.parent, create=False)
|
|
87
|
+
fd = _artifactlib._open_pinned_regular(parent / relative.name)
|
|
88
|
+
try:
|
|
89
|
+
info = os.fstat(fd)
|
|
90
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > limit:
|
|
91
|
+
raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "record is not bounded regular data")
|
|
92
|
+
chunks = []
|
|
93
|
+
size = 0
|
|
94
|
+
while True:
|
|
95
|
+
chunk = os.read(fd, min(65536, limit + 1 - size))
|
|
96
|
+
if not chunk:
|
|
97
|
+
break
|
|
98
|
+
chunks.append(chunk)
|
|
99
|
+
size += len(chunk)
|
|
100
|
+
if size > limit:
|
|
101
|
+
raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "record exceeds its bound")
|
|
102
|
+
raw = b"".join(chunks)
|
|
103
|
+
finally:
|
|
104
|
+
os.close(fd)
|
|
105
|
+
value = _artifactlib._decode(raw)
|
|
106
|
+
if not isinstance(value, dict):
|
|
107
|
+
raise approval.ApprovalError("INVALID_PENDING_APPROVAL", "record must be an object")
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@_serialized
|
|
112
|
+
def arm(root: str | Path, client: Any, spec_id: str, plan_id: str, *, delegate_methods: bool = False, token: str | None = None) -> dict[str, Any]:
|
|
113
|
+
root = _artifactlib._trusted_directory(root, "UNSAFE_ROOT")
|
|
114
|
+
if type(delegate_methods) is not bool:
|
|
115
|
+
raise approval.ApprovalError("INVALID_DELEGATION", "delegation must be an explicit boolean")
|
|
116
|
+
token = token or secrets.token_urlsafe(24)
|
|
117
|
+
if not approval.TOKEN_RE.fullmatch(token):
|
|
118
|
+
raise approval.ApprovalError("INVALID_TOKEN", "approval token has an invalid shape")
|
|
119
|
+
mode = "delegate-methods" if delegate_methods else "approve-only"
|
|
120
|
+
reply = f"approve-sprint {spec_id} {plan_id} {mode} {token}"
|
|
121
|
+
ph = hashlib.sha256(reply.encode("utf-8")).hexdigest()
|
|
122
|
+
context = client.call("sprint-approval-context", {"artifact_id": plan_id, "spec_id": spec_id, "delegate_methods": delegate_methods, "prompt_sha256": ph})
|
|
123
|
+
pending = {"format": FORMAT, "artifact_id": plan_id, "kind": "sprint-pair", "pair": context["pair"], "spec_context": context["spec_context"], "plan_context": context["plan_context"], "prompt_sha256": ph, "token_sha256": hashlib.sha256(token.encode("ascii")).hexdigest()}
|
|
124
|
+
pending["request_key"] = _hash({"pair": pending["pair"], "prompt_sha256": ph})
|
|
125
|
+
validate_pending(pending)
|
|
126
|
+
try:
|
|
127
|
+
approval._write_new(root, approval.PENDING, approval._canonical(pending))
|
|
128
|
+
except FileExistsError as exc:
|
|
129
|
+
raise approval.ApprovalError("PENDING_APPROVAL", "resolve the existing approval request first") from exc
|
|
130
|
+
try:
|
|
131
|
+
_artifactpromptlib.register(root, "approval", plan_id, reply)
|
|
132
|
+
except _artifactpromptlib.PromptRouteError:
|
|
133
|
+
(root / approval.PENDING).unlink()
|
|
134
|
+
raise
|
|
135
|
+
return {"spec_artifact_id": spec_id, "artifact_id": plan_id, "pair": pending["pair"], "reply": reply, "delegation": context.get("scope"), "approved": False}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _submission_path(pending: dict[str, Any]) -> Path:
|
|
139
|
+
return approval.PENDING.parent / ("sprint-submission-" + pending["request_key"] + ".json")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _base_request(pending: dict[str, Any]) -> dict[str, Any]:
|
|
143
|
+
pair = pending["pair"]
|
|
144
|
+
return {"artifact_id": pair["plan"]["artifact_id"], "spec_id": pair["spec"]["artifact_id"], "operation_id": "sprint-approval-" + pending["request_key"][:32], "expected": {k: pair["plan"][k] for k in ("revision", "model_sha256")}, "spec_expected": {k: pair["spec"][k] for k in ("revision", "model_sha256")}}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _submission(root: Path, pending: dict[str, Any]) -> dict[str, Any] | None:
|
|
148
|
+
try:
|
|
149
|
+
value = _read(root, _submission_path(pending))
|
|
150
|
+
except FileNotFoundError:
|
|
151
|
+
return None
|
|
152
|
+
if set(value) != {"format", "request_key", "request"} or value.get("format") != SUBMISSION_FORMAT or value.get("request_key") != pending["request_key"]:
|
|
153
|
+
raise approval.ApprovalError("INVALID_SUBMISSION", "submission belongs to a different armed pair")
|
|
154
|
+
request = value.get("request")
|
|
155
|
+
base = _base_request(pending)
|
|
156
|
+
if not isinstance(request, dict) or set(request) != set(base) | {"spec_source", "plan_source"} or any(request.get(k) != v for k, v in base.items()):
|
|
157
|
+
raise approval.ApprovalError("INVALID_SUBMISSION", "submission changed its frozen transaction")
|
|
158
|
+
for key in ("spec_source", "plan_source"):
|
|
159
|
+
source = request[key]
|
|
160
|
+
if (not isinstance(source, dict) or set(source) != {"source_ref", "source_sha256"}
|
|
161
|
+
or not _digest(source.get("source_sha256"))
|
|
162
|
+
or source.get("source_ref") != ".codearbiter/.artifacts/authority-sources/" + source["source_sha256"] + ".json"):
|
|
163
|
+
raise approval.ApprovalError("INVALID_SUBMISSION", "submission source locator is invalid")
|
|
164
|
+
return request
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _source(root: Path, pending: dict[str, Any], name: str, prompt: str, host: str, session_id: str) -> dict[str, str]:
|
|
168
|
+
locator = pending[name + "_context"]
|
|
169
|
+
context = _read(root, Path(locator["context_ref"]))
|
|
170
|
+
if (_hash(context) != locator["context_sha256"] or context.get("subject") != locator["subject"]
|
|
171
|
+
or context.get("prompt_sha256") != pending["prompt_sha256"]
|
|
172
|
+
or context.get("record") != {"sprint_pair": pending["pair"]}):
|
|
173
|
+
raise approval.ApprovalError("INVALID_AUTHORITY_SOURCE", "the armed engine context changed")
|
|
174
|
+
payload = {"sprint_pair": pending["pair"]}
|
|
175
|
+
producer = {"host": host, "session_id": session_id, "prompt_sha256": pending["prompt_sha256"]}
|
|
176
|
+
observed = {"format": "codearbiter.observation/0.2.0", "kind": "approval", "subject": context["subject"], "context_ref": locator["context_ref"], "context_sha256": locator["context_sha256"], "payload_sha256": _hash(payload), "producer_profile": "host-user-pair/0.1.0", "producer_run_id": f"{host}:{session_id}", "producer_result": producer, "producer_result_sha256": _hash(producer)}
|
|
177
|
+
oh = _hash(observed)
|
|
178
|
+
observation_ref = Path(".codearbiter/.artifacts/observations") / f"{oh}.json"
|
|
179
|
+
_artifactauthoritylib._publish_immutable(root, observation_ref, approval._canonical(observed))
|
|
180
|
+
event = {"format": "codearbiter.workflow-event/0.2.0", "kind": "approval", "authority_kind": "user_workflow", "subject": context["subject"], "actor": "interactive repository user", "origin": f"{host}:UserPromptSubmit:{session_id}", "verdict": "approved", "payload": payload, "source_text": prompt, "observation_ref": observation_ref.as_posix(), "observation_sha256": oh}
|
|
181
|
+
eh = _hash(event)
|
|
182
|
+
source_ref = Path(".codearbiter/.artifacts/authority-sources") / f"{eh}.json"
|
|
183
|
+
_artifactauthoritylib._publish_immutable(root, source_ref, approval._canonical(event))
|
|
184
|
+
return {"source_ref": source_ref.as_posix(), "source_sha256": eh}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _execute(root: Path, client: Any, pending: dict[str, Any], request: dict[str, Any]) -> dict[str, Any]:
|
|
188
|
+
try:
|
|
189
|
+
result = client.call("sprint-approve", request)
|
|
190
|
+
except _artifactlib.ArtifactError as exc:
|
|
191
|
+
if exc.code != "RECOVERY_REQUIRED":
|
|
192
|
+
raise
|
|
193
|
+
# Complete only the exact previously observed submission. A different
|
|
194
|
+
# pending transaction is not a reason to recover an unrelated operation.
|
|
195
|
+
client.call("recover", {"operation_id": request["operation_id"], "mode": "complete"})
|
|
196
|
+
result = client.call("sprint-approve", request)
|
|
197
|
+
if (result.get("authority_kind") != "user_workflow"
|
|
198
|
+
or result.get("spec", {}).get("artifact_id") != request["spec_id"]
|
|
199
|
+
or result.get("plan", {}).get("artifact_id") != request["artifact_id"]
|
|
200
|
+
or not isinstance(result.get("receipt"), str)):
|
|
201
|
+
raise approval.ApprovalError("INVALID_RECEIPT", "transaction returned an incomplete paired receipt")
|
|
202
|
+
# Remove the arm before the retained submission: a crash here must not
|
|
203
|
+
# leave an armed request whose only observed reply has already been erased.
|
|
204
|
+
if approval._load_pending(root) != pending:
|
|
205
|
+
raise approval.ApprovalError("PENDING_APPROVAL_MISMATCH", "pending pair changed during completion")
|
|
206
|
+
_artifactpromptlib.unregister(root, "approval", pending["artifact_id"])
|
|
207
|
+
(root / approval.PENDING).unlink()
|
|
208
|
+
(root / _submission_path(pending)).unlink(missing_ok=True)
|
|
209
|
+
return {"matched": True, "approved": True, "artifact_id": request["artifact_id"], "spec_artifact_id": request["spec_id"], "receipt": result["receipt"], "spec_receipt": result.get("spec_receipt"), "grant_receipt": result.get("grant_receipt"), "operation_id": request["operation_id"]}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@_serialized
|
|
213
|
+
def consume(root: Path, client: Any, pending: dict[str, Any], prompt: str, *, host: str, session_id: str) -> dict[str, Any]:
|
|
214
|
+
validate_pending(pending)
|
|
215
|
+
if approval._load_pending(root) != pending:
|
|
216
|
+
return {"matched": False, "approved": False}
|
|
217
|
+
parts = prompt.split(" ")
|
|
218
|
+
pair = pending["pair"]
|
|
219
|
+
mode = "delegate-methods" if pair["delegate_methods"] else "approve-only"
|
|
220
|
+
if len(parts) != 5 or parts[:4] != ["approve-sprint", pair["spec"]["artifact_id"], pair["plan"]["artifact_id"], mode]:
|
|
221
|
+
return {"matched": False, "approved": False}
|
|
222
|
+
if (not approval.TOKEN_RE.fullmatch(parts[4])
|
|
223
|
+
or not secrets.compare_digest(hashlib.sha256(parts[4].encode("ascii")).hexdigest(), pending["token_sha256"])
|
|
224
|
+
or not secrets.compare_digest(hashlib.sha256(prompt.encode("utf-8")).hexdigest(), pending["prompt_sha256"])):
|
|
225
|
+
return {"matched": False, "approved": False}
|
|
226
|
+
if not approval.HOST_RE.fullmatch(host) or not approval.SESSION_RE.fullmatch(session_id):
|
|
227
|
+
raise approval.ApprovalError("INVALID_HOST_CONTEXT", "host approval context is malformed")
|
|
228
|
+
request = _submission(root, pending)
|
|
229
|
+
if request is None:
|
|
230
|
+
for name in ("spec", "plan"):
|
|
231
|
+
current = approval._identity(client, pair[name]["artifact_id"])
|
|
232
|
+
if any(current.get(k) != v for k, v in pair[name].items()):
|
|
233
|
+
raise approval.ApprovalError("STALE_APPROVAL", "a paired artifact changed after review was requested")
|
|
234
|
+
request = _base_request(pending)
|
|
235
|
+
request["spec_source"] = _source(root, pending, "spec", prompt, host, session_id)
|
|
236
|
+
request["plan_source"] = _source(root, pending, "plan", prompt, host, session_id)
|
|
237
|
+
record = {"format": SUBMISSION_FORMAT, "request_key": pending["request_key"], "request": request}
|
|
238
|
+
try:
|
|
239
|
+
approval._write_new(root, _submission_path(pending), approval._canonical(record))
|
|
240
|
+
except FileExistsError:
|
|
241
|
+
# A concurrent genuine observer wins once. Replay its original
|
|
242
|
+
# sources rather than substituting a later session's metadata.
|
|
243
|
+
request = _submission(root, pending)
|
|
244
|
+
if request is None:
|
|
245
|
+
raise approval.ApprovalError("INVALID_SUBMISSION", "concurrent submission disappeared")
|
|
246
|
+
return _execute(root, client, pending, request)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@_serialized
|
|
250
|
+
def resume(root: str | Path, client: Any, plan_id: str) -> dict[str, Any]:
|
|
251
|
+
root = _artifactlib._trusted_directory(root, "UNSAFE_ROOT")
|
|
252
|
+
pending = approval._load_pending(root)
|
|
253
|
+
if pending is None or pending.get("format") != FORMAT or pending["artifact_id"] != plan_id:
|
|
254
|
+
raise approval.ApprovalError("NO_PENDING_APPROVAL", "no matching paired approval is pending")
|
|
255
|
+
request = _submission(root, pending)
|
|
256
|
+
if request is None:
|
|
257
|
+
raise approval.ApprovalError("NO_OBSERVED_APPROVAL", "arming alone is not a user reply; there is nothing to resume")
|
|
258
|
+
try:
|
|
259
|
+
return _execute(root, client, pending, request)
|
|
260
|
+
except _artifactlib.ArtifactError as exc:
|
|
261
|
+
if exc.code not in {"REVISION_CONFLICT", "OPERATION_ROLLED_BACK"}:
|
|
262
|
+
raise
|
|
263
|
+
# The engine checks journal replay before CAS. Only these closed-set
|
|
264
|
+
# outcomes establish that this exact old mutation did not commit.
|
|
265
|
+
if approval._load_pending(root) != pending:
|
|
266
|
+
raise approval.ApprovalError("PENDING_APPROVAL_MISMATCH", "pending request changed during failed-transaction recovery") from exc
|
|
267
|
+
_artifactpromptlib.unregister(root, "approval", pending["artifact_id"])
|
|
268
|
+
(root / approval.PENDING).unlink()
|
|
269
|
+
(root / _submission_path(pending)).unlink(missing_ok=True)
|
|
270
|
+
return {"matched": True, "approved": False, "artifact_id": plan_id,
|
|
271
|
+
"operation_id": request["operation_id"], "diagnostic": exc.code,
|
|
272
|
+
"reapproval_required": True}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@_serialized
|
|
276
|
+
def cancel(root: Path, pending: dict[str, Any]) -> dict[str, Any]:
|
|
277
|
+
if approval._load_pending(root) != pending:
|
|
278
|
+
raise approval.ApprovalError("PENDING_APPROVAL_MISMATCH", "pending pair changed before cancellation")
|
|
279
|
+
if _submission(root, pending) is not None:
|
|
280
|
+
raise approval.ApprovalError("SUBMITTED_APPROVAL", "recover the observed transaction before cancelling its pending state")
|
|
281
|
+
_artifactpromptlib.unregister(root, "approval", pending["artifact_id"])
|
|
282
|
+
(root / approval.PENDING).unlink()
|
|
283
|
+
return {"artifact_id": pending["artifact_id"], "cancelled": True}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def apply_decision(client: Any, request_file: str | Path) -> dict[str, Any]:
|
|
287
|
+
"""Read a bounded inert request; only the native closed producer grants authority."""
|
|
288
|
+
path = Path(request_file)
|
|
289
|
+
fd = _artifactlib._open_pinned_regular(path)
|
|
290
|
+
try:
|
|
291
|
+
info = os.fstat(fd)
|
|
292
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > 65536:
|
|
293
|
+
raise approval.ApprovalError("INVALID_SMARTS", "decision request must be bounded regular data")
|
|
294
|
+
data = os.read(fd, 65537)
|
|
295
|
+
if len(data) > 65536:
|
|
296
|
+
raise approval.ApprovalError("INVALID_SMARTS", "decision request exceeds the context budget")
|
|
297
|
+
finally:
|
|
298
|
+
os.close(fd)
|
|
299
|
+
request = _artifactlib._decode(data)
|
|
300
|
+
if not isinstance(request, dict):
|
|
301
|
+
raise approval.ApprovalError("INVALID_SMARTS", "decision request must be a JSON object")
|
|
302
|
+
return client.call("smarts-apply", request)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter — Codex hook bridge for independent artifact reviewers.
|
|
3
|
+
"""Correlate closed production review requests with documented Codex hooks."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
10
|
+
import hostapi # noqa: E402
|
|
11
|
+
import _artifactauthoritylib # noqa: E402
|
|
12
|
+
import _hooklib # noqa: E402
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
_hooklib.utf8_stdio()
|
|
17
|
+
payload = _hooklib.read_input()
|
|
18
|
+
if not isinstance(payload, dict):
|
|
19
|
+
return 0
|
|
20
|
+
event = payload.get("hook_event_name")
|
|
21
|
+
if event not in {"PreToolUse", "PostToolUse", "SubagentStart", "SubagentStop"}:
|
|
22
|
+
return 0
|
|
23
|
+
tool = payload.get("tool_name")
|
|
24
|
+
root = _hooklib.project_root(payload)
|
|
25
|
+
try:
|
|
26
|
+
if event in {"PreToolUse", "PostToolUse"} and tool != "spawn_agent":
|
|
27
|
+
result = _artifactauthoritylib.observe_verifier_hook(root, payload)
|
|
28
|
+
if result is None:
|
|
29
|
+
return 0
|
|
30
|
+
else:
|
|
31
|
+
result = _artifactauthoritylib.observe_codex_hook(root, payload)
|
|
32
|
+
if result is None:
|
|
33
|
+
return 0
|
|
34
|
+
except _artifactauthoritylib.AuthorityError as exc:
|
|
35
|
+
reason = f"codeArbiter authority producer blocked: {exc}"
|
|
36
|
+
if event == "PreToolUse":
|
|
37
|
+
print(json.dumps({"decision": "block", "reason": reason}))
|
|
38
|
+
else:
|
|
39
|
+
sys.stderr.write(reason + "\n")
|
|
40
|
+
return 0
|
|
41
|
+
if event == "PreToolUse" and result.get("state") == "AUTHORIZED":
|
|
42
|
+
print(json.dumps({
|
|
43
|
+
"decision": "allow",
|
|
44
|
+
"reason": (
|
|
45
|
+
"codeArbiter authorized this wrapper for the exact resolved child set: "
|
|
46
|
+
+ json.dumps(result["command_bindings"], sort_keys=True)
|
|
47
|
+
),
|
|
48
|
+
}))
|
|
49
|
+
if event == "SubagentStop" and result.get("state") == "COMPLETED":
|
|
50
|
+
print(json.dumps({
|
|
51
|
+
"continue": True,
|
|
52
|
+
"hookSpecificOutput": {
|
|
53
|
+
"hookEventName": "SubagentStop",
|
|
54
|
+
"additionalContext": (
|
|
55
|
+
"codeArbiter: independent review observation recorded for request "
|
|
56
|
+
+ result["request_id"]
|
|
57
|
+
+ "; publish it through the production authority adapter."
|
|
58
|
+
),
|
|
59
|
+
},
|
|
60
|
+
}))
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run(host) -> int:
|
|
65
|
+
_hooklib.set_host(host)
|
|
66
|
+
if host.name != "codex":
|
|
67
|
+
return 0
|
|
68
|
+
return main()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
raise SystemExit(run(hostapi.load_host()))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter — thin internal CLI for production artifact authority.
|
|
3
|
+
"""Arm, execute, and publish closed authority requests; not a public command."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
12
|
+
import _artifactauthoritylib # noqa: E402
|
|
13
|
+
import _artifactlib # noqa: E402
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _workspace_map(values: list[str]) -> dict[str, Path]:
|
|
17
|
+
result = {}
|
|
18
|
+
for value in values:
|
|
19
|
+
if "=" not in value:
|
|
20
|
+
raise argparse.ArgumentTypeError("workspace must be LABEL=PATH")
|
|
21
|
+
label, raw = value.split("=", 1)
|
|
22
|
+
if not label or not raw or label in result:
|
|
23
|
+
raise argparse.ArgumentTypeError("workspace labels and paths must be unique")
|
|
24
|
+
result[label] = Path(raw)
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(argv=None) -> int:
|
|
29
|
+
parser = argparse.ArgumentParser()
|
|
30
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
31
|
+
arm = sub.add_parser("arm")
|
|
32
|
+
arm.add_argument("--root", required=True)
|
|
33
|
+
arm.add_argument("--artifact-id", required=True)
|
|
34
|
+
arm.add_argument("--record-id", required=True)
|
|
35
|
+
arm.add_argument(
|
|
36
|
+
"--activity", required=True,
|
|
37
|
+
choices=("verification", "spec_review", "quality_review"),
|
|
38
|
+
)
|
|
39
|
+
arm.add_argument("--workspace", action="append", default=[], metavar="LABEL=PATH")
|
|
40
|
+
verify = sub.add_parser("verify")
|
|
41
|
+
verify.add_argument("--root", required=True)
|
|
42
|
+
verify.add_argument("--request-id", required=True)
|
|
43
|
+
publish = sub.add_parser("publish")
|
|
44
|
+
publish.add_argument("--root", required=True)
|
|
45
|
+
publish.add_argument("--request-id", required=True)
|
|
46
|
+
recover = sub.add_parser("recover")
|
|
47
|
+
recover.add_argument("--root", required=True)
|
|
48
|
+
recover.add_argument("--request-id", required=True)
|
|
49
|
+
recover.add_argument("--disposition", required=True, choices=("failed", "abandoned"))
|
|
50
|
+
args = parser.parse_args(argv)
|
|
51
|
+
root = Path(args.root).resolve(strict=True)
|
|
52
|
+
client = _artifactlib.ArtifactClient(
|
|
53
|
+
root, _artifactlib.helper_installation(__file__)
|
|
54
|
+
)
|
|
55
|
+
if args.command == "arm":
|
|
56
|
+
workspaces = _workspace_map(args.workspace)
|
|
57
|
+
result = _artifactauthoritylib.arm_request(
|
|
58
|
+
root, client, args.artifact_id, args.record_id, args.activity,
|
|
59
|
+
workspace_roots=workspaces or None,
|
|
60
|
+
)
|
|
61
|
+
elif args.command == "verify":
|
|
62
|
+
result = _artifactauthoritylib.run_verification(
|
|
63
|
+
root, client, args.request_id,
|
|
64
|
+
)
|
|
65
|
+
elif args.command == "publish":
|
|
66
|
+
result = _artifactauthoritylib.publish_request(root, client, args.request_id)
|
|
67
|
+
else:
|
|
68
|
+
result = _artifactauthoritylib.recover_request(
|
|
69
|
+
root, args.request_id, args.disposition
|
|
70
|
+
)
|
|
71
|
+
print(json.dumps(result, sort_keys=True))
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
try:
|
|
77
|
+
raise SystemExit(main())
|
|
78
|
+
except (_artifactauthoritylib.AuthorityError, _artifactlib.ArtifactError) as exc:
|
|
79
|
+
sys.stderr.write(str(exc) + "\n")
|
|
80
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Arm an exact user-workflow reconciliation request."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
11
|
+
import _artifactlib
|
|
12
|
+
import _reconciliationlib
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv=None):
|
|
16
|
+
parser = argparse.ArgumentParser()
|
|
17
|
+
parser.add_argument("--root", required=True)
|
|
18
|
+
parser.add_argument("--artifact-id", required=True)
|
|
19
|
+
parser.add_argument("--target-id")
|
|
20
|
+
parser.add_argument("--operation", choices=("task-reconcile", "scope-reconcile"))
|
|
21
|
+
parser.add_argument("--target-state", choices=("PENDING", "REVIEW", "BLOCKED"))
|
|
22
|
+
parser.add_argument("--reason", default="")
|
|
23
|
+
parser.add_argument("--assessment")
|
|
24
|
+
parser.add_argument("--cancel", action="store_true", help="cancel an armed request before its mutation attempt")
|
|
25
|
+
parser.add_argument("--cancel-orphan", action="store_true", help="cancel a pending request whose prompt route was never registered")
|
|
26
|
+
parser.add_argument("--recover", action="store_true", help="replay an in-flight mutation before clearing its marker")
|
|
27
|
+
parser.add_argument("--prompt", help="exact armed prompt required for --cancel")
|
|
28
|
+
args = parser.parse_args(argv)
|
|
29
|
+
root = Path(args.root).resolve(strict=True)
|
|
30
|
+
if args.cancel_orphan:
|
|
31
|
+
if args.cancel or args.recover or args.prompt or args.target_id or args.operation or args.target_state or args.assessment:
|
|
32
|
+
parser.error("--cancel-orphan excludes arming and exact-prompt cancellation fields")
|
|
33
|
+
print(json.dumps(_reconciliationlib.cancel_orphaned_reconciliation(root, args.artifact_id), sort_keys=True))
|
|
34
|
+
return 0
|
|
35
|
+
if args.cancel:
|
|
36
|
+
if args.recover or args.prompt is None or args.target_id or args.operation or args.target_state or args.assessment:
|
|
37
|
+
parser.error("--cancel requires --prompt and excludes arming fields")
|
|
38
|
+
print(json.dumps(_reconciliationlib.cancel_reconciliation(root, args.artifact_id, args.prompt), sort_keys=True))
|
|
39
|
+
return 0
|
|
40
|
+
if args.recover:
|
|
41
|
+
if args.prompt or args.target_id or args.operation or args.target_state or args.assessment:
|
|
42
|
+
parser.error("--recover excludes arming and cancellation fields")
|
|
43
|
+
client = _artifactlib.ArtifactClient(root, _artifactlib.helper_installation(__file__))
|
|
44
|
+
print(json.dumps(_reconciliationlib.recover_reconciliation(root, client, args.artifact_id), sort_keys=True))
|
|
45
|
+
return 0
|
|
46
|
+
if not args.target_id or not args.operation or not args.assessment or args.prompt is not None:
|
|
47
|
+
parser.error("arming requires --target-id, --operation, and --assessment, without --prompt")
|
|
48
|
+
client = _artifactlib.ArtifactClient(root, _artifactlib.helper_installation(__file__))
|
|
49
|
+
print(json.dumps(_reconciliationlib.arm_reconciliation(
|
|
50
|
+
root, client, args.artifact_id, args.target_id, args.operation,
|
|
51
|
+
target_state=args.target_state, reason=args.reason, assessment=args.assessment,
|
|
52
|
+
), sort_keys=True))
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
raise SystemExit(main())
|
|
@@ -376,7 +376,9 @@ def check_git_hook_freshness(root):
|
|
|
376
376
|
if "ignored because" in (probe.stderr or "").lower():
|
|
377
377
|
fail(f"git-hook backstop live-fire was ignored by Git: {probe.stderr.strip()[:240]}")
|
|
378
378
|
return
|
|
379
|
-
ok("git-hook backstop live-fire: selected Git executed the managed
|
|
379
|
+
ok("git-hook backstop live-fire: this host's selected Git executed the managed "
|
|
380
|
+
"pre-push shim (proves THIS host only — a sibling host sharing the registry "
|
|
381
|
+
"is not covered by this result, B3/#684)")
|
|
380
382
|
stale = _githooks.stale_registered_plugins(dropin_dir)
|
|
381
383
|
if stale:
|
|
382
384
|
names = ", ".join(sorted(stale))
|
|
@@ -313,7 +313,7 @@ class Host:
|
|
|
313
313
|
|
|
314
314
|
name = "claude"
|
|
315
315
|
adapter_name = "ca"
|
|
316
|
-
adapter_version = "2.21.
|
|
316
|
+
adapter_version = "2.21.12"
|
|
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,
|
|
@@ -68,6 +68,7 @@ import _approvallib # noqa: E402 — host-owned artifact approval capture
|
|
|
68
68
|
import _hooklib # noqa: E402
|
|
69
69
|
import _modelib # noqa: E402 — mode plane core (T-06..T-16, Lane A)
|
|
70
70
|
import _prerequisitelib # noqa: E402 — host-owned prerequisite capture
|
|
71
|
+
import _reconciliationlib # noqa: E402 — host-owned reconciliation capture
|
|
71
72
|
import _readinjectlib # noqa: E402 — marker_path(prefix=) (T-30)
|
|
72
73
|
|
|
73
74
|
|
|
@@ -449,9 +450,8 @@ def _handle_codex(payload, host, root, session_id, token, approval_context=""):
|
|
|
449
450
|
def _handle_user_prompt_submit(payload, host):
|
|
450
451
|
session_id = payload.get("session_id") or ""
|
|
451
452
|
root = _hooklib.project_root(payload)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
state_root = _hooklib.marker_root(payload)
|
|
453
|
+
active = _hooklib.arbiter_active(root)
|
|
454
|
+
state_root = _hooklib.marker_root(payload) if active else root
|
|
455
455
|
prompt = payload.get("prompt")
|
|
456
456
|
prompt = prompt if isinstance(prompt, str) else ""
|
|
457
457
|
token = _modelib.match_mode_token(prompt)
|
|
@@ -476,7 +476,24 @@ def _handle_user_prompt_submit(payload, host):
|
|
|
476
476
|
)
|
|
477
477
|
if prerequisite:
|
|
478
478
|
contexts.append(prerequisite)
|
|
479
|
+
reconciliation = _reconciliationlib.consume_from_hook(
|
|
480
|
+
root=root,
|
|
481
|
+
plugin_root=host.plugin_root(),
|
|
482
|
+
prompt=prompt,
|
|
483
|
+
host=host.name,
|
|
484
|
+
session_id=session_id,
|
|
485
|
+
)
|
|
486
|
+
if reconciliation:
|
|
487
|
+
contexts.append(reconciliation)
|
|
479
488
|
approval_context = "\n".join(contexts)
|
|
489
|
+
if not active:
|
|
490
|
+
if not approval_context:
|
|
491
|
+
return 0
|
|
492
|
+
if host.name == "codex":
|
|
493
|
+
print(json.dumps(_codex_allow_envelope(approval_context)))
|
|
494
|
+
else:
|
|
495
|
+
print(approval_context)
|
|
496
|
+
return 0
|
|
480
497
|
if host.name == "codex":
|
|
481
498
|
return _handle_codex(
|
|
482
499
|
payload, host, state_root, session_id, token, approval_context
|