@arbiterforge/ca-pi 0.14.10 → 0.14.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -9
- package/package.json +2 -1
- package/plugins/ca-pi/CHANGELOG.md +67 -0
- package/plugins/ca-pi/LICENSE +661 -0
- package/plugins/ca-pi/SKILLS.md +2 -2
- package/plugins/ca-pi/SPRINT.md +47 -0
- package/plugins/ca-pi/THIRD_PARTY_NOTICES.md +40 -0
- package/plugins/ca-pi/generated/command-catalog.json +2 -2
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/release.json +6 -6
- package/plugins/ca-pi/hooks/_approvallib.py +58 -22
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +2135 -0
- package/plugins/ca-pi/hooks/_artifactlib.py +164 -7
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +177 -0
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_overrideappendlib.py +5 -1
- package/plugins/ca-pi/hooks/_prerequisitelib.py +44 -22
- package/plugins/ca-pi/hooks/_reconciliationlib.py +472 -0
- package/plugins/ca-pi/hooks/_releaselib.py +345 -111
- package/plugins/ca-pi/hooks/_sprintapprovallib.py +302 -0
- package/plugins/ca-pi/hooks/artifact-authority-hook.py +127 -0
- package/plugins/ca-pi/hooks/artifact-authority.py +102 -0
- package/plugins/ca-pi/hooks/artifact-reconcile.py +57 -0
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/prompt-submit.py +20 -3
- package/plugins/ca-pi/hooks/releasehash.py +36 -23
- package/plugins/ca-pi/includes/artifacts.md +120 -13
- package/plugins/ca-pi/includes/sprint-authority.md +69 -0
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +7 -4
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +97 -29
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +7 -1
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +32 -15
- package/plugins/ca-pi/routines/release/SKILL.md +220 -70
- package/plugins/ca-pi/routines/skill-author/SKILL.md +26 -5
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +22 -10
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +170 -16
- package/plugins/ca-pi/skills/ca-new-skill/SKILL.md +116 -21
- package/plugins/ca-pi/skills/ca-release/SKILL.md +6 -4
|
@@ -244,6 +244,145 @@ def _resolve_workflow_pair(root: str | Path, slug: str) -> dict[str, object]:
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
|
|
247
|
+
def _installed_workflow_preflight(installation: Path) -> dict[str, object]:
|
|
248
|
+
"""Inspect installed prerequisites, not host trust or live-event evidence.
|
|
249
|
+
|
|
250
|
+
This never imports an adapter, executes a hook, or writes consumer state.
|
|
251
|
+
Package integrity and the actual authority boundaries remain separate checks.
|
|
252
|
+
A successful engine capabilities call alone cannot admit a host workflow.
|
|
253
|
+
"""
|
|
254
|
+
result: dict[str, object] = {
|
|
255
|
+
"host": None, "adapter_version": None, "resources_available": False,
|
|
256
|
+
"evidence_kind": "installed-resource-preflight", "live_host_verified": False,
|
|
257
|
+
"missing": [],
|
|
258
|
+
}
|
|
259
|
+
missing: list[str] = []
|
|
260
|
+
if installation.name != "artifacts" or installation.parent.name != "helpers":
|
|
261
|
+
result["missing"] = ["installed host package layout"]
|
|
262
|
+
return result
|
|
263
|
+
plugin = installation.parent.parent
|
|
264
|
+
|
|
265
|
+
def resource(relative: str, *, document: bool = False):
|
|
266
|
+
path = plugin / relative
|
|
267
|
+
try:
|
|
268
|
+
_trusted_directory(installation, "HOST_WORKFLOW_UNAVAILABLE")
|
|
269
|
+
_trusted_directory(path.parent, "HOST_WORKFLOW_UNAVAILABLE")
|
|
270
|
+
descriptor = _open_pinned_regular(path)
|
|
271
|
+
try:
|
|
272
|
+
info = os.fstat(descriptor)
|
|
273
|
+
reparse = getattr(info, "st_file_attributes", 0) & getattr(
|
|
274
|
+
stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)
|
|
275
|
+
if reparse or not stat.S_ISREG(info.st_mode) or info.st_size <= 0:
|
|
276
|
+
raise OSError("required installed resource is not a nonempty regular file")
|
|
277
|
+
return _decode(os.read(descriptor, MAX_RESPONSE + 1)) if document else True
|
|
278
|
+
finally:
|
|
279
|
+
os.close(descriptor)
|
|
280
|
+
except (OSError, ArtifactError):
|
|
281
|
+
missing.append(relative)
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
manifests = [("claude", "ca", ".claude-plugin/plugin.json"),
|
|
285
|
+
("codex", "ca-codex", ".codex-plugin/plugin.json")]
|
|
286
|
+
present = [row for row in manifests if (plugin / row[2]).exists()
|
|
287
|
+
or (plugin / row[2]).is_symlink()]
|
|
288
|
+
if not present:
|
|
289
|
+
present = [("pi", "ca-pi", "package.json")]
|
|
290
|
+
if len(present) != 1:
|
|
291
|
+
result["missing"] = ["one unambiguous installed host manifest"]
|
|
292
|
+
return result
|
|
293
|
+
host, name, manifest_path = present[0]
|
|
294
|
+
manifest = resource(manifest_path, document=True)
|
|
295
|
+
if (not isinstance(manifest, dict) or manifest.get("name") != name
|
|
296
|
+
or not isinstance(manifest.get("version"), str)
|
|
297
|
+
or re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?",
|
|
298
|
+
manifest["version"]) is None):
|
|
299
|
+
result["missing"] = ["matching installed host identity"]
|
|
300
|
+
return result
|
|
301
|
+
result.update(host=host, adapter_version=manifest["version"])
|
|
302
|
+
if host == "pi":
|
|
303
|
+
result["missing"] = ["Pi production prompt, verification and review authority"]
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
for filename in ("_approvallib.py", "_prerequisitelib.py", "_sprintapprovallib.py",
|
|
307
|
+
"_reconciliationlib.py", "_artifactauthoritylib.py", "_gitexec.py",
|
|
308
|
+
"artifact-authority.py", "artifact-authority-hook.py", "prompt-submit.py"):
|
|
309
|
+
resource("hooks/" + filename)
|
|
310
|
+
if host == "claude":
|
|
311
|
+
resource("agents/authority-reviewer.md")
|
|
312
|
+
registry = resource("hooks/hooks.json", document=True)
|
|
313
|
+
hooks = registry.get("hooks") if isinstance(registry, dict) else None
|
|
314
|
+
if not isinstance(hooks, dict):
|
|
315
|
+
result["missing"] = sorted(set(missing + ["valid installed hook registry"]))
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
token = "${CLAUDE_PLUGIN_ROOT}" if host == "claude" else "${PLUGIN_ROOT}"
|
|
319
|
+
command_field = "commandWindows" if host == "codex" and platform.system() == "Windows" else "command"
|
|
320
|
+
|
|
321
|
+
def registered(event: str, tool: str | None, script: str) -> bool:
|
|
322
|
+
groups = hooks.get(event)
|
|
323
|
+
if not isinstance(groups, list):
|
|
324
|
+
return False
|
|
325
|
+
commands = {f'{python} "{token}/hooks/{script}"' for python in ("python", "python3")}
|
|
326
|
+
for group in groups:
|
|
327
|
+
if not isinstance(group, dict):
|
|
328
|
+
continue
|
|
329
|
+
matcher = group.get("matcher", "")
|
|
330
|
+
if not isinstance(matcher, str):
|
|
331
|
+
continue
|
|
332
|
+
# Check the shipped literal alternatives, not a second regex DSL.
|
|
333
|
+
if (tool is None and matcher not in {"", "*"}) or (
|
|
334
|
+
tool is not None and tool not in matcher.split("|")):
|
|
335
|
+
continue
|
|
336
|
+
entries = group.get("hooks")
|
|
337
|
+
if not isinstance(entries, list):
|
|
338
|
+
continue
|
|
339
|
+
if any(isinstance(entry, dict) and entry.get("type") == "command"
|
|
340
|
+
and entry.get("async", False) is False
|
|
341
|
+
and isinstance(entry.get(command_field), str)
|
|
342
|
+
and entry[command_field] in commands for entry in entries):
|
|
343
|
+
return True
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
required = [("UserPromptSubmit", None, "prompt-submit.py")]
|
|
347
|
+
tools = ("Bash", "Agent") if host == "claude" else (
|
|
348
|
+
"Bash", "shell_command", "exec_command", "unified_exec", "spawn_agent")
|
|
349
|
+
required += [(event, tool, "artifact-authority-hook.py")
|
|
350
|
+
for event in ("PreToolUse", "PostToolUse") for tool in tools]
|
|
351
|
+
required += [(event, None, "artifact-authority-hook.py")
|
|
352
|
+
for event in ("SubagentStart", "SubagentStop")]
|
|
353
|
+
if host == "claude":
|
|
354
|
+
required += [("PostToolUseFailure", "Bash", "artifact-authority-hook.py"),
|
|
355
|
+
("PreToolUse", "SendMessage", "artifact-authority-hook.py")]
|
|
356
|
+
missing.extend(f"{event}:{tool or '*'}:{script}"
|
|
357
|
+
for event, tool, script in required if not registered(event, tool, script))
|
|
358
|
+
result["missing"] = sorted(set(missing))
|
|
359
|
+
result["resources_available"] = not missing
|
|
360
|
+
return result
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _require_host_workflow(client: "ArtifactClient") -> None:
|
|
364
|
+
readiness = client.workflow_preflight()
|
|
365
|
+
if readiness["resources_available"] is not True:
|
|
366
|
+
raise ArtifactError(
|
|
367
|
+
"HOST_WORKFLOW_UNAVAILABLE",
|
|
368
|
+
("installed HTML workflow prerequisites are unavailable: "
|
|
369
|
+
+ ", ".join(readiness["missing"]))[:400]
|
|
370
|
+
+ "; repair this package or use a supported host; no fallback artifacts",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# The native engine ships only through the release distribution channels, never
|
|
375
|
+
# through a main-branch checkout, so the repair is always "install from the
|
|
376
|
+
# channel". Named concretely so the user is not left to guess (ADR-0040).
|
|
377
|
+
PAYLOAD_REPAIR_HINT = (
|
|
378
|
+
"Claude Code: /plugin marketplace update codearbiter, then /plugin update ca@codearbiter; "
|
|
379
|
+
"Codex (registered without --ref): codex plugin remove ca-codex@codearbiter, "
|
|
380
|
+
"codex plugin marketplace remove codearbiter, "
|
|
381
|
+
"codex plugin marketplace add arbiterForge/codeArbiter --ref ca-codex-marketplace, "
|
|
382
|
+
"then codex plugin add ca-codex@codearbiter; Pi: pi install npm:@arbiterforge/ca-pi"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
247
386
|
def _require_authoring_capability(client: "ArtifactClient") -> None:
|
|
248
387
|
"""Fail one default-HTML route with a bounded repair diagnostic."""
|
|
249
388
|
try:
|
|
@@ -252,7 +391,7 @@ def _require_authoring_capability(client: "ArtifactClient") -> None:
|
|
|
252
391
|
raise ArtifactError(
|
|
253
392
|
"CAPABILITY_MISSING",
|
|
254
393
|
f"repair or reinstall the pinned artifact payload ({exc.code}); "
|
|
255
|
-
"new HTML work cannot fall back to Markdown",
|
|
394
|
+
f"new HTML work cannot fall back to Markdown. Repair: {PAYLOAD_REPAIR_HINT}",
|
|
256
395
|
) from exc
|
|
257
396
|
if (
|
|
258
397
|
not isinstance(capabilities, dict)
|
|
@@ -262,7 +401,7 @@ def _require_authoring_capability(client: "ArtifactClient") -> None:
|
|
|
262
401
|
raise ArtifactError(
|
|
263
402
|
"CAPABILITY_MISSING",
|
|
264
403
|
"repair or reinstall the qualified default-on artifact payload; "
|
|
265
|
-
"new HTML work cannot fall back to Markdown",
|
|
404
|
+
f"new HTML work cannot fall back to Markdown. Repair: {PAYLOAD_REPAIR_HINT}",
|
|
266
405
|
)
|
|
267
406
|
|
|
268
407
|
|
|
@@ -305,10 +444,13 @@ def _select_authoring_route(
|
|
|
305
444
|
"an installation-pinned artifact client bound to this repository is required",
|
|
306
445
|
)
|
|
307
446
|
_require_authoring_capability(client)
|
|
447
|
+
if _resolve_workflow_pair(root, slug) != selected:
|
|
448
|
+
raise ArtifactError("STALE_ROUTE", "artifact namespace changed during the capability probe")
|
|
449
|
+
_require_host_workflow(client)
|
|
308
450
|
if _resolve_workflow_pair(root, slug) != selected:
|
|
309
451
|
raise ArtifactError(
|
|
310
452
|
"STALE_ROUTE",
|
|
311
|
-
"artifact namespace changed during the
|
|
453
|
+
"artifact namespace changed during the installed host preflight",
|
|
312
454
|
)
|
|
313
455
|
return {"workflow": workflow, "lane": lane, "mode": "artifact", **selected}
|
|
314
456
|
|
|
@@ -319,8 +461,17 @@ def _preflight_plan_authoring(
|
|
|
319
461
|
*,
|
|
320
462
|
spec_artifact_id: str,
|
|
321
463
|
spec_normative_sha256: str,
|
|
464
|
+
draft_for_pair: bool = False,
|
|
322
465
|
) -> Path:
|
|
323
|
-
"""Verify
|
|
466
|
+
"""Verify the exact spec. Only initial sprint-pair preparation admits a ready draft.
|
|
467
|
+
|
|
468
|
+
Draft preparation never approves the spec or plan and never permits execution.
|
|
469
|
+
The ordinary feature/sequential path still requires an approved spec.
|
|
470
|
+
"""
|
|
471
|
+
if type(draft_for_pair) is not bool:
|
|
472
|
+
raise ArtifactError("INVALID_ROUTE", "draft_for_pair must be an explicit boolean")
|
|
473
|
+
if draft_for_pair and (not isinstance(selected, dict) or selected.get("workflow") != "sprint" or selected.get("lane") != "full"):
|
|
474
|
+
raise ArtifactError("INVALID_ROUTE", "draft preview is limited to full-lane initial sprint pairs")
|
|
324
475
|
if not isinstance(selected, dict):
|
|
325
476
|
raise ArtifactError("INVALID_ROUTE", "plan authoring requires a selected route")
|
|
326
477
|
slug = selected.get("slug")
|
|
@@ -362,6 +513,7 @@ def _preflight_plan_authoring(
|
|
|
362
513
|
):
|
|
363
514
|
raise ArtifactError("AUTHORITY_MISMATCH", "spec normative digest is invalid")
|
|
364
515
|
|
|
516
|
+
_require_host_workflow(client)
|
|
365
517
|
identity = client.call("identity", {"artifact_id": spec_artifact_id})
|
|
366
518
|
expected_path = f".codearbiter/specs/{slug}.html"
|
|
367
519
|
if (
|
|
@@ -376,7 +528,7 @@ def _preflight_plan_authoring(
|
|
|
376
528
|
)
|
|
377
529
|
approved = client.call(
|
|
378
530
|
"validate",
|
|
379
|
-
{"artifact_id": spec_artifact_id, "gate": "approved"},
|
|
531
|
+
{"artifact_id": spec_artifact_id, "gate": "ready" if draft_for_pair else "approved"},
|
|
380
532
|
permit_invalid=True,
|
|
381
533
|
)
|
|
382
534
|
authority = approved.get("authority")
|
|
@@ -385,8 +537,9 @@ def _preflight_plan_authoring(
|
|
|
385
537
|
or approved.get("artifact_id") != spec_artifact_id
|
|
386
538
|
or approved.get("normative_sha256") != spec_normative_sha256
|
|
387
539
|
or not isinstance(authority, dict)
|
|
388
|
-
or
|
|
389
|
-
or authority.get("
|
|
540
|
+
or approved.get("model_sha256") != identity.get("model_sha256")
|
|
541
|
+
or authority.get("state") != ("draft" if draft_for_pair else "approved")
|
|
542
|
+
or authority.get("authority_verified") is not (not draft_for_pair)
|
|
390
543
|
):
|
|
391
544
|
raise ArtifactError(
|
|
392
545
|
"AUTHORITY_UNVERIFIED",
|
|
@@ -644,6 +797,10 @@ class ArtifactClient:
|
|
|
644
797
|
self.installation = _trusted_directory(installation, "CAPABILITY_MISSING")
|
|
645
798
|
self.timeout = timeout
|
|
646
799
|
|
|
800
|
+
def workflow_preflight(self) -> dict[str, object]:
|
|
801
|
+
"""Read fresh installed prerequisites; never attest live host authority."""
|
|
802
|
+
return _installed_workflow_preflight(self.installation)
|
|
803
|
+
|
|
647
804
|
def _open_binary(self) -> int:
|
|
648
805
|
system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}.get(platform.system())
|
|
649
806
|
arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}.get(platform.machine().lower())
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# codeArbiter — cross-repository routing for exact artifact authority prompts.
|
|
3
|
+
"""Register and resolve target repositories without granting prompt authority."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import stat
|
|
12
|
+
import tempfile
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
REGISTRY_PARENT = Path(tempfile.gettempdir())
|
|
16
|
+
FORMAT = "codearbiter.artifact-prompt-route/0.1.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PromptRouteError(RuntimeError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _canonical(value):
|
|
24
|
+
return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _digest(value: bytes) -> str:
|
|
28
|
+
return hashlib.sha256(value).hexdigest()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _identity(root: Path) -> dict[str, str]:
|
|
32
|
+
root = root.resolve(strict=True)
|
|
33
|
+
info = root.lstat()
|
|
34
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
35
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISDIR(info.st_mode):
|
|
36
|
+
raise PromptRouteError("artifact prompt target is not a real directory")
|
|
37
|
+
return {
|
|
38
|
+
"path": str(root),
|
|
39
|
+
"filesystem_id": f"{int(getattr(info, 'st_dev', 0))}:{int(getattr(info, 'st_ino', 0))}",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _registry() -> Path:
|
|
44
|
+
namespace = f"uid-{os.getuid()}" if hasattr(os, "getuid") else "current-user"
|
|
45
|
+
root = REGISTRY_PARENT.resolve(strict=True) / f"codearbiter-prompt-routes-{namespace}"
|
|
46
|
+
root.mkdir(mode=0o700, exist_ok=True)
|
|
47
|
+
info = root.lstat()
|
|
48
|
+
reparse = getattr(info, "st_file_attributes", 0) & 0x400
|
|
49
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISDIR(info.st_mode):
|
|
50
|
+
raise PromptRouteError("artifact prompt registry is unsafe")
|
|
51
|
+
if hasattr(os, "getuid"):
|
|
52
|
+
if info.st_uid != os.getuid():
|
|
53
|
+
raise PromptRouteError("artifact prompt registry has another owner")
|
|
54
|
+
os.chmod(root, 0o700)
|
|
55
|
+
return root
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _key(route: str, artifact_id: str, repository: dict[str, str]) -> str:
|
|
59
|
+
return _digest(_canonical({"route": route, "artifact_id": artifact_id, "repository": repository}))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def register(root: str | Path, route: str, artifact_id: str, prompt: str,
|
|
63
|
+
*, binding_sha256: str | None = None) -> None:
|
|
64
|
+
if route not in {"approval", "prerequisite", "reconciliation"} or not isinstance(prompt, str) or not prompt:
|
|
65
|
+
raise PromptRouteError("artifact prompt route is invalid")
|
|
66
|
+
if binding_sha256 is not None and (
|
|
67
|
+
not isinstance(binding_sha256, str)
|
|
68
|
+
or len(binding_sha256) != 64
|
|
69
|
+
or any(ch not in "0123456789abcdef" for ch in binding_sha256)
|
|
70
|
+
):
|
|
71
|
+
raise PromptRouteError("artifact prompt binding is invalid")
|
|
72
|
+
repository = _identity(Path(root))
|
|
73
|
+
value = {
|
|
74
|
+
"format": FORMAT,
|
|
75
|
+
"route": route,
|
|
76
|
+
"artifact_id": artifact_id,
|
|
77
|
+
"prompt_sha256": _digest(prompt.encode("utf-8")),
|
|
78
|
+
"repository": repository,
|
|
79
|
+
}
|
|
80
|
+
if binding_sha256 is not None:
|
|
81
|
+
value["binding_sha256"] = binding_sha256
|
|
82
|
+
value["integrity_sha256"] = _digest(_canonical(value))
|
|
83
|
+
registry = _registry()
|
|
84
|
+
key = _key(route, artifact_id, repository)
|
|
85
|
+
path = registry / f"{key}.json"
|
|
86
|
+
data = _canonical(value)
|
|
87
|
+
# Each caller owns an exclusively created pending marker before registering.
|
|
88
|
+
# A prior route at this key therefore belongs to an interrupted request.
|
|
89
|
+
fd, temporary = tempfile.mkstemp(prefix=f".{key}.", suffix=".tmp", dir=registry)
|
|
90
|
+
try:
|
|
91
|
+
try:
|
|
92
|
+
view = memoryview(data)
|
|
93
|
+
while view:
|
|
94
|
+
written = os.write(fd, view)
|
|
95
|
+
if written <= 0:
|
|
96
|
+
raise OSError("short prompt-route write")
|
|
97
|
+
view = view[written:]
|
|
98
|
+
os.fsync(fd)
|
|
99
|
+
finally:
|
|
100
|
+
os.close(fd)
|
|
101
|
+
os.replace(temporary, path)
|
|
102
|
+
finally:
|
|
103
|
+
try:
|
|
104
|
+
os.unlink(temporary)
|
|
105
|
+
except FileNotFoundError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve(route: str, prompt: str) -> Path | None:
|
|
110
|
+
if route not in {"approval", "prerequisite", "reconciliation"} or not isinstance(prompt, str):
|
|
111
|
+
return None
|
|
112
|
+
prompt_hash = _digest(prompt.encode("utf-8"))
|
|
113
|
+
matches = []
|
|
114
|
+
for path in _registry().glob("*.json"):
|
|
115
|
+
try:
|
|
116
|
+
raw = path.read_bytes()
|
|
117
|
+
if len(raw) > 16384:
|
|
118
|
+
continue
|
|
119
|
+
value = json.loads(raw.decode("utf-8"))
|
|
120
|
+
unsigned = dict(value)
|
|
121
|
+
integrity = unsigned.pop("integrity_sha256", None)
|
|
122
|
+
repository = value.get("repository")
|
|
123
|
+
if (
|
|
124
|
+
value.get("format") != FORMAT
|
|
125
|
+
or value.get("route") != route
|
|
126
|
+
or value.get("prompt_sha256") != prompt_hash
|
|
127
|
+
or integrity != _digest(_canonical(unsigned))
|
|
128
|
+
or not isinstance(repository, dict)
|
|
129
|
+
):
|
|
130
|
+
continue
|
|
131
|
+
root = Path(repository.get("path", ""))
|
|
132
|
+
if _identity(root) != repository:
|
|
133
|
+
continue
|
|
134
|
+
matches.append(root.resolve(strict=True))
|
|
135
|
+
except (OSError, UnicodeError, ValueError, PromptRouteError):
|
|
136
|
+
continue
|
|
137
|
+
unique = {str(path): path for path in matches}
|
|
138
|
+
if len(unique) > 1:
|
|
139
|
+
raise PromptRouteError("artifact authority prompt matches multiple repositories")
|
|
140
|
+
return next(iter(unique.values()), None)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def unregister(root: str | Path, route: str, artifact_id: str) -> None:
|
|
144
|
+
repository = _identity(Path(root))
|
|
145
|
+
path = _registry() / f"{_key(route, artifact_id, repository)}.json"
|
|
146
|
+
try:
|
|
147
|
+
path.unlink()
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def binding(root: str | Path, route: str, artifact_id: str) -> str | None:
|
|
153
|
+
repository = _identity(Path(root))
|
|
154
|
+
path = _registry() / f"{_key(route, artifact_id, repository)}.json"
|
|
155
|
+
try:
|
|
156
|
+
raw = path.read_bytes()
|
|
157
|
+
if len(raw) > 16384:
|
|
158
|
+
raise OSError("oversized prompt route")
|
|
159
|
+
value = json.loads(raw.decode("utf-8"))
|
|
160
|
+
unsigned = dict(value)
|
|
161
|
+
integrity = unsigned.pop("integrity_sha256", None)
|
|
162
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
163
|
+
raise PromptRouteError("artifact prompt route is unreadable") from exc
|
|
164
|
+
if (
|
|
165
|
+
value.get("format") != FORMAT
|
|
166
|
+
or value.get("route") != route
|
|
167
|
+
or value.get("artifact_id") != artifact_id
|
|
168
|
+
or value.get("repository") != repository
|
|
169
|
+
or integrity != _digest(_canonical(unsigned))
|
|
170
|
+
):
|
|
171
|
+
raise PromptRouteError("artifact prompt route is malformed")
|
|
172
|
+
result = value.get("binding_sha256")
|
|
173
|
+
if not isinstance(result, str) or len(result) != 64 or any(
|
|
174
|
+
ch not in "0123456789abcdef" for ch in result
|
|
175
|
+
):
|
|
176
|
+
raise PromptRouteError("artifact prompt route has no closed binding")
|
|
177
|
+
return result
|
|
@@ -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.14.
|
|
14
|
+
adapter_version = "0.14.16"
|
|
15
15
|
update_target = "ca-pi"
|
|
16
16
|
update_tag_prefix = "ca-pi-v"
|
|
17
17
|
update_command = "pi update npm:@arbiterforge/ca-pi"
|
|
@@ -78,7 +78,11 @@ def _validate_file_type(path):
|
|
|
78
78
|
|
|
79
79
|
def _validate_existing(path, raw):
|
|
80
80
|
_validate_file_type(path)
|
|
81
|
-
|
|
81
|
+
# Historical audit bytes are immutable. A stray CR can exist in an older
|
|
82
|
+
# row without making a new O_APPEND write unsafe, so preserve it verbatim
|
|
83
|
+
# and require only that newly written rows use LF. Rewriting the file to
|
|
84
|
+
# normalize old bytes would violate the append-only contract.
|
|
85
|
+
if raw.startswith(b"\xef\xbb\xbf") or b"\x00" in raw:
|
|
82
86
|
raise OverrideAppendError("overrides.log is not canonical UTF-8 LF text")
|
|
83
87
|
try:
|
|
84
88
|
raw.decode("utf-8", errors="strict")
|
|
@@ -38,6 +38,8 @@ import time
|
|
|
38
38
|
from typing import Any
|
|
39
39
|
|
|
40
40
|
import _artifactlib
|
|
41
|
+
import _artifactauthoritylib
|
|
42
|
+
import _artifactpromptlib
|
|
41
43
|
from _hooklib import acquire_lock, release_lock
|
|
42
44
|
|
|
43
45
|
|
|
@@ -393,7 +395,7 @@ def _validate_record(value: Any, prerequisite_id: str) -> bool:
|
|
|
393
395
|
)
|
|
394
396
|
|
|
395
397
|
|
|
396
|
-
def _validate_confirmed_state(value: dict[str, Any]) -> None:
|
|
398
|
+
def _validate_confirmed_state(value: dict[str, Any], *, check_source: bool = True) -> None:
|
|
397
399
|
event = value["event"]
|
|
398
400
|
expected_subject = {
|
|
399
401
|
"artifact_id": value["artifact_id"],
|
|
@@ -444,7 +446,7 @@ def _validate_confirmed_state(value: dict[str, Any]) -> None:
|
|
|
444
446
|
expected_source = (
|
|
445
447
|
SOURCE_DIR / f"{_digest(_canonical(event))}.json"
|
|
446
448
|
).as_posix()
|
|
447
|
-
if value["authority_source"] != expected_source:
|
|
449
|
+
if check_source and value["authority_source"] != expected_source:
|
|
448
450
|
raise PrerequisiteError(
|
|
449
451
|
"INVALID_PENDING_PREREQUISITE",
|
|
450
452
|
"confirmed authority source does not match the event",
|
|
@@ -535,7 +537,7 @@ def _load_pending(root: Path, artifact_id: str) -> dict[str, Any] | None:
|
|
|
535
537
|
"INVALID_PENDING_PREREQUISITE", "captured state is incomplete"
|
|
536
538
|
)
|
|
537
539
|
else:
|
|
538
|
-
_validate_confirmed_state(value)
|
|
540
|
+
_validate_confirmed_state(value, check_source=False)
|
|
539
541
|
if value["mutation_request"] != _expected_mutation_request(value):
|
|
540
542
|
raise PrerequisiteError(
|
|
541
543
|
"INVALID_PENDING_PREREQUISITE",
|
|
@@ -601,16 +603,22 @@ def arm_user_prerequisite(
|
|
|
601
603
|
"PENDING_PREREQUISITE",
|
|
602
604
|
"resolve or cancel this plan's existing prerequisite request first",
|
|
603
605
|
) from exc
|
|
606
|
+
reply = (
|
|
607
|
+
f"satisfy-prerequisite {artifact_id} {prerequisite_id} "
|
|
608
|
+
f"{confirmation_nonce}"
|
|
609
|
+
)
|
|
610
|
+
try:
|
|
611
|
+
_artifactpromptlib.register(root, "prerequisite", artifact_id, reply)
|
|
612
|
+
except _artifactpromptlib.PromptRouteError:
|
|
613
|
+
(root / relative).unlink()
|
|
614
|
+
raise
|
|
604
615
|
return {
|
|
605
616
|
"artifact_id": artifact_id,
|
|
606
617
|
"prerequisite_id": prerequisite_id,
|
|
607
618
|
"title": record["title"],
|
|
608
619
|
"requirement": record["requirement"],
|
|
609
620
|
"review_packet_sha256": packet_sha256,
|
|
610
|
-
"reply":
|
|
611
|
-
f"satisfy-prerequisite {artifact_id} {prerequisite_id} "
|
|
612
|
-
f"{confirmation_nonce}"
|
|
613
|
-
),
|
|
621
|
+
"reply": reply,
|
|
614
622
|
"pending": relative.as_posix(),
|
|
615
623
|
}
|
|
616
624
|
|
|
@@ -752,12 +760,17 @@ def consume_user_prerequisite(
|
|
|
752
760
|
session_id=session_id,
|
|
753
761
|
now=observed_at,
|
|
754
762
|
)
|
|
755
|
-
source_ref, source_sha256 = _publish_event(root, pending)
|
|
756
763
|
if pending["state"] == "CONFIRMED":
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
764
|
+
observed_host, separator, observed_session = pending["event"]["origin"].partition(":UserPromptSubmit:")
|
|
765
|
+
if not separator:
|
|
766
|
+
raise PrerequisiteError("INVALID_PENDING_PREREQUISITE", "confirmed prompt origin is malformed")
|
|
767
|
+
try:
|
|
768
|
+
captured = _artifactauthoritylib.capture_user_prompt(
|
|
769
|
+
root, client, pending["event"], host=observed_host,
|
|
770
|
+
session_id=observed_session,
|
|
771
|
+
)
|
|
772
|
+
except _artifactauthoritylib.AuthorityError as exc:
|
|
773
|
+
raise PrerequisiteError("INVALID_AUTHORITY_SOURCE", str(exc)) from exc
|
|
761
774
|
receipt = captured.get("receipt")
|
|
762
775
|
if not isinstance(receipt, str) or RECEIPT_RE.fullmatch(receipt) is None:
|
|
763
776
|
raise PrerequisiteError("INVALID_RECEIPT", "capture returned no valid receipt")
|
|
@@ -774,18 +787,20 @@ def consume_user_prerequisite(
|
|
|
774
787
|
}
|
|
775
788
|
captured_state = dict(pending)
|
|
776
789
|
captured_state.update(
|
|
777
|
-
state="CAPTURED", receipt=receipt, mutation_request=mutation_request
|
|
790
|
+
state="CAPTURED", receipt=receipt, mutation_request=mutation_request,
|
|
791
|
+
authority_source=captured["authority_source"],
|
|
778
792
|
)
|
|
779
793
|
_replace_state(root, _pending_relative(artifact_id), captured_state)
|
|
780
794
|
pending = captured_state
|
|
781
795
|
result = client.call("prerequisite", pending["mutation_request"])
|
|
782
796
|
(root / _pending_relative(artifact_id)).unlink()
|
|
797
|
+
_artifactpromptlib.unregister(root, "prerequisite", artifact_id)
|
|
783
798
|
return {
|
|
784
799
|
"matched": True,
|
|
785
800
|
"satisfied": True,
|
|
786
801
|
"artifact_id": artifact_id,
|
|
787
802
|
"prerequisite_id": prerequisite_id,
|
|
788
|
-
"authority_source":
|
|
803
|
+
"authority_source": pending["authority_source"],
|
|
789
804
|
"receipt": pending["receipt"],
|
|
790
805
|
"revision": result.get("revision"),
|
|
791
806
|
"replayed": bool((result.get("transaction") or {}).get("replayed")),
|
|
@@ -817,6 +832,7 @@ def cancel_user_prerequisite(
|
|
|
817
832
|
"a confirmed prerequisite decision must be recovered, not cancelled",
|
|
818
833
|
)
|
|
819
834
|
(root / _pending_relative(artifact_id)).unlink()
|
|
835
|
+
_artifactpromptlib.unregister(root, "prerequisite", artifact_id)
|
|
820
836
|
return {
|
|
821
837
|
"artifact_id": artifact_id,
|
|
822
838
|
"prerequisite_id": prerequisite_id,
|
|
@@ -868,6 +884,7 @@ def supersede_user_prerequisite(
|
|
|
868
884
|
raise
|
|
869
885
|
else:
|
|
870
886
|
(root / _pending_relative(artifact_id)).unlink()
|
|
887
|
+
_artifactpromptlib.unregister(root, "prerequisite", artifact_id)
|
|
871
888
|
return {
|
|
872
889
|
"artifact_id": artifact_id,
|
|
873
890
|
"prerequisite_id": prerequisite_id,
|
|
@@ -878,6 +895,7 @@ def supersede_user_prerequisite(
|
|
|
878
895
|
"replayed": bool((result.get("transaction") or {}).get("replayed")),
|
|
879
896
|
}
|
|
880
897
|
(root / _pending_relative(artifact_id)).unlink()
|
|
898
|
+
_artifactpromptlib.unregister(root, "prerequisite", artifact_id)
|
|
881
899
|
return {
|
|
882
900
|
"artifact_id": artifact_id,
|
|
883
901
|
"prerequisite_id": prerequisite_id,
|
|
@@ -904,20 +922,23 @@ def consume_from_hook(
|
|
|
904
922
|
if attempted
|
|
905
923
|
else ""
|
|
906
924
|
)
|
|
907
|
-
if not (Path(root) / _pending_relative(match[0])).exists():
|
|
908
|
-
return (
|
|
909
|
-
"codeArbiter: prerequisite capture failed: no matching armed request"
|
|
910
|
-
if attempted
|
|
911
|
-
else ""
|
|
912
|
-
)
|
|
913
925
|
try:
|
|
926
|
+
routed = _artifactpromptlib.resolve("prerequisite", prompt)
|
|
927
|
+
root = routed or Path(root)
|
|
928
|
+
if not (Path(root) / _pending_relative(match[0])).exists():
|
|
929
|
+
return (
|
|
930
|
+
"codeArbiter: prerequisite capture failed: no matching armed request"
|
|
931
|
+
if attempted
|
|
932
|
+
else ""
|
|
933
|
+
)
|
|
914
934
|
client = _artifactlib.ArtifactClient(
|
|
915
935
|
root, Path(plugin_root) / "helpers" / "artifacts"
|
|
916
936
|
)
|
|
917
937
|
result = consume_user_prerequisite(
|
|
918
938
|
root, client, prompt, host=host, session_id=session_id
|
|
919
939
|
)
|
|
920
|
-
except (PrerequisiteError, _artifactlib.ArtifactError,
|
|
940
|
+
except (PrerequisiteError, _artifactlib.ArtifactError,
|
|
941
|
+
_artifactpromptlib.PromptRouteError, OSError) as exc:
|
|
921
942
|
return f"codeArbiter: prerequisite capture failed: {exc}"
|
|
922
943
|
if not result.get("satisfied"):
|
|
923
944
|
return "codeArbiter: prerequisite capture failed: confirmation did not match"
|
|
@@ -966,6 +987,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
966
987
|
if __name__ == "__main__":
|
|
967
988
|
try:
|
|
968
989
|
raise SystemExit(main())
|
|
969
|
-
except (PrerequisiteError, _artifactlib.ArtifactError
|
|
990
|
+
except (PrerequisiteError, _artifactlib.ArtifactError,
|
|
991
|
+
_artifactpromptlib.PromptRouteError) as exc:
|
|
970
992
|
sys.stderr.write(str(exc) + "\n")
|
|
971
993
|
raise SystemExit(1)
|