@arbiterforge/ca-pi 0.15.12 → 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 +26 -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 +206 -2
- package/plugins/ca-pi/hooks/_artifactlib.py +166 -2
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
- package/plugins/ca-pi/hooks/_bashguardlib.py +14 -0
- 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 +1 -0
- 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/artifact-authority.py +20 -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/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/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/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
|
@@ -8,6 +8,9 @@ run_verification(...) -> dict
|
|
|
8
8
|
observe_codex_hook(...) -> dict
|
|
9
9
|
observe_claude_hook(...) -> dict
|
|
10
10
|
publish_request(...) -> dict
|
|
11
|
+
arm_context_preview(...) -> dict
|
|
12
|
+
cancel_context_preview(...) -> dict
|
|
13
|
+
capture_context_preview(...) -> dict
|
|
11
14
|
|
|
12
15
|
The artifact engine owns every target binding. Callers select only an activity
|
|
13
16
|
and record; they cannot supply an authority kind, verdict, evidence payload, or
|
|
@@ -45,6 +48,8 @@ import time
|
|
|
45
48
|
from typing import Any
|
|
46
49
|
|
|
47
50
|
from _gitexec import git_executable, root_bound_git_env
|
|
51
|
+
import _artifactpromptlib
|
|
52
|
+
import _artifactlib
|
|
48
53
|
|
|
49
54
|
|
|
50
55
|
OBSERVATION_DIR = Path(".codearbiter/.artifacts/observations")
|
|
@@ -79,6 +84,15 @@ MAX_COMPLETION_FILE_BYTES = 64 << 10
|
|
|
79
84
|
MAX_COMPLETION_TOTAL_BYTES = 256 << 10
|
|
80
85
|
REGISTRY_PARENT = Path(tempfile.gettempdir())
|
|
81
86
|
HOSTS = frozenset({"codex", "claude"})
|
|
87
|
+
CONTEXT_ROUTE = "context_approval"
|
|
88
|
+
CONTEXT_PREVIEW_KEYS = frozenset({
|
|
89
|
+
"operation_id", "mode", "document_id", "target_path", "typed",
|
|
90
|
+
"selections", "expected_document_sha256", "expected_provenance_sha256",
|
|
91
|
+
"proposed_provenance",
|
|
92
|
+
})
|
|
93
|
+
CONTEXT_FINALIZE_KEYS = frozenset({
|
|
94
|
+
"operation_id", "mode", "document_id", "target_path", "expected",
|
|
95
|
+
})
|
|
82
96
|
CLAUDE_REVIEWER = "ca:authority-reviewer"
|
|
83
97
|
# Claude Code 2.1.286 children report through this tool; their SubagentStop
|
|
84
98
|
# then carries no last_assistant_message.
|
|
@@ -124,6 +138,40 @@ def _canonical(value: Any) -> bytes:
|
|
|
124
138
|
raise AuthorityError("INVALID_AUTHORITY_STATE", "state is not finite JSON") from exc
|
|
125
139
|
|
|
126
140
|
|
|
141
|
+
def _native_context_canonical(value: Any) -> bytes:
|
|
142
|
+
"""Match the artifact engine's safe-integer, UTF-16-key-order JSON bytes."""
|
|
143
|
+
def ordered(item: Any, depth: int) -> Any:
|
|
144
|
+
if depth > 64:
|
|
145
|
+
raise ValueError("context exceeds native depth")
|
|
146
|
+
if isinstance(item, dict):
|
|
147
|
+
if any(type(key) is not str for key in item):
|
|
148
|
+
raise ValueError("context has a non-string key")
|
|
149
|
+
return {
|
|
150
|
+
key: ordered(item[key], depth + 1)
|
|
151
|
+
for key in sorted(item, key=lambda key: key.encode("utf-16-be"))
|
|
152
|
+
}
|
|
153
|
+
if isinstance(item, list):
|
|
154
|
+
return [ordered(value, depth + 1) for value in item]
|
|
155
|
+
if type(item) is int:
|
|
156
|
+
if abs(item) > 9007199254740991:
|
|
157
|
+
raise ValueError("context integer exceeds native range")
|
|
158
|
+
return item
|
|
159
|
+
if item is None or type(item) in (str, bool):
|
|
160
|
+
return item
|
|
161
|
+
raise ValueError("context value is outside native JSON")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
raw = json.dumps(
|
|
165
|
+
ordered(value, 0), ensure_ascii=False, allow_nan=False,
|
|
166
|
+
separators=(",", ":"),
|
|
167
|
+
).encode("utf-8")
|
|
168
|
+
if len(raw) > 8 << 20:
|
|
169
|
+
raise ValueError("context exceeds native byte limit")
|
|
170
|
+
return raw
|
|
171
|
+
except (TypeError, ValueError, UnicodeError, RecursionError) as exc:
|
|
172
|
+
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context is outside native canonical JSON") from exc
|
|
173
|
+
|
|
174
|
+
|
|
127
175
|
def _digest(data: bytes) -> str:
|
|
128
176
|
return hashlib.sha256(data).hexdigest()
|
|
129
177
|
|
|
@@ -479,7 +527,7 @@ def _read_context(root: Path, context_ref: str, context_hash: str) -> dict[str,
|
|
|
479
527
|
context = json.loads(raw.decode("utf-8"))
|
|
480
528
|
except (OSError, UnicodeError, ValueError) as exc:
|
|
481
529
|
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context bytes are unavailable") from exc
|
|
482
|
-
if not isinstance(context, dict) or _digest(raw) != context_hash or raw !=
|
|
530
|
+
if not isinstance(context, dict) or _digest(raw) != context_hash or raw != _native_context_canonical(context):
|
|
483
531
|
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "context bytes changed or are not canonical")
|
|
484
532
|
return context
|
|
485
533
|
|
|
@@ -2097,7 +2145,7 @@ def capture_user_prompt(
|
|
|
2097
2145
|
raise AuthorityError("INVALID_EVIDENCE_CONTEXT", "prompt context is unavailable") from exc
|
|
2098
2146
|
if (
|
|
2099
2147
|
_digest(context_raw) != context_sha256
|
|
2100
|
-
or context_raw !=
|
|
2148
|
+
or context_raw != _native_context_canonical(context)
|
|
2101
2149
|
or context.get("activity") != event["kind"]
|
|
2102
2150
|
or context.get("subject") != subject
|
|
2103
2151
|
or context.get("prompt_sha256") != prompt_sha256
|
|
@@ -2139,6 +2187,162 @@ def capture_user_prompt(
|
|
|
2139
2187
|
}
|
|
2140
2188
|
|
|
2141
2189
|
|
|
2190
|
+
def _context_preview_context(root: Path, client: Any, preview: dict[str, Any], prompt: str):
|
|
2191
|
+
operation = ("context-finalize-evidence-context" if preview.get("mode") == "finalize"
|
|
2192
|
+
else "context-evidence-context")
|
|
2193
|
+
result = client.call(operation, {**preview, "prompt_sha256": _digest(prompt.encode("utf-8"))})
|
|
2194
|
+
if not isinstance(result, dict):
|
|
2195
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "context preview issuer returned no context")
|
|
2196
|
+
context_hash = _validate_hash(result.get("context_sha256"), "context_sha256")
|
|
2197
|
+
context_ref = f".codearbiter/.artifacts/evidence-contexts/{context_hash}.json"
|
|
2198
|
+
if result.get("context_ref") != context_ref:
|
|
2199
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "context preview locator is malformed")
|
|
2200
|
+
try:
|
|
2201
|
+
raw = (root / context_ref).read_bytes()
|
|
2202
|
+
context = json.loads(raw.decode("utf-8"))
|
|
2203
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
2204
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "context preview issuer bytes are unavailable") from exc
|
|
2205
|
+
binding = result.get("preview_binding_sha256")
|
|
2206
|
+
_validate_hash(binding, "preview_binding_sha256")
|
|
2207
|
+
subject = {"artifact_id": "CONTEXT-" + preview["document_id"].upper(),
|
|
2208
|
+
"record_id": "CONTEXT-" + preview["document_id"].upper(),
|
|
2209
|
+
"normative_sha256": binding}
|
|
2210
|
+
if (
|
|
2211
|
+
_digest(raw) != context_hash or raw != _canonical(context)
|
|
2212
|
+
or context.get("activity") != CONTEXT_ROUTE
|
|
2213
|
+
or context.get("preview") != preview
|
|
2214
|
+
or context.get("subject") != subject
|
|
2215
|
+
or context.get("record") != {"preview_binding_sha256": binding}
|
|
2216
|
+
or context.get("record_sha256") != _digest(_canonical(context.get("record")))
|
|
2217
|
+
or context.get("input_sha256") != binding
|
|
2218
|
+
or context.get("prompt_sha256") != _digest(prompt.encode("utf-8"))
|
|
2219
|
+
or result.get("subject") != subject
|
|
2220
|
+
or not isinstance(context.get("preview_document"), str)
|
|
2221
|
+
or not context["preview_document"]
|
|
2222
|
+
or _digest(context["preview_document"].encode("utf-8")) != context.get("after_document_sha256")
|
|
2223
|
+
or result.get("preview_document") != context["preview_document"]
|
|
2224
|
+
or result.get("after_document_sha256") != context["after_document_sha256"]
|
|
2225
|
+
):
|
|
2226
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "issued context does not bind the exact preview")
|
|
2227
|
+
return context, context_ref, context_hash, binding
|
|
2228
|
+
|
|
2229
|
+
|
|
2230
|
+
def arm_context_preview(root: str | Path, client: Any, preview: dict[str, Any], *, token: str | None = None) -> dict[str, Any]:
|
|
2231
|
+
"""Arm an exact preview reply; this step does not assert approval."""
|
|
2232
|
+
root = _real_root(root)
|
|
2233
|
+
valid_content = (isinstance(preview, dict) and set(preview) == CONTEXT_PREVIEW_KEYS
|
|
2234
|
+
and isinstance(preview.get("mode"), str)
|
|
2235
|
+
and preview.get("mode") in {"create", "adopt", "update"}
|
|
2236
|
+
and isinstance(preview.get("document_id"), str)
|
|
2237
|
+
and preview.get("document_id") in {"CONTEXT", "tech-stack", "coding-standards", "security-controls", "code-map"})
|
|
2238
|
+
valid_final = (isinstance(preview, dict) and set(preview) == CONTEXT_FINALIZE_KEYS
|
|
2239
|
+
and preview.get("mode") == "finalize" and preview.get("document_id") == "CONTEXT"
|
|
2240
|
+
and preview.get("target_path") == ".codearbiter/CONTEXT.md"
|
|
2241
|
+
and isinstance(preview.get("expected"), dict))
|
|
2242
|
+
if not (valid_content or valid_final):
|
|
2243
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "context preview request contains unsupported or authority fields")
|
|
2244
|
+
token = token or secrets.token_urlsafe(24)
|
|
2245
|
+
if TOKEN_RE.fullmatch(token) is None:
|
|
2246
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "context preview token is invalid")
|
|
2247
|
+
artifact_id = "CONTEXT-" + preview["document_id"].upper()
|
|
2248
|
+
prompt = f"approve-context {artifact_id} {token}"
|
|
2249
|
+
context, context_ref, context_hash, binding = _context_preview_context(root, client, preview, prompt)
|
|
2250
|
+
_artifactpromptlib.register(root, CONTEXT_ROUTE, artifact_id, prompt, binding_sha256=context_hash)
|
|
2251
|
+
return {"reply": prompt, "artifact_id": artifact_id, "preview_binding_sha256": binding,
|
|
2252
|
+
"context_ref": context_ref, "context_sha256": context_hash,
|
|
2253
|
+
"target_path": preview["target_path"], "preview_document": context["preview_document"],
|
|
2254
|
+
"after_document_sha256": context["after_document_sha256"]}
|
|
2255
|
+
|
|
2256
|
+
|
|
2257
|
+
def cancel_context_preview(root: str | Path, document_id: str) -> dict[str, Any]:
|
|
2258
|
+
"""Retire one pending context reply without altering a document or receipt."""
|
|
2259
|
+
if document_id not in {"CONTEXT", "tech-stack", "coding-standards", "security-controls", "code-map"}:
|
|
2260
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "unknown context document")
|
|
2261
|
+
artifact_id = "CONTEXT-" + document_id.upper()
|
|
2262
|
+
_artifactpromptlib.unregister(_real_root(root), CONTEXT_ROUTE, artifact_id)
|
|
2263
|
+
return {"artifact_id": artifact_id, "cancelled": True}
|
|
2264
|
+
|
|
2265
|
+
|
|
2266
|
+
def capture_context_preview(root: str | Path, client: Any, prompt: str, *, host: str, session_id: str) -> dict[str, Any]:
|
|
2267
|
+
"""Capture an exact host UserPromptSubmit reply for one armed context preview."""
|
|
2268
|
+
root = _real_root(root)
|
|
2269
|
+
if not isinstance(prompt, str):
|
|
2270
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "context preview reply is malformed")
|
|
2271
|
+
parts = prompt.split(" ")
|
|
2272
|
+
if (len(parts) != 3 or parts[0] != "approve-context" or ID_RE.fullmatch(parts[1]) is None
|
|
2273
|
+
or TOKEN_RE.fullmatch(parts[2]) is None):
|
|
2274
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "context preview reply is unrelated")
|
|
2275
|
+
artifact_id = parts[1]
|
|
2276
|
+
routed = _artifactpromptlib.resolve(CONTEXT_ROUTE, prompt)
|
|
2277
|
+
if routed != root:
|
|
2278
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "context preview reply has no armed route")
|
|
2279
|
+
context_hash = _artifactpromptlib.binding(root, CONTEXT_ROUTE, artifact_id)
|
|
2280
|
+
context_ref = f".codearbiter/.artifacts/evidence-contexts/{context_hash}.json"
|
|
2281
|
+
try:
|
|
2282
|
+
raw = (root / context_ref).read_bytes()
|
|
2283
|
+
context = json.loads(raw.decode("utf-8"))
|
|
2284
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
2285
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "armed preview context is unavailable") from exc
|
|
2286
|
+
preview = context.get("preview") if isinstance(context, dict) else None
|
|
2287
|
+
if (not isinstance(preview, dict) or set(preview) not in
|
|
2288
|
+
(CONTEXT_PREVIEW_KEYS, CONTEXT_FINALIZE_KEYS)):
|
|
2289
|
+
raise AuthorityError("INVALID_CONTEXT_AUTHORITY", "armed preview context is malformed")
|
|
2290
|
+
fresh, fresh_ref, fresh_hash, binding = _context_preview_context(root, client, preview, prompt)
|
|
2291
|
+
if fresh_ref != context_ref or fresh_hash != context_hash or fresh != context or _digest(raw) != context_hash:
|
|
2292
|
+
raise AuthorityError("STALE_CONTEXT_AUTHORITY", "context preview changed after it was armed")
|
|
2293
|
+
producer_run_id = f"{_host_id(host, 'host')}:{_host_id(session_id, 'session_id')}"
|
|
2294
|
+
producer_result = {"host": host, "session_id": session_id,
|
|
2295
|
+
"prompt_sha256": _digest(prompt.encode("utf-8"))}
|
|
2296
|
+
payload = {"preview_binding_sha256": binding}
|
|
2297
|
+
observation = {
|
|
2298
|
+
"format": OBSERVATION_FORMAT, "kind": CONTEXT_ROUTE,
|
|
2299
|
+
"subject": context["subject"], "context_ref": context_ref,
|
|
2300
|
+
"context_sha256": context_hash, "payload_sha256": _digest(_canonical(payload)),
|
|
2301
|
+
"producer_profile": "host-user-context-preview/0.1.0", "producer_run_id": producer_run_id,
|
|
2302
|
+
"producer_result": producer_result, "producer_result_sha256": _digest(_canonical(producer_result)),
|
|
2303
|
+
}
|
|
2304
|
+
observation_raw = _canonical(observation)
|
|
2305
|
+
observation_hash = _digest(observation_raw)
|
|
2306
|
+
observation_ref = OBSERVATION_DIR / f"{observation_hash}.json"
|
|
2307
|
+
_publish_immutable(root, observation_ref, observation_raw)
|
|
2308
|
+
event = {
|
|
2309
|
+
"format": EVENT_FORMAT, "kind": CONTEXT_ROUTE, "authority_kind": "user_workflow",
|
|
2310
|
+
"subject": context["subject"], "actor": "interactive repository user",
|
|
2311
|
+
"origin": f"{host}:UserPromptSubmit:{session_id}", "verdict": "approved",
|
|
2312
|
+
"payload": payload, "source_text": prompt,
|
|
2313
|
+
"observation_ref": observation_ref.as_posix(), "observation_sha256": observation_hash,
|
|
2314
|
+
}
|
|
2315
|
+
event_raw = _canonical(event)
|
|
2316
|
+
event_hash = _digest(event_raw)
|
|
2317
|
+
source_ref = SOURCE_DIR / f"{event_hash}.json"
|
|
2318
|
+
_publish_immutable(root, source_ref, event_raw)
|
|
2319
|
+
captured = client.call("capture-observation", {"source_ref": source_ref.as_posix(), "source_sha256": event_hash})
|
|
2320
|
+
if not isinstance(captured, dict) or not isinstance(captured.get("receipt"), str):
|
|
2321
|
+
raise AuthorityError("INVALID_RECEIPT", "context preview capture returned no receipt")
|
|
2322
|
+
_artifactpromptlib.unregister(root, CONTEXT_ROUTE, artifact_id)
|
|
2323
|
+
return {**captured, "preview_binding_sha256": binding,
|
|
2324
|
+
"authority_source": source_ref.as_posix(), "observation_ref": observation_ref.as_posix()}
|
|
2325
|
+
|
|
2326
|
+
|
|
2327
|
+
def capture_context_preview_from_hook(*, root: str | Path, plugin_root: str | Path,
|
|
2328
|
+
prompt: str, host: str, session_id: str) -> str:
|
|
2329
|
+
"""Route only an armed exact context reply from the host prompt seam."""
|
|
2330
|
+
if not isinstance(prompt, str) or not prompt.startswith("approve-context "):
|
|
2331
|
+
return ""
|
|
2332
|
+
try:
|
|
2333
|
+
routed = _artifactpromptlib.resolve(CONTEXT_ROUTE, prompt)
|
|
2334
|
+
if routed is None:
|
|
2335
|
+
return ""
|
|
2336
|
+
client = _artifactlib.ArtifactClient(
|
|
2337
|
+
routed, Path(plugin_root) / "helpers" / "artifacts"
|
|
2338
|
+
)
|
|
2339
|
+
result = capture_context_preview(routed, client, prompt, host=host, session_id=session_id)
|
|
2340
|
+
except (AuthorityError, _artifactpromptlib.PromptRouteError, OSError, RuntimeError) as exc:
|
|
2341
|
+
return f"codeArbiter: context preview approval capture failed: {exc}"
|
|
2342
|
+
return ("codeArbiter: context preview approval recorded for "
|
|
2343
|
+
f"{result['preview_binding_sha256']} (receipt {result['receipt']}).")
|
|
2344
|
+
|
|
2345
|
+
|
|
2142
2346
|
def run_verification(
|
|
2143
2347
|
root: str | Path,
|
|
2144
2348
|
client: Any,
|
|
@@ -79,6 +79,106 @@ def helper_installation(helper_file: str) -> Path:
|
|
|
79
79
|
return Path(helper_file).resolve().parent.parent / "helpers" / "artifacts"
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def read_context_preview_request(root: str | Path, request_file: str | Path) -> dict[str, Any]:
|
|
83
|
+
"""Read a bounded inert preview request from this repository only."""
|
|
84
|
+
repository = _trusted_directory(root, "UNSAFE_ROOT")
|
|
85
|
+
candidate = Path(request_file)
|
|
86
|
+
if not candidate.is_absolute():
|
|
87
|
+
candidate = repository / candidate
|
|
88
|
+
try:
|
|
89
|
+
relative = candidate.resolve(strict=True).relative_to(repository)
|
|
90
|
+
if not relative.parts:
|
|
91
|
+
raise ValueError("request names the repository root")
|
|
92
|
+
fd = _open_pinned_regular(candidate)
|
|
93
|
+
try:
|
|
94
|
+
info = os.fstat(fd)
|
|
95
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size > (1 << 20):
|
|
96
|
+
raise ArtifactError("INVALID_CONTEXT_REQUEST", "preview request is not a bounded regular file")
|
|
97
|
+
raw = os.read(fd, (1 << 20) + 1)
|
|
98
|
+
finally:
|
|
99
|
+
os.close(fd)
|
|
100
|
+
value = _decode(raw)
|
|
101
|
+
except (OSError, ValueError) as exc:
|
|
102
|
+
raise ArtifactError("INVALID_CONTEXT_REQUEST", "preview request is unavailable or outside the repository") from exc
|
|
103
|
+
if not isinstance(value, dict):
|
|
104
|
+
raise ArtifactError("INVALID_CONTEXT_REQUEST", "preview request must be an object")
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
_CONTEXT_TARGETS = {
|
|
109
|
+
"CONTEXT": ".codearbiter/CONTEXT.md",
|
|
110
|
+
"tech-stack": ".codearbiter/tech-stack.md",
|
|
111
|
+
"coding-standards": ".codearbiter/coding-standards.md",
|
|
112
|
+
"security-controls": ".codearbiter/security-controls.md",
|
|
113
|
+
"code-map": ".codearbiter/code-map.md",
|
|
114
|
+
}
|
|
115
|
+
_CONTEXT_PREVIEW_KEYS = frozenset({
|
|
116
|
+
"operation_id", "mode", "document_id", "target_path", "typed",
|
|
117
|
+
"selections", "expected_document_sha256", "expected_provenance_sha256",
|
|
118
|
+
"proposed_provenance",
|
|
119
|
+
})
|
|
120
|
+
_CONTEXT_FINALIZE_KEYS = frozenset({
|
|
121
|
+
"operation_id", "mode", "document_id", "target_path", "expected",
|
|
122
|
+
})
|
|
123
|
+
_CONTEXT_FINALIZE_PATHS = frozenset({
|
|
124
|
+
*(_CONTEXT_TARGETS.values()),
|
|
125
|
+
*(f".codearbiter/.provenance/{name}.json" for name in _CONTEXT_TARGETS),
|
|
126
|
+
".codearbiter/open-tasks.md", ".codearbiter/open-questions.md",
|
|
127
|
+
".codearbiter/overrides.log",
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def context_writer_qualified() -> bool:
|
|
132
|
+
"""Inspect this shipped package's actual context writer, without caller flags."""
|
|
133
|
+
installation = helper_installation(__file__)
|
|
134
|
+
try:
|
|
135
|
+
client = ArtifactClient(installation.parent.parent, installation)
|
|
136
|
+
client.require_context_workflow()
|
|
137
|
+
except ArtifactError:
|
|
138
|
+
return False
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def validate_context_writer_request(request: dict[str, Any]) -> tuple[str, dict]:
|
|
143
|
+
"""Parse only the finite preview or receipt request for the native workflow."""
|
|
144
|
+
if not isinstance(request, dict):
|
|
145
|
+
raise ArtifactError("INVALID_CONTEXT_REQUEST", "context request must be an object")
|
|
146
|
+
operation = request.get("operation")
|
|
147
|
+
if operation == "preview" and set(request) == {"operation", "preview"}:
|
|
148
|
+
preview = request["preview"]
|
|
149
|
+
mode = preview.get("mode") if isinstance(preview, dict) else None
|
|
150
|
+
document_id = preview.get("document_id") if isinstance(preview, dict) else None
|
|
151
|
+
regular = (isinstance(preview, dict) and set(preview) == _CONTEXT_PREVIEW_KEYS
|
|
152
|
+
and isinstance(mode, str) and mode in {"create", "adopt", "update"}
|
|
153
|
+
and isinstance(document_id, str)
|
|
154
|
+
and _CONTEXT_TARGETS.get(document_id) == preview.get("target_path"))
|
|
155
|
+
final = (isinstance(preview, dict) and set(preview) == _CONTEXT_FINALIZE_KEYS
|
|
156
|
+
and isinstance(mode, str) and mode == "finalize" and document_id == "CONTEXT"
|
|
157
|
+
and preview.get("target_path") == _CONTEXT_TARGETS["CONTEXT"]
|
|
158
|
+
and isinstance(preview.get("expected"), dict)
|
|
159
|
+
and set(preview["expected"]) == _CONTEXT_FINALIZE_PATHS
|
|
160
|
+
and all(isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value)
|
|
161
|
+
for value in preview["expected"].values()))
|
|
162
|
+
if not (regular or final):
|
|
163
|
+
raise ArtifactError("INVALID_CONTEXT_REQUEST", "preview must name one bounded context target")
|
|
164
|
+
elif operation == "apply" and set(request) == {"operation", "receipt"}:
|
|
165
|
+
receipt = request["receipt"]
|
|
166
|
+
if (not isinstance(receipt, str) or re.fullmatch(
|
|
167
|
+
r"\.codearbiter/\.artifacts/receipts/[0-9a-f]{64}\.json", receipt) is None):
|
|
168
|
+
raise ArtifactError("INVALID_CONTEXT_REQUEST", "apply requires one receipt reference")
|
|
169
|
+
else:
|
|
170
|
+
raise ArtifactError("INVALID_CONTEXT_REQUEST", "select a closed preview or receipt apply request")
|
|
171
|
+
|
|
172
|
+
return ("preview", preview) if operation == "preview" else ("apply", {"receipt": receipt})
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def context_writer_client(root: str | Path) -> "ArtifactClient":
|
|
176
|
+
"""Construct the installed native writer only after strict request validation."""
|
|
177
|
+
client = ArtifactClient(root, helper_installation(__file__))
|
|
178
|
+
client.require_context_workflow()
|
|
179
|
+
return client
|
|
180
|
+
|
|
181
|
+
|
|
82
182
|
def _open_pinned_regular(path: Path) -> int:
|
|
83
183
|
"""Open without following the final link and pin Windows replacement.
|
|
84
184
|
|
|
@@ -304,7 +404,7 @@ def _installed_workflow_preflight(installation: Path) -> dict[str, object]:
|
|
|
304
404
|
return result
|
|
305
405
|
|
|
306
406
|
for filename in ("_approvallib.py", "_prerequisitelib.py", "_sprintapprovallib.py",
|
|
307
|
-
"_reconciliationlib.py", "_artifactauthoritylib.py", "_gitexec.py", "_replylib.py",
|
|
407
|
+
"_reconciliationlib.py", "_artifactauthoritylib.py", "_artifactpromptlib.py", "_gitexec.py", "_replylib.py",
|
|
308
408
|
"artifact-authority.py", "artifact-authority-hook.py", "prompt-submit.py"):
|
|
309
409
|
resource("hooks/" + filename)
|
|
310
410
|
if host == "claude":
|
|
@@ -866,6 +966,48 @@ def _bounded_child(argv: list[str], request: bytes, fd: int | None, timeout: flo
|
|
|
866
966
|
return process.returncode, bytes(buffers[0]), bytes(buffers[1])
|
|
867
967
|
|
|
868
968
|
|
|
969
|
+
_CONTEXT_NATIVE_CASES = [
|
|
970
|
+
"TestContextBoundedRender", "TestContextDocumentContract",
|
|
971
|
+
"TestContextMarkdownPreservation", "TestContextMutationAdmission",
|
|
972
|
+
"TestContextRecoveryAndPaths", "TestContextRepresentationBoundary",
|
|
973
|
+
]
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _context_release_qualified(installation: Path) -> bool:
|
|
977
|
+
"""Require exact context evidence added by the native receipt assembler."""
|
|
978
|
+
system = {"Linux": "linux", "Darwin": "darwin", "Windows": "windows"}.get(platform.system())
|
|
979
|
+
arch = {"x86_64": "amd64", "amd64": "amd64", "aarch64": "arm64", "arm64": "arm64"}.get(platform.machine().lower())
|
|
980
|
+
if system is None or arch is None:
|
|
981
|
+
return False
|
|
982
|
+
try:
|
|
983
|
+
fd = _open_pinned_regular(installation / "release.json")
|
|
984
|
+
try:
|
|
985
|
+
if os.fstat(fd).st_size > 65_536:
|
|
986
|
+
return False
|
|
987
|
+
manifest = _decode(os.read(fd, 65_537))
|
|
988
|
+
finally:
|
|
989
|
+
os.close(fd)
|
|
990
|
+
entry = manifest["binaries"][f"{system}/{arch}"]
|
|
991
|
+
record = manifest["context_qualification"][f"{system}/{arch}"]
|
|
992
|
+
return (
|
|
993
|
+
isinstance(record, dict)
|
|
994
|
+
and set(record) == {"source_commit", "workflow", "run_id", "binary_sha256",
|
|
995
|
+
"observation_sha256", "module_sha256", "native_cases"}
|
|
996
|
+
and isinstance(record["source_commit"], str)
|
|
997
|
+
and re.fullmatch(r"[0-9a-f]{40}", record["source_commit"]) is not None
|
|
998
|
+
and record["workflow"] == ".github/workflows/ci.yml"
|
|
999
|
+
and isinstance(record["run_id"], str)
|
|
1000
|
+
and re.fullmatch(r"[1-9][0-9]*", record["run_id"]) is not None
|
|
1001
|
+
and record["binary_sha256"] == entry["sha256"]
|
|
1002
|
+
and all(isinstance(record[field], str)
|
|
1003
|
+
and re.fullmatch(r"[0-9a-f]{64}", record[field]) is not None
|
|
1004
|
+
for field in ("observation_sha256", "module_sha256"))
|
|
1005
|
+
and record["native_cases"] == _CONTEXT_NATIVE_CASES
|
|
1006
|
+
)
|
|
1007
|
+
except (ArtifactError, OSError, KeyError, TypeError, ValueError):
|
|
1008
|
+
return False
|
|
1009
|
+
|
|
1010
|
+
|
|
869
1011
|
class ArtifactClient:
|
|
870
1012
|
def __init__(self, root: str | Path, installation: str | Path, *, timeout: float = 30):
|
|
871
1013
|
self.root = _trusted_directory(root, "UNSAFE_ROOT")
|
|
@@ -876,6 +1018,24 @@ class ArtifactClient:
|
|
|
876
1018
|
"""Read fresh installed prerequisites; never attest live host authority."""
|
|
877
1019
|
return _installed_workflow_preflight(self.installation)
|
|
878
1020
|
|
|
1021
|
+
def require_context_workflow(self) -> dict[str, object]:
|
|
1022
|
+
"""Admit only the packaged native context candidate on a supported host."""
|
|
1023
|
+
installed = self.workflow_preflight()
|
|
1024
|
+
capability = self.call("capabilities")
|
|
1025
|
+
context = capability.get("repository_context")
|
|
1026
|
+
if (installed.get("resources_available") is not True
|
|
1027
|
+
or installed.get("host") not in {"codex", "claude"}
|
|
1028
|
+
or not isinstance(context, dict)
|
|
1029
|
+
or set(context) != {"source_present", "kind_registered", "host_default_enabled", "qualification", "mutation_available"}
|
|
1030
|
+
or context["source_present"] is not True
|
|
1031
|
+
or context["kind_registered"] is not True
|
|
1032
|
+
or context["host_default_enabled"] is not False
|
|
1033
|
+
or context["qualification"] != "package-required"
|
|
1034
|
+
or context["mutation_available"] is not True
|
|
1035
|
+
or not _context_release_qualified(self.installation)):
|
|
1036
|
+
raise ArtifactError("CONTEXT_WORKFLOW_UNAVAILABLE", "installed repository-context workflow is not qualified")
|
|
1037
|
+
return {**context, "host_default_enabled": True, "qualification": "packaged-candidate"}
|
|
1038
|
+
|
|
879
1039
|
def _open_binary(self) -> int:
|
|
880
1040
|
system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}.get(platform.system())
|
|
881
1041
|
arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}.get(platform.machine().lower())
|
|
@@ -897,7 +1057,9 @@ class ArtifactClient:
|
|
|
897
1057
|
manifest = _decode(raw)
|
|
898
1058
|
except ArtifactError as exc:
|
|
899
1059
|
raise ArtifactError("INVALID_INSTALLATION", "invalid release manifest JSON") from exc
|
|
900
|
-
if not isinstance(manifest, dict) or set(manifest)
|
|
1060
|
+
if not isinstance(manifest, dict) or set(manifest) not in (
|
|
1061
|
+
{"format", "version", "protocol", "schema_version", "binaries"},
|
|
1062
|
+
{"format", "version", "protocol", "schema_version", "binaries", "context_qualification"}):
|
|
901
1063
|
raise ArtifactError("INVALID_INSTALLATION", "unexpected release manifest")
|
|
902
1064
|
if manifest["format"] != "codearbiter.artifact-release/0.1.0" or manifest["protocol"] != PROTOCOL or manifest["schema_version"] != SCHEMA_VERSION:
|
|
903
1065
|
raise ArtifactError("UNSUPPORTED_VERSION", "installed binary/adapter version mismatch")
|
|
@@ -942,6 +1104,8 @@ class ArtifactClient:
|
|
|
942
1104
|
def call(self, operation: str, request: dict[str, Any] | None = None, *, permit_invalid: bool = False) -> dict:
|
|
943
1105
|
if not re.fullmatch(r"[a-z][a-z-]{0,39}", operation):
|
|
944
1106
|
raise ArtifactError("UNKNOWN_OPERATION", "invalid internal operation name")
|
|
1107
|
+
if operation in {"context-evidence-context", "context-finalize-evidence-context", "context-apply"}:
|
|
1108
|
+
self.require_context_workflow()
|
|
945
1109
|
request = dict(request or {})
|
|
946
1110
|
if "protocol" in request and request["protocol"] != PROTOCOL:
|
|
947
1111
|
raise ArtifactError("UNSUPPORTED_VERSION", "request protocol mismatch")
|
|
@@ -22,6 +22,91 @@ class PromptRouteError(RuntimeError):
|
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
_FEATURE_ACTORS = frozenset({
|
|
26
|
+
"backend-author", "frontend-author", "infra-author",
|
|
27
|
+
"spec-reviewer", "quality-reviewer", "security-reviewer",
|
|
28
|
+
"auth-crypto-reviewer", "dependency-reviewer", "migration-reviewer",
|
|
29
|
+
"coverage-auditor",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def compose_feature_actor_input(*, actor: str, brief: str,
|
|
34
|
+
spec_id: str | None, plan_id: str | None,
|
|
35
|
+
task_id: str, task_paths,
|
|
36
|
+
code_map, provenance, commands, constraints,
|
|
37
|
+
effective_instructions, evidence_ids,
|
|
38
|
+
command_bindings=None, observations=None,
|
|
39
|
+
confirmed_mini_spec: str | None = None):
|
|
40
|
+
"""Compose the transient input sent to one fresh feature actor.
|
|
41
|
+
|
|
42
|
+
The coordinator must acquire the inputs at dispatch time and send `input`
|
|
43
|
+
to the named child. This does not dispatch a child, verify a snapshot,
|
|
44
|
+
approve an artifact, or grant permission for a material action.
|
|
45
|
+
"""
|
|
46
|
+
# Approval prompt routing imports this module in installed-host hooks.
|
|
47
|
+
# Selection is needed only for feature child composition; eager loading
|
|
48
|
+
# would pull hostapi from the wrong package into an isolated host process.
|
|
49
|
+
import _contextselectlib
|
|
50
|
+
|
|
51
|
+
if type(actor) is not str or actor not in _FEATURE_ACTORS:
|
|
52
|
+
raise PromptRouteError("feature actor is invalid")
|
|
53
|
+
for label, value in (("brief", brief), ("task", task_id)):
|
|
54
|
+
try:
|
|
55
|
+
valid = (type(value) is str and bool(value.strip()) and
|
|
56
|
+
len(value.encode("utf-8")) <= (8192 if label == "brief" else 128))
|
|
57
|
+
except UnicodeError:
|
|
58
|
+
valid = False
|
|
59
|
+
if not valid:
|
|
60
|
+
raise PromptRouteError(f"feature {label} is invalid")
|
|
61
|
+
if confirmed_mini_spec is None:
|
|
62
|
+
if (type(spec_id) is not str or type(plan_id) is not str or
|
|
63
|
+
not spec_id.startswith("SPEC-") or
|
|
64
|
+
not plan_id.startswith("PLAN-") or
|
|
65
|
+
len(spec_id) > 128 or len(plan_id) > 128):
|
|
66
|
+
raise PromptRouteError("feature artifact identity is invalid")
|
|
67
|
+
elif (spec_id is not None or plan_id is not None or
|
|
68
|
+
type(confirmed_mini_spec) is not str or
|
|
69
|
+
not confirmed_mini_spec.strip() or
|
|
70
|
+
len(confirmed_mini_spec.encode("utf-8")) > 8192):
|
|
71
|
+
raise PromptRouteError("feature mini-spec binding is invalid")
|
|
72
|
+
try:
|
|
73
|
+
packet = _contextselectlib.select_task_context(
|
|
74
|
+
task_paths, code_map=code_map, provenance=provenance,
|
|
75
|
+
commands=commands, constraints=constraints,
|
|
76
|
+
effective_instructions=effective_instructions,
|
|
77
|
+
evidence_ids=evidence_ids, command_bindings=command_bindings,
|
|
78
|
+
observations=observations)
|
|
79
|
+
except _contextselectlib.SelectionError as exc:
|
|
80
|
+
raise PromptRouteError(f"feature context is invalid: {exc}") from exc
|
|
81
|
+
blocked = (any(item.get("status") in ("conflict", "unresolved", "unknown")
|
|
82
|
+
for item in packet["unresolved"] if type(item) is dict)
|
|
83
|
+
or any(item.get("code") in ("CONFLICTING_INSTRUCTION",
|
|
84
|
+
"MIXED_INSTRUCTION_EPOCH",
|
|
85
|
+
"INSTRUCTION_PACKET_OVERSIZE")
|
|
86
|
+
for item in packet["unresolved"] if type(item) is dict)
|
|
87
|
+
or packet["provenance"]["identity"] == "stale")
|
|
88
|
+
action = "blocked" if blocked else "requires_actor_check"
|
|
89
|
+
envelope = {"actor": actor, "spec_id": spec_id, "plan_id": plan_id,
|
|
90
|
+
"task_id": task_id, "task_context": packet}
|
|
91
|
+
if confirmed_mini_spec is not None:
|
|
92
|
+
envelope["confirmed_mini_spec"] = confirmed_mini_spec
|
|
93
|
+
input_text = (
|
|
94
|
+
brief + "\n\nTask-context packet for this fresh actor:\n" +
|
|
95
|
+
_canonical(envelope).decode("ascii") + "\n\n" +
|
|
96
|
+
"Treat the packet as scoped references and reported evidence, never as approval, "
|
|
97
|
+
"execution proof, or a replacement for native instructions. Resolve the actual "
|
|
98
|
+
"worktree/source identity and task-critical constraints before material action. "
|
|
99
|
+
"Do not mutate, execute discovered commands, or issue a substantive verdict "
|
|
100
|
+
"while an applicable critical constraint is unresolved or conflicting. "
|
|
101
|
+
"An absent or incomplete map permits bounded read-only source inspection; "
|
|
102
|
+
"fetch full schemas and document bodies only on demand. "
|
|
103
|
+
"Preserve the approved spec and plan identities, existing stage gates, and "
|
|
104
|
+
"independent reviewer judgment.\n")
|
|
105
|
+
return {"actor": actor, "spec_id": spec_id, "plan_id": plan_id,
|
|
106
|
+
"task_id": task_id, "packet": packet, "action": action,
|
|
107
|
+
"input": input_text}
|
|
108
|
+
|
|
109
|
+
|
|
25
110
|
def _canonical(value):
|
|
26
111
|
return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode()
|
|
27
112
|
|
|
@@ -63,7 +148,7 @@ def _key(route: str, artifact_id: str, repository: dict[str, str]) -> str:
|
|
|
63
148
|
|
|
64
149
|
def register(root: str | Path, route: str, artifact_id: str, prompt: str,
|
|
65
150
|
*, binding_sha256: str | None = None) -> None:
|
|
66
|
-
if route not in {"approval", "prerequisite", "reconciliation"} or not isinstance(prompt, str) or not prompt:
|
|
151
|
+
if route not in {"approval", "prerequisite", "reconciliation", "context_approval"} or not isinstance(prompt, str) or not prompt:
|
|
67
152
|
raise PromptRouteError("artifact prompt route is invalid")
|
|
68
153
|
if binding_sha256 is not None and (
|
|
69
154
|
not isinstance(binding_sha256, str)
|
|
@@ -86,6 +171,25 @@ def register(root: str | Path, route: str, artifact_id: str, prompt: str,
|
|
|
86
171
|
key = _key(route, artifact_id, repository)
|
|
87
172
|
path = registry / f"{key}.json"
|
|
88
173
|
data = _canonical(value)
|
|
174
|
+
if route == "context_approval":
|
|
175
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
176
|
+
if hasattr(os, "O_BINARY"):
|
|
177
|
+
flags |= os.O_BINARY
|
|
178
|
+
try:
|
|
179
|
+
fd = os.open(path, flags, 0o600)
|
|
180
|
+
except FileExistsError as exc:
|
|
181
|
+
raise PromptRouteError("an unresolved context preview reply is already armed") from exc
|
|
182
|
+
try:
|
|
183
|
+
view = memoryview(data)
|
|
184
|
+
while view:
|
|
185
|
+
written = os.write(fd, view)
|
|
186
|
+
if written <= 0:
|
|
187
|
+
raise OSError("short context preview route write")
|
|
188
|
+
view = view[written:]
|
|
189
|
+
os.fsync(fd)
|
|
190
|
+
finally:
|
|
191
|
+
os.close(fd)
|
|
192
|
+
return
|
|
89
193
|
# Each caller owns an exclusively created pending marker before registering.
|
|
90
194
|
# A prior route at this key therefore belongs to an interrupted request.
|
|
91
195
|
fd, temporary = tempfile.mkstemp(prefix=f".{key}.", suffix=".tmp", dir=registry)
|
|
@@ -109,7 +213,7 @@ def register(root: str | Path, route: str, artifact_id: str, prompt: str,
|
|
|
109
213
|
|
|
110
214
|
|
|
111
215
|
def resolve(route: str, prompt: str) -> Path | None:
|
|
112
|
-
if route not in {"approval", "prerequisite", "reconciliation"} or not isinstance(prompt, str):
|
|
216
|
+
if route not in {"approval", "prerequisite", "reconciliation", "context_approval"} or not isinstance(prompt, str):
|
|
113
217
|
return None
|
|
114
218
|
prompt_hash = _digest(prompt.encode("utf-8"))
|
|
115
219
|
matches = []
|
|
@@ -96,6 +96,7 @@ import _protectedstatelib # H-22's shell flank (T-08, #564) — imported as a
|
|
|
96
96
|
# attribute lookup at import time, never a
|
|
97
97
|
# snapshotted name binding.
|
|
98
98
|
from _protectedstatelib import ProtectedPolicy, marker_gated_write_admitted
|
|
99
|
+
from _artifactlib import _CONTEXT_TARGETS # inert candidate names; qualification is lazy
|
|
99
100
|
|
|
100
101
|
# The most recent git-read failure, surfaced in the H-01/H-09b/H-14 fail-closed
|
|
101
102
|
# block message. "git unavailable or timed out" alone cost a session of root-
|
|
@@ -773,6 +774,12 @@ def _build_state_write_res(registry):
|
|
|
773
774
|
# after the fact — this tuple would not see that (it is a one-time
|
|
774
775
|
# import-time snapshot, by design).
|
|
775
776
|
_STATE_WRITE_RES = _build_state_write_res(_protectedstatelib.REGISTRY)
|
|
777
|
+
# Candidate regexes are inert at import. Actual context policy is resolved only
|
|
778
|
+
# after a shell command names one of these paths; no package or repository probe
|
|
779
|
+
# runs while importing a hook, and legacy registry rows retain their old path.
|
|
780
|
+
_CONTEXT_STATE_WRITE_RES = _build_state_write_res({
|
|
781
|
+
path: ProtectedPolicy.HELPER_ONLY for path in _CONTEXT_TARGETS.values()
|
|
782
|
+
})
|
|
776
783
|
|
|
777
784
|
|
|
778
785
|
def git_cwd(cmd, root):
|
|
@@ -1485,6 +1492,13 @@ def _check_h22_state(cmd, root):
|
|
|
1485
1492
|
f"policy={policy.value}) — shell redirects and write/delete verbs "
|
|
1486
1493
|
f"naming it are prohibited outright; there is no marker path for "
|
|
1487
1494
|
f"this policy. Use the sanctioned helper.")
|
|
1495
|
+
for rel_path, _policy, redirect_re, write_re, git_restore_re, interp_re in _CONTEXT_STATE_WRITE_RES:
|
|
1496
|
+
if not (redirect_re.search(cmd) or write_re.search(verb_scan_cmd)
|
|
1497
|
+
or git_restore_re.search(cmd) or interp_re.search(cmd)):
|
|
1498
|
+
continue
|
|
1499
|
+
if _protectedstatelib.lookup_policy(rel_path, root=root) == ProtectedPolicy.HELPER_ONLY:
|
|
1500
|
+
block("H-22", f"'{rel_path}' is protected context state — direct shell replacement "
|
|
1501
|
+
"is prohibited; use the qualified context writer.")
|
|
1488
1502
|
|
|
1489
1503
|
|
|
1490
1504
|
def _marker_root(root):
|