@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.
- package/CHANGELOG.md +21 -0
- package/README.ko.md +13 -1
- package/README.md +13 -1
- package/docs/distribution.md +2 -2
- package/package.json +2 -1
- package/packaging/homebrew/context-guard.rb.template +5 -1
- package/plugins/context-guard/.claude-plugin/plugin.json +1 -1
- package/plugins/context-guard/README.ko.md +11 -1
- package/plugins/context-guard/README.md +13 -2
- package/plugins/context-guard/bin/context-guard-artifact +1 -1
- package/plugins/context-guard/bin/context-guard-cost +327 -0
- package/plugins/context-guard/bin/context-guard-pack +626 -3
- package/plugins/context-guard/bin/context-guard-setup +89 -12
- package/plugins/context-guard/bin/context-guard-task-memory +635 -0
- package/plugins/context-guard/lib/context_guard_commands.py +21 -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
|
@@ -1506,17 +1506,65 @@ def normalize_scope(raw_scope: str | None) -> str:
|
|
|
1506
1506
|
return scope
|
|
1507
1507
|
|
|
1508
1508
|
|
|
1509
|
+
def _user_scope_path_issue(
|
|
1510
|
+
path: Path,
|
|
1511
|
+
*,
|
|
1512
|
+
label: str,
|
|
1513
|
+
expected_directory: bool,
|
|
1514
|
+
allow_missing: bool,
|
|
1515
|
+
) -> str | None:
|
|
1516
|
+
"""Return a fail-closed reason for an existing user-scope path."""
|
|
1517
|
+
try:
|
|
1518
|
+
metadata = os.lstat(path)
|
|
1519
|
+
except FileNotFoundError:
|
|
1520
|
+
return None if allow_missing else f"{label} does not exist: {path}"
|
|
1521
|
+
except OSError as error:
|
|
1522
|
+
return f"could not inspect {label}: {error.__class__.__name__}"
|
|
1523
|
+
|
|
1524
|
+
if metadata.st_uid not in {0, os.geteuid()}:
|
|
1525
|
+
return f"refusing unsafe {label}: it is not owned by root or the effective user"
|
|
1526
|
+
if metadata.st_mode & (stat.S_IWGRP | stat.S_IWOTH):
|
|
1527
|
+
return f"refusing unsafe {label}: group/world write bits are set"
|
|
1528
|
+
if expected_directory:
|
|
1529
|
+
if not stat.S_ISDIR(metadata.st_mode):
|
|
1530
|
+
return f"refusing unsafe {label}: it is not a directory"
|
|
1531
|
+
elif not stat.S_ISREG(metadata.st_mode):
|
|
1532
|
+
return f"refusing unsafe {label}: it is not a regular file"
|
|
1533
|
+
return None
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
def _validate_user_scope_home(home: Path) -> None:
|
|
1537
|
+
issue = _user_scope_path_issue(
|
|
1538
|
+
home,
|
|
1539
|
+
label="resolved HOME",
|
|
1540
|
+
expected_directory=True,
|
|
1541
|
+
allow_missing=False,
|
|
1542
|
+
)
|
|
1543
|
+
if issue:
|
|
1544
|
+
raise SystemExit(issue)
|
|
1545
|
+
|
|
1546
|
+
|
|
1509
1547
|
def resolve_scope_root(raw_root: str | None, scope: str) -> Path:
|
|
1510
1548
|
if scope == "project":
|
|
1511
1549
|
return resolve_setup_root(raw_root)
|
|
1512
1550
|
home = Path.home().expanduser().resolve()
|
|
1513
1551
|
if home == Path(home.anchor or "/"):
|
|
1514
1552
|
raise SystemExit("Refusing user-scope setup because HOME resolves to a filesystem root.")
|
|
1515
|
-
|
|
1516
|
-
raise SystemExit(f"Refusing user-scope setup because HOME is not a directory: {home}")
|
|
1553
|
+
_validate_user_scope_home(home)
|
|
1517
1554
|
return home
|
|
1518
1555
|
|
|
1519
1556
|
|
|
1557
|
+
def effective_scope(raw_root: str | None, raw_scope: str | None, *, allow_home_settings: bool) -> str:
|
|
1558
|
+
scope = normalize_scope(raw_scope)
|
|
1559
|
+
if scope != "project" or not allow_home_settings:
|
|
1560
|
+
return scope
|
|
1561
|
+
project_root = resolve_setup_root(raw_root)
|
|
1562
|
+
home_settings = Path.home().expanduser().resolve() / SETTINGS_REL
|
|
1563
|
+
if (project_root / SETTINGS_REL).expanduser().resolve() == home_settings:
|
|
1564
|
+
return "user"
|
|
1565
|
+
return scope
|
|
1566
|
+
|
|
1567
|
+
|
|
1520
1568
|
def explicit_agent_selection(args: argparse.Namespace) -> list[str] | None:
|
|
1521
1569
|
values: list[str] = []
|
|
1522
1570
|
for attr in ("agent", "only"):
|
|
@@ -1549,6 +1597,20 @@ def validate_settings_target(root: Path, settings_path: Path, *, allow_home_sett
|
|
|
1549
1597
|
claude_dir.resolve().relative_to(root)
|
|
1550
1598
|
except ValueError as exc:
|
|
1551
1599
|
raise SystemExit(f"Claude settings directory resolves outside project root: {claude_dir}") from exc
|
|
1600
|
+
if root == home_settings.parent.parent:
|
|
1601
|
+
for path, label, expected_directory, allow_missing in (
|
|
1602
|
+
(root, "resolved HOME", True, False),
|
|
1603
|
+
(claude_dir, "existing .claude directory", True, True),
|
|
1604
|
+
(settings_path, "existing settings.json", False, True),
|
|
1605
|
+
):
|
|
1606
|
+
issue = _user_scope_path_issue(
|
|
1607
|
+
path,
|
|
1608
|
+
label=label,
|
|
1609
|
+
expected_directory=expected_directory,
|
|
1610
|
+
allow_missing=allow_missing,
|
|
1611
|
+
)
|
|
1612
|
+
if issue:
|
|
1613
|
+
raise SystemExit(issue)
|
|
1552
1614
|
|
|
1553
1615
|
|
|
1554
1616
|
def _base_open_flags() -> int:
|
|
@@ -2970,14 +3032,21 @@ def doctor_check(
|
|
|
2970
3032
|
return check
|
|
2971
3033
|
|
|
2972
3034
|
|
|
2973
|
-
def _setup_command(
|
|
2974
|
-
|
|
2975
|
-
|
|
3035
|
+
def _setup_command(
|
|
3036
|
+
args: argparse.Namespace,
|
|
3037
|
+
*,
|
|
3038
|
+
apply: bool,
|
|
3039
|
+
root: Path | None = None,
|
|
3040
|
+
scope: str | None = None,
|
|
3041
|
+
) -> str:
|
|
3042
|
+
scope = scope or normalize_scope(getattr(args, "scope", "project"))
|
|
3043
|
+
parts = ["context-guard", "setup", "--scope", scope]
|
|
3044
|
+
if root is not None and scope == "project":
|
|
2976
3045
|
parts.extend(["--root", str(root)])
|
|
2977
3046
|
selected = explicit_agent_selection(args)
|
|
2978
3047
|
if selected:
|
|
2979
3048
|
parts.extend(["--agent", ",".join(selected)])
|
|
2980
|
-
elif
|
|
3049
|
+
elif scope == "user":
|
|
2981
3050
|
parts.extend(["--agent", "claude"])
|
|
2982
3051
|
if getattr(args, "allow_path_helper_fallback", False):
|
|
2983
3052
|
parts.append("--allow-path-helper-fallback")
|
|
@@ -3068,7 +3137,11 @@ def run_doctor(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3068
3137
|
writing settings, writing rule files, or creating rollback records.
|
|
3069
3138
|
"""
|
|
3070
3139
|
require_no_follow_file_ops_supported()
|
|
3071
|
-
scope =
|
|
3140
|
+
scope = effective_scope(
|
|
3141
|
+
args.root,
|
|
3142
|
+
getattr(args, "scope", "project"),
|
|
3143
|
+
allow_home_settings=bool(getattr(args, "allow_home_settings", False)),
|
|
3144
|
+
)
|
|
3072
3145
|
root = resolve_scope_root(args.root, scope)
|
|
3073
3146
|
settings_path = root / SETTINGS_REL
|
|
3074
3147
|
helper_check = _helper_availability_check(include_diet=not getattr(args, "no_diet_scan", False), allow_path_fallback=bool(getattr(args, "allow_path_helper_fallback", False)))
|
|
@@ -3190,7 +3263,7 @@ def run_doctor(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3190
3263
|
"medium",
|
|
3191
3264
|
"ContextGuard setup is not fully applied for the requested selections.",
|
|
3192
3265
|
detail={"planned_action_count": len(actions), "planned_actions": actions},
|
|
3193
|
-
next_action=_setup_command(args, apply=False, root=root),
|
|
3266
|
+
next_action=_setup_command(args, apply=False, root=root, scope=scope),
|
|
3194
3267
|
))
|
|
3195
3268
|
else:
|
|
3196
3269
|
checks.append(doctor_check(
|
|
@@ -3225,7 +3298,7 @@ def run_doctor(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3225
3298
|
"medium",
|
|
3226
3299
|
"Some requested adapters still have planned or unsupported setup actions.",
|
|
3227
3300
|
detail={"adapters": adapter_warnings},
|
|
3228
|
-
next_action=_setup_command(args, apply=False, root=root),
|
|
3301
|
+
next_action=_setup_command(args, apply=False, root=root, scope=scope),
|
|
3229
3302
|
))
|
|
3230
3303
|
else:
|
|
3231
3304
|
checks.append(doctor_check(
|
|
@@ -3279,9 +3352,9 @@ def run_doctor(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
3279
3352
|
detail=diet_scan,
|
|
3280
3353
|
))
|
|
3281
3354
|
|
|
3282
|
-
recommended = [_setup_command(args, apply=False, root=root)]
|
|
3355
|
+
recommended = [_setup_command(args, apply=False, root=root, scope=scope)]
|
|
3283
3356
|
if changed or adapter_warnings:
|
|
3284
|
-
recommended.append(_setup_command(args, apply=True, root=root))
|
|
3357
|
+
recommended.append(_setup_command(args, apply=True, root=root, scope=scope))
|
|
3285
3358
|
return {
|
|
3286
3359
|
"schema_version": "contextguard.doctor.v1",
|
|
3287
3360
|
"status": _doctor_status(checks),
|
|
@@ -4050,7 +4123,11 @@ def render_quiet_narration_text(result: dict[str, Any]) -> str:
|
|
|
4050
4123
|
|
|
4051
4124
|
def run(args: argparse.Namespace) -> SetupResult:
|
|
4052
4125
|
require_no_follow_file_ops_supported()
|
|
4053
|
-
scope =
|
|
4126
|
+
scope = effective_scope(
|
|
4127
|
+
args.root,
|
|
4128
|
+
getattr(args, "scope", "project"),
|
|
4129
|
+
allow_home_settings=bool(getattr(args, "allow_home_settings", False)),
|
|
4130
|
+
)
|
|
4054
4131
|
root = resolve_scope_root(args.root, scope)
|
|
4055
4132
|
settings_path = root / SETTINGS_REL
|
|
4056
4133
|
warnings: list[str] = []
|