@ictechgy/context-guard 0.4.16 → 0.6.0

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 (30) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.ko.md +91 -1
  3. package/README.md +95 -1
  4. package/docs/distribution.md +100 -0
  5. package/package.json +5 -1
  6. package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
  7. package/plugins/context-guard/README.ko.md +34 -1
  8. package/plugins/context-guard/README.md +36 -1
  9. package/plugins/context-guard/bin/bash_reference_policy.py +967 -0
  10. package/plugins/context-guard/bin/context-guard-artifact +1 -1
  11. package/plugins/context-guard/bin/context-guard-bench +4216 -103
  12. package/plugins/context-guard/bin/context-guard-failed-nudge +95 -23
  13. package/plugins/context-guard/bin/context-guard-guard-read +6 -2
  14. package/plugins/context-guard/bin/context-guard-mcp +2 -1
  15. package/plugins/context-guard/bin/context-guard-pack +2086 -142
  16. package/plugins/context-guard/bin/context-guard-rewrite-bash +497 -45
  17. package/plugins/context-guard/bin/context-guard-sanitize-output +178 -22
  18. package/plugins/context-guard/bin/context-guard-setup +901 -28
  19. package/plugins/context-guard/bin/context-guard-statusline +33 -2
  20. package/plugins/context-guard/bin/context-guard-statusline-merged +71 -20
  21. package/plugins/context-guard/bin/context-guard-task-memory +635 -0
  22. package/plugins/context-guard/bin/context-guard-trim-output +706 -35
  23. package/plugins/context-guard/lib/context_guard_commands.py +25 -1
  24. package/plugins/context-guard/lib/context_pack_git_boundary.py +19 -0
  25. package/plugins/context-guard/lib/context_pack_identity.py +115 -0
  26. package/plugins/context-guard/lib/context_pack_receipts.py +9 -0
  27. package/plugins/context-guard/lib/context_pack_rendering.py +12 -0
  28. package/plugins/context-guard/lib/context_pack_scanning.py +10 -0
  29. package/plugins/context-guard/lib/context_pack_selection.py +6 -0
  30. package/plugins/context-guard/lib/credential_policy.py +10 -2
@@ -10,6 +10,7 @@ IMPLEMENTATION_PAIRS = (('context_guard_cli.py', 'context-guard'),
10
10
  ('cache_score.py', 'context-guard-cache-score'),
11
11
  ('benchmark_runner.py', 'context-guard-bench'),
12
12
  ('context_escrow.py', 'context-guard-artifact'),
13
+ ('task_memory.py', 'context-guard-task-memory'),
13
14
  ('context_compress.py', 'context-guard-compress'),
14
15
  ('context_guard_mcp.py', 'context-guard-mcp'),
15
16
  ('context_pack.py', 'context-guard-pack'),
@@ -29,17 +30,25 @@ IMPLEMENTATION_PAIRS = (('context_guard_cli.py', 'context-guard'),
29
30
  ('trim_command_output.py', 'context-guard-trim-output'))
30
31
 
31
32
  HELPER_PAIRS = (('hook_secret_patterns.py', 'lib/hook_secret_patterns.py'),
33
+ ('bash_reference_policy.py', 'bin/bash_reference_policy.py'),
32
34
  ('credential_policy.py', 'lib/credential_policy.py'),
33
35
  ('context_guard_commands.py', 'lib/context_guard_commands.py'),
34
36
  ('context_guard_command_manifest_loader.py',
35
37
  'lib/context_guard_command_manifest_loader.py'),
36
- ('transcript_usage_reducer.py', 'lib/transcript_usage_reducer.py'))
38
+ ('transcript_usage_reducer.py', 'lib/transcript_usage_reducer.py'),
39
+ ('context_pack_identity.py', 'lib/context_pack_identity.py'),
40
+ ('context_pack_scanning.py', 'lib/context_pack_scanning.py'),
41
+ ('context_pack_selection.py', 'lib/context_pack_selection.py'),
42
+ ('context_pack_rendering.py', 'lib/context_pack_rendering.py'),
43
+ ('context_pack_git_boundary.py', 'lib/context_pack_git_boundary.py'),
44
+ ('context_pack_receipts.py', 'lib/context_pack_receipts.py'))
37
45
 
