@arbiterforge/ca-pi 0.15.10 → 0.16.0
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 +1 -1
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +45 -0
- package/plugins/ca-pi/SKILLS.md +1 -1
- package/plugins/ca-pi/agents/infra-author.md +7 -1
- package/plugins/ca-pi/agents/scout.md +40 -8
- package/plugins/ca-pi/generated/command-catalog.json +1 -1
- 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 +104 -6
- package/plugins/ca-pi/hooks/_activationlib.py +97 -13
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +7 -2
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +519 -16
- package/plugins/ca-pi/hooks/_artifactlib.py +244 -7
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
- package/plugins/ca-pi/hooks/_bashguardlib.py +23 -8
- package/plugins/ca-pi/hooks/_contextreportlib.py +564 -6
- package/plugins/ca-pi/hooks/_contextselectlib.py +464 -0
- package/plugins/ca-pi/hooks/_contextsnapshotlib.py +999 -0
- package/plugins/ca-pi/hooks/_debughandofflib.py +767 -0
- package/plugins/ca-pi/hooks/_hooklib.py +2 -1
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_intentlib.py +5 -5
- package/plugins/ca-pi/hooks/_protectedlib.py +3 -3
- package/plugins/ca-pi/hooks/_protectedstatelib.py +152 -2
- package/plugins/ca-pi/hooks/_provenancelib.py +604 -34
- package/plugins/ca-pi/hooks/_readinjectlib.py +131 -1
- package/plugins/ca-pi/hooks/_sensitivelib.py +28 -14
- package/plugins/ca-pi/hooks/artifact-authority-hook.py +1 -0
- package/plugins/ca-pi/hooks/artifact-authority.py +25 -1
- package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
- package/plugins/ca-pi/hooks/doctor.py +4 -2
- package/plugins/ca-pi/hooks/git-enforce.py +7 -6
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
- package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
- package/plugins/ca-pi/hooks/session-start.py +26 -7
- package/plugins/ca-pi/includes/artifacts.md +41 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
- package/plugins/ca-pi/includes/helper-invocation.md +128 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
- package/plugins/ca-pi/includes/routing-table.md +1 -1
- package/plugins/ca-pi/includes/security-gate-record.md +5 -3
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +1 -1
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
- package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
- package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
- package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
- package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
- package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
- package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
- package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +55 -8
|
@@ -123,6 +123,8 @@ import json
|
|
|
123
123
|
import math
|
|
124
124
|
import os
|
|
125
125
|
import re
|
|
126
|
+
import tempfile
|
|
127
|
+
import time
|
|
126
128
|
import unicodedata
|
|
127
129
|
|
|
128
130
|
import _provenancelib
|
|
@@ -791,11 +793,23 @@ def governing_docs(rel, index, runner=None):
|
|
|
791
793
|
# paths. This is git-free. A non-provenance Read exits here with zero
|
|
792
794
|
# git calls.
|
|
793
795
|
entry_paths = set()
|
|
796
|
+
v2_docs = []
|
|
794
797
|
if isinstance(provenance, dict):
|
|
795
|
-
for record in provenance.
|
|
798
|
+
for doc_name, record in provenance.items():
|
|
796
799
|
try:
|
|
797
800
|
if not isinstance(record, dict):
|
|
798
801
|
continue
|
|
802
|
+
if record.get("schema") == _provenancelib.V2_SCHEMA_VERSION:
|
|
803
|
+
if not _provenancelib.valid_provenance_record(record):
|
|
804
|
+
continue
|
|
805
|
+
cited = any(
|
|
806
|
+
evidence.get("kind") == "content" and evidence.get("path") == rel
|
|
807
|
+
for field in record["fields"] for claim in field["claims"]
|
|
808
|
+
for evidence in claim["evidence"]
|
|
809
|
+
)
|
|
810
|
+
if cited or record["document"]["path"] == rel:
|
|
811
|
+
v2_docs.append(doc_name)
|
|
812
|
+
continue
|
|
799
813
|
entries = record.get("entries")
|
|
800
814
|
if not isinstance(entries, list):
|
|
801
815
|
continue
|
|
@@ -810,6 +824,13 @@ def governing_docs(rel, index, runner=None):
|
|
|
810
824
|
except Exception: # noqa: BLE001
|
|
811
825
|
continue
|
|
812
826
|
|
|
827
|
+
# A v2 content dependency is relevant advisory context, but its raw
|
|
828
|
+
# digest and semantic review cannot be proved by the legacy Git-hash
|
|
829
|
+
# gate. Point to the action-boundary audit without claiming freshness.
|
|
830
|
+
for doc_name in v2_docs:
|
|
831
|
+
result.append({"text": "{}.md context identity and semantic review unverified here -- run context-check".format(
|
|
832
|
+
_strip_context_controls(doc_name)), "tier": "standards"})
|
|
833
|
+
|
|
813
834
|
# Step B: only call batch_hash when rel is a known provenance entry path.
|
|
814
835
|
# Absence from entry_paths → SKIP; zero git calls.
|
|
815
836
|
if rel and rel in entry_paths:
|
|
@@ -1166,6 +1187,104 @@ def spec_pointers(rel, index):
|
|
|
1166
1187
|
|
|
1167
1188
|
_artifact_legacy_compute_injection = compute_injection
|
|
1168
1189
|
|
|
1190
|
+
def _session_context_generation_path(root, session_id):
|
|
1191
|
+
# One bounded value per session avoids a shared unbounded session map and
|
|
1192
|
+
# lets unrelated sessions advance without a read/modify/write race.
|
|
1193
|
+
return marker_path(root, session_id, 'compaction-generation',
|
|
1194
|
+
prefix='readinject-context-')
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _linklike(path):
|
|
1198
|
+
return os.path.islink(path) or getattr(os.path, 'isjunction', lambda _: False)(path)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def _safe_context_generation_path(root, session_id):
|
|
1202
|
+
if not isinstance(root, (str, os.PathLike)) or not os.path.isabs(root):
|
|
1203
|
+
return None
|
|
1204
|
+
path = _session_context_generation_path(root, session_id)
|
|
1205
|
+
parent = os.path.dirname(path)
|
|
1206
|
+
if (_linklike(root) or _linklike(os.path.join(root, '.codearbiter')) or _linklike(parent)
|
|
1207
|
+
or _linklike(path) or _linklike(path + '.lock')):
|
|
1208
|
+
return None
|
|
1209
|
+
return path
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def read_context_generation(root, session_id):
|
|
1213
|
+
"""Read a bounded worktree-local epoch; None means corrupt/unsafe/unknown."""
|
|
1214
|
+
try:
|
|
1215
|
+
path = _safe_context_generation_path(root, session_id)
|
|
1216
|
+
if path is None:
|
|
1217
|
+
return None
|
|
1218
|
+
with open(path, 'rb') as stream:
|
|
1219
|
+
raw = stream.read(257)
|
|
1220
|
+
if len(raw) > 256 or not raw or not raw.isdigit():
|
|
1221
|
+
return None
|
|
1222
|
+
value = int(raw)
|
|
1223
|
+
return value if value <= 1_000_000_000 else None
|
|
1224
|
+
except FileNotFoundError:
|
|
1225
|
+
return 0
|
|
1226
|
+
except Exception: # noqa: BLE001 - advisory hook never blocks a read
|
|
1227
|
+
return None
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def bump_context_generation(root, session_id):
|
|
1231
|
+
"""Invalidate surviving read markers after compaction, without a scan."""
|
|
1232
|
+
if not session_id:
|
|
1233
|
+
return False
|
|
1234
|
+
lock = None
|
|
1235
|
+
lock_acquired = False
|
|
1236
|
+
temporary = None
|
|
1237
|
+
try:
|
|
1238
|
+
path = _safe_context_generation_path(root, session_id)
|
|
1239
|
+
if path is None:
|
|
1240
|
+
return False
|
|
1241
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
1242
|
+
if _safe_context_generation_path(root, session_id) != path:
|
|
1243
|
+
return False
|
|
1244
|
+
lock = path + '.lock'
|
|
1245
|
+
for _ in range(25):
|
|
1246
|
+
try:
|
|
1247
|
+
os.mkdir(lock)
|
|
1248
|
+
lock_acquired = True
|
|
1249
|
+
break
|
|
1250
|
+
except FileExistsError:
|
|
1251
|
+
time.sleep(0.01)
|
|
1252
|
+
else:
|
|
1253
|
+
return False
|
|
1254
|
+
current = read_context_generation(root, session_id)
|
|
1255
|
+
if current is None or current >= 1_000_000_000:
|
|
1256
|
+
return False # Corrupt bytes stay intact and force redelivery.
|
|
1257
|
+
fd, temporary = tempfile.mkstemp(prefix='readinject-context-',
|
|
1258
|
+
dir=os.path.dirname(path))
|
|
1259
|
+
with os.fdopen(fd, 'wb') as stream:
|
|
1260
|
+
stream.write(str(current + 1).encode('ascii'))
|
|
1261
|
+
if _safe_context_generation_path(root, session_id) != path:
|
|
1262
|
+
return False
|
|
1263
|
+
os.replace(temporary, path)
|
|
1264
|
+
temporary = None
|
|
1265
|
+
return True
|
|
1266
|
+
except Exception: # noqa: BLE001 - task-time delivery remains the primary route
|
|
1267
|
+
return False
|
|
1268
|
+
finally:
|
|
1269
|
+
if temporary is not None:
|
|
1270
|
+
try:
|
|
1271
|
+
os.unlink(temporary)
|
|
1272
|
+
except OSError:
|
|
1273
|
+
pass
|
|
1274
|
+
if lock_acquired:
|
|
1275
|
+
try:
|
|
1276
|
+
os.rmdir(lock)
|
|
1277
|
+
except OSError:
|
|
1278
|
+
pass
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def _context_epoch_session(root, session_id):
|
|
1282
|
+
generation = read_context_generation(root, session_id)
|
|
1283
|
+
if generation is None:
|
|
1284
|
+
return None
|
|
1285
|
+
return (str(session_id) + '|context:' + str(generation)
|
|
1286
|
+
if generation else session_id)
|
|
1287
|
+
|
|
1169
1288
|
|
|
1170
1289
|
def compute_injection(root, session_id, rel, runner=None):
|
|
1171
1290
|
# Bind the injected pointers and dedup epoch to ONE validated HTML snapshot.
|
|
@@ -1173,8 +1292,19 @@ def compute_injection(root, session_id, rel, runner=None):
|
|
|
1173
1292
|
# under B's marker, suppressing a later stable B read. HTML therefore builds
|
|
1174
1293
|
# once before dedup; the same entries drive both context and epoch. This is
|
|
1175
1294
|
# advisory identity only, never approval or execution authority.
|
|
1295
|
+
# Preserve the legacy self-read zero-I/O path before any epoch lookup.
|
|
1176
1296
|
if not isinstance(rel, str) or rel.replace("\\", "/").split("/", 1)[0] == ".codearbiter":
|
|
1177
1297
|
return _artifact_legacy_compute_injection(root, session_id, rel, runner)
|
|
1298
|
+
session_id = _context_epoch_session(root, session_id)
|
|
1299
|
+
if session_id is None:
|
|
1300
|
+
# Unknown/corrupt epoch: bounded advisory redelivery without writing
|
|
1301
|
+
# a fresh marker for every read. The actor delivery planner owns its
|
|
1302
|
+
# separate two-attempt accounting and action gap.
|
|
1303
|
+
try:
|
|
1304
|
+
index = build_index(root)
|
|
1305
|
+
return assemble_context(governing_docs(rel, index, runner), budget=150)
|
|
1306
|
+
except Exception: # noqa: BLE001
|
|
1307
|
+
return ""
|
|
1178
1308
|
try:
|
|
1179
1309
|
from _artifactlib import has_html
|
|
1180
1310
|
if not has_html(root):
|
|
@@ -281,7 +281,7 @@ def sensitive_scan_added_lines(diff_text):
|
|
|
281
281
|
# bindings use a separate marker namespace; a user-authored source string or
|
|
282
282
|
# an old line-only approval cannot impersonate one of these records.
|
|
283
283
|
# SecurityScan(crypto, digests): classification and exact approval identities.
|
|
284
|
-
# security_scan_lines(lines) -> SecurityScan|None: line-bound scan or refusal.
|
|
284
|
+
# security_scan_lines(lines, path=None) -> SecurityScan|None: line-bound scan or refusal.
|
|
285
285
|
# security_scan_source(path, source, added_rows) -> SecurityScan|None.
|
|
286
286
|
# security_scan_diff(text) -> SecurityScan|None: None means untrusted context.
|
|
287
287
|
SecurityScan = namedtuple("SecurityScan", "crypto digests")
|
|
@@ -320,20 +320,23 @@ def _cross_line_crypto_spans(lines):
|
|
|
320
320
|
return spans
|
|
321
321
|
|
|
322
322
|
|
|
323
|
-
def _scan_bound_lines(lines, contextual_bindings):
|
|
323
|
+
def _scan_bound_lines(lines, contextual_bindings, path=None):
|
|
324
324
|
"""Each cross-line match needs its own recognized contextual identity."""
|
|
325
325
|
spans = _cross_line_crypto_spans(lines)
|
|
326
326
|
if not spans <= contextual_bindings.keys():
|
|
327
327
|
return None
|
|
328
328
|
return SecurityScan(bool(spans) or any(CRYPTO_RE.search(line) for line in lines), {
|
|
329
|
-
line_digest(line) for line in lines
|
|
329
|
+
line_digest(line, path) for line in lines
|
|
330
330
|
if CRYPTO_RE.search(line) or SECRET_RE.search(line)
|
|
331
331
|
} | set(contextual_bindings.values()))
|
|
332
332
|
|
|
333
333
|
|
|
334
|
-
def security_scan_lines(lines):
|
|
335
|
-
"""Preserve same-line bindings; refuse crypto spanning unbound lines.
|
|
336
|
-
|
|
334
|
+
def security_scan_lines(lines, path=None):
|
|
335
|
+
"""Preserve same-line bindings; refuse crypto spanning unbound lines.
|
|
336
|
+
|
|
337
|
+
`path` binds each approval to the file the line lives in, so a reviewed
|
|
338
|
+
line cannot be admitted again after moving to a different file."""
|
|
339
|
+
return _scan_bound_lines(lines, {}, path)
|
|
337
340
|
|
|
338
341
|
|
|
339
342
|
def security_scan_source(path, source, added_rows):
|
|
@@ -348,7 +351,7 @@ def security_scan_source(path, source, added_rows):
|
|
|
348
351
|
if any(row < 1 or row > len(lines) for row in added_rows):
|
|
349
352
|
return None
|
|
350
353
|
added = [lines[row - 1] for row in added_rows]
|
|
351
|
-
result = security_scan_lines(added)
|
|
354
|
+
result = security_scan_lines(added, path)
|
|
352
355
|
candidates = {row for row in added_rows
|
|
353
356
|
if _is_context_item_line(lines[row - 1])}
|
|
354
357
|
if not candidates:
|
|
@@ -405,7 +408,7 @@ def security_scan_source(path, source, added_rows):
|
|
|
405
408
|
bindings = {(positions[first[0]], first[1], positions[last[0]], last[1]): contexts[last[0]]
|
|
406
409
|
for first, last in import_spans
|
|
407
410
|
if first[0] in positions and last[0] in positions}
|
|
408
|
-
result = _scan_bound_lines(added, bindings)
|
|
411
|
+
result = _scan_bound_lines(added, bindings, path)
|
|
409
412
|
if result is None:
|
|
410
413
|
return None
|
|
411
414
|
return SecurityScan(result.crypto or bool(contexts), result.digests | set(contexts.values()))
|
|
@@ -484,7 +487,9 @@ def security_scan_diff(diff_text):
|
|
|
484
487
|
# in the legacy permissive walker before context validation even runs.
|
|
485
488
|
if not any(line.startswith("+") and _is_context_item_line(line[1:])
|
|
486
489
|
for line in section):
|
|
487
|
-
|
|
490
|
+
# One file per section: bind its approvals to that file's path.
|
|
491
|
+
paths = {path for path, _ in attributed if path}
|
|
492
|
+
contextual = security_scan_lines(added, paths.pop() if len(paths) == 1 else None)
|
|
488
493
|
else:
|
|
489
494
|
if not section[0].startswith("diff --git "):
|
|
490
495
|
return None
|
|
@@ -508,14 +513,23 @@ def security_scan_diff(diff_text):
|
|
|
508
513
|
return result
|
|
509
514
|
|
|
510
515
|
|
|
511
|
-
def line_digest(line):
|
|
516
|
+
def line_digest(line, path=None):
|
|
512
517
|
"""Digest of one added diff line, for the security-gate binding
|
|
513
518
|
(H-09b/H-10b). The gate-pass marker stores these digests instead of being
|
|
514
519
|
an empty `touch`d file, so a recorded pass admits only the exact sensitive
|
|
515
|
-
lines it reviewed
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
520
|
+
lines it reviewed. A pass has no time window, so the digest also binds the
|
|
521
|
+
repository-relative `path` the line was reviewed in: the identical line in
|
|
522
|
+
a different file is a different digest and needs its own review. Without
|
|
523
|
+
a path (an unattributable diff section) both the recording and the
|
|
524
|
+
checking side fall back to the line alone. Trailing whitespace is
|
|
525
|
+
stripped so CRLF translation between worktree and index never breaks the
|
|
526
|
+
match."""
|
|
527
|
+
text = line.rstrip()
|
|
528
|
+
if path is None:
|
|
529
|
+
return hashlib.sha256(text.encode("utf-8", "replace")).hexdigest()
|
|
530
|
+
return "path-v1:" + hashlib.sha256(json.dumps(
|
|
531
|
+
[path, text], separators=(",", ":"), ensure_ascii=False
|
|
532
|
+
).encode("utf-8", "replace")).hexdigest()
|
|
519
533
|
|
|
520
534
|
|
|
521
535
|
def content_digest(text):
|
|
@@ -64,6 +64,7 @@ def claude_main() -> int:
|
|
|
64
64
|
_artifactauthoritylib.observe_verifier_hook(root, payload, host="claude")
|
|
65
65
|
elif (
|
|
66
66
|
(event in {"PreToolUse", "PostToolUse"} and tool == "Agent")
|
|
67
|
+
or (event == "PostToolUse" and tool == _artifactauthoritylib.CLAUDE_HANDBACK_TOOL)
|
|
67
68
|
or (event == "PreToolUse" and tool == "SendMessage")
|
|
68
69
|
or event in {"SubagentStart", "SubagentStop"}
|
|
69
70
|
):
|
|
@@ -11,6 +11,7 @@ import sys
|
|
|
11
11
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
12
12
|
import _artifactauthoritylib # noqa: E402
|
|
13
13
|
import _artifactlib # noqa: E402
|
|
14
|
+
import _artifactpromptlib # noqa: E402
|
|
14
15
|
import hostapi # noqa: E402
|
|
15
16
|
|
|
16
17
|
|
|
@@ -38,6 +39,10 @@ def main(argv=None) -> int:
|
|
|
38
39
|
choices=("verification", "spec_review", "quality_review"),
|
|
39
40
|
)
|
|
40
41
|
arm.add_argument("--workspace", action="append", default=[], metavar="LABEL=PATH")
|
|
42
|
+
arm.add_argument("--completion-receipt", action="append", metavar="RECEIPT_REF",
|
|
43
|
+
help="exact published verification receipt selected for completion review; repeat per task")
|
|
44
|
+
arm.add_argument("--supporting-file", action="append", metavar="ABSOLUTE_PATH",
|
|
45
|
+
help="explicit bounded regular file retained as completion review data")
|
|
41
46
|
arm.add_argument(
|
|
42
47
|
"--host", choices=sorted(_artifactauthoritylib.HOSTS), default=None,
|
|
43
48
|
help="observing host; defaults to the host this package was built for",
|
|
@@ -50,6 +55,15 @@ def main(argv=None) -> int:
|
|
|
50
55
|
"--codex-review-profile", choices=("native-v1",), default=None,
|
|
51
56
|
help="select only after qualifying the actual registered native V1 UUID interface",
|
|
52
57
|
)
|
|
58
|
+
context_arm = sub.add_parser("arm-context")
|
|
59
|
+
context_arm.add_argument("--root", required=True)
|
|
60
|
+
context_arm.add_argument("--request-file", required=True)
|
|
61
|
+
context_cancel = sub.add_parser("cancel-context")
|
|
62
|
+
context_cancel.add_argument("--root", required=True)
|
|
63
|
+
context_cancel.add_argument("--document-id", required=True)
|
|
64
|
+
context_apply = sub.add_parser("apply-context")
|
|
65
|
+
context_apply.add_argument("--root", required=True)
|
|
66
|
+
context_apply.add_argument("--receipt", required=True)
|
|
53
67
|
verify = sub.add_parser("verify")
|
|
54
68
|
verify.add_argument("--root", required=True)
|
|
55
69
|
verify.add_argument("--request-id", required=True)
|
|
@@ -62,6 +76,9 @@ def main(argv=None) -> int:
|
|
|
62
76
|
recover.add_argument("--disposition", required=True, choices=("failed", "abandoned"))
|
|
63
77
|
args = parser.parse_args(argv)
|
|
64
78
|
root = Path(args.root).resolve(strict=True)
|
|
79
|
+
if args.command == "cancel-context":
|
|
80
|
+
print(json.dumps(_artifactauthoritylib.cancel_context_preview(root, args.document_id), sort_keys=True))
|
|
81
|
+
return 0
|
|
65
82
|
client = _artifactlib.ArtifactClient(
|
|
66
83
|
root, _artifactlib.helper_installation(__file__)
|
|
67
84
|
)
|
|
@@ -83,6 +100,7 @@ def main(argv=None) -> int:
|
|
|
83
100
|
workspace_roots=workspaces or None, host=host,
|
|
84
101
|
reviewer_model=args.reviewer_model,
|
|
85
102
|
codex_review_profile=args.codex_review_profile,
|
|
103
|
+
completion_receipts=args.completion_receipt, supporting_files=args.supporting_file,
|
|
86
104
|
)
|
|
87
105
|
if host == "claude" and args.activity == "verification":
|
|
88
106
|
# The Claude verifier hook pins this exact shipped script and
|
|
@@ -91,6 +109,11 @@ def main(argv=None) -> int:
|
|
|
91
109
|
result["verify_command"] = (
|
|
92
110
|
f'python "{script}" verify --root "{root}" --request-id {result["request_id"]}'
|
|
93
111
|
)
|
|
112
|
+
elif args.command == "arm-context":
|
|
113
|
+
preview = _artifactlib.read_context_preview_request(root, args.request_file)
|
|
114
|
+
result = _artifactauthoritylib.arm_context_preview(root, client, preview)
|
|
115
|
+
elif args.command == "apply-context":
|
|
116
|
+
result = client.call("context-apply", {"receipt": args.receipt})
|
|
94
117
|
elif args.command == "verify":
|
|
95
118
|
result = _artifactauthoritylib.run_verification(
|
|
96
119
|
root, client, args.request_id,
|
|
@@ -108,6 +131,7 @@ def main(argv=None) -> int:
|
|
|
108
131
|
if __name__ == "__main__":
|
|
109
132
|
try:
|
|
110
133
|
raise SystemExit(main())
|
|
111
|
-
except (_artifactauthoritylib.AuthorityError, _artifactlib.ArtifactError
|
|
134
|
+
except (_artifactauthoritylib.AuthorityError, _artifactlib.ArtifactError,
|
|
135
|
+
_artifactpromptlib.PromptRouteError) as exc:
|
|
112
136
|
sys.stderr.write(str(exc) + "\n")
|
|
113
137
|
raise SystemExit(1)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# codeArbiter — private bounded debug handoff validation entry.
|
|
2
|
+
# Read only stdin and the installed validator; emit closed, redacted diagnostics.
|
|
3
|
+
# Bytecode suppression precedes the local import and no project state is touched.
|
|
4
|
+
#
|
|
5
|
+
# main(argv) -> int: validate one bounded packet and write one result envelope.
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
sys.dont_write_bytecode = True
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
import _debughandofflib as handoff
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
PROTOCOL = "codearbiter.debug-validation/1.0.0"
|
|
17
|
+
DIRECT_CODES = frozenset({
|
|
18
|
+
"INPUT_LIMIT", "DEPTH_LIMIT", "INVALID_UTF8", "INVALID_JSON",
|
|
19
|
+
"DUPLICATE_KEY", "UNSUPPORTED_PROTOCOL", "REQUIRED_FIELD",
|
|
20
|
+
"UNKNOWN_FIELD", "TYPE", "VALUE",
|
|
21
|
+
})
|
|
22
|
+
REFERENCE_CODES = frozenset({"DUPLICATE_ID", "DANGLING_REFERENCE"})
|
|
23
|
+
PATH = re.compile(r"\$(?:\.[A-Za-z_]+|\[[0-9]+\])*\Z", re.ASCII)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _result(valid, errors, truncated=False):
|
|
27
|
+
return {"protocol": PROTOCOL, "valid": valid,
|
|
28
|
+
"errors": errors, "truncated": truncated}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _diagnostic(code, path="$"):
|
|
32
|
+
if code in DIRECT_CODES or code in ("USAGE", "INPUT_IO", "INTERNAL"):
|
|
33
|
+
public_code = code
|
|
34
|
+
elif code in REFERENCE_CODES:
|
|
35
|
+
public_code = "REFERENCE"
|
|
36
|
+
else:
|
|
37
|
+
public_code = "SEMANTIC"
|
|
38
|
+
if type(path) is not str or len(path) > 256 or not PATH.fullmatch(path):
|
|
39
|
+
return {"code": "INTERNAL", "path": "$"}
|
|
40
|
+
return {"code": public_code, "path": path}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _write(result):
|
|
44
|
+
raw = (json.dumps(result, ensure_ascii=True, separators=(",", ":"))
|
|
45
|
+
+ "\n").encode("utf-8")
|
|
46
|
+
if len(raw) > 8192:
|
|
47
|
+
result = _result(False, [{"code": "INTERNAL", "path": "$"}])
|
|
48
|
+
raw = (json.dumps(result, separators=(",", ":")) + "\n").encode("utf-8")
|
|
49
|
+
exit_code = 3
|
|
50
|
+
else:
|
|
51
|
+
exit_code = 0 if result["valid"] else 2
|
|
52
|
+
if result["errors"] and result["errors"][0]["code"] in ("INPUT_IO", "INTERNAL"):
|
|
53
|
+
exit_code = 3
|
|
54
|
+
try:
|
|
55
|
+
sys.stdout.buffer.write(raw)
|
|
56
|
+
sys.stdout.buffer.flush()
|
|
57
|
+
except OSError:
|
|
58
|
+
return 3
|
|
59
|
+
return exit_code
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main(argv=None):
|
|
63
|
+
if argv is None:
|
|
64
|
+
argv = sys.argv[1:]
|
|
65
|
+
if argv != ["validate"]:
|
|
66
|
+
return _write(_result(False, [_diagnostic("USAGE")]))
|
|
67
|
+
try:
|
|
68
|
+
raw = sys.stdin.buffer.read(65537)
|
|
69
|
+
except OSError:
|
|
70
|
+
return _write(_result(False, [_diagnostic("INPUT_IO")]))
|
|
71
|
+
except Exception:
|
|
72
|
+
return _write(_result(False, [_diagnostic("INTERNAL")]))
|
|
73
|
+
try:
|
|
74
|
+
packet, error = handoff.decode_packet(raw)
|
|
75
|
+
if error is not None:
|
|
76
|
+
return _write(_result(False, [_diagnostic(error)]))
|
|
77
|
+
found, truncated = handoff.validate_semantics_bounded(packet, ceiling=16)
|
|
78
|
+
errors = [_diagnostic(code, path) for code, path in found]
|
|
79
|
+
return _write(_result(not found, errors, truncated))
|
|
80
|
+
except Exception:
|
|
81
|
+
return _write(_result(False, [_diagnostic("INTERNAL")]))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
raise SystemExit(main())
|
|
@@ -23,7 +23,9 @@ from _gitexec import git_executable, root_bound_git_env # noqa: E402
|
|
|
23
23
|
import hostapi # noqa: E402 — host seam (ADR-0011): plugin-root resolution
|
|
24
24
|
import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
|
|
25
25
|
import _githooks # noqa: E402 — #556: git-hook drop-in registry freshness
|
|
26
|
-
from _hooklib import
|
|
26
|
+
from _hooklib import ( # noqa: E402
|
|
27
|
+
frontmatter_enabled, get_host, initialized_body_text, set_host, utf8_stdio,
|
|
28
|
+
)
|
|
27
29
|
|
|
28
30
|
HOOK_SCRIPTS = ("session-start.py", "pre-bash.py", "pre-write.py",
|
|
29
31
|
"pre-edit.py", "post-write-edit.py", "prune-transcript.py",
|
|
@@ -298,7 +300,7 @@ def check_repo():
|
|
|
298
300
|
if enabled:
|
|
299
301
|
with open(ctx, encoding="utf-8", errors="replace") as f:
|
|
300
302
|
body = f.read()
|
|
301
|
-
if
|
|
303
|
+
if initialized_body_text(body):
|
|
302
304
|
ok("project is initialized (<!--INITIALIZED--> marker present)")
|
|
303
305
|
else:
|
|
304
306
|
warn(f"no <!--INITIALIZED--> marker — startup will route to "
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
#
|
|
12
12
|
# It mirrors pre-bash.py's commit/push gates and reuses the SAME detection
|
|
13
13
|
# primitives from _hooklib (CRYPTO_RE / SECRET_RE / line_digest / content_digest
|
|
14
|
-
# / is_migration_path /
|
|
14
|
+
# / is_migration_path / sensitive_scan_added_lines), so the two
|
|
15
15
|
# enforcement points can never drift on what counts as sensitive or as a
|
|
16
16
|
# migration, on how a gate-pass marker binds to the lines it approved, or on
|
|
17
17
|
# which path (gate-events.log, #279) is exempt from the sensitive-line scan.
|
|
@@ -30,9 +30,9 @@ import hostapi # noqa: E402 — host seam (ADR-0011)
|
|
|
30
30
|
import _entrylib # noqa: E402 — shared run() dispatch (jscpd dedup)
|
|
31
31
|
from _gitexec import git_executable # noqa: E402
|
|
32
32
|
from _hooklib import ( # noqa: E402
|
|
33
|
-
CRYPTO_RE,
|
|
33
|
+
CRYPTO_RE, SECRET_RE, SECURITY_DIFF_GIT_ARGS,
|
|
34
34
|
arbiter_active, content_digest, is_migration_path, line_digest,
|
|
35
|
-
|
|
35
|
+
security_scan_diff, set_host, utf8_stdio,
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
|
|
@@ -223,8 +223,9 @@ def pre_commit(root):
|
|
|
223
223
|
block("H-01", f"Direct commit to {target} is prohibited (ORCHESTRATOR §3) — this is "
|
|
224
224
|
f"the git-level backstop (#161). Create a feature branch.")
|
|
225
225
|
|
|
226
|
-
# H-09b / H-10b: a commit introducing crypto/secret changes needs a
|
|
227
|
-
# line-covering security-gate pass
|
|
226
|
+
# H-09b / H-10b: a commit introducing crypto/secret changes needs a recorded,
|
|
227
|
+
# line-covering security-gate pass (bound by line digest, no time window).
|
|
228
|
+
# Reuses the exact _hooklib primitives.
|
|
228
229
|
added = cached_added_lines(cwd)
|
|
229
230
|
if added is None:
|
|
230
231
|
block("H-09b", "the staged diff for the crypto/secret scan could not be read — "
|
|
@@ -237,7 +238,7 @@ def pre_commit(root):
|
|
|
237
238
|
skill = "crypto-compliance" if touches_crypto else "secret-handling"
|
|
238
239
|
marker_root = _marker_root(root)
|
|
239
240
|
marker = os.path.join(marker_root, ".codearbiter", ".markers", "security-gate-passed")
|
|
240
|
-
if not
|
|
241
|
+
if not os.path.isfile(marker):
|
|
241
242
|
block(tag, f"This commit introduces {kind} changes, but no security-gate pass is "
|
|
242
243
|
f"recorded (#161 git backstop). Run the {skill} gate, then commit.")
|
|
243
244
|
approved = _marker_set(marker_root, "security-gate-passed")
|
|
@@ -313,7 +313,7 @@ class Host:
|
|
|
313
313
|
|
|
314
314
|
name = "claude"
|
|
315
315
|
adapter_name = "ca"
|
|
316
|
-
adapter_version = "2.
|
|
316
|
+
adapter_version = "2.24.0"
|
|
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,
|