@ictechgy/context-guard 0.4.11 → 0.4.12
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/CHANGELOG.md +4 -0
- package/README.ko.md +19 -12
- package/README.md +11 -11
- package/package.json +1 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/bin/context-guard +42 -46
- package/plugins/context-guard/bin/context-guard-audit +3 -3
- package/plugins/context-guard/bin/context-guard-bench +136 -16
- package/plugins/context-guard/bin/context-guard-cache-score +29 -2
- package/plugins/context-guard/bin/context-guard-compress +89 -27
- package/plugins/context-guard/bin/context-guard-filter +88 -18
- package/plugins/context-guard/bin/context-guard-pack +28 -2
- package/plugins/context-guard/bin/context-guard-read-symbol +27 -0
- package/plugins/context-guard/bin/context-guard-sanitize-output +169 -6
- package/plugins/context-guard/bin/context-guard-setup +21 -5
- package/plugins/context-guard/bin/context-guard-tool-prune +48 -10
- package/plugins/context-guard/bin/context-guard-trim-output +109 -52
- package/plugins/context-guard/lib/context_guard_command_manifest_loader.py +123 -0
- package/plugins/context-guard/lib/context_guard_commands.py +4 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Trusted literal loader for the ContextGuard command manifest.
|
|
2
|
+
|
|
3
|
+
The command manifest is intentionally a literal-only Python data file so release
|
|
4
|
+
gates and runtime dispatchers can inspect it without executing manifest code.
|
|
5
|
+
This helper centralizes the bounded no-follow read and AST-literal parsing logic
|
|
6
|
+
used by the runtime dispatcher, release gates, and tests.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import stat
|
|
14
|
+
from typing import Any, Iterable, Mapping
|
|
15
|
+
|
|
16
|
+
MAX_COMMAND_MANIFEST_BYTES = 128 * 1024
|
|
17
|
+
|
|
18
|
+
COMMAND_MANIFEST_LITERAL_NAMES = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"IMPLEMENTATION_PAIRS",
|
|
21
|
+
"HELPER_PAIRS",
|
|
22
|
+
"NPM_BINS",
|
|
23
|
+
"NPM_BIN_PATHS",
|
|
24
|
+
"DISPATCHER_SUBCOMMANDS",
|
|
25
|
+
"LEGACY_WRAPPERS",
|
|
26
|
+
"ENTRYPOINT_SMOKE_CASES",
|
|
27
|
+
"PLUGIN_ENTRYPOINTS",
|
|
28
|
+
"DISPATCHER_SMOKE_CASES",
|
|
29
|
+
"EXPECTED_COMMAND_PACK_FILES",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def manifest_open_flags() -> int | None:
|
|
35
|
+
if not hasattr(os, "O_NOFOLLOW"):
|
|
36
|
+
return None
|
|
37
|
+
flags = os.O_RDONLY | os.O_NOFOLLOW
|
|
38
|
+
if hasattr(os, "O_CLOEXEC"):
|
|
39
|
+
flags |= os.O_CLOEXEC
|
|
40
|
+
if hasattr(os, "O_NONBLOCK"):
|
|
41
|
+
flags |= os.O_NONBLOCK
|
|
42
|
+
if hasattr(os, "O_NOCTTY"):
|
|
43
|
+
flags |= os.O_NOCTTY
|
|
44
|
+
return flags
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_manifest_source(path: Path, *, max_bytes: int = MAX_COMMAND_MANIFEST_BYTES) -> str | None:
|
|
48
|
+
flags = manifest_open_flags()
|
|
49
|
+
if flags is None:
|
|
50
|
+
return None
|
|
51
|
+
fd = -1
|
|
52
|
+
try:
|
|
53
|
+
fd = os.open(path, flags)
|
|
54
|
+
st = os.fstat(fd)
|
|
55
|
+
if not stat.S_ISREG(st.st_mode) or st.st_size > max_bytes:
|
|
56
|
+
return None
|
|
57
|
+
chunks: list[bytes] = []
|
|
58
|
+
total = 0
|
|
59
|
+
while True:
|
|
60
|
+
chunk = os.read(fd, min(64 * 1024, max_bytes + 1 - total))
|
|
61
|
+
if not chunk:
|
|
62
|
+
break
|
|
63
|
+
chunks.append(chunk)
|
|
64
|
+
total += len(chunk)
|
|
65
|
+
if total > max_bytes:
|
|
66
|
+
return None
|
|
67
|
+
return b"".join(chunks).decode("utf-8")
|
|
68
|
+
except (OSError, UnicodeDecodeError):
|
|
69
|
+
return None
|
|
70
|
+
finally:
|
|
71
|
+
if fd >= 0:
|
|
72
|
+
try:
|
|
73
|
+
os.close(fd)
|
|
74
|
+
except OSError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def literal_command_manifest_from_source(
|
|
79
|
+
source: str,
|
|
80
|
+
*,
|
|
81
|
+
allowed_names: Iterable[str] = COMMAND_MANIFEST_LITERAL_NAMES,
|
|
82
|
+
) -> dict[str, Any]:
|
|
83
|
+
try:
|
|
84
|
+
tree = ast.parse(source)
|
|
85
|
+
except SyntaxError as exc:
|
|
86
|
+
raise ValueError(f"invalid Python manifest syntax: line {exc.lineno}: {exc.msg}") from exc
|
|
87
|
+
allowed = set(allowed_names)
|
|
88
|
+
values: dict[str, Any] = {}
|
|
89
|
+
for node in tree.body:
|
|
90
|
+
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
|
|
91
|
+
continue
|
|
92
|
+
target: str | None = None
|
|
93
|
+
value: ast.expr | None = None
|
|
94
|
+
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
95
|
+
target = node.target.id
|
|
96
|
+
value = node.value
|
|
97
|
+
elif isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
98
|
+
target = node.targets[0].id
|
|
99
|
+
value = node.value
|
|
100
|
+
if target is None:
|
|
101
|
+
raise ValueError(f"unsupported executable manifest statement: {type(node).__name__}")
|
|
102
|
+
if target not in allowed or value is None:
|
|
103
|
+
raise ValueError(f"unsupported manifest assignment: {target}")
|
|
104
|
+
try:
|
|
105
|
+
values[target] = ast.literal_eval(value)
|
|
106
|
+
except (SyntaxError, ValueError) as exc:
|
|
107
|
+
raise ValueError(f"manifest assignment must be a literal: {target}") from exc
|
|
108
|
+
return values
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def command_manifest_namespace(values: Mapping[str, Any], *, required: Iterable[str] = ()) -> type:
|
|
112
|
+
missing = sorted(set(required) - set(values))
|
|
113
|
+
if missing:
|
|
114
|
+
raise ValueError(f"trusted command manifest missing required literals: {', '.join(missing)}")
|
|
115
|
+
return type("CommandManifest", (), dict(values))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def load_command_manifest(path: Path, *, required: Iterable[str] = ()) -> type:
|
|
119
|
+
source = read_manifest_source(path)
|
|
120
|
+
if source is None:
|
|
121
|
+
raise ValueError(f"could not load trusted command manifest source: {path}")
|
|
122
|
+
values = literal_command_manifest_from_source(source)
|
|
123
|
+
return command_manifest_namespace(values, required=required)
|
|
@@ -28,7 +28,9 @@ IMPLEMENTATION_PAIRS = (('context_guard_cli.py', 'context-guard'),
|
|
|
28
28
|
('trim_command_output.py', 'context-guard-trim-output'))
|
|
29
29
|
|
|
30
30
|
HELPER_PAIRS = (('hook_secret_patterns.py', 'lib/hook_secret_patterns.py'),
|
|
31
|
-
('context_guard_commands.py', 'lib/context_guard_commands.py')
|
|
31
|
+
('context_guard_commands.py', 'lib/context_guard_commands.py'),
|
|
32
|
+
('context_guard_command_manifest_loader.py',
|
|
33
|
+
'lib/context_guard_command_manifest_loader.py'))
|
|
32
34
|
|
|
33
35
|
NPM_BINS = ('context-guard',
|
|
34
36
|
'context-guard-cost',
|
|
@@ -226,5 +228,6 @@ EXPECTED_COMMAND_PACK_FILES = ('plugins/context-guard/bin/claude-read-symbol',
|
|
|
226
228
|
'plugins/context-guard/bin/context-guard-statusline-merged',
|
|
227
229
|
'plugins/context-guard/bin/context-guard-tool-prune',
|
|
228
230
|
'plugins/context-guard/bin/context-guard-trim-output',
|
|
231
|
+
'plugins/context-guard/lib/context_guard_command_manifest_loader.py',
|
|
229
232
|
'plugins/context-guard/lib/context_guard_commands.py',
|
|
230
233
|
'plugins/context-guard/lib/hook_secret_patterns.py')
|