@ictechgy/context-guard 0.5.1 → 0.7.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.
@@ -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'),
@@ -34,13 +35,20 @@ HELPER_PAIRS = (('hook_secret_patterns.py', 'lib/hook_secret_patterns.py'),
34
35
  ('context_guard_commands.py', 'lib/context_guard_commands.py'),
35
36
  ('context_guard_command_manifest_loader.py',
36
37
  'lib/context_guard_command_manifest_loader.py'),
37
- ('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'))
38
45
 
39
46
  NPM_BINS = ('context-guard',
40
47
  'context-guard-cost',
41
48
  'context-guard-cache-score',
42
49
  'context-guard-bench',
43
50
  'context-guard-artifact',
51
+ 'context-guard-task-memory',
44
52
  'context-guard-compress',
45
53
  'context-guard-mcp',
46
54
  'context-guard-pack',
@@ -64,6 +72,7 @@ NPM_BIN_PATHS = {'context-guard': 'plugins/context-guard/bin/context-guard',
64
72
  'context-guard-cache-score': 'plugins/context-guard/bin/context-guard-cache-score',
65
73
  'context-guard-bench': 'plugins/context-guard/bin/context-guard-bench',
66
74
  'context-guard-artifact': 'plugins/context-guard/bin/context-guard-artifact',
75
+ 'context-guard-task-memory': 'plugins/context-guard/bin/context-guard-task-memory',
67
76
  'context-guard-compress': 'plugins/context-guard/bin/context-guard-compress',
68
77
  'context-guard-mcp': 'plugins/context-guard/bin/context-guard-mcp',
69
78
  'context-guard-pack': 'plugins/context-guard/bin/context-guard-pack',
@@ -95,6 +104,7 @@ DISPATCHER_SUBCOMMANDS = {'setup': ('context-guard-setup',),
95
104
  'sanitize': ('context-guard-sanitize-output',),
96
105
  'filter': ('context-guard-filter',),
97
106
  'artifact': ('context-guard-artifact',),
107
+ 'task-memory': ('context-guard-task-memory',),
98
108
  'pack': ('context-guard-pack',),
99
109
  'tool-prune': ('context-guard-tool-prune',),
100
110
  'compress': ('context-guard-compress',),
@@ -129,6 +139,7 @@ ENTRYPOINT_SMOKE_CASES = {'context-guard': {'args': ['--version'], 'mode': 'text
129
139
  'context-guard-read-symbol': {'args': ['--help'], 'mode': 'text'},
130
140
  'context-guard-sanitize-output': {'args': ['--help'], 'mode': 'text'},
131
141
  'context-guard-artifact': {'args': ['--help'], 'mode': 'text'},
142
+ 'context-guard-task-memory': {'args': ['--help'], 'mode': 'text'},
132
143
  'context-guard-audit': {'args': ['--help'], 'mode': 'text'},
133
144
  'context-guard-bench': {'args': ['--help'], 'mode': 'text'},
134
145
  'context-guard-compress': {'args': ['--help'], 'mode': 'text'},
@@ -176,6 +187,7 @@ PLUGIN_ENTRYPOINTS = ('claude-read-symbol',
176
187
  'claude-trim-output',
177
188
  'context-guard',
178
189
  'context-guard-artifact',
190
+ 'context-guard-task-memory',
179
191
  'context-guard-audit',
180
192
  'context-guard-bench',
181
193
  'context-guard-compress',
@@ -203,6 +215,7 @@ DISPATCHER_SMOKE_CASES = ({'entrypoint': 'context-guard', 'args': ['experiments'
203
215
  {'entrypoint': 'context-guard', 'args': ['route-advisor', '--help'], 'mode': 'text'},
204
216
  {'entrypoint': 'context-guard', 'args': ['cache-score', '--help'], 'mode': 'text'},
205
217
  {'entrypoint': 'context-guard', 'args': ['mcp', '--help'], 'mode': 'text'},
218
+ {'entrypoint': 'context-guard', 'args': ['task-memory', '--help'], 'mode': 'text'},
206
219
  {'entrypoint': 'context-guard-pack', 'args': ['suggest', '--help'], 'mode': 'text'},
207
220
  {'entrypoint': 'context-guard-pack', 'args': ['auto', '--help'], 'mode': 'text'})
208
221
 
@@ -222,6 +235,7 @@ EXPECTED_COMMAND_PACK_FILES = ('plugins/context-guard/bin/claude-read-symbol',
222
235
  'plugins/context-guard/bin/bash_reference_policy.py',
223
236
  'plugins/context-guard/bin/context-guard',
224
237
  'plugins/context-guard/bin/context-guard-artifact',
238
+ 'plugins/context-guard/bin/context-guard-task-memory',
225
239
  'plugins/context-guard/bin/context-guard-audit',
226
240
  'plugins/context-guard/bin/context-guard-bench',
227
241
  'plugins/context-guard/bin/context-guard-cache-score',
@@ -245,5 +259,11 @@ EXPECTED_COMMAND_PACK_FILES = ('plugins/context-guard/bin/claude-read-symbol',
245
259
  'plugins/context-guard/lib/credential_policy.py',
246
260
  'plugins/context-guard/lib/context_guard_command_manifest_loader.py',
247
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',
248
268
  'plugins/context-guard/lib/hook_secret_patterns.py',
249
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)