@arbiterforge/ca-pi 0.15.10 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +45 -0
- package/plugins/ca-pi/SKILLS.md +1 -1
- package/plugins/ca-pi/agents/infra-author.md +7 -1
- package/plugins/ca-pi/agents/scout.md +40 -8
- package/plugins/ca-pi/generated/command-catalog.json +1 -1
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/release.json +104 -6
- package/plugins/ca-pi/hooks/_activationlib.py +97 -13
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +7 -2
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +519 -16
- package/plugins/ca-pi/hooks/_artifactlib.py +244 -7
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
- package/plugins/ca-pi/hooks/_bashguardlib.py +23 -8
- package/plugins/ca-pi/hooks/_contextreportlib.py +564 -6
- package/plugins/ca-pi/hooks/_contextselectlib.py +464 -0
- package/plugins/ca-pi/hooks/_contextsnapshotlib.py +999 -0
- package/plugins/ca-pi/hooks/_debughandofflib.py +767 -0
- package/plugins/ca-pi/hooks/_hooklib.py +2 -1
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_intentlib.py +5 -5
- package/plugins/ca-pi/hooks/_protectedlib.py +3 -3
- package/plugins/ca-pi/hooks/_protectedstatelib.py +152 -2
- package/plugins/ca-pi/hooks/_provenancelib.py +604 -34
- package/plugins/ca-pi/hooks/_readinjectlib.py +131 -1
- package/plugins/ca-pi/hooks/_sensitivelib.py +28 -14
- package/plugins/ca-pi/hooks/artifact-authority-hook.py +1 -0
- package/plugins/ca-pi/hooks/artifact-authority.py +25 -1
- package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
- package/plugins/ca-pi/hooks/doctor.py +4 -2
- package/plugins/ca-pi/hooks/git-enforce.py +7 -6
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
- package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
- package/plugins/ca-pi/hooks/session-start.py +26 -7
- package/plugins/ca-pi/includes/artifacts.md +41 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
- package/plugins/ca-pi/includes/helper-invocation.md +128 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
- package/plugins/ca-pi/includes/routing-table.md +1 -1
- package/plugins/ca-pi/includes/security-gate-record.md +5 -3
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +1 -1
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
- package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
- package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
- package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
- package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
- package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
- package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
- package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
- package/plugins/ca-pi/skills/ca-task/SKILL.md +55 -8
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Bounded stdlib bridge to an installation-pinned ca-artifact executable.
|
|
3
3
|
|
|
4
|
-
No imports perform I/O. No PATH search, environment-selected
|
|
5
|
-
parser duplication, command execution
|
|
4
|
+
No imports perform I/O. No native-binary PATH search, environment-selected
|
|
5
|
+
engine, schema parser duplication, document command execution, or host registration.
|
|
6
6
|
The caller supplies a trusted installation directory, never a repository setting.
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
@@ -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":
|
|
@@ -742,10 +842,83 @@ def _preflight_current_acceptance(
|
|
|
742
842
|
}
|
|
743
843
|
|
|
744
844
|
|
|
745
|
-
def
|
|
845
|
+
def _native_git_excludes(git: Path, root: Path) -> dict[str, str]:
|
|
846
|
+
"""Resolve only the governed root's ignore policy with the producer's Git.
|
|
847
|
+
|
|
848
|
+
Git expands the explicit setting or its documented XDG/home default. No
|
|
849
|
+
general configuration or host environment crosses the native boundary.
|
|
850
|
+
Mapped roots with different policies still must pass exact closure equality.
|
|
851
|
+
"""
|
|
852
|
+
from _gitexec import root_bound_git_env
|
|
853
|
+
|
|
854
|
+
default = os.environ.get("XDG_CONFIG_HOME")
|
|
855
|
+
default = str(Path(default) / "git" / "ignore") if default else "~/.config/git/ignore"
|
|
856
|
+
environment = root_bound_git_env()
|
|
857
|
+
environment.pop("GIT_CONFIG", None) # git-config-only override; inventory commands ignore it.
|
|
858
|
+
try:
|
|
859
|
+
code, raw, _ = _bounded_child(
|
|
860
|
+
[str(git), "--no-lazy-fetch", "--no-optional-locks", "-C", str(root),
|
|
861
|
+
"-c", "core.fsmonitor=false", "-c", "protocol.allow=never", "config",
|
|
862
|
+
"--null", "--path", "--default", default, "--get", "core.excludesFile"],
|
|
863
|
+
b"", None, 5, environment=environment, cwd=root)
|
|
864
|
+
if code != 0 or not raw.endswith(b"\0") or b"\0" in raw[:-1] or len(raw) > 4096:
|
|
865
|
+
return {}
|
|
866
|
+
value = raw[:-1].decode("utf-8", "strict")
|
|
867
|
+
if not value:
|
|
868
|
+
return {}
|
|
869
|
+
path = Path(value)
|
|
870
|
+
path = (path if path.is_absolute() else root / path).resolve()
|
|
871
|
+
except (ArtifactError, OSError, RuntimeError, UnicodeError, ValueError):
|
|
872
|
+
return {}
|
|
873
|
+
return {"GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "core.excludesFile",
|
|
874
|
+
"GIT_CONFIG_VALUE_0": str(path)}
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _native_child_environment(argv: list[str]) -> dict[str, str]:
|
|
878
|
+
"""Transport one host Git identity for fixed native workspace probes only.
|
|
879
|
+
|
|
880
|
+
CODEARBITER_GIT_EXECUTABLE is the shared _gitexec host override. Resolve the
|
|
881
|
+
ordinary host PATH in this bridge, excluding CWD/relative entries and the
|
|
882
|
+
governed checkout; the native engine receives only that identity and the
|
|
883
|
+
resolved core.excludesFile setting, never PATH or general ambient state.
|
|
884
|
+
Cold ordinary operations remain usable when no safe Git is available.
|
|
885
|
+
"""
|
|
886
|
+
try:
|
|
887
|
+
root = Path(argv[argv.index("--root") + 1]).resolve(strict=True)
|
|
888
|
+
boundaries = [root, *(parent for parent in root.parents if (parent / ".git").exists())]
|
|
889
|
+
except (ValueError, IndexError, OSError):
|
|
890
|
+
return {}
|
|
891
|
+
configured = os.environ.get("CODEARBITER_GIT_EXECUTABLE")
|
|
892
|
+
if configured:
|
|
893
|
+
candidates = [Path(configured)]
|
|
894
|
+
else:
|
|
895
|
+
name = "git.exe" if os.name == "nt" else "git"
|
|
896
|
+
candidates = [Path(directory) / name for directory in os.environ.get("PATH", "").split(os.pathsep)
|
|
897
|
+
if directory and Path(directory).is_absolute()]
|
|
898
|
+
for candidate in candidates:
|
|
899
|
+
try:
|
|
900
|
+
if not candidate.is_absolute() or any(candidate.is_relative_to(boundary) for boundary in boundaries):
|
|
901
|
+
continue
|
|
902
|
+
resolved = candidate.resolve(strict=True)
|
|
903
|
+
if (any(resolved.is_relative_to(boundary) for boundary in boundaries)
|
|
904
|
+
or not stat.S_ISREG(resolved.stat().st_mode) or not os.access(resolved, os.X_OK)):
|
|
905
|
+
continue
|
|
906
|
+
except (OSError, RuntimeError):
|
|
907
|
+
continue
|
|
908
|
+
return {"CODEARBITER_GIT_EXECUTABLE": str(resolved),
|
|
909
|
+
**_native_git_excludes(resolved, root)}
|
|
910
|
+
return {}
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _bounded_child(argv: list[str], request: bytes, fd: int | None, timeout: float,
|
|
914
|
+
*, environment: dict[str, str] | None = None,
|
|
915
|
+
cwd: Path | None = None) -> tuple[int, bytes, bytes]:
|
|
746
916
|
options = dict(stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
747
|
-
close_fds=True, start_new_session=True,
|
|
748
|
-
|
|
917
|
+
close_fds=True, start_new_session=True,
|
|
918
|
+
env=_native_child_environment(argv) if environment is None else environment)
|
|
919
|
+
if cwd is not None:
|
|
920
|
+
options["cwd"] = cwd
|
|
921
|
+
if os.name != "nt" and fd is not None:
|
|
749
922
|
options["pass_fds"] = (fd,)
|
|
750
923
|
process = subprocess.Popen(argv, **options)
|
|
751
924
|
buffers = [bytearray(), bytearray()]
|
|
@@ -793,6 +966,48 @@ def _bounded_child(argv: list[str], request: bytes, fd: int, timeout: float) ->
|
|
|
793
966
|
return process.returncode, bytes(buffers[0]), bytes(buffers[1])
|
|
794
967
|
|
|
795
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
|
+
|
|
796
1011
|
class ArtifactClient:
|
|
797
1012
|
def __init__(self, root: str | Path, installation: str | Path, *, timeout: float = 30):
|
|
798
1013
|
self.root = _trusted_directory(root, "UNSAFE_ROOT")
|
|
@@ -803,6 +1018,24 @@ class ArtifactClient:
|
|
|
803
1018
|
"""Read fresh installed prerequisites; never attest live host authority."""
|
|
804
1019
|
return _installed_workflow_preflight(self.installation)
|
|
805
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
|
+
|
|
806
1039
|
def _open_binary(self) -> int:
|
|
807
1040
|
system = {"Linux":"linux", "Darwin":"darwin", "Windows":"windows"}.get(platform.system())
|
|
808
1041
|
arch = {"x86_64":"amd64", "amd64":"amd64", "aarch64":"arm64", "arm64":"arm64"}.get(platform.machine().lower())
|
|
@@ -824,7 +1057,9 @@ class ArtifactClient:
|
|
|
824
1057
|
manifest = _decode(raw)
|
|
825
1058
|
except ArtifactError as exc:
|
|
826
1059
|
raise ArtifactError("INVALID_INSTALLATION", "invalid release manifest JSON") from exc
|
|
827
|
-
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"}):
|
|
828
1063
|
raise ArtifactError("INVALID_INSTALLATION", "unexpected release manifest")
|
|
829
1064
|
if manifest["format"] != "codearbiter.artifact-release/0.1.0" or manifest["protocol"] != PROTOCOL or manifest["schema_version"] != SCHEMA_VERSION:
|
|
830
1065
|
raise ArtifactError("UNSUPPORTED_VERSION", "installed binary/adapter version mismatch")
|
|
@@ -869,6 +1104,8 @@ class ArtifactClient:
|
|
|
869
1104
|
def call(self, operation: str, request: dict[str, Any] | None = None, *, permit_invalid: bool = False) -> dict:
|
|
870
1105
|
if not re.fullmatch(r"[a-z][a-z-]{0,39}", operation):
|
|
871
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()
|
|
872
1109
|
request = dict(request or {})
|
|
873
1110
|
if "protocol" in request and request["protocol"] != PROTOCOL:
|
|
874
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 = []
|
|
@@ -83,8 +83,8 @@ import sys
|
|
|
83
83
|
from _hooklib import (
|
|
84
84
|
ADR_LIFECYCLE_LOG_BASENAME, AUDIT_LOG_BASENAMES, AUDIT_LOG_FLAT_BASENAMES,
|
|
85
85
|
AUDIT_LOG_NAMES, CRYPTO_RE, DECISION_AUDIT_LOG_NAMES, DECISION_LOG_BASENAME, DECISIONS_DIR_RE,
|
|
86
|
-
GATE_MARKER_NAMES,
|
|
87
|
-
content_digest, is_migration_path, line_digest,
|
|
86
|
+
GATE_MARKER_NAMES, SECRET_RE, SECURITY_DIFF_GIT_ARGS, block,
|
|
87
|
+
content_digest, is_migration_path, line_digest, security_scan_diff,
|
|
88
88
|
)
|
|
89
89
|
from _gitexec import git_executable
|
|
90
90
|
import _gitlib # reused for its spawn-free, worktree-aware (.git-as-a-FILE /
|
|
@@ -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):
|
|
@@ -1508,11 +1522,12 @@ def _check_h09b_h10b_crypto_secret(commit, add, cwd, root):
|
|
|
1508
1522
|
"""H-09b / H-10b: BLOCK a commit that introduces crypto/secret changes without
|
|
1509
1523
|
a recorded security-gate pass. The crypto-compliance / secret-handling skills
|
|
1510
1524
|
record the pass via hooks/security-pass.py — a marker holding the digest of
|
|
1511
|
-
every sensitive line the gate approved. Two checks, both required:
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1525
|
+
every sensitive line the gate approved. Two checks, both required: a pass
|
|
1526
|
+
is recorded AND it covers every sensitive line being committed. Coverage is
|
|
1527
|
+
by line digest with no time window (as H-14 binds migrations by content): a
|
|
1528
|
+
pass stays valid for exactly the lines it reviewed, and any new or changed
|
|
1529
|
+
sensitive line is uncovered and blocks, so a pass for one diff can never
|
|
1530
|
+
launder a different one. Scans the staged diff, plus the worktree diff when
|
|
1516
1531
|
the commit uses -a/--all or the same command stages files."""
|
|
1517
1532
|
if not commit:
|
|
1518
1533
|
return
|
|
@@ -1541,7 +1556,7 @@ def _check_h09b_h10b_crypto_secret(commit, add, cwd, root):
|
|
|
1541
1556
|
tag = "H-09b" if touches_crypto else "H-10b"
|
|
1542
1557
|
skill = "crypto-compliance" if touches_crypto else "secret-handling"
|
|
1543
1558
|
marker = os.path.join(_marker_root(root), ".codearbiter", ".markers", "security-gate-passed")
|
|
1544
|
-
if not
|
|
1559
|
+
if not os.path.isfile(marker):
|
|
1545
1560
|
block(tag, f"This commit introduces {kind} changes, but no security-gate pass is "
|
|
1546
1561
|
f"recorded (.codearbiter/.markers/security-gate-passed). Run the "
|
|
1547
1562
|
f"{skill} gate (it records the pass), then commit. To bypass a "
|