claude-dev-env 8.48.4 → 8.48.6
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/package.json
CHANGED
|
@@ -28,11 +28,31 @@ class WeeklyUtilizationProbeError(Exception):
|
|
|
28
28
|
"""Raised when an account's usage meters cannot be measured."""
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def _usage_pause_scripts_directory() -> Path:
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
def _usage_pause_scripts_directory(module_file: Path) -> Path:
|
|
32
|
+
"""Return the usage-pause scripts directory beside this scripts tree.
|
|
33
|
+
|
|
34
|
+
::
|
|
35
|
+
|
|
36
|
+
packages/claude-dev-env/scripts/x.py -> packages/claude-dev-env/.agents/skills/usage-pause/scripts
|
|
37
|
+
~/.claude/scripts/x.py (link) -> ~/.agents/skills/usage-pause/scripts
|
|
38
|
+
|
|
39
|
+
The installer links ~/.claude/scripts to ~/.agents/scripts, so the
|
|
40
|
+
resolved scripts tree already sits inside the agents home.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
module_file: Path of a module inside the scripts tree.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The directory that holds the usage-pause resolver.
|
|
47
|
+
"""
|
|
48
|
+
package_root = module_file.resolve().parent.parent
|
|
49
|
+
agents_home = (
|
|
34
50
|
package_root
|
|
35
|
-
|
|
51
|
+
if package_root.name.endswith(USAGE_PAUSE_AGENTS_HOME_DIRECTORY_NAME)
|
|
52
|
+
else package_root / USAGE_PAUSE_AGENTS_HOME_DIRECTORY_NAME
|
|
53
|
+
)
|
|
54
|
+
return (
|
|
55
|
+
agents_home
|
|
36
56
|
/ USAGE_PAUSE_SKILL_DIRECTORY_NAME
|
|
37
57
|
/ USAGE_PAUSE_SKILL_NAME
|
|
38
58
|
/ USAGE_PAUSE_SCRIPTS_DIRECTORY_NAME
|
|
@@ -48,7 +68,7 @@ def _load_resolve_usage_window_module() -> ModuleType:
|
|
|
48
68
|
already_loaded = sys.modules.get(RESOLVE_USAGE_WINDOW_MODULE_NAME)
|
|
49
69
|
if already_loaded is not None:
|
|
50
70
|
return already_loaded
|
|
51
|
-
scripts_directory = _usage_pause_scripts_directory()
|
|
71
|
+
scripts_directory = _usage_pause_scripts_directory(Path(__file__))
|
|
52
72
|
module_path = scripts_directory / RESOLVE_USAGE_WINDOW_FILENAME
|
|
53
73
|
if not module_path.is_file():
|
|
54
74
|
raise WeeklyUtilizationProbeError(
|
|
@@ -90,12 +110,43 @@ class AccountUsageMeters:
|
|
|
90
110
|
weekly_resets_at: datetime | None
|
|
91
111
|
|
|
92
112
|
|
|
113
|
+
def _read_access_token(
|
|
114
|
+
usage_window_resolver: ModuleType, credentials_path: Path, now: datetime
|
|
115
|
+
) -> str | None:
|
|
116
|
+
"""Return the bearer token that reads one account's meters.
|
|
117
|
+
|
|
118
|
+
::
|
|
119
|
+
|
|
120
|
+
credential file holds a token -> that token
|
|
121
|
+
no token, the session's own credential path -> the session ingress token
|
|
122
|
+
no token, another account's credential path -> None
|
|
123
|
+
|
|
124
|
+
A cloud session has no credential file, only the ingress token, and that
|
|
125
|
+
token belongs to the account the session runs on.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
usage_window_resolver: The loaded usage-pause resolver module.
|
|
129
|
+
credentials_path: The account's CLI credential file.
|
|
130
|
+
now: The current time, for the token expiry check.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
A bearer token, or None when the account has none.
|
|
134
|
+
"""
|
|
135
|
+
file_token = usage_window_resolver.read_oauth_access_token(credentials_path, now)
|
|
136
|
+
if file_token is not None:
|
|
137
|
+
return file_token
|
|
138
|
+
session_credentials_path = usage_window_resolver.default_credentials_path()
|
|
139
|
+
if credentials_path.resolve() != session_credentials_path.resolve():
|
|
140
|
+
return None
|
|
141
|
+
return usage_window_resolver.read_session_ingress_token()
|
|
142
|
+
|
|
143
|
+
|
|
93
144
|
def _fetch_account_usage_payload(
|
|
94
145
|
usage_window_resolver: ModuleType, credentials_path: Path
|
|
95
146
|
) -> dict[str, object]:
|
|
96
147
|
now = datetime.now().astimezone()
|
|
97
148
|
try:
|
|
98
|
-
access_token = usage_window_resolver
|
|
149
|
+
access_token = _read_access_token(usage_window_resolver, credentials_path, now)
|
|
99
150
|
if access_token is None:
|
|
100
151
|
raise WeeklyUtilizationProbeError(
|
|
101
152
|
NO_ACCESS_TOKEN_ERROR_TEMPLATE.format(credentials_path=credentials_path)
|
|
@@ -684,6 +684,11 @@ def _is_code_rules_enforcer_hook(all_hook_record: dict[str, object]) -> bool:
|
|
|
684
684
|
)
|
|
685
685
|
|
|
686
686
|
|
|
687
|
+
def _is_windows_host() -> bool:
|
|
688
|
+
"""Report whether hook command paths compare without regard to case."""
|
|
689
|
+
return os.name == "nt"
|
|
690
|
+
|
|
691
|
+
|
|
687
692
|
def _is_managed_codex_enforcer_hook(
|
|
688
693
|
all_hook_record: dict[str, object], managed_command: str
|
|
689
694
|
) -> bool:
|
|
@@ -693,7 +698,7 @@ def _is_managed_codex_enforcer_hook(
|
|
|
693
698
|
return False
|
|
694
699
|
normalized_command = command.replace("\\", path_separator)
|
|
695
700
|
normalized_managed_command = managed_command.replace("\\", path_separator)
|
|
696
|
-
if
|
|
701
|
+
if _is_windows_host():
|
|
697
702
|
return normalized_command.casefold() == normalized_managed_command.casefold()
|
|
698
703
|
return normalized_command == normalized_managed_command
|
|
699
704
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import importlib
|
|
6
|
+
import os
|
|
5
7
|
import subprocess
|
|
6
8
|
import sys
|
|
7
9
|
import urllib.error
|
|
@@ -44,18 +46,95 @@ def test_probe_reads_both_meters_and_resets(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
44
46
|
)
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
def
|
|
49
|
+
def test_probe_reads_the_session_token_for_the_session_account(
|
|
50
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
51
|
+
) -> None:
|
|
52
|
+
session_credentials = Path("home") / ".claude" / ".credentials.json"
|
|
53
|
+
all_fetched_tokens: list[str] = []
|
|
54
|
+
|
|
55
|
+
class Windows:
|
|
56
|
+
session_utilization = 15.0
|
|
57
|
+
session_resets_at = None
|
|
58
|
+
weekly_utilization = 70.0
|
|
59
|
+
weekly_resets_at = None
|
|
60
|
+
|
|
48
61
|
class Resolver:
|
|
49
62
|
def read_oauth_access_token(self, credentials_path: Path, now: datetime) -> None:
|
|
50
63
|
return None
|
|
51
64
|
|
|
65
|
+
def default_credentials_path(self) -> Path:
|
|
66
|
+
return session_credentials
|
|
67
|
+
|
|
68
|
+
def read_session_ingress_token(self) -> str:
|
|
69
|
+
return "session-token"
|
|
70
|
+
|
|
71
|
+
def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
|
|
72
|
+
all_fetched_tokens.append(access_token)
|
|
73
|
+
return {"usage": 70.0}
|
|
74
|
+
|
|
75
|
+
def extract_usage_windows(self, payload: dict[str, object]) -> Windows:
|
|
76
|
+
return Windows()
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr(usage, "_load_resolve_usage_window_module", lambda: Resolver())
|
|
79
|
+
|
|
80
|
+
assert usage.probe_account_meters(session_credentials) == usage.AccountUsageMeters(
|
|
81
|
+
15.0, None, 70.0, None
|
|
82
|
+
)
|
|
83
|
+
assert all_fetched_tokens == ["session-token"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_probe_rejects_missing_access_token_for_another_account(
|
|
87
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
88
|
+
) -> None:
|
|
89
|
+
class Resolver:
|
|
90
|
+
def read_oauth_access_token(self, credentials_path: Path, now: datetime) -> None:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def default_credentials_path(self) -> Path:
|
|
94
|
+
return Path("home") / ".claude" / ".credentials.json"
|
|
95
|
+
|
|
96
|
+
def read_session_ingress_token(self) -> str:
|
|
97
|
+
return "session-token"
|
|
98
|
+
|
|
52
99
|
def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
|
|
53
100
|
pytest.fail("meter request must not run")
|
|
54
101
|
|
|
55
102
|
monkeypatch.setattr(usage, "_load_resolve_usage_window_module", lambda: Resolver())
|
|
56
103
|
|
|
57
104
|
with pytest.raises(usage.WeeklyUtilizationProbeError, match="bearer token"):
|
|
58
|
-
usage.probe_account_meters(Path("
|
|
105
|
+
usage.probe_account_meters(Path("home") / ".claude-profiles" / "ev" / ".credentials.json")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _create_directory_link(*, from_link: Path, to_target: Path) -> None:
|
|
109
|
+
if sys.platform.startswith("win32"):
|
|
110
|
+
importlib.import_module("_winapi").CreateJunction(str(to_target), str(from_link))
|
|
111
|
+
return
|
|
112
|
+
os.symlink(to_target, from_link, target_is_directory=True)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_scripts_directory_follows_the_installed_scripts_link(tmp_path: Path) -> None:
|
|
116
|
+
home = tmp_path.resolve()
|
|
117
|
+
(home / ".agents" / "scripts").mkdir(parents=True)
|
|
118
|
+
(home / ".claude").mkdir()
|
|
119
|
+
_create_directory_link(
|
|
120
|
+
from_link=home / ".claude" / "scripts", to_target=home / ".agents" / "scripts"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
scripts_directory = usage._usage_pause_scripts_directory(
|
|
124
|
+
home / ".claude" / "scripts" / "claude_chain_usage.py"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
assert scripts_directory == home / ".agents" / "skills" / "usage-pause" / "scripts"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_scripts_directory_in_the_package_tree(tmp_path: Path) -> None:
|
|
131
|
+
package_root = tmp_path.resolve() / "claude-dev-env"
|
|
132
|
+
|
|
133
|
+
scripts_directory = usage._usage_pause_scripts_directory(
|
|
134
|
+
package_root / "scripts" / "claude_chain_usage.py"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
assert scripts_directory == package_root / ".agents" / "skills" / "usage-pause" / "scripts"
|
|
59
138
|
|
|
60
139
|
|
|
61
140
|
def test_probe_wraps_network_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -79,7 +158,7 @@ def test_failed_module_load_does_not_poison_cache(
|
|
|
79
158
|
(tmp_path / "resolve_usage_window.py").write_text(
|
|
80
159
|
"raise ImportError('intentional load failure')\n", encoding="utf-8"
|
|
81
160
|
)
|
|
82
|
-
monkeypatch.setattr(usage, "_usage_pause_scripts_directory", lambda: tmp_path)
|
|
161
|
+
monkeypatch.setattr(usage, "_usage_pause_scripts_directory", lambda _module_file: tmp_path)
|
|
83
162
|
|
|
84
163
|
with pytest.raises(ImportError, match="intentional load failure"):
|
|
85
164
|
usage._load_resolve_usage_window_module()
|
|
@@ -1290,7 +1290,7 @@ def test_posix_case_distinct_enforcer_path_remains_foreign(
|
|
|
1290
1290
|
target_manifest = _codex_hook_manifest(
|
|
1291
1291
|
_codex_hook_group(managed_hook, foreign_hook)
|
|
1292
1292
|
)
|
|
1293
|
-
monkeypatch.setattr(materializer
|
|
1293
|
+
monkeypatch.setattr(materializer, "_is_windows_host", lambda: False)
|
|
1294
1294
|
|
|
1295
1295
|
projected_manifest, _, _ = _publish_codex_hook_fixture(
|
|
1296
1296
|
source, target, target_manifest
|
|
@@ -1315,7 +1315,7 @@ def test_windows_case_variant_enforcer_path_is_managed(
|
|
|
1315
1315
|
target_manifest = _codex_hook_manifest(
|
|
1316
1316
|
_codex_hook_group({**managed_hook, "command": case_variant_command})
|
|
1317
1317
|
)
|
|
1318
|
-
monkeypatch.setattr(materializer
|
|
1318
|
+
monkeypatch.setattr(materializer, "_is_windows_host", lambda: True)
|
|
1319
1319
|
|
|
1320
1320
|
projected_manifest, _, _ = _publish_codex_hook_fixture(
|
|
1321
1321
|
source, target, target_manifest
|