@ictechgy/context-guard 0.4.15 → 0.5.1
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 +80 -0
- package/README.ko.md +128 -2
- package/README.md +144 -3
- package/docs/distribution.md +100 -0
- package/package.json +4 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +43 -1
- package/plugins/context-guard/README.md +44 -1
- package/plugins/context-guard/bin/bash_reference_policy.py +967 -0
- package/plugins/context-guard/bin/context-guard-artifact +90 -9
- package/plugins/context-guard/bin/context-guard-audit +169 -66
- package/plugins/context-guard/bin/context-guard-bench +9865 -211
- package/plugins/context-guard/bin/context-guard-compress +90 -8
- package/plugins/context-guard/bin/context-guard-diet +1 -7
- package/plugins/context-guard/bin/context-guard-experiments +5 -1
- package/plugins/context-guard/bin/context-guard-failed-nudge +777 -83
- package/plugins/context-guard/bin/context-guard-guard-read +496 -57
- package/plugins/context-guard/bin/context-guard-mcp +2 -1
- package/plugins/context-guard/bin/context-guard-pack +1570 -150
- package/plugins/context-guard/bin/context-guard-read-symbol +7 -2
- package/plugins/context-guard/bin/context-guard-rewrite-bash +2669 -236
- package/plugins/context-guard/bin/context-guard-sanitize-output +723 -92
- package/plugins/context-guard/bin/context-guard-setup +1944 -222
- package/plugins/context-guard/bin/context-guard-statusline +163 -55
- package/plugins/context-guard/bin/context-guard-statusline-merged +78 -23
- package/plugins/context-guard/bin/context-guard-tool-prune +44 -11
- package/plugins/context-guard/bin/context-guard-trim-output +795 -48
- package/plugins/context-guard/brief/README.md +19 -0
- package/plugins/context-guard/brief/narration-mode.quiet.md +21 -0
- package/plugins/context-guard/lib/context_guard_commands.py +10 -2
- package/plugins/context-guard/lib/credential_policy.py +185 -0
- package/plugins/context-guard/lib/transcript_usage_reducer.py +378 -0
|
@@ -134,8 +134,14 @@ class FallbackLineSanitizer:
|
|
|
134
134
|
r"([A-Za-z0-9_.-]*(?:api[_-]?key|token|secret|password|passwd|pwd)[A-Za-z0-9_.-]*\s*[:=]\s*)\S+)"
|
|
135
135
|
)
|
|
136
136
|
|
|
137
|
-
def __init__(
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
show_paths: bool = False,
|
|
141
|
+
context: str = "unknown_text",
|
|
142
|
+
) -> None:
|
|
138
143
|
self.show_paths = show_paths
|
|
144
|
+
self.context = context
|
|
139
145
|
self.redactions = 0
|
|
140
146
|
|
|
141
147
|
def sanitize(self, raw_line: str) -> tuple[str, bool]:
|
|
@@ -151,7 +157,32 @@ class FallbackLineSanitizer:
|
|
|
151
157
|
return line, bool(count)
|
|
152
158
|
|
|
153
159
|
|
|
154
|
-
def
|
|
160
|
+
def instantiate_line_sanitizer(
|
|
161
|
+
factory: object,
|
|
162
|
+
*,
|
|
163
|
+
show_paths: bool,
|
|
164
|
+
context: str,
|
|
165
|
+
private_roots: tuple[str, ...] = (),
|
|
166
|
+
) -> object:
|
|
167
|
+
try:
|
|
168
|
+
return factory( # type: ignore[operator]
|
|
169
|
+
show_paths=show_paths,
|
|
170
|
+
context=context,
|
|
171
|
+
private_roots=private_roots,
|
|
172
|
+
)
|
|
173
|
+
except TypeError:
|
|
174
|
+
if context != "unknown_text" or private_roots:
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
"adjacent sanitizer does not support required explicit context"
|
|
177
|
+
)
|
|
178
|
+
return factory(show_paths=show_paths) # type: ignore[operator]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def load_line_sanitizer(
|
|
182
|
+
show_paths: bool,
|
|
183
|
+
context: str = "unknown_text",
|
|
184
|
+
private_roots: tuple[str, ...] = (),
|
|
185
|
+
) -> object:
|
|
155
186
|
"""Reuse the shipped strong sanitizer when present; else fall back locally.
|
|
156
187
|
|
|
157
188
|
Mirrors context_escrow.py so the compress CLI redacts with the same rules
|
|
@@ -168,16 +199,36 @@ def load_line_sanitizer(show_paths: bool) -> object:
|
|
|
168
199
|
if spec is None:
|
|
169
200
|
raise RuntimeError("import spec unavailable")
|
|
170
201
|
module = importlib.util.module_from_spec(spec)
|
|
171
|
-
loader.
|
|
172
|
-
|
|
202
|
+
sys.modules[loader.name] = module
|
|
203
|
+
try:
|
|
204
|
+
loader.exec_module(module)
|
|
205
|
+
except Exception:
|
|
206
|
+
sys.modules.pop(loader.name, None)
|
|
207
|
+
raise
|
|
208
|
+
return instantiate_line_sanitizer(
|
|
209
|
+
module.LineSanitizer,
|
|
210
|
+
show_paths=show_paths,
|
|
211
|
+
context=context,
|
|
212
|
+
private_roots=private_roots,
|
|
213
|
+
)
|
|
173
214
|
except Exception as exc:
|
|
174
215
|
raise RuntimeError(f"could not load sanitizer {candidate}: {exc}") from exc
|
|
175
|
-
return FallbackLineSanitizer(show_paths=show_paths)
|
|
216
|
+
return FallbackLineSanitizer(show_paths=show_paths, context=context)
|
|
176
217
|
|
|
177
218
|
|
|
178
|
-
def sanitize_text(
|
|
219
|
+
def sanitize_text(
|
|
220
|
+
text: str,
|
|
221
|
+
*,
|
|
222
|
+
show_paths: bool = False,
|
|
223
|
+
context: str = "unknown_text",
|
|
224
|
+
private_roots: tuple[str, ...] = (),
|
|
225
|
+
) -> tuple[str, int]:
|
|
179
226
|
"""Redact secrets line-by-line, returning sanitized text and redacted-line count."""
|
|
180
|
-
sanitizer = load_line_sanitizer(
|
|
227
|
+
sanitizer = load_line_sanitizer(
|
|
228
|
+
show_paths,
|
|
229
|
+
context=context,
|
|
230
|
+
private_roots=private_roots,
|
|
231
|
+
)
|
|
181
232
|
redacted = 0
|
|
182
233
|
out: list[str] = []
|
|
183
234
|
for line in text.splitlines(True):
|
|
@@ -737,13 +788,20 @@ def compress_text(
|
|
|
737
788
|
max_bytes: int,
|
|
738
789
|
protected_policy_enabled: bool = False,
|
|
739
790
|
compression_mode: str = "conservative",
|
|
791
|
+
sanitization_context: str = "unknown_text",
|
|
792
|
+
private_roots: tuple[str, ...] = (),
|
|
740
793
|
) -> tuple[str, dict[str, object]]:
|
|
741
794
|
"""Sanitize first, then classify and compress, then build the receipt.
|
|
742
795
|
|
|
743
796
|
Redaction runs on the raw input so no secret can leak into the classifier,
|
|
744
797
|
the compressed body, or the metadata that follows.
|
|
745
798
|
"""
|
|
746
|
-
sanitized, redacted_lines = sanitize_text(
|
|
799
|
+
sanitized, redacted_lines = sanitize_text(
|
|
800
|
+
text,
|
|
801
|
+
show_paths=show_paths,
|
|
802
|
+
context=sanitization_context,
|
|
803
|
+
private_roots=private_roots,
|
|
804
|
+
)
|
|
747
805
|
parsed_json: object = JSON_PARSE_FAILED
|
|
748
806
|
if forced_type is not None:
|
|
749
807
|
content_type, type_source = forced_type, "override"
|
|
@@ -789,6 +847,9 @@ def compress_text(
|
|
|
789
847
|
protected_policy_enabled=protected_policy_enabled,
|
|
790
848
|
compression_mode=compression_mode,
|
|
791
849
|
)
|
|
850
|
+
redaction_metadata = metadata.get("redaction")
|
|
851
|
+
if isinstance(redaction_metadata, dict):
|
|
852
|
+
redaction_metadata["context"] = sanitization_context
|
|
792
853
|
return compressed, metadata
|
|
793
854
|
|
|
794
855
|
|
|
@@ -837,6 +898,10 @@ def run_compress(args: argparse.Namespace) -> int:
|
|
|
837
898
|
max_bytes=max_bytes,
|
|
838
899
|
protected_policy_enabled=bool(args.protected_policy),
|
|
839
900
|
compression_mode=compression_mode,
|
|
901
|
+
sanitization_context=(
|
|
902
|
+
"source_code" if forced_type == "code" else args.sanitize_context
|
|
903
|
+
),
|
|
904
|
+
private_roots=tuple(args.private_root),
|
|
840
905
|
)
|
|
841
906
|
if args.json:
|
|
842
907
|
payload = {"metadata": metadata, "content": compressed}
|
|
@@ -883,6 +948,23 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
883
948
|
action="store_true",
|
|
884
949
|
help="show raw absolute paths instead of path hashes; local debugging only because private paths may be exposed",
|
|
885
950
|
)
|
|
951
|
+
parser.add_argument(
|
|
952
|
+
"--sanitize-context",
|
|
953
|
+
choices=(
|
|
954
|
+
"unknown_text",
|
|
955
|
+
"command_search_diff",
|
|
956
|
+
"filesystem_listing",
|
|
957
|
+
"source_code",
|
|
958
|
+
),
|
|
959
|
+
default="unknown_text",
|
|
960
|
+
help="declare the input origin for conservative secret/path sanitization",
|
|
961
|
+
)
|
|
962
|
+
parser.add_argument(
|
|
963
|
+
"--private-root",
|
|
964
|
+
action="append",
|
|
965
|
+
default=[],
|
|
966
|
+
help="private root for filesystem_listing sanitization; may be repeated",
|
|
967
|
+
)
|
|
886
968
|
parser.add_argument("--max-bytes", type=int, default=DEFAULT_MAX_BYTES, help="maximum stdin bytes to read before truncating")
|
|
887
969
|
parser.set_defaults(func=run_compress)
|
|
888
970
|
return parser
|
|
@@ -106,8 +106,6 @@ HEAVY_PROJECT_DENIES: tuple[tuple[str, str, str], ...] = (
|
|
|
106
106
|
(".claude-token-optimizer", ".claude-token-optimizer", "Read(./.claude-token-optimizer/**)"),
|
|
107
107
|
)
|
|
108
108
|
SENSITIVE_PROJECT_DENIES: tuple[tuple[str, str, str], ...] = (
|
|
109
|
-
(".env", ".env", "Read(./.env)"),
|
|
110
|
-
(".env.*", ".env.*", "Read(./.env.*)"),
|
|
111
109
|
(".npmrc", ".npmrc", "Read(./.npmrc)"),
|
|
112
110
|
(".pypirc", ".pypirc", "Read(./.pypirc)"),
|
|
113
111
|
(".netrc", ".netrc", "Read(./.netrc)"),
|
|
@@ -512,15 +510,11 @@ def path_target_denied(deny_entries: list[str], recommended: str) -> bool:
|
|
|
512
510
|
|
|
513
511
|
|
|
514
512
|
def project_path_exists(root: Path, rel: str) -> bool:
|
|
515
|
-
if rel == ".env":
|
|
516
|
-
return (root / ".env").exists()
|
|
517
|
-
if rel == ".env.*":
|
|
518
|
-
return any(path.name.startswith(".env.") for path in root.iterdir() if path.exists())
|
|
519
513
|
return (root / rel).exists()
|
|
520
514
|
|
|
521
515
|
|
|
522
516
|
def generic_context_pattern(rel: str) -> str:
|
|
523
|
-
if rel in {".
|
|
517
|
+
if rel in {".npmrc", ".pypirc", ".netrc"}:
|
|
524
518
|
return rel
|
|
525
519
|
if rel.endswith(".*"):
|
|
526
520
|
return rel
|
|
@@ -6664,7 +6664,11 @@ def local_proxy_store_response_artifact(
|
|
|
6664
6664
|
) -> dict[str, Any]:
|
|
6665
6665
|
artifact = load_local_proxy_artifact_module()
|
|
6666
6666
|
directory = artifact.normalize_allowed_first_absolute_symlink(Path(artifact_dir).expanduser())
|
|
6667
|
-
sanitized_text, redacted_lines = artifact.sanitize_text(
|
|
6667
|
+
sanitized_text, redacted_lines = artifact.sanitize_text(
|
|
6668
|
+
response_text,
|
|
6669
|
+
show_paths=show_artifact_paths,
|
|
6670
|
+
context="unknown_text",
|
|
6671
|
+
)
|
|
6668
6672
|
content_bytes = len(sanitized_text.encode("utf-8", errors="replace"))
|
|
6669
6673
|
content_sha = hashlib.sha256(sanitized_text.encode("utf-8", errors="replace")).hexdigest()
|
|
6670
6674
|
command_preview = f"local-proxy response sandbox upstream_status={upstream_status}"
|