@ictechgy/context-guard 0.4.15 → 0.4.16
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 +8 -0
- package/README.ko.md +46 -1
- package/README.md +58 -2
- package/package.json +1 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +18 -0
- package/plugins/context-guard/README.md +18 -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 +5765 -224
- 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 +705 -83
- package/plugins/context-guard/bin/context-guard-guard-read +490 -55
- package/plugins/context-guard/bin/context-guard-pack +110 -11
- package/plugins/context-guard/bin/context-guard-read-symbol +7 -2
- package/plugins/context-guard/bin/context-guard-rewrite-bash +2204 -223
- package/plugins/context-guard/bin/context-guard-sanitize-output +560 -85
- package/plugins/context-guard/bin/context-guard-setup +1073 -147
- package/plugins/context-guard/bin/context-guard-statusline +131 -54
- package/plugins/context-guard/bin/context-guard-statusline-merged +7 -3
- package/plugins/context-guard/bin/context-guard-tool-prune +44 -11
- package/plugins/context-guard/bin/context-guard-trim-output +89 -13
- 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 +6 -2
- package/plugins/context-guard/lib/credential_policy.py +177 -0
- package/plugins/context-guard/lib/transcript_usage_reducer.py +378 -0
|
@@ -165,6 +165,7 @@ class SourceSpec:
|
|
|
165
165
|
label: str | None = None
|
|
166
166
|
input_index: int = 0
|
|
167
167
|
origin: str = "cli"
|
|
168
|
+
sanitization_context: str = "source_code"
|
|
168
169
|
|
|
169
170
|
|
|
170
171
|
@dataclass
|
|
@@ -205,9 +206,32 @@ class PackError(ValueError):
|
|
|
205
206
|
pass
|
|
206
207
|
|
|
207
208
|
|
|
209
|
+
SANITIZATION_CONTEXTS = frozenset(
|
|
210
|
+
{
|
|
211
|
+
"unknown_text",
|
|
212
|
+
"command_search_diff",
|
|
213
|
+
"filesystem_listing",
|
|
214
|
+
"source_code",
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def parse_sanitization_context(value: object) -> str:
|
|
220
|
+
context = str(value or "unknown_text")
|
|
221
|
+
if context not in SANITIZATION_CONTEXTS:
|
|
222
|
+
raise PackError(f"unsupported sanitization context: {context}")
|
|
223
|
+
return context
|
|
224
|
+
|
|
225
|
+
|
|
208
226
|
class FallbackLineSanitizer:
|
|
209
|
-
def __init__(
|
|
227
|
+
def __init__(
|
|
228
|
+
self,
|
|
229
|
+
*,
|
|
230
|
+
show_paths: bool = False,
|
|
231
|
+
context: str = "unknown_text",
|
|
232
|
+
) -> None:
|
|
210
233
|
self.show_paths = show_paths
|
|
234
|
+
self.context = context
|
|
211
235
|
self.redactions = 0
|
|
212
236
|
|
|
213
237
|
def sanitize(self, raw_line: str) -> tuple[str, bool]:
|
|
@@ -252,7 +276,12 @@ def load_line_sanitizer_factory() -> Any:
|
|
|
252
276
|
if spec is None:
|
|
253
277
|
raise RuntimeError("import spec unavailable")
|
|
254
278
|
module = importlib.util.module_from_spec(spec)
|
|
255
|
-
loader.
|
|
279
|
+
sys.modules[loader.name] = module
|
|
280
|
+
try:
|
|
281
|
+
loader.exec_module(module)
|
|
282
|
+
except Exception:
|
|
283
|
+
sys.modules.pop(loader.name, None)
|
|
284
|
+
raise
|
|
256
285
|
_LINE_SANITIZER_FACTORY_CACHE = module.LineSanitizer
|
|
257
286
|
return _LINE_SANITIZER_FACTORY_CACHE
|
|
258
287
|
except Exception as exc:
|
|
@@ -261,13 +290,53 @@ def load_line_sanitizer_factory() -> Any:
|
|
|
261
290
|
return _LINE_SANITIZER_FACTORY_CACHE
|
|
262
291
|
|
|
263
292
|
|
|
264
|
-
def
|
|
293
|
+
def instantiate_line_sanitizer(
|
|
294
|
+
factory: Any,
|
|
295
|
+
*,
|
|
296
|
+
show_paths: bool,
|
|
297
|
+
context: str,
|
|
298
|
+
private_roots: tuple[str, ...] = (),
|
|
299
|
+
) -> object:
|
|
300
|
+
try:
|
|
301
|
+
return factory(
|
|
302
|
+
show_paths=show_paths,
|
|
303
|
+
context=context,
|
|
304
|
+
private_roots=private_roots,
|
|
305
|
+
)
|
|
306
|
+
except TypeError:
|
|
307
|
+
if context != "unknown_text" or private_roots:
|
|
308
|
+
raise RuntimeError(
|
|
309
|
+
"adjacent sanitizer does not support required explicit context"
|
|
310
|
+
)
|
|
311
|
+
return factory(show_paths=show_paths)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def load_line_sanitizer(
|
|
315
|
+
show_paths: bool = False,
|
|
316
|
+
context: str = "unknown_text",
|
|
317
|
+
private_roots: tuple[str, ...] = (),
|
|
318
|
+
) -> object:
|
|
265
319
|
sanitizer_factory = load_line_sanitizer_factory()
|
|
266
|
-
return
|
|
320
|
+
return instantiate_line_sanitizer(
|
|
321
|
+
sanitizer_factory,
|
|
322
|
+
show_paths=show_paths,
|
|
323
|
+
context=context,
|
|
324
|
+
private_roots=private_roots,
|
|
325
|
+
)
|
|
267
326
|
|
|
268
327
|
|
|
269
|
-
def sanitize_text(
|
|
270
|
-
|
|
328
|
+
def sanitize_text(
|
|
329
|
+
text: str,
|
|
330
|
+
*,
|
|
331
|
+
show_paths: bool = False,
|
|
332
|
+
context: str = "unknown_text",
|
|
333
|
+
private_roots: tuple[str, ...] = (),
|
|
334
|
+
) -> tuple[str, int]:
|
|
335
|
+
sanitizer = load_line_sanitizer(
|
|
336
|
+
show_paths,
|
|
337
|
+
context=context,
|
|
338
|
+
private_roots=private_roots,
|
|
339
|
+
)
|
|
271
340
|
redacted = 0
|
|
272
341
|
out: list[str] = []
|
|
273
342
|
for line in text.splitlines(True):
|
|
@@ -278,7 +347,13 @@ def sanitize_text(text: str, *, show_paths: bool = False) -> tuple[str, int]:
|
|
|
278
347
|
return "".join(out), redacted
|
|
279
348
|
|
|
280
349
|
|
|
281
|
-
def sanitize_source_lines(
|
|
350
|
+
def sanitize_source_lines(
|
|
351
|
+
handle: Any,
|
|
352
|
+
requested: LineRange | None,
|
|
353
|
+
*,
|
|
354
|
+
context: str = "source_code",
|
|
355
|
+
private_roots: tuple[str, ...] = (),
|
|
356
|
+
) -> tuple[list[str], int, int]:
|
|
282
357
|
"""Sanitize a source stream while retaining only the requested line window.
|
|
283
358
|
|
|
284
359
|
Explicit line-window retrieval still scans the complete file so global
|
|
@@ -286,7 +361,10 @@ def sanitize_source_lines(handle: Any, requested: LineRange | None) -> tuple[lis
|
|
|
286
361
|
outputs, but it no longer materializes a sanitized all-lines list before
|
|
287
362
|
slicing.
|
|
288
363
|
"""
|
|
289
|
-
sanitizer = load_line_sanitizer(
|
|
364
|
+
sanitizer = load_line_sanitizer(
|
|
365
|
+
context=context,
|
|
366
|
+
private_roots=private_roots,
|
|
367
|
+
)
|
|
290
368
|
selected: list[str] = []
|
|
291
369
|
redacted = 0
|
|
292
370
|
total_lines = 0
|
|
@@ -886,6 +964,9 @@ def read_manifest(path: Path) -> list[SourceSpec]:
|
|
|
886
964
|
lines=lines,
|
|
887
965
|
label=cap_label(item.get("label")),
|
|
888
966
|
origin="manifest",
|
|
967
|
+
sanitization_context=parse_sanitization_context(
|
|
968
|
+
item.get("sanitization_context", item.get("context"))
|
|
969
|
+
),
|
|
889
970
|
))
|
|
890
971
|
return out
|
|
891
972
|
|
|
@@ -917,6 +998,9 @@ def parse_source_spec(raw: str) -> SourceSpec:
|
|
|
917
998
|
lines=lines,
|
|
918
999
|
label=cap_label(values.get("label")),
|
|
919
1000
|
origin="cli",
|
|
1001
|
+
sanitization_context=parse_sanitization_context(
|
|
1002
|
+
values.get("sanitization_context", values.get("context"))
|
|
1003
|
+
),
|
|
920
1004
|
)
|
|
921
1005
|
|
|
922
1006
|
|
|
@@ -1094,7 +1178,16 @@ def resolve_source(root: Path, spec: SourceSpec) -> tuple[ResolvedSource | None,
|
|
|
1094
1178
|
try:
|
|
1095
1179
|
with handle:
|
|
1096
1180
|
requested = spec.lines
|
|
1097
|
-
selected, total_lines, redacted_lines = sanitize_source_lines(
|
|
1181
|
+
selected, total_lines, redacted_lines = sanitize_source_lines(
|
|
1182
|
+
handle,
|
|
1183
|
+
requested,
|
|
1184
|
+
context=spec.sanitization_context,
|
|
1185
|
+
private_roots=(
|
|
1186
|
+
(str(root),)
|
|
1187
|
+
if spec.sanitization_context == "filesystem_listing"
|
|
1188
|
+
else ()
|
|
1189
|
+
),
|
|
1190
|
+
)
|
|
1098
1191
|
except OSError:
|
|
1099
1192
|
return None, omission(spec, "unsafe_path", path=display, redacted_path=redacted_path)
|
|
1100
1193
|
if total_lines <= 0:
|
|
@@ -1977,10 +2070,16 @@ def run_git_diff(root: Path, diff_ref: str) -> str:
|
|
|
1977
2070
|
except (OSError, UnicodeError, subprocess.TimeoutExpired) as exc:
|
|
1978
2071
|
raise PackError(f"could not read diff: {exc.__class__.__name__}") from exc
|
|
1979
2072
|
if proc.returncode != 0:
|
|
1980
|
-
detail = sanitize_text(
|
|
2073
|
+
detail = sanitize_text(
|
|
2074
|
+
proc.stderr or proc.stdout or "git diff failed",
|
|
2075
|
+
context="command_search_diff",
|
|
2076
|
+
)[0].strip().splitlines()
|
|
1981
2077
|
message = detail[0] if detail else "git diff failed"
|
|
1982
2078
|
raise PackError(f"could not read diff: {cap_label(message, default='git diff failed', limit=160)}")
|
|
1983
|
-
return sanitize_text(
|
|
2079
|
+
return sanitize_text(
|
|
2080
|
+
proc.stdout[:MAX_SUGGEST_INPUT_BYTES],
|
|
2081
|
+
context="command_search_diff",
|
|
2082
|
+
)[0]
|
|
1984
2083
|
|
|
1985
2084
|
|
|
1986
2085
|
def collect_diff_candidates(root: Path, diff_ref: str, query_terms: set[str], context_lines: int) -> list[SuggestCandidate]:
|
|
@@ -51,7 +51,12 @@ def _load_sanitize_output():
|
|
|
51
51
|
if spec is None:
|
|
52
52
|
continue
|
|
53
53
|
module = importlib.util.module_from_spec(spec)
|
|
54
|
-
loader.
|
|
54
|
+
sys.modules[loader.name] = module
|
|
55
|
+
try:
|
|
56
|
+
loader.exec_module(module)
|
|
57
|
+
except Exception:
|
|
58
|
+
sys.modules.pop(loader.name, None)
|
|
59
|
+
raise
|
|
55
60
|
return module
|
|
56
61
|
raise ImportError("sanitize_output helper not found in " + ", ".join(searched))
|
|
57
62
|
|
|
@@ -412,7 +417,7 @@ def strip_line_for_brace_count(line: str, in_block_comment: bool = False) -> tup
|
|
|
412
417
|
|
|
413
418
|
|
|
414
419
|
def redact_symbol_content(content: str) -> str:
|
|
415
|
-
sanitizer = LineSanitizer(show_paths=
|
|
420
|
+
sanitizer = LineSanitizer(show_paths=False, context="source_code")
|
|
416
421
|
return "".join(sanitizer.sanitize(line)[0] for line in content.splitlines(keepends=True))
|
|
417
422
|
|
|
418
423
|
|