38
46
  NPM_BINS = ('context-guard',
39
47
  'context-guard-cost',
40
48
  'context-guard-cache-score',
41
49
  'context-guard-bench',
42
50
  'context-guard-artifact',
51
+ 'context-guard-task-memory',
43
52
  'context-guard-compress',
44
53
  'context-guard-mcp',
45
54
  'context-guard-pack',
@@ -63,6 +72,7 @@ NPM_BIN_PATHS = {'context-guard': 'plugins/context-guard/bin/context-guard',
63
72
  'context-guard-cache-score': 'plugins/context-guard/bin/context-guard-cache-score',
64
73
  'context-guard-bench': 'plugins/context-guard/bin/context-guard-bench',
65
74
  'context-guard-artifact': 'plugins/context-guard/bin/context-guard-artifact',
75
+ 'context-guard-task-memory': 'plugins/context-guard/bin/context-guard-task-memory',
66
76
  'context-guard-compress': 'plugins/context-guard/bin/context-guard-compress',
67
77
  'context-guard-mcp': 'plugins/context-guard/bin/context-guard-mcp',
68
78
  'context-guard-pack': 'plugins/context-guard/bin/context-guard-pack',
@@ -89,10 +99,12 @@ DISPATCHER_SUBCOMMANDS = {'setup': ('context-guard-setup',),
89
99
  'scan': ('context-guard-diet', 'scan'),
90
100
  'trim-output': ('context-guard-trim-output',),
91
101
  'trim': ('context-guard-trim-output',),
102
+ 'reference': ('context-guard-trim-output', '--expand-bash-reference'),
92
103
  'sanitize-output': ('context-guard-sanitize-output',),
93
104
  'sanitize': ('context-guard-sanitize-output',),
94
105
  'filter': ('context-guard-filter',),
95
106
  'artifact': ('context-guard-artifact',),
107
+ 'task-memory': ('context-guard-task-memory',),
96
108
  'pack': ('context-guard-pack',),
97
109
  'tool-prune': ('context-guard-tool-prune',),
98
110
  'compress': ('context-guard-compress',),
@@ -127,6 +139,7 @@ ENTRYPOINT_SMOKE_CASES = {'context-guard': {'args': ['--version'], 'mode': 'text
127
139
  'context-guard-read-symbol': {'args': ['--help'], 'mode': 'text'},
128
140
  'context-guard-sanitize-output': {'args': ['--help'], 'mode': 'text'},
129
141
  'context-guard-artifact': {'args': ['--help'], 'mode': 'text'},
142
+ 'context-guard-task-memory': {'args': ['--help'], 'mode': 'text'},
130
143
  'context-guard-audit': {'args': ['--help'], 'mode': 'text'},
131
144
  'context-guard-bench': {'args': ['--help'], 'mode': 'text'},
132
145
  'context-guard-compress': {'args': ['--help'], 'mode': 'text'},
@@ -174,6 +187,7 @@ PLUGIN_ENTRYPOINTS = ('claude-read-symbol',
174
187
  'claude-trim-output',
175
188
  'context-guard',
176
189
  'context-guard-artifact',
190
+ 'context-guard-task-memory',
177
191
  'context-guard-audit',
178
192
  'context-guard-bench',
179
193
  'context-guard-compress',
@@ -196,10 +210,12 @@ PLUGIN_ENTRYPOINTS = ('claude-read-symbol',
196
210
  'context-guard-trim-output')
197
211
 
198
212
  DISPATCHER_SMOKE_CASES = ({'entrypoint': 'context-guard', 'args': ['experiments', 'list', '--json'], 'mode': 'json'},
213
+ {'entrypoint': 'context-guard', 'args': ['reference', '--help'], 'mode': 'text'},
199
214
  {'entrypoint': 'context-guard', 'args': ['cost', '--help'], 'mode': 'text'},
200
215
  {'entrypoint': 'context-guard', 'args': ['route-advisor', '--help'], 'mode': 'text'},
201
216
  {'entrypoint': 'context-guard', 'args': ['cache-score', '--help'], 'mode': 'text'},
202
217
  {'entrypoint': 'context-guard', 'args': ['mcp', '--help'], 'mode': 'text'},
218
+ {'entrypoint': 'context-guard', 'args': ['task-memory', '--help'], 'mode': 'text'},
203
219
  {'entrypoint': 'context-guard-pack', 'args': ['suggest', '--help'], 'mode': 'text'},
204
220
  {'entrypoint': 'context-guard-pack', 'args': ['auto', '--help'], 'mode': 'text'})
205
221
 
@@ -216,8 +232,10 @@ EXPECTED_COMMAND_PACK_FILES = ('plugins/context-guard/bin/claude-read-symbol',
216
232
  'plugins/context-guard/bin/claude-token-statusline',
217
233
  'plugins/context-guard/bin/claude-token-statusline-merged',
218
234
  'plugins/context-guard/bin/claude-trim-output',
235
+ 'plugins/context-guard/bin/bash_reference_policy.py',
219
236
  'plugins/context-guard/bin/context-guard',
220
237
  'plugins/context-guard/bin/context-guard-artifact',
238
+ 'plugins/context-guard/bin/context-guard-task-memory',
221
239
  'plugins/context-guard/bin/context-guard-audit',
222
240
  'plugins/context-guard/bin/context-guard-bench',
223
241
  'plugins/context-guard/bin/context-guard-cache-score',
@@ -241,5 +259,11 @@ EXPECTED_COMMAND_PACK_FILES = ('plugins/context-guard/bin/claude-read-symbol',
241
259
  'plugins/context-guard/lib/credential_policy.py',
242
260
  'plugins/context-guard/lib/context_guard_command_manifest_loader.py',
243
261
  'plugins/context-guard/lib/context_guard_commands.py',
262
+ 'plugins/context-guard/lib/context_pack_git_boundary.py',
263
+ 'plugins/context-guard/lib/context_pack_identity.py',
264
+ 'plugins/context-guard/lib/context_pack_receipts.py',
265
+ 'plugins/context-guard/lib/context_pack_rendering.py',
266
+ 'plugins/context-guard/lib/context_pack_scanning.py',
267
+ 'plugins/context-guard/lib/context_pack_selection.py',
244
268
  'plugins/context-guard/lib/hook_secret_patterns.py',
245
269
  'plugins/context-guard/lib/transcript_usage_reducer.py')
@@ -0,0 +1,19 @@
1
+ """Narrow fail-closed Git environment boundary for context selection."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+
6
+
7
+ def sanitized_environment(_source: dict[str, str]) -> dict[str, str]:
8
+ return {
9
+ "GIT_ASKPASS": "/usr/bin/false",
10
+ "GIT_CONFIG_GLOBAL": "/dev/null",
11
+ "GIT_CONFIG_NOSYSTEM": "1",
12
+ "GIT_NO_LAZY_FETCH": "1",
13
+ "GIT_NO_REPLACE_OBJECTS": "1",
14
+ "GIT_TERMINAL_PROMPT": "0",
15
+ "LANG": "C",
16
+ "LC_ALL": "C",
17
+ "PATH": "/usr/bin:/bin",
18
+ "SSH_ASKPASS": "/usr/bin/false",
19
+ }
@@ -0,0 +1,115 @@
1
+ """Captured-byte and semantic identities for context-pack artifacts."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path, PurePosixPath
8
+ import re
9
+ import stat
10
+ from typing import Any
11
+
12
+
13
+ SHA256_RE = re.compile(r"[0-9a-f]{64}")
14
+ SEMANTIC_NAME_RE = re.compile(r"[a-z0-9][a-z0-9-]{0,63}")
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ArtifactIdentity:
19
+ captured_sha256: str
20
+ semantic_sha256: str
21
+
22
+
23
+ def _digest(domain: bytes, value: object) -> str:
24
+ encoded = json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("ascii")
25
+ return hashlib.sha256(domain + encoded).hexdigest()
26
+
27
+
28
+ def artifact_identity(*, path: str, mode: str, size: int, sha256: str, semantic_output_sha256: str) -> ArtifactIdentity:
29
+ parsed = PurePosixPath(path)
30
+ if not path or parsed.is_absolute() or parsed.as_posix() != path or ".." in parsed.parts or "\\" in path or "\x00" in path:
31
+ raise ValueError("artifact path must be safe and repository-relative")
32
+ if mode not in {"0644", "0755"} or isinstance(size, bool) or size < 0:
33
+ raise ValueError("artifact mode/size is invalid")
34
+ if SHA256_RE.fullmatch(sha256) is None or SHA256_RE.fullmatch(semantic_output_sha256) is None:
35
+ raise ValueError("artifact digest/semantic output is invalid")
36
+ return ArtifactIdentity(
37
+ captured_sha256=_digest(
38
+ b"contextguard.context-pack-captured/v1\0",
39
+ {"mode": mode, "path": path, "sha256": sha256, "size": size},
40
+ ),
41
+ semantic_sha256=_digest(
42
+ b"contextguard.context-pack-semantic/v1\0",
43
+ {"semantic_output_sha256": semantic_output_sha256},
44
+ ),
45
+ )
46
+
47
+
48
+ def _captured_file(root: Path, relative: str, semantic_output_sha256: str) -> dict[str, Any]:
49
+ path = root / relative
50
+ current = root
51
+ for part in PurePosixPath(relative).parts:
52
+ current = current / part
53
+ if stat.S_ISLNK(current.lstat().st_mode):
54
+ raise ValueError(f"identity artifact contains a symlink: {relative}")
55
+ metadata = path.lstat()
56
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1:
57
+ raise ValueError(f"identity artifact is not a regular file: {relative}")
58
+ mode = f"{stat.S_IMODE(metadata.st_mode):04o}"
59
+ if mode not in {"0644", "0755"} or metadata.st_size > 4 * 1024 * 1024:
60
+ raise ValueError(f"identity artifact mode or size is unsafe: {relative}")
61
+ raw = path.read_bytes()
62
+ after = path.lstat()
63
+ if (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns) != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns):
64
+ raise ValueError(f"identity artifact changed while reading: {relative}")
65
+ digest = hashlib.sha256(raw).hexdigest()
66
+ identity = artifact_identity(
67
+ path=relative, mode=mode, size=len(raw), sha256=digest,
68
+ semantic_output_sha256=semantic_output_sha256,
69
+ )
70
+ return {
71
+ "captured_sha256": identity.captured_sha256,
72
+ "mode": mode,
73
+ "path": relative,
74
+ "semantic_sha256": identity.semantic_sha256,
75
+ "sha256": digest,
76
+ "size": len(raw),
77
+ }
78
+
79
+
80
+ def derive_manifest_identities(root: Path, manifest: dict[str, Any]) -> list[dict[str, Any]]:
81
+ if set(manifest) != {"modules", "schema_version", "semantic_oracle_sha256"} or manifest.get("schema_version") != "contextguard.context-pack-modules/v1":
82
+ raise ValueError("invalid context-pack module manifest")
83
+ modules = manifest.get("modules")
84
+ oracle = manifest.get("semantic_oracle_sha256")
85
+ if not isinstance(oracle, str) or SHA256_RE.fullmatch(oracle) is None:
86
+ raise ValueError("invalid context-pack semantic oracle")
87
+ if not isinstance(modules, list) or not modules:
88
+ raise ValueError("context-pack module manifest is empty")
89
+ derived = []
90
+ for entry in modules:
91
+ if not isinstance(entry, dict) or set(entry) != {"canonical_path", "plugin_path", "role", "semantic_output"}:
92
+ raise ValueError("invalid context-pack module entry")
93
+ role = entry["role"]
94
+ semantic = entry["semantic_output"]
95
+ if (
96
+ not isinstance(role, str)
97
+ or SEMANTIC_NAME_RE.fullmatch(role) is None
98
+ or not isinstance(semantic, str)
99
+ or SEMANTIC_NAME_RE.fullmatch(semantic) is None
100
+ ):
101
+ raise ValueError("invalid context-pack semantic declaration")
102
+ module_oracle = _digest(
103
+ b"contextguard.context-pack-module-semantic/v1\0",
104
+ {
105
+ "oracle_sha256": oracle,
106
+ "role": role,
107
+ "semantic_output": semantic,
108
+ },
109
+ )
110
+ derived.append({
111
+ "role": role,
112
+ "canonical": _captured_file(root, entry["canonical_path"], module_oracle),
113
+ "plugin": _captured_file(root, entry["plugin_path"], module_oracle),
114
+ })
115
+ return derived
@@ -0,0 +1,9 @@
1
+ """Narrow canonical receipt serialization primitives used by the context packer."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+
8
+ def canonical_json_bytes(value: Any) -> bytes:
9
+ return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("ascii")
@@ -0,0 +1,12 @@
1
+ """Narrow byte-identity rendering primitives used by the context packer."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+
6
+
7
+ def byte_len(text: str) -> int:
8
+ return len(text.encode("utf-8", errors="replace"))
9
+
10
+
11
+ def sha256_text(text: str) -> str:
12
+ return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
@@ -0,0 +1,10 @@
1
+ """Narrow source-scanning primitives used by the context packer."""
2
+ from __future__ import annotations
3
+
4
+
5
+ def input_limit_exceeded(*, bytes_read: int, lines_read: int, max_bytes: int, max_lines: int) -> str | None:
6
+ if bytes_read > max_bytes:
7
+ return "source_input_bytes_exceeded"
8
+ if lines_read > max_lines:
9
+ return "source_input_lines_exceeded"
10
+ return None
@@ -0,0 +1,6 @@
1
+ """Narrow deterministic selection primitives used by the context packer."""
2
+ from __future__ import annotations
3
+
4
+
5
+ def priority_key(priority: int, input_index: int, path: str) -> tuple[int, int, str]:
6
+ return (-priority, input_index, path)
@@ -7,7 +7,9 @@ import re
7
7
  SECRET_KEY = (
8
8
  r"[A-Za-z0-9_.-]*(?:api[_-]?key|apikey|token|secret|password|passwd|pwd|"
9
9
  r"private[_-]?key|access[_-]?key|client[_-]?secret|credential|signature|sig|"
10
- r"ssh[_-]?key|pgp[_-]?private[_-]?key)[A-Za-z0-9_.-]*"
10
+ r"ssh[_-]?key|ssh[_-]?command|pgp[_-]?private[_-]?key)[A-Za-z0-9_.-]*"
11
+ r"|(?:pass|[A-Za-z0-9_.-]+[._-]pass)"
12
+ r"(?:[._-](?:v\d+|prod|production|dev|test|backup))?|smtp[_-]?pass"
11
13
  r"|(?:session(?:[_-]?(?:id|token))?|sessionid|sid|jsessionid|"
12
14
  r"csrf(?:[_-]?token)?|xsrf(?:[_-]?token)?)"
13
15
  r"|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN|"
@@ -34,6 +36,7 @@ EXACT_SENSITIVE_KEYS = frozenset(
34
36
  "client_secret",
35
37
  "cookie",
36
38
  "credential",
39
+ "credential_helper",
37
40
  "credentials",
38
41
  "csrf",
39
42
  "csrf_token",
@@ -41,6 +44,7 @@ EXACT_SENSITIVE_KEYS = frozenset(
41
44
  "id_token",
42
45
  "jsessionid",
43
46
  "jwt",
47
+ "pass",
44
48
  "password",
45
49
  "passwd",
46
50
  "private_key",
@@ -57,6 +61,9 @@ EXACT_SENSITIVE_KEYS = frozenset(
57
61
  "sid",
58
62
  "sig",
59
63
  "signature",
64
+ "smtp_pass",
65
+ "smtppass",
66
+ "ssh_command",
60
67
  "ssh_key",
61
68
  "sshkey",
62
69
  "token",
@@ -80,10 +87,11 @@ SENSITIVE_KEY_SUFFIXES = (
80
87
  "_secret",
81
88
  "_secret_key",
82
89
  "_session_token",
90
+ "_ssh_command",
83
91
  "_token",
84
92
  )
85
93
  SENSITIVE_KEY_QUALIFIER_RE = re.compile(
86
- r"(?:^|_)(?:api_?key|apikey|token|secret|password|passwd|pwd|"
94
+ r"(?:^|_)(?:api_?key|apikey|token|secret|password|passwd|pass|pwd|"
87
95
  r"private_key|access_key|client_secret|credential|signature|sig|"
88
96
  r"session_id|session_token)(?:_(?:v\d+|prod|production|dev|test|backup))?$"
89
97
  )