@ictechgy/context-guard 0.4.14 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.ko.md +72 -1
  3. package/README.md +85 -2
  4. package/docs/benchmark-fixtures/image-context-pack-full-evidence.prompt.example.md +28 -0
  5. package/docs/benchmark-fixtures/image-context-pack-packed-evidence.prompt.example.md +31 -0
  6. package/docs/benchmark-fixtures/image-context-pack.evidence.example.jsonl +2 -0
  7. package/docs/benchmark-fixtures/image-context-pack.tasks.example.json +18 -0
  8. package/docs/benchmark-fixtures/image-context-pack.variants.example.json +10 -0
  9. package/docs/benchmark-workflow-examples.md +16 -0
  10. package/docs/experimental-benchmark-fixtures.md +52 -1
  11. package/package.json +2 -1
  12. package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
  13. package/plugins/context-guard/README.ko.md +43 -0
  14. package/plugins/context-guard/README.md +43 -0
  15. package/plugins/context-guard/bin/context-guard-artifact +90 -9
  16. package/plugins/context-guard/bin/context-guard-audit +169 -66
  17. package/plugins/context-guard/bin/context-guard-bench +7038 -307
  18. package/plugins/context-guard/bin/context-guard-compress +90 -8
  19. package/plugins/context-guard/bin/context-guard-diet +1 -7
  20. package/plugins/context-guard/bin/context-guard-experiments +3085 -134
  21. package/plugins/context-guard/bin/context-guard-failed-nudge +705 -83
  22. package/plugins/context-guard/bin/context-guard-guard-read +490 -55
  23. package/plugins/context-guard/bin/context-guard-mcp +999 -0
  24. package/plugins/context-guard/bin/context-guard-pack +744 -20
  25. package/plugins/context-guard/bin/context-guard-read-symbol +7 -2
  26. package/plugins/context-guard/bin/context-guard-rewrite-bash +2204 -223
  27. package/plugins/context-guard/bin/context-guard-sanitize-output +560 -85
  28. package/plugins/context-guard/bin/context-guard-setup +1073 -147
  29. package/plugins/context-guard/bin/context-guard-statusline +131 -54
  30. package/plugins/context-guard/bin/context-guard-statusline-merged +7 -3
  31. package/plugins/context-guard/bin/context-guard-tool-prune +44 -11
  32. package/plugins/context-guard/bin/context-guard-trim-output +89 -13
  33. package/plugins/context-guard/brief/README.md +19 -0
  34. package/plugins/context-guard/brief/narration-mode.quiet.md +21 -0
  35. package/plugins/context-guard/lib/context_guard_commands.py +14 -2
  36. package/plugins/context-guard/lib/credential_policy.py +177 -0
  37. 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__(self, *, show_paths: bool = False) -> None:
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 load_line_sanitizer(show_paths: bool) -> object:
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.exec_module(module)
172
- return module.LineSanitizer(show_paths=show_paths)
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(text: str, *, show_paths: bool = False) -> tuple[str, int]:
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(show_paths)
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(text, show_paths=show_paths)
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 {".env", ".npmrc", ".pypirc", ".netrc"}:
517
+ if rel in {".npmrc", ".pypirc", ".netrc"}:
524
518
  return rel
525
519
  if rel.endswith(".*"):
526
520
  return rel