@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.
- package/CHANGELOG.md +74 -0
- package/README.ko.md +91 -1
- package/README.md +95 -1
- package/docs/distribution.md +100 -0
- package/package.json +5 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +34 -1
- package/plugins/context-guard/README.md +36 -1
- package/plugins/context-guard/bin/bash_reference_policy.py +967 -0
- package/plugins/context-guard/bin/context-guard-artifact +1 -1
- package/plugins/context-guard/bin/context-guard-bench +4216 -103
- package/plugins/context-guard/bin/context-guard-failed-nudge +95 -23
- package/plugins/context-guard/bin/context-guard-guard-read +6 -2
- package/plugins/context-guard/bin/context-guard-mcp +2 -1
- package/plugins/context-guard/bin/context-guard-pack +2086 -142
- package/plugins/context-guard/bin/context-guard-rewrite-bash +497 -45
- package/plugins/context-guard/bin/context-guard-sanitize-output +178 -22
- package/plugins/context-guard/bin/context-guard-setup +901 -28
- package/plugins/context-guard/bin/context-guard-statusline +33 -2
- package/plugins/context-guard/bin/context-guard-statusline-merged +71 -20
- package/plugins/context-guard/bin/context-guard-task-memory +635 -0
- package/plugins/context-guard/bin/context-guard-trim-output +706 -35
- package/plugins/context-guard/lib/context_guard_commands.py +25 -1
- package/plugins/context-guard/lib/context_pack_git_boundary.py +19 -0
- package/plugins/context-guard/lib/context_pack_identity.py +115 -0
- package/plugins/context-guard/lib/context_pack_receipts.py +9 -0
- package/plugins/context-guard/lib/context_pack_rendering.py +12 -0
- package/plugins/context-guard/lib/context_pack_scanning.py +10 -0
- package/plugins/context-guard/lib/context_pack_selection.py +6 -0
- package/plugins/context-guard/lib/credential_policy.py +10 -2
|
@@ -23,6 +23,7 @@ import math
|
|
|
23
23
|
import os
|
|
24
24
|
import re
|
|
25
25
|
import shlex
|
|
26
|
+
import shutil
|
|
26
27
|
import stat
|
|
27
28
|
import sys
|
|
28
29
|
import time
|
|
@@ -64,7 +65,7 @@ STATE_LOCK_TIMEOUT_SECONDS = 2.0
|
|
|
64
65
|
STATE_LOCK_POLL_SECONDS = 0.01
|
|
65
66
|
CGW1_SENTINEL = "--context-guard-wrapper-v1"
|
|
66
67
|
CGW1_COMMAND_SEARCH_DIFF = "command_search_diff"
|
|
67
|
-
CGW1_SHELL_ARGV = ("bash", "-
|
|
68
|
+
CGW1_SHELL_ARGV = ("bash", "-c")
|
|
68
69
|
LEGACY_V0_MAX_LINES = "220"
|
|
69
70
|
PROTOCOL_CGW1 = "cgw1"
|
|
70
71
|
PROTOCOL_LEGACY_V0 = "legacy-v0"
|
|
@@ -236,6 +237,60 @@ def _wrapper_prefixes() -> dict[str, tuple[str, ...]]:
|
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
|
|
240
|
+
def _approved_python_runtime() -> str:
|
|
241
|
+
canonical = os.path.realpath(sys.executable)
|
|
242
|
+
if not canonical or not os.path.isabs(canonical) or not os.path.isfile(canonical) or not os.access(canonical, os.X_OK):
|
|
243
|
+
raise RuntimeError("approved Python runtime is unavailable")
|
|
244
|
+
return canonical
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _approved_bash_runtime() -> str:
|
|
248
|
+
found = shutil.which("bash", path=os.defpath)
|
|
249
|
+
if not found:
|
|
250
|
+
raise RuntimeError("approved Bash runtime is unavailable")
|
|
251
|
+
canonical = os.path.realpath(found)
|
|
252
|
+
if not os.path.isfile(canonical) or not os.access(canonical, os.X_OK):
|
|
253
|
+
raise RuntimeError("approved Bash runtime is unavailable")
|
|
254
|
+
return canonical
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _approved_env_runtime() -> str:
|
|
258
|
+
found = shutil.which("env", path=os.defpath)
|
|
259
|
+
if not found:
|
|
260
|
+
raise RuntimeError("approved env runtime is unavailable")
|
|
261
|
+
canonical = os.path.realpath(found)
|
|
262
|
+
if not os.path.isfile(canonical) or not os.access(canonical, os.X_OK):
|
|
263
|
+
raise RuntimeError("approved env runtime is unavailable")
|
|
264
|
+
return canonical
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _runtime_shell_argv() -> tuple[str, ...]:
|
|
268
|
+
return (
|
|
269
|
+
_approved_env_runtime(),
|
|
270
|
+
"-u", "BASH_ENV",
|
|
271
|
+
"-u", "ENV",
|
|
272
|
+
"-u", "PYTHONHOME",
|
|
273
|
+
"-u", "PYTHONPATH",
|
|
274
|
+
"-u", "PYTHONSTARTUP",
|
|
275
|
+
"-u", "SHELLOPTS",
|
|
276
|
+
"-u", "BASHOPTS",
|
|
277
|
+
"-u", "PS4",
|
|
278
|
+
_approved_bash_runtime(),
|
|
279
|
+
"--noprofile",
|
|
280
|
+
"--norc",
|
|
281
|
+
"-p",
|
|
282
|
+
"-c",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _runtime_wrapper_prefixes() -> dict[str, tuple[str, ...]]:
|
|
287
|
+
adjacent = _wrapper_prefixes()
|
|
288
|
+
return {
|
|
289
|
+
kind: (_approved_python_runtime(), "-I", prefix[-1])
|
|
290
|
+
for kind, prefix in adjacent.items()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
239
294
|
def _argv(command: str) -> tuple[str, ...] | None:
|
|
240
295
|
try:
|
|
241
296
|
values = shlex.split(command, posix=True)
|
|
@@ -263,7 +318,14 @@ def _is_wrapper_shaped(argv: tuple[str, ...]) -> bool:
|
|
|
263
318
|
return (
|
|
264
319
|
re.fullmatch(r"python(?:\d+(?:\.\d+)?)?", first) is not None
|
|
265
320
|
and len(argv) > 1
|
|
266
|
-
and
|
|
321
|
+
and (
|
|
322
|
+
os.path.basename(argv[1]) in known
|
|
323
|
+
or (
|
|
324
|
+
len(argv) > 2
|
|
325
|
+
and argv[1] == "-I"
|
|
326
|
+
and os.path.basename(argv[2]) in known
|
|
327
|
+
)
|
|
328
|
+
)
|
|
267
329
|
)
|
|
268
330
|
|
|
269
331
|
|
|
@@ -274,32 +336,42 @@ def command_identity(command: str) -> CommandIdentity:
|
|
|
274
336
|
return CommandIdentity(PROTOCOL_FOREIGN, sha256_text(command))
|
|
275
337
|
|
|
276
338
|
prefixes = _wrapper_prefixes()
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"
|
|
281
|
-
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
339
|
+
runtime_prefixes = _runtime_wrapper_prefixes()
|
|
340
|
+
current_shell_argv = _runtime_shell_argv()
|
|
341
|
+
for prefix, shell_argv in (
|
|
342
|
+
(runtime_prefixes["sanitize"], current_shell_argv),
|
|
343
|
+
(prefixes["sanitize"], CGW1_SHELL_ARGV),
|
|
344
|
+
):
|
|
345
|
+
sanitize_cgw1 = prefix + (
|
|
346
|
+
CGW1_SENTINEL,
|
|
347
|
+
CGW1_COMMAND_SEARCH_DIFF,
|
|
348
|
+
"--",
|
|
349
|
+
*shell_argv,
|
|
350
|
+
)
|
|
351
|
+
if len(argv) == len(sanitize_cgw1) + 1 and argv[:-1] == sanitize_cgw1:
|
|
352
|
+
logical = argv[-1]
|
|
353
|
+
if logical and not _is_wrapper_shaped(_argv(logical) or ()):
|
|
354
|
+
return CommandIdentity(PROTOCOL_CGW1, sha256_text(logical))
|
|
287
355
|
|
|
288
356
|
# The frozen A1 producer still emits this exact v0 envelope for trim.
|
|
289
357
|
# The historical producer emitted it for sanitize as well, so both known
|
|
290
358
|
# adjacent wrapper identities remain allowlisted for one compatibility
|
|
291
359
|
# window. No other --/bash/-lc spelling is unwrapped.
|
|
292
|
-
for
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
360
|
+
for prefix_set, shell_argv in (
|
|
361
|
+
(prefixes, CGW1_SHELL_ARGV),
|
|
362
|
+
(runtime_prefixes, current_shell_argv),
|
|
363
|
+
):
|
|
364
|
+
for prefix in prefix_set.values():
|
|
365
|
+
legacy_v0 = prefix + (
|
|
366
|
+
"--max-lines",
|
|
367
|
+
LEGACY_V0_MAX_LINES,
|
|
368
|
+
"--",
|
|
369
|
+
*shell_argv,
|
|
370
|
+
)
|
|
371
|
+
if len(argv) == len(legacy_v0) + 1 and argv[:-1] == legacy_v0:
|
|
372
|
+
logical = argv[-1]
|
|
373
|
+
if logical and not _is_wrapper_shaped(_argv(logical) or ()):
|
|
374
|
+
return CommandIdentity(PROTOCOL_LEGACY_V0, sha256_text(logical))
|
|
303
375
|
|
|
304
376
|
protocol = PROTOCOL_FOREIGN if (
|
|
305
377
|
_is_wrapper_shaped(argv) or CGW1_SENTINEL in argv
|
|
@@ -520,9 +520,13 @@ def raw_read_range_outcome(
|
|
|
520
520
|
def find_read_symbol_command() -> str:
|
|
521
521
|
script_dir = Path(__file__).resolve().parent
|
|
522
522
|
if (script_dir / "context-guard-read-symbol").exists():
|
|
523
|
-
return
|
|
523
|
+
return shlex.join(
|
|
524
|
+
[os.path.realpath(sys.executable), "-I", str((script_dir / "context-guard-read-symbol").resolve())]
|
|
525
|
+
)
|
|
524
526
|
if (script_dir / "read_symbol.py").exists():
|
|
525
|
-
return
|
|
527
|
+
return shlex.join(
|
|
528
|
+
[os.path.realpath(sys.executable), "-I", str((script_dir / "read_symbol.py").resolve())]
|
|
529
|
+
)
|
|
526
530
|
return "context-guard-read-symbol"
|
|
527
531
|
|
|
528
532
|
|
|
@@ -32,6 +32,7 @@ MAX_CALLS = 1000
|
|
|
32
32
|
MAX_ARTIFACTS = 1000
|
|
33
33
|
MAX_VISITED_ENTRIES = 4000
|
|
34
34
|
HELPER_TIMEOUT = 10.0
|
|
35
|
+
HELPER_TERMINATE_GRACE = 0.1
|
|
35
36
|
MAX_HELPER_STDERR = 16 * 1024
|
|
36
37
|
MAX_HELPER_OUTPUT = 5 * 1024 * 1024
|
|
37
38
|
MAX_RESPONSE_BYTES = 128 * 1024
|
|
@@ -456,7 +457,7 @@ class Server:
|
|
|
456
457
|
# process group and inherited pipes alive. Give the whole group a
|
|
457
458
|
# bounded TERM grace period, then escalate the group independently
|
|
458
459
|
# of the direct child's return code.
|
|
459
|
-
grace_deadline = time.monotonic() +
|
|
460
|
+
grace_deadline = time.monotonic() + HELPER_TERMINATE_GRACE
|
|
460
461
|
group_exists = True
|
|
461
462
|
while time.monotonic() < grace_deadline:
|
|
462
463
|
try:
|