@arbiterforge/ca-pi 0.21.6 → 0.21.7
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 +18 -0
- 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 +36 -36
- package/plugins/ca-pi/hooks/_approvallib.py +2 -0
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +26 -13
- package/plugins/ca-pi/hooks/_bashguardlib.py +190 -7
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_prerequisitelib.py +2 -0
- package/plugins/ca-pi/hooks/_replylib.py +61 -8
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/includes/artifacts-attestation.md +20 -17
- package/plugins/ca-pi/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ and durable project context. You decide. codeArbiter enforces.
|
|
|
10
10
|
<img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
|
|
11
11
|
<img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
|
|
12
12
|
<img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
|
|
13
|
-
<img alt="version 3.2.
|
|
13
|
+
<img alt="version 3.2.7" src="https://img.shields.io/badge/version-3.2.7-2b7489">
|
|
14
14
|
<img alt="core lanes" src="https://img.shields.io/badge/core_lanes-18-555">
|
|
15
15
|
<img alt="skills" src="https://img.shields.io/badge/skills-22-555">
|
|
16
16
|
<img alt="agents" src="https://img.shields.io/badge/agents-20-555">
|
package/package.json
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to `ca-pi` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.21.7] - 2026-10-21
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Keep ordinary uppercase instructions such as `Approve DATA migration` out of
|
|
12
|
+
artifact reply capture unless that all-letter code belongs to a retained
|
|
13
|
+
request. Genuine coded replies and mismatch protection remain intact.
|
|
14
|
+
- Recognize verifier authority only from the actual wrapper invocation, not
|
|
15
|
+
inert PowerShell data mentioning its filename.
|
|
16
|
+
- Permit bounded literal read-only Python diagnostics using isolated `-I`
|
|
17
|
+
stdin input and literal `git show HEAD:<protected-path>` or `Path` reads.
|
|
18
|
+
No arbitrary script, subprocess, shell expansion or write exemption is added.
|
|
19
|
+
|
|
20
|
+
- Ordinary approval, sprint, prerequisite, and reconciliation instructions no longer enter legacy reply-code capture. Historical requests and actual host permissions retain their checks.
|
|
21
|
+
- Bounded literal JSON sent to the installed read-only diagnostic validator no longer triggers protected-state write guards merely by citing source paths. Ambiguous or executable shell forms remain guarded.
|
|
22
|
+
- Explicit staging in a multiline batch no longer borrows flags from later commands. Every staging command retains wildcard, directory, continuation, and working-directory checks.
|
|
23
|
+
- Explicit draft source refreshes now preserve started work and historical evidence instead of requiring an untouched initial approval pair. Source and plan revision checks and historical approval boundaries remain intact.
|
|
24
|
+
|
|
7
25
|
## [0.21.6] - 2026-10-19
|
|
8
26
|
|
|
9
27
|
### Fixed
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,43 +6,43 @@
|
|
|
6
6
|
"binaries": {
|
|
7
7
|
"darwin/amd64": {
|
|
8
8
|
"file": "ca-artifact-darwin-amd64",
|
|
9
|
-
"sha256": "
|
|
9
|
+
"sha256": "da9898ed084adde20454997c8a94490cc38c210dfe399cfe9e48bf85205e82c4",
|
|
10
10
|
"native_tested": true
|
|
11
11
|
},
|
|
12
12
|
"darwin/arm64": {
|
|
13
13
|
"file": "ca-artifact-darwin-arm64",
|
|
14
|
-
"sha256": "
|
|
14
|
+
"sha256": "8e23e57e617d2c7864c2cba779b2cfd19dbc556e876a68e5c2c150455c58fe1f",
|
|
15
15
|
"native_tested": true
|
|
16
16
|
},
|
|
17
17
|
"linux/amd64": {
|
|
18
18
|
"file": "ca-artifact-linux-amd64",
|
|
19
|
-
"sha256": "
|
|
19
|
+
"sha256": "3835ecfc7f6172e0d19c79b9c213eb211e530a46384df51b96820712ce471864",
|
|
20
20
|
"native_tested": true
|
|
21
21
|
},
|
|
22
22
|
"linux/arm64": {
|
|
23
23
|
"file": "ca-artifact-linux-arm64",
|
|
24
|
-
"sha256": "
|
|
24
|
+
"sha256": "8564bfec069ab1ce30db37135197e8c4e093a2bc4ce6ea7f308029799f09af09",
|
|
25
25
|
"native_tested": true
|
|
26
26
|
},
|
|
27
27
|
"windows/amd64": {
|
|
28
28
|
"file": "ca-artifact-windows-amd64.exe",
|
|
29
|
-
"sha256": "
|
|
29
|
+
"sha256": "55641212ecde2ddf078e70bcc44940696a68065f2dc323e69b3de804d24611ee",
|
|
30
30
|
"native_tested": true
|
|
31
31
|
},
|
|
32
32
|
"windows/arm64": {
|
|
33
33
|
"file": "ca-artifact-windows-arm64.exe",
|
|
34
|
-
"sha256": "
|
|
34
|
+
"sha256": "7d92b708e52ebbb0a08bb146f6a4c372277b2aa58716ca56b9bad04d671dc995",
|
|
35
35
|
"native_tested": true
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"context_qualification": {
|
|
39
39
|
"darwin/amd64": {
|
|
40
|
-
"source_commit": "
|
|
40
|
+
"source_commit": "ddb07a3af1fc56692d1cff3fd4579058ee6c9ef7",
|
|
41
41
|
"workflow": ".github/workflows/ci.yml",
|
|
42
|
-
"run_id": "
|
|
43
|
-
"binary_sha256": "
|
|
44
|
-
"observation_sha256": "
|
|
45
|
-
"module_sha256": "
|
|
42
|
+
"run_id": "39769650796",
|
|
43
|
+
"binary_sha256": "da9898ed084adde20454997c8a94490cc38c210dfe399cfe9e48bf85205e82c4",
|
|
44
|
+
"observation_sha256": "ba23366af537c89797803bc21f45368dcbca3ee18871fce020032695a4db70ee",
|
|
45
|
+
"module_sha256": "ee801a17cb5132ceb5500b62e24e74d593e79feae3db50c07777ef8ff47f567d",
|
|
46
46
|
"native_cases": [
|
|
47
47
|
"TestContextBoundedRender",
|
|
48
48
|
"TestContextDocumentContract",
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
]
|
|
54
54
|
},
|
|
55
55
|
"darwin/arm64": {
|
|
56
|
-
"source_commit": "
|
|
56
|
+
"source_commit": "ddb07a3af1fc56692d1cff3fd4579058ee6c9ef7",
|
|
57
57
|
"workflow": ".github/workflows/ci.yml",
|
|
58
|
-
"run_id": "
|
|
59
|
-
"binary_sha256": "
|
|
60
|
-
"observation_sha256": "
|
|
61
|
-
"module_sha256": "
|
|
58
|
+
"run_id": "39769650796",
|
|
59
|
+
"binary_sha256": "8e23e57e617d2c7864c2cba779b2cfd19dbc556e876a68e5c2c150455c58fe1f",
|
|
60
|
+
"observation_sha256": "84141d37a93b7c204acb93dd4e043f4cf841ffacad1c0a497eb1d9d9f8852a7c",
|
|
61
|
+
"module_sha256": "ee801a17cb5132ceb5500b62e24e74d593e79feae3db50c07777ef8ff47f567d",
|
|
62
62
|
"native_cases": [
|
|
63
63
|
"TestContextBoundedRender",
|
|
64
64
|
"TestContextDocumentContract",
|
|
@@ -69,12 +69,12 @@
|
|
|
69
69
|
]
|
|
70
70
|
},
|
|
71
71
|
"linux/amd64": {
|
|
72
|
-
"source_commit": "
|
|
72
|
+
"source_commit": "ddb07a3af1fc56692d1cff3fd4579058ee6c9ef7",
|
|
73
73
|
"workflow": ".github/workflows/ci.yml",
|
|
74
|
-
"run_id": "
|
|
75
|
-
"binary_sha256": "
|
|
76
|
-
"observation_sha256": "
|
|
77
|
-
"module_sha256": "
|
|
74
|
+
"run_id": "39769650796",
|
|
75
|
+
"binary_sha256": "3835ecfc7f6172e0d19c79b9c213eb211e530a46384df51b96820712ce471864",
|
|
76
|
+
"observation_sha256": "cdc3731052edd16a558fade51587f6fe3679da6c1c3282250d4de5e9eb29658e",
|
|
77
|
+
"module_sha256": "ee801a17cb5132ceb5500b62e24e74d593e79feae3db50c07777ef8ff47f567d",
|
|
78
78
|
"native_cases": [
|
|
79
79
|
"TestContextBoundedRender",
|
|
80
80
|
"TestContextDocumentContract",
|
|
@@ -85,12 +85,12 @@
|
|
|
85
85
|
]
|
|
86
86
|
},
|
|
87
87
|
"linux/arm64": {
|
|
88
|
-
"source_commit": "
|
|
88
|
+
"source_commit": "ddb07a3af1fc56692d1cff3fd4579058ee6c9ef7",
|
|
89
89
|
"workflow": ".github/workflows/ci.yml",
|
|
90
|
-
"run_id": "
|
|
91
|
-
"binary_sha256": "
|
|
92
|
-
"observation_sha256": "
|
|
93
|
-
"module_sha256": "
|
|
90
|
+
"run_id": "39769650796",
|
|
91
|
+
"binary_sha256": "8564bfec069ab1ce30db37135197e8c4e093a2bc4ce6ea7f308029799f09af09",
|
|
92
|
+
"observation_sha256": "13e4cbf8422754dfa6478785bd9a2d3dd0af1cdfb10e0a654048ee79c4b3787c",
|
|
93
|
+
"module_sha256": "ee801a17cb5132ceb5500b62e24e74d593e79feae3db50c07777ef8ff47f567d",
|
|
94
94
|
"native_cases": [
|
|
95
95
|
"TestContextBoundedRender",
|
|
96
96
|
"TestContextDocumentContract",
|
|
@@ -101,12 +101,12 @@
|
|
|
101
101
|
]
|
|
102
102
|
},
|
|
103
103
|
"windows/amd64": {
|
|
104
|
-
"source_commit": "
|
|
104
|
+
"source_commit": "ddb07a3af1fc56692d1cff3fd4579058ee6c9ef7",
|
|
105
105
|
"workflow": ".github/workflows/ci.yml",
|
|
106
|
-
"run_id": "
|
|
107
|
-
"binary_sha256": "
|
|
108
|
-
"observation_sha256": "
|
|
109
|
-
"module_sha256": "
|
|
106
|
+
"run_id": "39769650796",
|
|
107
|
+
"binary_sha256": "55641212ecde2ddf078e70bcc44940696a68065f2dc323e69b3de804d24611ee",
|
|
108
|
+
"observation_sha256": "27e759e25cc94f952cea7606423dea542cefce0ae934c25ec1ac21dbc3eed6ee",
|
|
109
|
+
"module_sha256": "ee801a17cb5132ceb5500b62e24e74d593e79feae3db50c07777ef8ff47f567d",
|
|
110
110
|
"native_cases": [
|
|
111
111
|
"TestContextBoundedRender",
|
|
112
112
|
"TestContextDocumentContract",
|
|
@@ -117,12 +117,12 @@
|
|
|
117
117
|
]
|
|
118
118
|
},
|
|
119
119
|
"windows/arm64": {
|
|
120
|
-
"source_commit": "
|
|
120
|
+
"source_commit": "ddb07a3af1fc56692d1cff3fd4579058ee6c9ef7",
|
|
121
121
|
"workflow": ".github/workflows/ci.yml",
|
|
122
|
-
"run_id": "
|
|
123
|
-
"binary_sha256": "
|
|
124
|
-
"observation_sha256": "
|
|
125
|
-
"module_sha256": "
|
|
122
|
+
"run_id": "39769650796",
|
|
123
|
+
"binary_sha256": "7d92b708e52ebbb0a08bb146f6a4c372277b2aa58716ca56b9bad04d671dc995",
|
|
124
|
+
"observation_sha256": "345404cb624bb6a4306e6a838d383fc1d4b99f95db6352ad90fd394edf072f5c",
|
|
125
|
+
"module_sha256": "ee801a17cb5132ceb5500b62e24e74d593e79feae3db50c07777ef8ff47f567d",
|
|
126
126
|
"native_cases": [
|
|
127
127
|
"TestContextBoundedRender",
|
|
128
128
|
"TestContextDocumentContract",
|
|
@@ -480,6 +480,8 @@ def consume_from_hook(*, root: str | Path, plugin_root: str | Path, prompt: str,
|
|
|
480
480
|
prepared = _replylib.prepare(raw, "approval")
|
|
481
481
|
prompt = prepared["text"]
|
|
482
482
|
attempted = prepared["attempted"]
|
|
483
|
+
if not attempted:
|
|
484
|
+
return ""
|
|
483
485
|
|
|
484
486
|
def missed(reason: str) -> str:
|
|
485
487
|
_replylib.record_near_miss(root, raw, reason)
|
|
@@ -1889,12 +1889,16 @@ def _command_name(value: str) -> str:
|
|
|
1889
1889
|
return value.replace("\\", "/").rsplit("/", 1)[-1].casefold()
|
|
1890
1890
|
|
|
1891
1891
|
|
|
1892
|
-
def
|
|
1892
|
+
def _command_lexer(command: str) -> shlex.shlex:
|
|
1893
1893
|
lexer = shlex.shlex(command, posix=True)
|
|
1894
1894
|
lexer.whitespace_split = True
|
|
1895
1895
|
lexer.commenters = ""
|
|
1896
1896
|
lexer.escape = "" # Native Windows paths retain backslashes inside both quotes.
|
|
1897
|
-
return
|
|
1897
|
+
return lexer
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
def _command_tokens(command: str) -> list[str]:
|
|
1901
|
+
return list(_command_lexer(command))
|
|
1898
1902
|
|
|
1899
1903
|
|
|
1900
1904
|
def _npm_package(manifest: Path) -> dict[str, Any]:
|
|
@@ -2705,6 +2709,17 @@ def _model_id(value: Any, field: str) -> str:
|
|
|
2705
2709
|
return value
|
|
2706
2710
|
|
|
2707
2711
|
|
|
2712
|
+
def _verification_wrapper_start(tokens: list[str]) -> bool:
|
|
2713
|
+
return (
|
|
2714
|
+
len(tokens) >= 2
|
|
2715
|
+
and _command_name(tokens[0]) in {
|
|
2716
|
+
"python", "python.exe", "python3", "python3.exe", "py", "py.exe",
|
|
2717
|
+
}
|
|
2718
|
+
and _command_name(tokens[1]) == "artifact-authority.py"
|
|
2719
|
+
and (len(tokens) == 2 or tokens[2] == "verify")
|
|
2720
|
+
)
|
|
2721
|
+
|
|
2722
|
+
|
|
2708
2723
|
def _verification_selector(event: dict[str, Any]) -> tuple[str, Path, str] | None:
|
|
2709
2724
|
"""Recognize only the closed wrapper selector, never caller-authored child argv."""
|
|
2710
2725
|
if event.get("tool_name") not in {
|
|
@@ -2718,21 +2733,19 @@ def _verification_selector(event: dict[str, Any]) -> tuple[str, Path, str] | Non
|
|
|
2718
2733
|
if not isinstance(command, str):
|
|
2719
2734
|
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "exec command is absent")
|
|
2720
2735
|
compound = any(marker in command for marker in ("\n", "\r", ";", "&", "|", ">", "<", "`", "$"))
|
|
2736
|
+
tokens: list[str] = []
|
|
2737
|
+
lexer = _command_lexer(command)
|
|
2721
2738
|
try:
|
|
2722
|
-
tokens
|
|
2739
|
+
tokens.extend(lexer)
|
|
2723
2740
|
except ValueError as exc:
|
|
2724
|
-
#
|
|
2725
|
-
|
|
2741
|
+
# Retain the parsed selector prefix even when a later argument is
|
|
2742
|
+
# malformed. A filename in ordinary shell source selects no authority.
|
|
2743
|
+
if len(tokens) == 1:
|
|
2744
|
+
tokens.append(lexer.token) # An unfinished quoted script still refuses.
|
|
2745
|
+
if not _verification_wrapper_start(tokens):
|
|
2726
2746
|
return None
|
|
2727
2747
|
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier wrapper command is malformed") from exc
|
|
2728
|
-
wrapper_start = (
|
|
2729
|
-
len(tokens) >= 2
|
|
2730
|
-
and _command_name(tokens[0]) in {
|
|
2731
|
-
"python", "python.exe", "python3", "python3.exe", "py", "py.exe",
|
|
2732
|
-
}
|
|
2733
|
-
and _command_name(tokens[1]) == "artifact-authority.py"
|
|
2734
|
-
and (len(tokens) == 2 or tokens[2] == "verify")
|
|
2735
|
-
)
|
|
2748
|
+
wrapper_start = _verification_wrapper_start(tokens)
|
|
2736
2749
|
if compound:
|
|
2737
2750
|
if wrapper_start:
|
|
2738
2751
|
raise AuthorityError("UNSUPPORTED_HOST_SEAM", "verifier wrapper command is compound")
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
# on a shell mutation of a registered
|
|
76
76
|
# protected-state file
|
|
77
77
|
|
|
78
|
+
import json
|
|
78
79
|
import os
|
|
79
80
|
import re
|
|
80
81
|
import subprocess
|
|
@@ -246,7 +247,12 @@ ARGS = r"(?P<args>(?:\"[^\"]*\"|'[^']*'|[^|;&])*)"
|
|
|
246
247
|
COMMIT_SUBCOMMAND = r"commit(?:-(?!graph(?![-\w]))[\w-]+)?(?![-\w])"
|
|
247
248
|
COMMIT_RE = re.compile(GIT + r"\s+" + COMMIT_SUBCOMMAND + ARGS)
|
|
248
249
|
PUSH_RE = re.compile(GIT + r"\s+push\b" + ARGS)
|
|
249
|
-
|
|
250
|
+
# Staging argv ends at an unquoted command newline. Keep quoted multiline
|
|
251
|
+
# filenames intact; otherwise a later `git push -u` becomes a wildcard add.
|
|
252
|
+
# run_guards inspects EVERY match, so narrowing the first capture cannot hide
|
|
253
|
+
# a later unsafe add. Commit/push parsing retains its existing contract.
|
|
254
|
+
ADD_ARGS = r"(?P<args>(?:\"[^\"]*\"|'[^']*'|[^|;&\r\n])*)"
|
|
255
|
+
ADD_RE = re.compile(GIT + r"\s+add\b" + ADD_ARGS)
|
|
250
256
|
# #485 AC-4, the audit of the sibling matchers. Neither needs the same
|
|
251
257
|
# treatment, and both were left alone deliberately:
|
|
252
258
|
#
|
|
@@ -1480,10 +1486,12 @@ def _check_h03_wildcard_add(add, cwd):
|
|
|
1480
1486
|
src/ just as surely as `git add -A` does."""
|
|
1481
1487
|
if not add:
|
|
1482
1488
|
return
|
|
1483
|
-
|
|
1489
|
+
args = add.group("args")
|
|
1490
|
+
if (WILDCARD_ADD_RE.search(args)
|
|
1491
|
+
or _has_literal_flag(args, {"-A", "--all", "-u", "--update"})):
|
|
1484
1492
|
block("H-03", "'git add -A' / 'git add .' / 'git add --all' / 'git add -u' "
|
|
1485
1493
|
"are prohibited. Stage files explicitly (commit-gate skill).")
|
|
1486
|
-
why = add_violation(
|
|
1494
|
+
why = add_violation(args, cwd)
|
|
1487
1495
|
if why:
|
|
1488
1496
|
block("H-03", f"Wildcard staging is prohibited — {why} stages a "
|
|
1489
1497
|
f"non-explicit file set. Stage files explicitly, one path "
|
|
@@ -1844,6 +1852,164 @@ def _check_h14_migration(commit, add, cwd, root):
|
|
|
1844
1852
|
f"bypass a migration gate, /override logs the exception.")
|
|
1845
1853
|
|
|
1846
1854
|
|
|
1855
|
+
def _literal_python_read_view(cmd):
|
|
1856
|
+
"""Recognize only isolated, literal Python read diagnostics on stdin.
|
|
1857
|
+
|
|
1858
|
+
This is a closed read grammar, not an exemption for Python/subprocess.
|
|
1859
|
+
Unknown code, import shadowing, shell expansion, and extra commands keep
|
|
1860
|
+
the original guard view. Equivalent literal read commands still traverse
|
|
1861
|
+
every gate. No code is executed or filesystem state consulted here.
|
|
1862
|
+
"""
|
|
1863
|
+
if "\n" not in cmd or " -I" not in cmd:
|
|
1864
|
+
return cmd
|
|
1865
|
+
try:
|
|
1866
|
+
if len(cmd.encode("utf-8")) > 32768:
|
|
1867
|
+
return cmd
|
|
1868
|
+
except UnicodeError:
|
|
1869
|
+
return cmd
|
|
1870
|
+
if re.search(r"\r(?!\n)|[\x00\u2018-\u201b]", cmd):
|
|
1871
|
+
return cmd
|
|
1872
|
+
text = cmd.replace("\r\n", "\n")
|
|
1873
|
+
# -I excludes workspace/PYTHONPATH modules: otherwise an apparent
|
|
1874
|
+
# standard-library import could execute a project-authored replacement.
|
|
1875
|
+
invocation = r"(?:python(?:3(?:\.\d+)*)?|py)(?:\.exe)? -I(?: -B)? -"
|
|
1876
|
+
fallback = cmd
|
|
1877
|
+
match = re.fullmatch(
|
|
1878
|
+
r"@'\n(?P<body>[\s\S]*?)\n'@[ \t]*\|[ \t]*(?P<invoke>" + invocation + r")[ \t]*\n?", text)
|
|
1879
|
+
if match:
|
|
1880
|
+
body = match["body"]
|
|
1881
|
+
# The consumer appears after its script in PowerShell. If the
|
|
1882
|
+
# read grammar refuses it, expose that consumer before the intact
|
|
1883
|
+
# source too, so interpreter/write guards cannot miss an earlier path.
|
|
1884
|
+
fallback = match["invoke"] + "\n" + cmd
|
|
1885
|
+
if re.search(r"(?m)^[ \t]*'@", body):
|
|
1886
|
+
return cmd
|
|
1887
|
+
else:
|
|
1888
|
+
match = re.fullmatch(
|
|
1889
|
+
invocation + r"[ \t]+<<(?P<quote>['\"])(?P<end>[A-Za-z_][A-Za-z0-9_]{0,31})"
|
|
1890
|
+
r"(?P=quote)[ \t]*\n(?P<body>[\s\S]*?)\n(?P=end)[ \t]*\n?", text)
|
|
1891
|
+
if not match:
|
|
1892
|
+
return cmd
|
|
1893
|
+
body = match["body"]
|
|
1894
|
+
if any(line == match["end"] for line in body.splitlines()):
|
|
1895
|
+
return cmd
|
|
1896
|
+
# Ordinary commands pay no AST import/parse cost.
|
|
1897
|
+
import ast
|
|
1898
|
+
try:
|
|
1899
|
+
tree = ast.parse(body)
|
|
1900
|
+
except (SyntaxError, ValueError, RecursionError):
|
|
1901
|
+
return fallback
|
|
1902
|
+
if len(tree.body) > 32 or sum(1 for _ in ast.walk(tree)) > 256:
|
|
1903
|
+
return fallback
|
|
1904
|
+
|
|
1905
|
+
def literal(node):
|
|
1906
|
+
return node.value if isinstance(node, ast.Constant) and isinstance(node.value, str) else None
|
|
1907
|
+
|
|
1908
|
+
def read_path(node):
|
|
1909
|
+
value = literal(node)
|
|
1910
|
+
if value is not None and re.fullmatch(
|
|
1911
|
+
r"\.codearbiter/(?:overrides\.log|CONTEXT\.md|decisions/[A-Za-z0-9][A-Za-z0-9._-]*\.(?:md|jsonl))",
|
|
1912
|
+
value,
|
|
1913
|
+
):
|
|
1914
|
+
return value
|
|
1915
|
+
return None
|
|
1916
|
+
|
|
1917
|
+
imports, reads = set(), []
|
|
1918
|
+
for statement in tree.body:
|
|
1919
|
+
if (isinstance(statement, ast.Import) and len(statement.names) == 1
|
|
1920
|
+
and statement.names[0].name == "subprocess" and statement.names[0].asname is None):
|
|
1921
|
+
imports.add("subprocess")
|
|
1922
|
+
continue
|
|
1923
|
+
if (isinstance(statement, ast.ImportFrom) and statement.level == 0
|
|
1924
|
+
and statement.module == "pathlib" and len(statement.names) == 1
|
|
1925
|
+
and statement.names[0].name == "Path" and statement.names[0].asname is None):
|
|
1926
|
+
imports.add("Path")
|
|
1927
|
+
continue
|
|
1928
|
+
if not (isinstance(statement, ast.Expr) and isinstance(statement.value, ast.Call)
|
|
1929
|
+
and isinstance(statement.value.func, ast.Name) and statement.value.func.id == "print"
|
|
1930
|
+
and len(statement.value.args) == 1 and not statement.value.keywords):
|
|
1931
|
+
return fallback
|
|
1932
|
+
call = statement.value.args[0]
|
|
1933
|
+
if not isinstance(call, ast.Call) or not isinstance(call.func, ast.Attribute):
|
|
1934
|
+
return fallback
|
|
1935
|
+
owner = call.func.value
|
|
1936
|
+
if (isinstance(owner, ast.Name) and owner.id == "subprocess" and "subprocess" in imports
|
|
1937
|
+
and call.func.attr == "check_output" and len(call.args) == 1
|
|
1938
|
+
and isinstance(call.args[0], (ast.List, ast.Tuple)) and len(call.keywords) == 1
|
|
1939
|
+
and call.keywords[0].arg == "text" and isinstance(call.keywords[0].value, ast.Constant)
|
|
1940
|
+
and call.keywords[0].value.value is True):
|
|
1941
|
+
args = [literal(value) for value in call.args[0].elts]
|
|
1942
|
+
if (len(args) != 3 or args[:2] != ["git", "show"] or args[2] is None
|
|
1943
|
+
or not args[2].startswith("HEAD:") or not read_path(ast.Constant(args[2][5:]))):
|
|
1944
|
+
return fallback
|
|
1945
|
+
reads.append(" ".join(args))
|
|
1946
|
+
continue
|
|
1947
|
+
if (isinstance(owner, ast.Call) and isinstance(owner.func, ast.Name)
|
|
1948
|
+
and owner.func.id == "Path" and "Path" in imports
|
|
1949
|
+
and len(owner.args) == 1 and not owner.keywords and not call.args):
|
|
1950
|
+
path = read_path(owner.args[0])
|
|
1951
|
+
allowed = call.func.attr in {"exists", "is_file", "read_text"} and not call.keywords
|
|
1952
|
+
if call.func.attr == "read_text" and len(call.keywords) == 1:
|
|
1953
|
+
allowed = call.keywords[0].arg == "encoding" and literal(call.keywords[0].value) == "utf-8"
|
|
1954
|
+
if path is not None and allowed:
|
|
1955
|
+
reads.append("cat " + path)
|
|
1956
|
+
continue
|
|
1957
|
+
return fallback
|
|
1958
|
+
return "\n".join(reads) if reads else fallback
|
|
1959
|
+
|
|
1960
|
+
|
|
1961
|
+
def _debug_validation_command_view(cmd):
|
|
1962
|
+
"""Exclude literal stdin data only for this package's read-only validator.
|
|
1963
|
+
|
|
1964
|
+
Accept one PowerShell single-quoted here-string piped to the exact installed
|
|
1965
|
+
helper with literal ``-B ... validate`` argv. Everything else keeps the raw
|
|
1966
|
+
guard view: expandable strings, wrappers, other helpers, extra commands,
|
|
1967
|
+
redirections and early terminators cannot become an exemption.
|
|
1968
|
+
"""
|
|
1969
|
+
# PowerShell accepts lone CR newlines and curly single-quote terminators.
|
|
1970
|
+
# Keep those forms raw rather than missing an early terminator and hiding
|
|
1971
|
+
# executable code. JSON can represent these data characters with escapes.
|
|
1972
|
+
if re.search(r"\r(?!\n)|[\u2018-\u201b]", cmd):
|
|
1973
|
+
return cmd
|
|
1974
|
+
opening = re.match(r"\A@'\r?\n", cmd)
|
|
1975
|
+
if opening is None:
|
|
1976
|
+
return cmd
|
|
1977
|
+
closing = re.search(r"(?m)^[ \t]*'@", cmd[opening.end():])
|
|
1978
|
+
if closing is None or closing.group() != "'@":
|
|
1979
|
+
return cmd
|
|
1980
|
+
body = cmd[opening.end():opening.end() + closing.start()]
|
|
1981
|
+
try:
|
|
1982
|
+
if len(body.encode("utf-8")) > 65536 or not isinstance(json.loads(body), dict):
|
|
1983
|
+
return cmd
|
|
1984
|
+
except (ValueError, UnicodeError, RecursionError):
|
|
1985
|
+
return cmd
|
|
1986
|
+
tail = cmd[opening.end() + closing.end():].strip()
|
|
1987
|
+
if not tail.startswith("|"):
|
|
1988
|
+
return cmd
|
|
1989
|
+
invocation = tail[1:].strip()
|
|
1990
|
+
if re.search(r"[\r\n;&|<>`$%!^*?{}\[\]()]", invocation):
|
|
1991
|
+
return cmd
|
|
1992
|
+
tokens, end = [], 0
|
|
1993
|
+
for match in re.finditer(r'''(?:[^\s'"\x00]|'[^']*'|"[^"]*")+''', invocation):
|
|
1994
|
+
if invocation[end:match.start()].strip():
|
|
1995
|
+
return cmd
|
|
1996
|
+
token = match.group()
|
|
1997
|
+
if len(token) >= 2 and token[0] == token[-1] and token[0] in "\"'":
|
|
1998
|
+
token = token[1:-1]
|
|
1999
|
+
tokens.append(token)
|
|
2000
|
+
end = match.end()
|
|
2001
|
+
if invocation[end:].strip() or len(tokens) != 4 or tokens[1] != "-B" or tokens[3] != "validate":
|
|
2002
|
+
return cmd
|
|
2003
|
+
executable = tokens[0].replace("\\", "/").rsplit("/", 1)[-1]
|
|
2004
|
+
if not re.fullmatch(r"(?:python(?:[23](?:\.\d+)*)?|py)(?:\.exe)?", executable, re.I):
|
|
2005
|
+
return cmd
|
|
2006
|
+
helper = tokens[2]
|
|
2007
|
+
known = os.path.join(os.path.dirname(os.path.abspath(__file__)), "debug-handoff.py")
|
|
2008
|
+
if not os.path.isabs(helper) or os.path.normcase(os.path.normpath(helper)) != os.path.normcase(known):
|
|
2009
|
+
return cmd
|
|
2010
|
+
return invocation
|
|
2011
|
+
|
|
2012
|
+
|
|
1847
2013
|
def run_guards(payload, root, ti):
|
|
1848
2014
|
"""Run every H-NN gate against `ti` (the already host-normalized Bash/
|
|
1849
2015
|
PowerShell tool_input — see pre-bash.py's `_run`, which resolves it via
|
|
@@ -1853,7 +2019,8 @@ def run_guards(payload, root, ti):
|
|
|
1853
2019
|
sys.exit(0). This is the composed body pre-bash.py's `_run` used to run
|
|
1854
2020
|
inline (issue #320) — moved here verbatim, split into one function per
|
|
1855
2021
|
gate, each still carrying its H-NN ID."""
|
|
1856
|
-
cmd = ti.get("command", "") or ""
|
|
2022
|
+
cmd = _debug_validation_command_view(ti.get("command", "") or "")
|
|
2023
|
+
cmd = _literal_python_read_view(cmd)
|
|
1857
2024
|
|
|
1858
2025
|
# Heredoc bodies are stdin text, not arguments — match the git command over
|
|
1859
2026
|
# a body-stripped view so message content (which may contain `;`/`|`/`&`,
|
|
@@ -1905,9 +2072,25 @@ def run_guards(payload, root, ti):
|
|
|
1905
2072
|
heredoc_shell_fallback and PUSH_RE.search(cmd))
|
|
1906
2073
|
_check_push_gates(push, cwd)
|
|
1907
2074
|
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2075
|
+
# Preserve quoted literal paths AND check executable continuations inside
|
|
2076
|
+
# nested/double quotes. Outside quotes, join POSIX/PowerShell/cmd escapes
|
|
2077
|
+
# so a split flag remains one argv item. Neither view changes other guards.
|
|
2078
|
+
quoted_staging = re.sub(r'''("[^"]*"|'[^']*')|[\\`^](?:\r\n|\n|\r)''',
|
|
2079
|
+
lambda match: match.group(1) or "", cmd)
|
|
2080
|
+
joined_staging = re.sub(r"[\\`^](?:\r\n|\n|\r)", "", cmd)
|
|
2081
|
+
adds = []
|
|
2082
|
+
for staging_cmd in dict.fromkeys((quoted_staging, joined_staging)):
|
|
2083
|
+
# Continuations can form the heredoc operator or shell name itself.
|
|
2084
|
+
# Re-evaluate execution per view, without rewriting quoted paths again.
|
|
2085
|
+
staging_view = HEREDOC_RE.sub(r"\3 ", staging_cmd)
|
|
2086
|
+
adds.extend(ADD_RE.finditer(staging_view))
|
|
2087
|
+
if "<<" in staging_cmd and (
|
|
2088
|
+
_heredoc_fed_to_shell(staging_cmd) or _has_shell_executor(staging_cmd)):
|
|
2089
|
+
adds.extend(ADD_RE.finditer(staging_cmd))
|
|
2090
|
+
for staging in adds:
|
|
2091
|
+
_check_h03_wildcard_add(staging, git_cwd(staging.group(0), exec_root))
|
|
2092
|
+
# Commit security/migration checks still see that the batch stages files.
|
|
2093
|
+
add = adds[0] if adds else None
|
|
1911
2094
|
_check_h05_audit_log(cmd)
|
|
1912
2095
|
_check_h11_decisions(cmd)
|
|
1913
2096
|
_check_h18_context_md(cmd)
|
|
@@ -11,7 +11,7 @@ import hostapi # noqa: E402
|
|
|
11
11
|
class PiHost(hostapi.Host):
|
|
12
12
|
name = "pi"
|
|
13
13
|
adapter_name = "@arbiterforge/ca-pi"
|
|
14
|
-
adapter_version = "0.21.
|
|
14
|
+
adapter_version = "0.21.7"
|
|
15
15
|
update_target = "ca-pi"
|
|
16
16
|
update_tag_prefix = "ca-pi-v"
|
|
17
17
|
update_command = "pi update npm:@arbiterforge/ca-pi"
|
|
@@ -950,6 +950,8 @@ def consume_from_hook(
|
|
|
950
950
|
prepared = _replylib.prepare(raw, "prerequisite")
|
|
951
951
|
prompt = prepared["text"]
|
|
952
952
|
attempted = prepared["attempted"]
|
|
953
|
+
if not attempted:
|
|
954
|
+
return ""
|
|
953
955
|
if prepared["notice"]:
|
|
954
956
|
_replylib.record_near_miss(root, raw, "code")
|
|
955
957
|
return prepared["notice"]
|
|
@@ -406,19 +406,72 @@ DIAGNOSTICS = Path(".codearbiter/.markers/reply-diagnostics.log")
|
|
|
406
406
|
DIAGNOSTICS_CAP = 256 * 1024
|
|
407
407
|
|
|
408
408
|
|
|
409
|
+
def _capture_attempt(raw, route: str) -> bool:
|
|
410
|
+
"""Recognize an explicit legacy reply, not an ordinary leading verb.
|
|
411
|
+
|
|
412
|
+
Keep near-miss diagnostics for a coded/token reply with appended prose,
|
|
413
|
+
without repairing that prose into an accepted reply. Merely saying
|
|
414
|
+
``Approve`` or ``reconcile the plan`` must not start legacy capture.
|
|
415
|
+
"""
|
|
416
|
+
if not isinstance(raw, str) or not raw or len(raw) > MAX_INPUT:
|
|
417
|
+
return False
|
|
418
|
+
words = _clean(raw).split(" ")
|
|
419
|
+
verb = words[0].lower()
|
|
420
|
+
if SHORT_ROUTE.get(verb) != route:
|
|
421
|
+
return False
|
|
422
|
+
for (candidate, count), shape in GRAMMAR.items():
|
|
423
|
+
if candidate != verb or len(words) < count:
|
|
424
|
+
continue
|
|
425
|
+
fields = list(zip(words[1:count], shape))
|
|
426
|
+
if any((kind == "I" and not ID_RE.fullmatch(word))
|
|
427
|
+
or (kind == "M" and word.lower() not in MODES)
|
|
428
|
+
or (kind == "S" and word.upper() not in STATES)
|
|
429
|
+
for word, kind in fields[:-1]):
|
|
430
|
+
continue
|
|
431
|
+
token = fields[-1][0].rstrip(",.!?")
|
|
432
|
+
if CODE_RE.fullmatch(token.upper()):
|
|
433
|
+
if not token.isalpha():
|
|
434
|
+
return True
|
|
435
|
+
# Ordinary words overlap the code alphabet in EVERY casing:
|
|
436
|
+
# "Approve DATA migration" is not a protocol request. Only a
|
|
437
|
+
# retained matching code makes an all-letter token diagnostic.
|
|
438
|
+
# Exact armed replies remain case-insensitive; prose is not repaired.
|
|
439
|
+
if any(value.get("route") == route
|
|
440
|
+
and value.get("short_reply", "").rsplit(" ", 1)[-1] == token.upper()
|
|
441
|
+
for _path, value, _alive in _entries()):
|
|
442
|
+
return True
|
|
443
|
+
continue
|
|
444
|
+
if "I" in shape and re.fullmatch(r"[A-Za-z0-9_-]{12,128}", token):
|
|
445
|
+
# The long form's ID/token grammar also fits natural prose such
|
|
446
|
+
# as "approve the implementation". Require a retained request
|
|
447
|
+
# for its named subject before classifying it as a near miss.
|
|
448
|
+
prefix = canonicalize(" ".join(words[:count])).rsplit(" ", 1)[0]
|
|
449
|
+
if any(value.get("route") == route
|
|
450
|
+
and value.get("full_reply", "").rsplit(" ", 1)[0] == prefix
|
|
451
|
+
for _path, value, _alive in _entries()):
|
|
452
|
+
return True
|
|
453
|
+
# Old full-token routes can predate the short-code registry.
|
|
454
|
+
import _artifactpromptlib
|
|
455
|
+
try:
|
|
456
|
+
return _artifactpromptlib.resolve(route, canonicalize(raw)) is not None
|
|
457
|
+
except _artifactpromptlib.PromptRouteError:
|
|
458
|
+
# Let the adapter report the ambiguous legacy route through
|
|
459
|
+
# its existing capture-failure boundary, never approve it.
|
|
460
|
+
return True
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
|
|
409
464
|
def prepare(raw, route: str) -> dict[str, object]:
|
|
410
465
|
"""The text an adapter should match, plus a notice owned by ``route``.
|
|
411
466
|
|
|
412
|
-
|
|
413
|
-
|
|
467
|
+
Only explicit legacy replies enter capture. Ordinary user intent stays
|
|
468
|
+
byte-for-byte unchanged and never becomes a request for another code.
|
|
414
469
|
"""
|
|
470
|
+
if not _capture_attempt(raw, route):
|
|
471
|
+
return {"text": raw, "notice": "", "attempted": False}
|
|
415
472
|
canonical = canonicalize(raw)
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
first = _clean(raw).split(" ", 1)[0].lower()
|
|
419
|
-
mine = SHORT_ROUTE.get(first) == route
|
|
420
|
-
expanded = expand(canonical) if mine else {"text": canonical, "notice": ""}
|
|
421
|
-
return {"text": expanded["text"], "notice": expanded["notice"] if mine else "", "attempted": mine}
|
|
473
|
+
expanded = expand(canonical)
|
|
474
|
+
return {"text": expanded["text"], "notice": expanded["notice"], "attempted": True}
|
|
422
475
|
|
|
423
476
|
|
|
424
477
|
def miss_notice(example: str) -> str:
|
|
@@ -313,7 +313,7 @@ class Host:
|
|
|
313
313
|
|
|
314
314
|
name = "claude"
|
|
315
315
|
adapter_name = "ca"
|
|
316
|
-
adapter_version = "3.2.
|
|
316
|
+
adapter_version = "3.2.7"
|
|
317
317
|
|
|
318
318
|
# Update-notifier descriptor. Each independently versioned host overrides
|
|
319
319
|
# these three values in its per-plugin _host.py. Keeping the target,
|
|
@@ -93,24 +93,27 @@ approval, dispatch, acceptance, commit proof, or finalization. This is the
|
|
|
93
93
|
repository's cooperative same-user attestation model, not cryptographic identity
|
|
94
94
|
proof or protection from an unrestricted same-user filesystem writer.
|
|
95
95
|
|
|
96
|
-
For ordinary
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
query both current identities. The installed engine's explicit `plan-bind` request with
|
|
96
|
+
For ordinary planning and resume, create or maintain a plan as `draft_preview` and
|
|
97
|
+
consume existing criterion IDs. Source rebinding records the selected requirements;
|
|
98
|
+
it does not require a new approval or reset existing work. A normative change can make
|
|
99
|
+
historical evidence inapplicable. Preserve that history without relabeling it as current.
|
|
100
|
+
|
|
101
|
+
When an in-scope correction revises the source spec, query both current identities.
|
|
102
|
+
The installed engine's explicit `plan-bind` request with
|
|
104
103
|
`binding_mode: draft_preview` requires the plan's existing `expected` CAS and the spec's
|
|
105
|
-
`spec_expected` revision/model CAS. It
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
`spec_expected` revision/model CAS. It refreshes only the existing same-ID, same-slug
|
|
105
|
+
pair, including plans with started, reviewed, blocked or evidenced work. Optional typed
|
|
106
|
+
plan corrections must leave both documents ready. Execution, stops, observations and
|
|
107
|
+
historical approvals are retained; the refreshed plan has draft governance and a
|
|
108
|
+
`draft_preview` binding. This operation grants no approval, delegation, prerequisite
|
|
109
|
+
satisfaction, execution or acceptance. Revalidate the current pair and reassess only
|
|
110
|
+
affected work and evidence. Do not arm combined approval for ordinary maintenance.
|
|
111
|
+
|
|
112
|
+
Inspect the installed operation schema first. An engine that rejects this supported
|
|
113
|
+
source refresh has a capability gap; retain the stale binding honestly while continuing
|
|
114
|
+
unaffected authorized work. Never edit generated HTML, recreate the pair or erase history
|
|
115
|
+
to satisfy a legacy initial-approval restriction. The historical initial combined approval
|
|
116
|
+
path still requires an unapproved, unstarted and unevidenced initial pair.
|
|
114
117
|
|
|
115
118
|
## Historical paired approval and delegated methods
|
|
116
119
|
|