@arbiterforge/ca-pi 0.10.12 → 0.14.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/package.json +2 -1
- package/plugins/ca-pi/CHANGELOG.md +165 -0
- package/plugins/ca-pi/SKILLS.md +1 -1
- package/plugins/ca-pi/SPRINT.md +40 -10
- package/plugins/ca-pi/agents/backend-author.md +1 -1
- package/plugins/ca-pi/agents/frontend-author.md +1 -1
- 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 +38 -0
- package/plugins/ca-pi/hooks/_approvallib.py +355 -0
- package/plugins/ca-pi/hooks/_artifactlib.py +855 -0
- package/plugins/ca-pi/hooks/_cleanuplib.py +1553 -0
- package/plugins/ca-pi/hooks/_hooklib.py +37 -13
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_intentlib.py +31 -0
- package/plugins/ca-pi/hooks/_planfilelib.py +34 -1
- package/plugins/ca-pi/hooks/_protectedstatelib.py +11 -1
- package/plugins/ca-pi/hooks/_readinjectlib.py +120 -0
- package/plugins/ca-pi/hooks/_releaselib.py +933 -85
- package/plugins/ca-pi/hooks/_standuplib.py +38 -10
- package/plugins/ca-pi/hooks/doctor.py +103 -0
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/pi-bridge.py +5 -1
- package/plugins/ca-pi/hooks/prompt-submit.py +33 -10
- package/plugins/ca-pi/hooks/releasehash.py +46 -32
- package/plugins/ca-pi/hooks/session-start.py +5 -4
- package/plugins/ca-pi/includes/artifacts.md +166 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +2 -1
- package/plugins/ca-pi/includes/farm.md +27 -4
- package/plugins/ca-pi/includes/routing-table.md +2 -2
- package/plugins/ca-pi/includes/verification-boundary.md +27 -0
- package/plugins/ca-pi/package.json +18 -0
- package/plugins/ca-pi/routines/INDEX.md +4 -4
- package/plugins/ca-pi/routines/brainstorming/SKILL.md +23 -4
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +30 -5
- package/plugins/ca-pi/routines/executing-plans/SKILL.md +52 -7
- package/plugins/ca-pi/routines/finishing-a-development-branch/SKILL.md +25 -3
- package/plugins/ca-pi/routines/refactor/SKILL.md +3 -2
- package/plugins/ca-pi/routines/release/SKILL.md +150 -44
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +53 -9
- package/plugins/ca-pi/routines/subagent-driven-development/references/farm-dispatch.md +20 -3
- package/plugins/ca-pi/routines/tdd/SKILL.md +27 -5
- package/plugins/ca-pi/routines/using-git-worktrees/SKILL.md +11 -2
- package/plugins/ca-pi/routines/writing-plans/SKILL.md +39 -10
- package/plugins/ca-pi/routines/writing-plans/references/farm-plan.md +34 -13
- package/plugins/ca-pi/skills/ca-chore/SKILL.md +7 -3
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +3 -2
- package/plugins/ca-pi/skills/ca-doctor/SKILL.md +14 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +42 -8
- package/plugins/ca-pi/skills/ca-refactor/SKILL.md +4 -2
- package/plugins/ca-pi/skills/ca-release/SKILL.md +7 -6
- package/plugins/ca-pi/skills/ca-standup/SKILL.md +36 -8
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Bounded stdlib bridge to an installation-pinned ca-artifact executable.
|
|
3
|
+
|
|
4
|
+
No imports perform I/O. No PATH search, environment-selected executable, schema
|
|
5
|
+
parser duplication, command execution from a document, or new host registration.
|
|
6
|
+
The caller supplies a trusted installation directory, never a repository setting.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import platform
|
|
15
|
+
import re
|
|
16
|
+
import stat
|
|
17
|
+
import subprocess
|
|
18
|
+
import tempfile
|
|
19
|
+
import threading
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
PROTOCOL = "codearbiter.artifact-api/0.1.0"
|
|
23
|
+
SCHEMA_VERSION = "0.3.1"
|
|
24
|
+
MAX_REQUEST = 8 << 20
|
|
25
|
+
MAX_RESPONSE = 65536
|
|
26
|
+
READ_OPERATIONS = frozenset({"capabilities", "schema", "read", "outline", "identity", "validate", "index", "snapshot", "diff", "repair-preview", "eligible", "farm-verify", "migration-preview"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ArtifactError(RuntimeError):
|
|
30
|
+
def __init__(self, code: str, message: str):
|
|
31
|
+
self.code = code
|
|
32
|
+
super().__init__(f"{code}: {message}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _object(pairs):
|
|
36
|
+
value = {}
|
|
37
|
+
for key, item in pairs:
|
|
38
|
+
if key in value:
|
|
39
|
+
raise ArtifactError("INVALID_RESPONSE", "duplicate JSON key")
|
|
40
|
+
value[key] = item
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _decode(raw: bytes) -> Any:
|
|
45
|
+
if len(raw) > MAX_RESPONSE:
|
|
46
|
+
raise ArtifactError("INVALID_RESPONSE", "oversized JSON envelope")
|
|
47
|
+
# Bound nesting independently of the interpreter's recursion-limit setting.
|
|
48
|
+
depth = 0
|
|
49
|
+
quoted = escaped = False
|
|
50
|
+
for byte in raw:
|
|
51
|
+
if quoted:
|
|
52
|
+
if escaped:
|
|
53
|
+
escaped = False
|
|
54
|
+
elif byte == 92:
|
|
55
|
+
escaped = True
|
|
56
|
+
elif byte == 34:
|
|
57
|
+
quoted = False
|
|
58
|
+
elif byte == 34:
|
|
59
|
+
quoted = True
|
|
60
|
+
elif byte in (91, 123):
|
|
61
|
+
depth += 1
|
|
62
|
+
if depth > 128:
|
|
63
|
+
raise ArtifactError("INVALID_RESPONSE", "JSON nesting exceeds 128 levels")
|
|
64
|
+
elif byte in (93, 125):
|
|
65
|
+
depth -= 1
|
|
66
|
+
try:
|
|
67
|
+
return json.loads(raw.decode("utf-8", "strict"), object_pairs_hook=_object,
|
|
68
|
+
parse_constant=lambda _: (_ for _ in ()).throw(ValueError("nonfinite")))
|
|
69
|
+
except (ValueError, UnicodeError, RecursionError) as exc:
|
|
70
|
+
raise ArtifactError("INVALID_RESPONSE", "invalid bounded JSON") from exc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def helper_installation(helper_file: str) -> Path:
|
|
74
|
+
"""Shipped helpers live in <plugin>/hooks; the trusted payload is a sibling.
|
|
75
|
+
|
|
76
|
+
Source-checkout callers use an explicit installation instead of guessing a
|
|
77
|
+
binary under the repository being governed.
|
|
78
|
+
"""
|
|
79
|
+
return Path(helper_file).resolve().parent.parent / "helpers" / "artifacts"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _open_pinned_regular(path: Path) -> int:
|
|
83
|
+
"""Open without following the final link and pin Windows replacement.
|
|
84
|
+
|
|
85
|
+
The Windows handle shares reads only, so a verified executable cannot be
|
|
86
|
+
replaced or opened for writing before CreateProcess consumes its path.
|
|
87
|
+
"""
|
|
88
|
+
if os.name != "nt":
|
|
89
|
+
flags = os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC | os.O_NONBLOCK
|
|
90
|
+
return os.open(path, flags)
|
|
91
|
+
import ctypes
|
|
92
|
+
import msvcrt
|
|
93
|
+
create_file = ctypes.WinDLL("kernel32", use_last_error=True).CreateFileW
|
|
94
|
+
create_file.argtypes = [ctypes.c_wchar_p, ctypes.c_uint32, ctypes.c_uint32,
|
|
95
|
+
ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32,
|
|
96
|
+
ctypes.c_void_p]
|
|
97
|
+
create_file.restype = ctypes.c_void_p
|
|
98
|
+
handle = create_file(str(path), 0x80000000, 0x00000001, None, 3,
|
|
99
|
+
0x00200000, None)
|
|
100
|
+
if handle == ctypes.c_void_p(-1).value:
|
|
101
|
+
raise OSError(ctypes.get_last_error(), "CreateFileW failed", str(path))
|
|
102
|
+
try:
|
|
103
|
+
return msvcrt.open_osfhandle(handle, os.O_RDONLY)
|
|
104
|
+
except BaseException:
|
|
105
|
+
ctypes.WinDLL("kernel32", use_last_error=True).CloseHandle(handle)
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _stage_darwin_executable(fd: int, directory: Path) -> Path:
|
|
110
|
+
"""Materialize verified descriptor bytes for macOS executable launch.
|
|
111
|
+
|
|
112
|
+
macOS exposes inherited descriptors below /dev/fd but does not permit
|
|
113
|
+
executing a native binary through that path. The caller supplies a fresh,
|
|
114
|
+
private temporary directory; no installation or repository pathname is
|
|
115
|
+
reopened while producing the executable image.
|
|
116
|
+
"""
|
|
117
|
+
target = directory / "ca-artifact"
|
|
118
|
+
output = os.open(
|
|
119
|
+
target,
|
|
120
|
+
os.O_WRONLY
|
|
121
|
+
| os.O_CREAT
|
|
122
|
+
| os.O_EXCL
|
|
123
|
+
| getattr(os, "O_CLOEXEC", 0)
|
|
124
|
+
| getattr(os, "O_NOFOLLOW", 0),
|
|
125
|
+
stat.S_IRUSR | stat.S_IWUSR,
|
|
126
|
+
)
|
|
127
|
+
try:
|
|
128
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
129
|
+
while chunk := os.read(fd, 1 << 20):
|
|
130
|
+
remaining = memoryview(chunk)
|
|
131
|
+
while remaining:
|
|
132
|
+
written = os.write(output, remaining)
|
|
133
|
+
if written <= 0:
|
|
134
|
+
raise OSError("short write while staging verified executable")
|
|
135
|
+
remaining = remaining[written:]
|
|
136
|
+
os.fsync(output)
|
|
137
|
+
os.fchmod(output, stat.S_IRUSR | stat.S_IXUSR)
|
|
138
|
+
finally:
|
|
139
|
+
os.close(output)
|
|
140
|
+
return target
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _trusted_directory(path: str | Path, code: str) -> Path:
|
|
144
|
+
requested = Path(path).absolute()
|
|
145
|
+
try:
|
|
146
|
+
if os.name == "nt":
|
|
147
|
+
import ctypes
|
|
148
|
+
attributes = ctypes.WinDLL("kernel32", use_last_error=True).GetFileAttributesW
|
|
149
|
+
attributes.argtypes = [ctypes.c_wchar_p]
|
|
150
|
+
attributes.restype = ctypes.c_uint32
|
|
151
|
+
current = requested
|
|
152
|
+
while current != current.parent:
|
|
153
|
+
value = attributes(str(current))
|
|
154
|
+
if value == 0xffffffff or value & 0x400:
|
|
155
|
+
raise OSError("reparse point or unreadable component")
|
|
156
|
+
current = current.parent
|
|
157
|
+
elif Path(os.path.realpath(requested)) != requested:
|
|
158
|
+
raise OSError("symlink component")
|
|
159
|
+
resolved = requested.resolve(strict=True)
|
|
160
|
+
if not resolved.is_dir():
|
|
161
|
+
raise OSError("not a directory")
|
|
162
|
+
return resolved
|
|
163
|
+
except OSError as exc:
|
|
164
|
+
raise ArtifactError(code, "directory must be an existing real path without links or reparse points") from exc
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _workflow_path_exists(root: Path, path: Path) -> bool:
|
|
168
|
+
"""Inspect one canonical artifact path without following links or writing."""
|
|
169
|
+
for parent in (root / ".codearbiter", path.parent):
|
|
170
|
+
try:
|
|
171
|
+
info = parent.lstat()
|
|
172
|
+
except FileNotFoundError:
|
|
173
|
+
return False
|
|
174
|
+
except OSError as exc:
|
|
175
|
+
raise ArtifactError(
|
|
176
|
+
"UNSAFE_ARTIFACT_PATH", "canonical artifact directory is unreadable"
|
|
177
|
+
) from exc
|
|
178
|
+
reparse = getattr(info, "st_file_attributes", 0) & getattr(
|
|
179
|
+
stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0
|
|
180
|
+
)
|
|
181
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISDIR(info.st_mode):
|
|
182
|
+
raise ArtifactError(
|
|
183
|
+
"UNSAFE_ARTIFACT_PATH",
|
|
184
|
+
"canonical artifact directory must be a real directory",
|
|
185
|
+
)
|
|
186
|
+
try:
|
|
187
|
+
info = path.lstat()
|
|
188
|
+
except FileNotFoundError:
|
|
189
|
+
return False
|
|
190
|
+
except OSError as exc:
|
|
191
|
+
raise ArtifactError(
|
|
192
|
+
"UNSAFE_ARTIFACT_PATH", "canonical artifact path is unreadable"
|
|
193
|
+
) from exc
|
|
194
|
+
reparse = getattr(info, "st_file_attributes", 0) & getattr(
|
|
195
|
+
stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0
|
|
196
|
+
)
|
|
197
|
+
if stat.S_ISLNK(info.st_mode) or reparse or not stat.S_ISREG(info.st_mode):
|
|
198
|
+
raise ArtifactError(
|
|
199
|
+
"UNSAFE_ARTIFACT_PATH", "canonical artifact path must be a regular file"
|
|
200
|
+
)
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _resolve_workflow_pair(root: str | Path, slug: str) -> dict[str, object]:
|
|
205
|
+
"""Select an exact-format spec/plan authority without parsing or writing it."""
|
|
206
|
+
trusted_root = _trusted_directory(root, "UNSAFE_ROOT")
|
|
207
|
+
if not isinstance(slug, str) or not re.fullmatch(r"[a-z][a-z0-9-]{0,99}", slug):
|
|
208
|
+
raise ArtifactError("INVALID_SLUG", "slug must match the canonical artifact slug grammar")
|
|
209
|
+
|
|
210
|
+
candidates = {
|
|
211
|
+
kind: {
|
|
212
|
+
extension: trusted_root / ".codearbiter" / f"{kind}s" / f"{slug}.{extension}"
|
|
213
|
+
for extension in ("md", "html")
|
|
214
|
+
}
|
|
215
|
+
for kind in ("spec", "plan")
|
|
216
|
+
}
|
|
217
|
+
present = {
|
|
218
|
+
kind: [
|
|
219
|
+
extension
|
|
220
|
+
for extension, path in paths.items()
|
|
221
|
+
if _workflow_path_exists(trusted_root, path)
|
|
222
|
+
]
|
|
223
|
+
for kind, paths in candidates.items()
|
|
224
|
+
}
|
|
225
|
+
if len(present["spec"]) > 1 or len(present["plan"]) > 1:
|
|
226
|
+
raise ArtifactError(
|
|
227
|
+
"AMBIGUOUS_ARTIFACT", "multiple canonical files claim the workflow slug"
|
|
228
|
+
)
|
|
229
|
+
if present["plan"] and not present["spec"]:
|
|
230
|
+
raise ArtifactError("ORPHAN_PLAN", "canonical plan has no matching spec")
|
|
231
|
+
if present["spec"] and present["plan"] and present["spec"] != present["plan"]:
|
|
232
|
+
raise ArtifactError(
|
|
233
|
+
"AMBIGUOUS_ARTIFACT", "spec and plan use different authority formats"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
extension = present["spec"][0] if present["spec"] else "html"
|
|
237
|
+
state = "pair" if present["plan"] else "spec" if present["spec"] else "absent"
|
|
238
|
+
return {
|
|
239
|
+
"slug": slug,
|
|
240
|
+
"format": extension,
|
|
241
|
+
"state": state,
|
|
242
|
+
"spec_path": candidates["spec"][extension],
|
|
243
|
+
"plan_path": candidates["plan"][extension],
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _require_authoring_capability(client: "ArtifactClient") -> None:
|
|
248
|
+
"""Fail one default-HTML route with a bounded repair diagnostic."""
|
|
249
|
+
try:
|
|
250
|
+
capabilities = client.call("capabilities")
|
|
251
|
+
except ArtifactError as exc:
|
|
252
|
+
raise ArtifactError(
|
|
253
|
+
"CAPABILITY_MISSING",
|
|
254
|
+
f"repair or reinstall the pinned artifact payload ({exc.code}); "
|
|
255
|
+
"new HTML work cannot fall back to Markdown",
|
|
256
|
+
) from exc
|
|
257
|
+
if (
|
|
258
|
+
not isinstance(capabilities, dict)
|
|
259
|
+
or capabilities.get("repository_operations_available") is not True
|
|
260
|
+
or capabilities.get("host_default_enabled") is not True
|
|
261
|
+
):
|
|
262
|
+
raise ArtifactError(
|
|
263
|
+
"CAPABILITY_MISSING",
|
|
264
|
+
"repair or reinstall the qualified default-on artifact payload; "
|
|
265
|
+
"new HTML work cannot fall back to Markdown",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _select_authoring_route(
|
|
270
|
+
root: str | Path,
|
|
271
|
+
slug: str,
|
|
272
|
+
*,
|
|
273
|
+
workflow: str,
|
|
274
|
+
lane: str,
|
|
275
|
+
client: object | None = None,
|
|
276
|
+
html_requested: bool = False,
|
|
277
|
+
) -> dict[str, object]:
|
|
278
|
+
"""Select inline or exact-format authoring without creating artifacts."""
|
|
279
|
+
if workflow not in {"feature", "sprint"}:
|
|
280
|
+
raise ArtifactError("INVALID_WORKFLOW", "workflow must be feature or sprint")
|
|
281
|
+
if lane not in {"small", "full"}:
|
|
282
|
+
raise ArtifactError("INVALID_LANE", "lane must be small or full")
|
|
283
|
+
if type(html_requested) is not bool:
|
|
284
|
+
raise ArtifactError(
|
|
285
|
+
"INVALID_ROUTE", "html_requested must be an explicit boolean"
|
|
286
|
+
)
|
|
287
|
+
# Retain the rollout-era keyword for internal caller compatibility. It no
|
|
288
|
+
# longer opts an absent full-lane slug back into Markdown authority.
|
|
289
|
+
if lane == "small":
|
|
290
|
+
return {
|
|
291
|
+
"workflow": workflow,
|
|
292
|
+
"lane": lane,
|
|
293
|
+
"mode": "inline",
|
|
294
|
+
"slug": slug,
|
|
295
|
+
"spec_path": None,
|
|
296
|
+
"plan_path": None,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
selected = _resolve_workflow_pair(root, slug)
|
|
300
|
+
if selected["format"] == "html":
|
|
301
|
+
trusted_root = Path(selected["spec_path"]).parent.parent.parent
|
|
302
|
+
if type(client) is not ArtifactClient or client.root != trusted_root:
|
|
303
|
+
raise ArtifactError(
|
|
304
|
+
"CAPABILITY_MISSING",
|
|
305
|
+
"an installation-pinned artifact client bound to this repository is required",
|
|
306
|
+
)
|
|
307
|
+
_require_authoring_capability(client)
|
|
308
|
+
if _resolve_workflow_pair(root, slug) != selected:
|
|
309
|
+
raise ArtifactError(
|
|
310
|
+
"STALE_ROUTE",
|
|
311
|
+
"artifact namespace changed during the capability probe",
|
|
312
|
+
)
|
|
313
|
+
return {"workflow": workflow, "lane": lane, "mode": "artifact", **selected}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _preflight_plan_authoring(
|
|
317
|
+
selected: dict[str, object],
|
|
318
|
+
client: object,
|
|
319
|
+
*,
|
|
320
|
+
spec_artifact_id: str,
|
|
321
|
+
spec_normative_sha256: str,
|
|
322
|
+
) -> Path:
|
|
323
|
+
"""Verify one approved HTML spec and return its unwritten plan target."""
|
|
324
|
+
if not isinstance(selected, dict):
|
|
325
|
+
raise ArtifactError("INVALID_ROUTE", "plan authoring requires a selected route")
|
|
326
|
+
slug = selected.get("slug")
|
|
327
|
+
spec_path = selected.get("spec_path")
|
|
328
|
+
plan_path = selected.get("plan_path")
|
|
329
|
+
if (
|
|
330
|
+
selected.get("mode") != "artifact"
|
|
331
|
+
or selected.get("format") != "html"
|
|
332
|
+
or selected.get("state") != "spec"
|
|
333
|
+
or not isinstance(slug, str)
|
|
334
|
+
or not isinstance(spec_path, Path)
|
|
335
|
+
or not isinstance(plan_path, Path)
|
|
336
|
+
):
|
|
337
|
+
raise ArtifactError(
|
|
338
|
+
"INVALID_ROUTE",
|
|
339
|
+
"plan authoring requires one existing HTML spec and no plan",
|
|
340
|
+
)
|
|
341
|
+
root = spec_path.parent.parent.parent
|
|
342
|
+
current = _resolve_workflow_pair(root, slug)
|
|
343
|
+
if any(
|
|
344
|
+
current.get(field) != selected.get(field)
|
|
345
|
+
for field in ("slug", "format", "state", "spec_path", "plan_path")
|
|
346
|
+
):
|
|
347
|
+
raise ArtifactError("STALE_ROUTE", "selected artifact route is no longer current")
|
|
348
|
+
if (
|
|
349
|
+
spec_path != root / ".codearbiter" / "specs" / f"{slug}.html"
|
|
350
|
+
or plan_path != root / ".codearbiter" / "plans" / f"{slug}.html"
|
|
351
|
+
):
|
|
352
|
+
raise ArtifactError("INVALID_ROUTE", "plan target must match the HTML spec slug")
|
|
353
|
+
if type(client) is not ArtifactClient or client.root != root:
|
|
354
|
+
raise ArtifactError(
|
|
355
|
+
"CAPABILITY_MISSING",
|
|
356
|
+
"an installation-pinned artifact client must be bound to the selected repository",
|
|
357
|
+
)
|
|
358
|
+
if not isinstance(spec_artifact_id, str) or not spec_artifact_id:
|
|
359
|
+
raise ArtifactError("AUTHORITY_MISMATCH", "spec artifact identity is required")
|
|
360
|
+
if not isinstance(spec_normative_sha256, str) or not re.fullmatch(
|
|
361
|
+
r"[0-9a-f]{64}", spec_normative_sha256
|
|
362
|
+
):
|
|
363
|
+
raise ArtifactError("AUTHORITY_MISMATCH", "spec normative digest is invalid")
|
|
364
|
+
|
|
365
|
+
identity = client.call("identity", {"artifact_id": spec_artifact_id})
|
|
366
|
+
expected_path = f".codearbiter/specs/{slug}.html"
|
|
367
|
+
if (
|
|
368
|
+
identity.get("artifact_id") != spec_artifact_id
|
|
369
|
+
or identity.get("kind") != "spec"
|
|
370
|
+
or identity.get("path") != expected_path
|
|
371
|
+
or identity.get("normative_sha256") != spec_normative_sha256
|
|
372
|
+
):
|
|
373
|
+
raise ArtifactError(
|
|
374
|
+
"AUTHORITY_MISMATCH",
|
|
375
|
+
"caller spec identity does not match the selected current HTML spec",
|
|
376
|
+
)
|
|
377
|
+
approved = client.call(
|
|
378
|
+
"validate",
|
|
379
|
+
{"artifact_id": spec_artifact_id, "gate": "approved"},
|
|
380
|
+
permit_invalid=True,
|
|
381
|
+
)
|
|
382
|
+
authority = approved.get("authority")
|
|
383
|
+
if (
|
|
384
|
+
approved.get("valid") is not True
|
|
385
|
+
or approved.get("artifact_id") != spec_artifact_id
|
|
386
|
+
or approved.get("normative_sha256") != spec_normative_sha256
|
|
387
|
+
or not isinstance(authority, dict)
|
|
388
|
+
or authority.get("state") != "approved"
|
|
389
|
+
or authority.get("authority_verified") is not True
|
|
390
|
+
):
|
|
391
|
+
raise ArtifactError(
|
|
392
|
+
"AUTHORITY_UNVERIFIED",
|
|
393
|
+
"plan authoring requires the exact current ready and approved spec",
|
|
394
|
+
)
|
|
395
|
+
if _resolve_workflow_pair(root, slug) != current:
|
|
396
|
+
raise ArtifactError(
|
|
397
|
+
"STALE_ROUTE",
|
|
398
|
+
"artifact namespace changed during plan-authoring preflight",
|
|
399
|
+
)
|
|
400
|
+
return plan_path
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _preflight_current_acceptance(
|
|
404
|
+
selected: dict[str, object],
|
|
405
|
+
client: object,
|
|
406
|
+
*,
|
|
407
|
+
spec_artifact_id: str,
|
|
408
|
+
plan_artifact_id: str,
|
|
409
|
+
) -> dict[str, object]:
|
|
410
|
+
"""Return engine-bound proof that one exact HTML pair is currently accepted.
|
|
411
|
+
|
|
412
|
+
This is a private workflow boundary, not a public command. Caller-provided
|
|
413
|
+
labels, hashes, state, or receipts are deliberately outside its input shape.
|
|
414
|
+
"""
|
|
415
|
+
route_fields = {"slug", "format", "state", "spec_path", "plan_path"}
|
|
416
|
+
if not isinstance(selected, dict) or set(selected) != route_fields:
|
|
417
|
+
raise ArtifactError(
|
|
418
|
+
"INVALID_ROUTE",
|
|
419
|
+
"current acceptance requires an exact resolver-produced route",
|
|
420
|
+
)
|
|
421
|
+
slug = selected.get("slug")
|
|
422
|
+
spec_path = selected.get("spec_path")
|
|
423
|
+
plan_path = selected.get("plan_path")
|
|
424
|
+
if (
|
|
425
|
+
selected.get("format") != "html"
|
|
426
|
+
or selected.get("state") != "pair"
|
|
427
|
+
or not isinstance(slug, str)
|
|
428
|
+
or not isinstance(spec_path, Path)
|
|
429
|
+
or not isinstance(plan_path, Path)
|
|
430
|
+
):
|
|
431
|
+
raise ArtifactError(
|
|
432
|
+
"INVALID_ROUTE", "current acceptance requires one complete HTML pair"
|
|
433
|
+
)
|
|
434
|
+
root = spec_path.parent.parent.parent
|
|
435
|
+
if (
|
|
436
|
+
spec_path != root / ".codearbiter" / "specs" / f"{slug}.html"
|
|
437
|
+
or plan_path != root / ".codearbiter" / "plans" / f"{slug}.html"
|
|
438
|
+
):
|
|
439
|
+
raise ArtifactError(
|
|
440
|
+
"INVALID_ROUTE", "selected paths must be the canonical same-slug HTML pair"
|
|
441
|
+
)
|
|
442
|
+
current = _resolve_workflow_pair(root, slug)
|
|
443
|
+
if any(current.get(field) != selected.get(field) for field in route_fields):
|
|
444
|
+
raise ArtifactError(
|
|
445
|
+
"STALE_ROUTE", "selected HTML pair is no longer the sole current authority"
|
|
446
|
+
)
|
|
447
|
+
if type(client) is not ArtifactClient or client.root != root:
|
|
448
|
+
raise ArtifactError(
|
|
449
|
+
"CAPABILITY_MISSING",
|
|
450
|
+
"an installation-pinned artifact client must be bound to the selected repository",
|
|
451
|
+
)
|
|
452
|
+
if not all(
|
|
453
|
+
isinstance(value, str) and re.fullmatch(r"[A-Z][A-Z0-9_-]{0,127}", value)
|
|
454
|
+
for value in (spec_artifact_id, plan_artifact_id)
|
|
455
|
+
):
|
|
456
|
+
raise ArtifactError(
|
|
457
|
+
"AUTHORITY_MISMATCH", "exact spec and plan artifact identities are required"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
expected = {
|
|
461
|
+
"spec": f".codearbiter/specs/{slug}.html",
|
|
462
|
+
"plan": f".codearbiter/plans/{slug}.html",
|
|
463
|
+
}
|
|
464
|
+
entries = {}
|
|
465
|
+
for kind, artifact_id in (
|
|
466
|
+
("spec", spec_artifact_id),
|
|
467
|
+
("plan", plan_artifact_id),
|
|
468
|
+
):
|
|
469
|
+
matches = [entry for entry in client.index(kind) if entry.get("path") == expected[kind]]
|
|
470
|
+
if len(matches) != 1 or matches[0].get("artifact_id") != artifact_id:
|
|
471
|
+
raise ArtifactError(
|
|
472
|
+
"AUTHORITY_MISMATCH",
|
|
473
|
+
f"caller {kind} identity does not match the selected HTML pair",
|
|
474
|
+
)
|
|
475
|
+
entries[kind] = matches[0]
|
|
476
|
+
|
|
477
|
+
identities = {}
|
|
478
|
+
for kind, artifact_id in (
|
|
479
|
+
("spec", spec_artifact_id),
|
|
480
|
+
("plan", plan_artifact_id),
|
|
481
|
+
):
|
|
482
|
+
identity = client.call("identity", {"artifact_id": artifact_id})
|
|
483
|
+
authority = identity.get("authority")
|
|
484
|
+
if (
|
|
485
|
+
identity.get("artifact_id") != artifact_id
|
|
486
|
+
or identity.get("kind") != kind
|
|
487
|
+
or identity.get("path") != expected[kind]
|
|
488
|
+
or identity.get("model_sha256") != entries[kind].get("model_sha256")
|
|
489
|
+
or identity.get("normative_sha256")
|
|
490
|
+
!= entries[kind].get("normative_sha256")
|
|
491
|
+
or not isinstance(authority, dict)
|
|
492
|
+
or authority.get("state") != "approved"
|
|
493
|
+
or authority.get("authority_verified") is not True
|
|
494
|
+
):
|
|
495
|
+
raise ArtifactError(
|
|
496
|
+
"AUTHORITY_UNVERIFIED",
|
|
497
|
+
f"selected {kind} lacks exact current workflow approval",
|
|
498
|
+
)
|
|
499
|
+
identities[kind] = identity
|
|
500
|
+
|
|
501
|
+
eligible = client.call("eligible", {"artifact_id": plan_artifact_id})
|
|
502
|
+
if (
|
|
503
|
+
eligible.get("artifact_id") != plan_artifact_id
|
|
504
|
+
or eligible.get("model_sha256") != identities["plan"].get("model_sha256")
|
|
505
|
+
or eligible.get("all_accepted_and_current") is not True
|
|
506
|
+
or not isinstance(eligible.get("tasks"), list)
|
|
507
|
+
or not eligible["tasks"]
|
|
508
|
+
or any(
|
|
509
|
+
not isinstance(task, dict)
|
|
510
|
+
or task.get("state") != "ACCEPTED"
|
|
511
|
+
or task.get("current_evidence") is not True
|
|
512
|
+
for task in eligible["tasks"]
|
|
513
|
+
)
|
|
514
|
+
):
|
|
515
|
+
raise ArtifactError(
|
|
516
|
+
"ACCEPTANCE_REQUIRED",
|
|
517
|
+
"the exact selected HTML plan is not fully accepted with current evidence",
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
task_ids = [
|
|
521
|
+
record.get("id")
|
|
522
|
+
for record in client.outline(plan_artifact_id)
|
|
523
|
+
if record.get("kind") == "tasks" and record.get("retired") is False
|
|
524
|
+
]
|
|
525
|
+
if not task_ids or any(not isinstance(task_id, str) for task_id in task_ids):
|
|
526
|
+
raise ArtifactError("INVALID_RESPONSE", "accepted plan has no live task identity")
|
|
527
|
+
pages = list(client.contextual_pages(plan_artifact_id, task_ids[0], 65536))
|
|
528
|
+
if not pages:
|
|
529
|
+
raise ArtifactError("INVALID_RESPONSE", "engine returned no acceptance context")
|
|
530
|
+
records = {
|
|
531
|
+
item.get("key"): item.get("record")
|
|
532
|
+
for page in pages
|
|
533
|
+
for item in page.get("items", [])
|
|
534
|
+
if isinstance(item, dict)
|
|
535
|
+
}
|
|
536
|
+
plan_context = records.get(f"{plan_artifact_id}#@context")
|
|
537
|
+
spec_context = records.get(f"{spec_artifact_id}#@context")
|
|
538
|
+
spec_ref = plan_context.get("spec_ref") if isinstance(plan_context, dict) else None
|
|
539
|
+
if (
|
|
540
|
+
not isinstance(spec_context, dict)
|
|
541
|
+
or not isinstance(spec_ref, dict)
|
|
542
|
+
or spec_ref.get("artifact_id") != spec_artifact_id
|
|
543
|
+
or spec_ref.get("normative_sha256")
|
|
544
|
+
!= identities["spec"].get("normative_sha256")
|
|
545
|
+
or spec_ref.get("binding_mode") != "approved_source"
|
|
546
|
+
):
|
|
547
|
+
raise ArtifactError(
|
|
548
|
+
"AUTHORITY_MISMATCH",
|
|
549
|
+
"selected plan is not bound to the exact approved HTML spec",
|
|
550
|
+
)
|
|
551
|
+
final_page = pages[-1]
|
|
552
|
+
receipts = {
|
|
553
|
+
"context_ticket": final_page.get("context_ticket"),
|
|
554
|
+
"vector_sha256": final_page.get("vector_sha256"),
|
|
555
|
+
"manifest_sha256": final_page.get("manifest_sha256"),
|
|
556
|
+
"input_sha256": eligible.get("input_sha256"),
|
|
557
|
+
}
|
|
558
|
+
if any(
|
|
559
|
+
not isinstance(value, str) or not re.fullmatch(r"[0-9a-f]{64}", value)
|
|
560
|
+
for value in receipts.values()
|
|
561
|
+
):
|
|
562
|
+
raise ArtifactError(
|
|
563
|
+
"INVALID_RESPONSE", "engine acceptance proof lacks bounded receipt identities"
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
final_spec = client.call("identity", {"artifact_id": spec_artifact_id})
|
|
567
|
+
final_plan = client.call("identity", {"artifact_id": plan_artifact_id})
|
|
568
|
+
final_eligible = client.call("eligible", {"artifact_id": plan_artifact_id})
|
|
569
|
+
if (
|
|
570
|
+
final_spec.get("model_sha256") != identities["spec"].get("model_sha256")
|
|
571
|
+
or final_plan.get("model_sha256") != identities["plan"].get("model_sha256")
|
|
572
|
+
or final_eligible != eligible
|
|
573
|
+
):
|
|
574
|
+
raise ArtifactError(
|
|
575
|
+
"STALE_ROUTE", "artifact identity or acceptance changed during preflight"
|
|
576
|
+
)
|
|
577
|
+
final_route = _resolve_workflow_pair(root, slug)
|
|
578
|
+
if final_route != selected:
|
|
579
|
+
raise ArtifactError(
|
|
580
|
+
"STALE_ROUTE", "selected HTML pair changed during acceptance preflight"
|
|
581
|
+
)
|
|
582
|
+
return {
|
|
583
|
+
"spec_identity": identities["spec"],
|
|
584
|
+
"plan_identity": identities["plan"],
|
|
585
|
+
"eligible": eligible,
|
|
586
|
+
"receipts": receipts,
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _bounded_child(argv: list[str], request: bytes, fd: int, timeout: float) -> tuple[int, bytes, bytes]:
|
|
591
|
+
options = dict(stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
592
|
+
close_fds=True, start_new_session=True, env={})
|
|
593
|
+
if os.name != "nt":
|
|
594
|
+
options["pass_fds"] = (fd,)
|
|
595
|
+
process = subprocess.Popen(argv, **options)
|
|
596
|
+
buffers = [bytearray(), bytearray()]
|
|
597
|
+
overflow = threading.Event()
|
|
598
|
+
|
|
599
|
+
def drain(pipe, slot, limit):
|
|
600
|
+
try:
|
|
601
|
+
while True:
|
|
602
|
+
chunk = pipe.read(4096)
|
|
603
|
+
if not chunk:
|
|
604
|
+
return
|
|
605
|
+
if len(buffers[slot]) + len(chunk) > limit:
|
|
606
|
+
overflow.set()
|
|
607
|
+
process.kill()
|
|
608
|
+
return
|
|
609
|
+
buffers[slot].extend(chunk)
|
|
610
|
+
finally:
|
|
611
|
+
pipe.close()
|
|
612
|
+
|
|
613
|
+
def write():
|
|
614
|
+
try:
|
|
615
|
+
process.stdin.write(request)
|
|
616
|
+
process.stdin.close()
|
|
617
|
+
except (BrokenPipeError, OSError):
|
|
618
|
+
pass
|
|
619
|
+
|
|
620
|
+
threads = [threading.Thread(target=drain, args=(process.stdout, 0, MAX_RESPONSE), daemon=True),
|
|
621
|
+
threading.Thread(target=drain, args=(process.stderr, 1, 8192), daemon=True),
|
|
622
|
+
threading.Thread(target=write, daemon=True)]
|
|
623
|
+
for thread in threads:
|
|
624
|
+
thread.start()
|
|
625
|
+
timed_out = False
|
|
626
|
+
try:
|
|
627
|
+
process.wait(timeout=timeout)
|
|
628
|
+
except subprocess.TimeoutExpired:
|
|
629
|
+
timed_out = True
|
|
630
|
+
process.kill()
|
|
631
|
+
process.wait()
|
|
632
|
+
for thread in threads:
|
|
633
|
+
thread.join(timeout=2)
|
|
634
|
+
if timed_out:
|
|
635
|
+
raise ArtifactError("TIMEOUT", "subprocess interrupted; reconcile mutation operation_id before retry")
|
|
636
|
+
if overflow.is_set() or any(t.is_alive() for t in threads):
|
|
637
|
+
raise ArtifactError("RESPONSE_TOO_LARGE", "subprocess output exceeded its contract")
|
|
638
|
+
return process.returncode, bytes(buffers[0]), bytes(buffers[1])
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class ArtifactClient:
|
|
642
|
+
def __init__(self, root: str | Path, installation: str | Path, *, timeout: float = 30):
|
|
643
|
+
self.root = _trusted_directory(root, "UNSAFE_ROOT")
|
|
644
|
+
self.installation = _trusted_directory(installation, "CAPABILITY_MISSING")
|
|
645
|
+
self.timeout = timeout
|
|
646
|
+
|
|
647
|
+
def _open_binary(self) -> int:
|
|
648
|
+
system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}.get(platform.system())
|
|
649
|
+
arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}.get(platform.machine().lower())
|
|
650
|
+
if system is None or arch is None:
|
|
651
|
+
raise ArtifactError("UNSUPPORTED_PLATFORM", "unsupported architecture")
|
|
652
|
+
manifest_path = self.installation / "release.json"
|
|
653
|
+
try:
|
|
654
|
+
mf = _open_pinned_regular(manifest_path)
|
|
655
|
+
try:
|
|
656
|
+
info = os.fstat(mf)
|
|
657
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > 65536:
|
|
658
|
+
raise ArtifactError("INVALID_INSTALLATION", "release manifest must be a bounded regular file")
|
|
659
|
+
raw = os.read(mf, 65537)
|
|
660
|
+
if len(raw) > 65536:
|
|
661
|
+
raise ArtifactError("INVALID_INSTALLATION", "oversized release manifest")
|
|
662
|
+
finally:
|
|
663
|
+
os.close(mf)
|
|
664
|
+
try:
|
|
665
|
+
manifest = _decode(raw)
|
|
666
|
+
except ArtifactError as exc:
|
|
667
|
+
raise ArtifactError("INVALID_INSTALLATION", "invalid release manifest JSON") from exc
|
|
668
|
+
if not isinstance(manifest, dict) or set(manifest) != {"format", "version", "protocol", "schema_version", "binaries"}:
|
|
669
|
+
raise ArtifactError("INVALID_INSTALLATION", "unexpected release manifest")
|
|
670
|
+
if manifest["format"] != "codearbiter.artifact-release/0.1.0" or manifest["protocol"] != PROTOCOL or manifest["schema_version"] != SCHEMA_VERSION:
|
|
671
|
+
raise ArtifactError("UNSUPPORTED_VERSION", "installed binary/adapter version mismatch")
|
|
672
|
+
if not isinstance(manifest["binaries"], dict) or not isinstance(manifest["version"], str):
|
|
673
|
+
raise ArtifactError("INVALID_INSTALLATION", "malformed release manifest fields")
|
|
674
|
+
entry = manifest["binaries"].get(f"{system}/{arch}")
|
|
675
|
+
if not isinstance(entry, dict) or set(entry) != {"file", "sha256", "native_tested"}:
|
|
676
|
+
raise ArtifactError("CAPABILITY_MISSING", "no matching tested binary in the installation")
|
|
677
|
+
if entry["native_tested"] is not True:
|
|
678
|
+
raise ArtifactError("UNVERIFIED_PLATFORM", "cross-compilation is not native qualification")
|
|
679
|
+
name = entry["file"]
|
|
680
|
+
expected_name = f"ca-artifact-{system}-{arch}" + (".exe" if system == "windows" else "")
|
|
681
|
+
if name != expected_name:
|
|
682
|
+
raise ArtifactError("INVALID_INSTALLATION", "binary filename does not match the selected platform")
|
|
683
|
+
if not isinstance(entry["sha256"], str) or not re.fullmatch(r"[0-9a-f]{64}", entry["sha256"]):
|
|
684
|
+
raise ArtifactError("INVALID_INSTALLATION", "binary digest is malformed")
|
|
685
|
+
binary_path = self.installation / name
|
|
686
|
+
fd = _open_pinned_regular(binary_path)
|
|
687
|
+
except OSError as exc:
|
|
688
|
+
raise ArtifactError("CAPABILITY_MISSING", "install the pinned artifact payload; no PATH fallback is permitted") from exc
|
|
689
|
+
try:
|
|
690
|
+
info = os.fstat(fd)
|
|
691
|
+
if (not stat.S_ISREG(info.st_mode) or info.st_size > 32 << 20
|
|
692
|
+
or (system != "windows" and not info.st_mode & stat.S_IXUSR)):
|
|
693
|
+
raise ArtifactError("INVALID_INSTALLATION", "binary is not a bounded executable regular file")
|
|
694
|
+
digest = hashlib.sha256()
|
|
695
|
+
while chunk := os.read(fd, 1 << 20):
|
|
696
|
+
digest.update(chunk)
|
|
697
|
+
if digest.hexdigest() != entry["sha256"]:
|
|
698
|
+
raise ArtifactError("PACKAGE_INTEGRITY", "installed executable bytes differ from release manifest")
|
|
699
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
700
|
+
header = os.read(fd, 4)
|
|
701
|
+
magic = {"linux":(b"\x7fELF",), "darwin":(b"\xcf\xfa\xed\xfe",b"\xca\xfe\xba\xbe",b"\xbe\xba\xfe\xca"), "windows":(b"MZ",)}[system]
|
|
702
|
+
if not header.startswith(magic):
|
|
703
|
+
raise ArtifactError("INVALID_INSTALLATION", "unexpected native executable format")
|
|
704
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
705
|
+
return fd
|
|
706
|
+
except BaseException:
|
|
707
|
+
os.close(fd)
|
|
708
|
+
raise
|
|
709
|
+
|
|
710
|
+
def call(self, operation: str, request: dict[str, Any] | None = None, *, permit_invalid: bool = False) -> dict:
|
|
711
|
+
if not re.fullmatch(r"[a-z][a-z-]{0,39}", operation):
|
|
712
|
+
raise ArtifactError("UNKNOWN_OPERATION", "invalid internal operation name")
|
|
713
|
+
request = dict(request or {})
|
|
714
|
+
if "protocol" in request and request["protocol"] != PROTOCOL:
|
|
715
|
+
raise ArtifactError("UNSUPPORTED_VERSION", "request protocol mismatch")
|
|
716
|
+
request["protocol"] = PROTOCOL
|
|
717
|
+
try:
|
|
718
|
+
raw = json.dumps(request, ensure_ascii=True, allow_nan=False, separators=(",", ":")).encode()
|
|
719
|
+
except (TypeError, ValueError) as exc:
|
|
720
|
+
raise ArtifactError("INVALID_REQUEST", "request must contain finite JSON values") from exc
|
|
721
|
+
if len(raw) > MAX_REQUEST:
|
|
722
|
+
raise ArtifactError("REQUEST_TOO_LARGE", "request exceeds 8 MiB")
|
|
723
|
+
fd = self._open_binary()
|
|
724
|
+
try:
|
|
725
|
+
# Execute the verified open descriptor, not a raceable pathname.
|
|
726
|
+
if platform.system() == "Linux":
|
|
727
|
+
executable = f"/proc/self/fd/{fd}"
|
|
728
|
+
elif platform.system() == "Darwin":
|
|
729
|
+
with tempfile.TemporaryDirectory(prefix="ca-artifact-exec-") as temporary:
|
|
730
|
+
try:
|
|
731
|
+
directory = Path(os.path.realpath(temporary))
|
|
732
|
+
executable = _stage_darwin_executable(fd, directory)
|
|
733
|
+
os.chmod(directory, stat.S_IRUSR | stat.S_IXUSR)
|
|
734
|
+
code, stdout, stderr = _bounded_child(
|
|
735
|
+
[str(executable), operation, "--root", str(self.root), "--request", "-"],
|
|
736
|
+
raw, fd, self.timeout)
|
|
737
|
+
except OSError as exc:
|
|
738
|
+
raise ArtifactError(
|
|
739
|
+
"CAPABILITY_MISSING",
|
|
740
|
+
"could not stage the verified macOS artifact executable",
|
|
741
|
+
) from exc
|
|
742
|
+
finally:
|
|
743
|
+
os.chmod(directory, stat.S_IRWXU)
|
|
744
|
+
executable = None
|
|
745
|
+
else:
|
|
746
|
+
executable = str(self.installation / self._binary_name())
|
|
747
|
+
if executable is not None:
|
|
748
|
+
code, stdout, stderr = _bounded_child(
|
|
749
|
+
[executable, operation, "--root", str(self.root), "--request", "-"],
|
|
750
|
+
raw, fd, self.timeout)
|
|
751
|
+
finally:
|
|
752
|
+
os.close(fd)
|
|
753
|
+
result = _decode(stdout)
|
|
754
|
+
if not isinstance(result, dict) or result.get("protocol") != PROTOCOL or result.get("operation") != operation or not isinstance(result.get("ok"), bool):
|
|
755
|
+
raise ArtifactError("INVALID_RESPONSE", "subprocess response does not match request")
|
|
756
|
+
if not result["ok"]:
|
|
757
|
+
error = result.get("error")
|
|
758
|
+
if (set(result) != {"protocol", "operation", "ok", "error"}
|
|
759
|
+
or not isinstance(error, dict)
|
|
760
|
+
or not {"code", "message"} <= set(error)
|
|
761
|
+
or set(error) - {"code", "message", "symbol", "field", "operation_id"}
|
|
762
|
+
or any(not isinstance(v, str) for v in error.values())
|
|
763
|
+
or not re.fullmatch(r"[A-Z][A-Z0-9_]{0,79}", error["code"])
|
|
764
|
+
or code == 0):
|
|
765
|
+
raise ArtifactError("INVALID_RESPONSE", "malformed failure envelope")
|
|
766
|
+
raise ArtifactError(error["code"], error["message"][:512])
|
|
767
|
+
if set(result) != {"protocol", "operation", "ok", "result"} or not isinstance(result.get("result"), dict):
|
|
768
|
+
raise ArtifactError("INVALID_RESPONSE", "malformed success envelope")
|
|
769
|
+
if code != 0 and not (permit_invalid and operation == "validate" and code == 2 and result["result"].get("valid") is False):
|
|
770
|
+
raise ArtifactError("SUBPROCESS_FAILED", f"operation returned exit {code}")
|
|
771
|
+
if not isinstance(result.get("result"), dict):
|
|
772
|
+
raise ArtifactError("INVALID_RESPONSE", "subprocess result must be an object")
|
|
773
|
+
return result["result"]
|
|
774
|
+
|
|
775
|
+
@staticmethod
|
|
776
|
+
def _binary_name() -> str:
|
|
777
|
+
system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}[platform.system()]
|
|
778
|
+
arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}[platform.machine().lower()]
|
|
779
|
+
return f"ca-artifact-{system}-{arch}" + (".exe" if system == "windows" else "")
|
|
780
|
+
|
|
781
|
+
@staticmethod
|
|
782
|
+
def _check_page(page: dict, offset: int, field: str):
|
|
783
|
+
values = page.get(field)
|
|
784
|
+
if not isinstance(values, list) or "next_offset" not in page:
|
|
785
|
+
raise ArtifactError("INVALID_RESPONSE", "malformed bounded page")
|
|
786
|
+
next_offset = page["next_offset"]
|
|
787
|
+
if next_offset is not None and (type(next_offset) is not int or next_offset <= offset or not values):
|
|
788
|
+
raise ArtifactError("INVALID_RESPONSE", "pagination must make forward progress")
|
|
789
|
+
|
|
790
|
+
def index(self, kind: str | None = None):
|
|
791
|
+
request = {"budget": 16384}
|
|
792
|
+
if kind:
|
|
793
|
+
request["kind"] = kind
|
|
794
|
+
for _ in range(1025):
|
|
795
|
+
page = self.call("index", request)
|
|
796
|
+
self._check_page(page, request.get("offset", 0), "entries")
|
|
797
|
+
yield from page["entries"]
|
|
798
|
+
if page["next_offset"] is None:
|
|
799
|
+
return
|
|
800
|
+
request.update(offset=page["next_offset"], catalog_sha256=page["catalog_sha256"])
|
|
801
|
+
raise ArtifactError("INVALID_RESPONSE", "index did not terminate")
|
|
802
|
+
|
|
803
|
+
def outline(self, artifact_id: str):
|
|
804
|
+
request = {"artifact_id": artifact_id, "budget": 16384}
|
|
805
|
+
for _ in range(8193):
|
|
806
|
+
page = self.call("outline", request)
|
|
807
|
+
self._check_page(page, request.get("offset", 0), "records")
|
|
808
|
+
yield from page["records"]
|
|
809
|
+
if page["next_offset"] is None:
|
|
810
|
+
return
|
|
811
|
+
request.update(offset=page["next_offset"], model_sha256=page["model_sha256"])
|
|
812
|
+
raise ArtifactError("INVALID_RESPONSE", "outline did not terminate")
|
|
813
|
+
|
|
814
|
+
def contextual_pages(self, artifact_id: str, symbol: str, budget: int = 16384):
|
|
815
|
+
request = {"artifact_id": artifact_id, "symbol": symbol, "mode": "contextual", "budget": budget}
|
|
816
|
+
for _ in range(8193):
|
|
817
|
+
page = self.call("read", request)
|
|
818
|
+
complete = page.get("context_complete")
|
|
819
|
+
if type(complete) is not bool:
|
|
820
|
+
raise ArtifactError("INVALID_RESPONSE", "context page lacks a completeness flag")
|
|
821
|
+
if not complete and (not page.get("next_cursor") or page["next_cursor"] == request.get("cursor")):
|
|
822
|
+
raise ArtifactError("INCOMPLETE_CONTEXT", "incomplete context must have an advancing continuation")
|
|
823
|
+
yield page
|
|
824
|
+
if complete:
|
|
825
|
+
return
|
|
826
|
+
request["cursor"] = page["next_cursor"]
|
|
827
|
+
raise ArtifactError("INVALID_RESPONSE", "context did not terminate")
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def has_html(root: str | Path) -> bool:
|
|
831
|
+
root = Path(root)
|
|
832
|
+
return any(
|
|
833
|
+
next((root / ".codearbiter" / kind).glob("*.html"), None) is not None for kind in ("specs", "plans"))
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def checked_spec_index(root, installation) -> list[dict]:
|
|
837
|
+
"""Fresh per-call authority validation, deliberately not an mtime cache."""
|
|
838
|
+
if not has_html(root):
|
|
839
|
+
return []
|
|
840
|
+
client = ArtifactClient(root, installation)
|
|
841
|
+
return [{"spec": x["artifact_id"], "path": x["path"], "globs": x.get("governs") or [], "artifact_html": True}
|
|
842
|
+
for x in client.index("spec") if x["authority"]["authority_verified"] and x.get("governs")]
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def resolve_spec_file(filename, installation) -> tuple[ArtifactClient, dict]:
|
|
846
|
+
path = Path(filename).absolute()
|
|
847
|
+
if path.suffix != ".html" or path.parent.name != "specs" or path.parent.parent.name != ".codearbiter":
|
|
848
|
+
raise ArtifactError("INVALID_PATH", "expected canonical HTML spec path")
|
|
849
|
+
root = path.parent.parent.parent
|
|
850
|
+
client = ArtifactClient(root, installation)
|
|
851
|
+
relative = path.relative_to(root).as_posix()
|
|
852
|
+
matches = [x for x in client.index("spec") if x["path"] == relative]
|
|
853
|
+
if len(matches) != 1:
|
|
854
|
+
raise ArtifactError("SPEC_NOT_FOUND", "canonical path did not resolve uniquely")
|
|
855
|
+
return client, matches[0]
|