claude-dev-env 2.2.0 → 2.2.1
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/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pyproject.toml +22 -0
- package/hooks/blocking/code_rules_shared.py +4 -2
- package/hooks/blocking/pii_payload_scan.py +78 -20
- package/hooks/blocking/pii_prevention_blocker.py +3 -1
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +221 -0
- package/hooks/validators/ruff_integration.py +2 -1
- package/hooks/validators/run_all_validators.py +460 -24
- package/hooks/validators/test_ruff_integration.py +21 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +223 -3
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/cleanup-command-forms.md +23 -0
- package/scripts/check.ps1 +32 -6
- package/skills/usage-pause/SKILL.md +5 -4
- package/skills/usage-pause/scripts/resolve_usage_window.py +68 -13
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +121 -9
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +7 -3
|
@@ -18,7 +18,9 @@ SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
|
18
18
|
if str(SCRIPTS_DIRECTORY) not in sys.path:
|
|
19
19
|
sys.path.insert(0, str(SCRIPTS_DIRECTORY))
|
|
20
20
|
|
|
21
|
-
from usage_pause_constants.resolve_usage_window_constants import (
|
|
21
|
+
from usage_pause_constants.resolve_usage_window_constants import ( # noqa: E402
|
|
22
|
+
DESKTOP_ENTRYPOINT_VALUE,
|
|
23
|
+
ENTRYPOINT_ENV_VAR,
|
|
22
24
|
SESSION_INGRESS_TOKEN_FILE_ENV_VAR as INGRESS_TOKEN_FILE_ENV_VAR,
|
|
23
25
|
)
|
|
24
26
|
|
|
@@ -188,8 +190,7 @@ class TestReadOauthAccessToken:
|
|
|
188
190
|
credentials_path.write_text("{not valid json", encoding="utf-8")
|
|
189
191
|
with caplog.at_level(logging.WARNING):
|
|
190
192
|
assert (
|
|
191
|
-
resolver.read_oauth_access_token(credentials_path, local_now())
|
|
192
|
-
is None
|
|
193
|
+
resolver.read_oauth_access_token(credentials_path, local_now()) is None
|
|
193
194
|
)
|
|
194
195
|
assert any("unreadable" in each_message for each_message in caplog.messages)
|
|
195
196
|
|
|
@@ -240,7 +241,43 @@ class TestReadSessionIngressToken:
|
|
|
240
241
|
assert INGRESS_TOKEN_FILE_ENV_VAR == "CLAUDE_SESSION_INGRESS_TOKEN_FILE"
|
|
241
242
|
|
|
242
243
|
|
|
244
|
+
class TestRunningOnDesktopHost:
|
|
245
|
+
def should_be_true_when_entrypoint_names_the_desktop_app(
|
|
246
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
247
|
+
) -> None:
|
|
248
|
+
resolver = load_resolver_module()
|
|
249
|
+
monkeypatch.setenv(ENTRYPOINT_ENV_VAR, DESKTOP_ENTRYPOINT_VALUE)
|
|
250
|
+
assert resolver.running_on_desktop_host() is True
|
|
251
|
+
|
|
252
|
+
def should_be_false_when_entrypoint_names_a_non_desktop_host(
|
|
253
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
254
|
+
) -> None:
|
|
255
|
+
resolver = load_resolver_module()
|
|
256
|
+
monkeypatch.setenv(ENTRYPOINT_ENV_VAR, "cli")
|
|
257
|
+
assert resolver.running_on_desktop_host() is False
|
|
258
|
+
|
|
259
|
+
def should_be_false_when_entrypoint_is_unset(
|
|
260
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
261
|
+
) -> None:
|
|
262
|
+
resolver = load_resolver_module()
|
|
263
|
+
monkeypatch.delenv(ENTRYPOINT_ENV_VAR, raising=False)
|
|
264
|
+
assert resolver.running_on_desktop_host() is False
|
|
265
|
+
|
|
266
|
+
def should_pin_the_desktop_entrypoint_contract_to_the_literal_marker(
|
|
267
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
268
|
+
) -> None:
|
|
269
|
+
resolver = load_resolver_module()
|
|
270
|
+
monkeypatch.setenv("CLAUDE_CODE_ENTRYPOINT", "claude-desktop")
|
|
271
|
+
assert resolver.running_on_desktop_host() is True
|
|
272
|
+
assert ENTRYPOINT_ENV_VAR == "CLAUDE_CODE_ENTRYPOINT"
|
|
273
|
+
assert DESKTOP_ENTRYPOINT_VALUE == "claude-desktop"
|
|
274
|
+
|
|
275
|
+
|
|
243
276
|
class TestResolveAccessToken:
|
|
277
|
+
@pytest.fixture(autouse=True)
|
|
278
|
+
def force_non_desktop_host(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
279
|
+
monkeypatch.delenv(ENTRYPOINT_ENV_VAR, raising=False)
|
|
280
|
+
|
|
244
281
|
def should_use_ingress_token_when_credential_file_missing(
|
|
245
282
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
246
283
|
) -> None:
|
|
@@ -295,6 +332,57 @@ class TestResolveAccessToken:
|
|
|
295
332
|
assert resolver.resolve_access_token(missing_credentials, local_now()) is None
|
|
296
333
|
|
|
297
334
|
|
|
335
|
+
class TestResolveAccessTokenHostAware:
|
|
336
|
+
def should_ignore_a_valid_cli_credential_token_on_the_desktop_host(
|
|
337
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
338
|
+
) -> None:
|
|
339
|
+
resolver = load_resolver_module()
|
|
340
|
+
now = local_now()
|
|
341
|
+
credentials_path = tmp_path / ".credentials.json"
|
|
342
|
+
future_milliseconds = int((now + timedelta(hours=1)).timestamp() * 1000)
|
|
343
|
+
write_credentials(
|
|
344
|
+
credentials_path, future_milliseconds, access_token="cli-credential-token"
|
|
345
|
+
)
|
|
346
|
+
monkeypatch.setenv(ENTRYPOINT_ENV_VAR, DESKTOP_ENTRYPOINT_VALUE)
|
|
347
|
+
monkeypatch.delenv(INGRESS_TOKEN_FILE_ENV_VAR, raising=False)
|
|
348
|
+
chosen_token = resolver.resolve_access_token(credentials_path, now)
|
|
349
|
+
assert chosen_token != "cli-credential-token"
|
|
350
|
+
assert chosen_token is None
|
|
351
|
+
|
|
352
|
+
def should_use_the_ingress_token_over_the_cli_credential_on_the_desktop_host(
|
|
353
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
354
|
+
) -> None:
|
|
355
|
+
resolver = load_resolver_module()
|
|
356
|
+
now = local_now()
|
|
357
|
+
credentials_path = tmp_path / ".credentials.json"
|
|
358
|
+
future_milliseconds = int((now + timedelta(hours=1)).timestamp() * 1000)
|
|
359
|
+
write_credentials(
|
|
360
|
+
credentials_path, future_milliseconds, access_token="cli-credential-token"
|
|
361
|
+
)
|
|
362
|
+
token_file = tmp_path / "ingress-token"
|
|
363
|
+
token_file.write_text("ingress-token-value", encoding="utf-8")
|
|
364
|
+
monkeypatch.setenv(ENTRYPOINT_ENV_VAR, DESKTOP_ENTRYPOINT_VALUE)
|
|
365
|
+
monkeypatch.setenv(INGRESS_TOKEN_FILE_ENV_VAR, str(token_file))
|
|
366
|
+
chosen_token = resolver.resolve_access_token(credentials_path, now)
|
|
367
|
+
assert chosen_token == "ingress-token-value"
|
|
368
|
+
assert chosen_token != "cli-credential-token"
|
|
369
|
+
|
|
370
|
+
def should_return_the_cli_credential_token_on_a_non_desktop_host(
|
|
371
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
372
|
+
) -> None:
|
|
373
|
+
resolver = load_resolver_module()
|
|
374
|
+
now = local_now()
|
|
375
|
+
credentials_path = tmp_path / ".credentials.json"
|
|
376
|
+
future_milliseconds = int((now + timedelta(hours=1)).timestamp() * 1000)
|
|
377
|
+
write_credentials(
|
|
378
|
+
credentials_path, future_milliseconds, access_token="cli-credential-token"
|
|
379
|
+
)
|
|
380
|
+
monkeypatch.setenv(ENTRYPOINT_ENV_VAR, "cli")
|
|
381
|
+
monkeypatch.delenv(INGRESS_TOKEN_FILE_ENV_VAR, raising=False)
|
|
382
|
+
chosen_token = resolver.resolve_access_token(credentials_path, now)
|
|
383
|
+
assert chosen_token == "cli-credential-token"
|
|
384
|
+
|
|
385
|
+
|
|
298
386
|
class TestExtractUsageWindows:
|
|
299
387
|
def should_extract_session_and_weekly_buckets(self) -> None:
|
|
300
388
|
resolver = load_resolver_module()
|
|
@@ -361,13 +449,20 @@ class TestBuildPausePlan:
|
|
|
361
449
|
|
|
362
450
|
class TestCommandLine:
|
|
363
451
|
def run_resolver(
|
|
364
|
-
self,
|
|
452
|
+
self,
|
|
453
|
+
*arguments: str,
|
|
454
|
+
ingress_token_file: str | None = None,
|
|
455
|
+
entrypoint: str | None = None,
|
|
365
456
|
) -> subprocess.CompletedProcess[str]:
|
|
366
457
|
child_environment = dict(os.environ)
|
|
367
458
|
if ingress_token_file is None:
|
|
368
459
|
child_environment.pop(INGRESS_TOKEN_FILE_ENV_VAR, None)
|
|
369
460
|
else:
|
|
370
461
|
child_environment[INGRESS_TOKEN_FILE_ENV_VAR] = ingress_token_file
|
|
462
|
+
if entrypoint is None:
|
|
463
|
+
child_environment.pop(ENTRYPOINT_ENV_VAR, None)
|
|
464
|
+
else:
|
|
465
|
+
child_environment[ENTRYPOINT_ENV_VAR] = entrypoint
|
|
371
466
|
return subprocess.run(
|
|
372
467
|
[sys.executable, str(RESOLVER_PATH), *arguments],
|
|
373
468
|
capture_output=True,
|
|
@@ -403,9 +498,7 @@ class TestCommandLine:
|
|
|
403
498
|
assert str(missing_path) in resolved_payload["error"]
|
|
404
499
|
assert f"{INGRESS_TOKEN_FILE_ENV_VAR} unset" in resolved_payload["error"]
|
|
405
500
|
|
|
406
|
-
def should_name_the_ingress_token_path_in_the_error(
|
|
407
|
-
self, tmp_path: Path
|
|
408
|
-
) -> None:
|
|
501
|
+
def should_name_the_ingress_token_path_in_the_error(self, tmp_path: Path) -> None:
|
|
409
502
|
missing_credentials = tmp_path / "absent.json"
|
|
410
503
|
stale_ingress = tmp_path / "stale-ingress-token"
|
|
411
504
|
completed = self.run_resolver(
|
|
@@ -429,10 +522,29 @@ class TestCommandLine:
|
|
|
429
522
|
assert completed.returncode == 2
|
|
430
523
|
resolved_payload = json.loads(completed.stdout)
|
|
431
524
|
assert (
|
|
432
|
-
f"{INGRESS_TOKEN_FILE_ENV_VAR} set but empty"
|
|
433
|
-
in resolved_payload["error"]
|
|
525
|
+
f"{INGRESS_TOKEN_FILE_ENV_VAR} set but empty" in resolved_payload["error"]
|
|
434
526
|
)
|
|
435
527
|
|
|
528
|
+
def should_exit_two_with_a_host_aware_error_on_the_desktop_host(
|
|
529
|
+
self, tmp_path: Path
|
|
530
|
+
) -> None:
|
|
531
|
+
credentials_path = tmp_path / ".credentials.json"
|
|
532
|
+
future_milliseconds = int((local_now() + timedelta(hours=1)).timestamp() * 1000)
|
|
533
|
+
write_credentials(
|
|
534
|
+
credentials_path, future_milliseconds, access_token="cli-credential-token"
|
|
535
|
+
)
|
|
536
|
+
completed = self.run_resolver(
|
|
537
|
+
"--credentials-path",
|
|
538
|
+
str(credentials_path),
|
|
539
|
+
entrypoint="claude-desktop",
|
|
540
|
+
)
|
|
541
|
+
assert completed.returncode == 2
|
|
542
|
+
assert "cli-credential-token" not in completed.stdout
|
|
543
|
+
error_text = json.loads(completed.stdout)["error"]
|
|
544
|
+
assert "different auth session" in error_text
|
|
545
|
+
assert "/usage" in error_text
|
|
546
|
+
assert "/usage-pause" in error_text
|
|
547
|
+
|
|
436
548
|
def should_reject_invalid_override_with_error_payload(self) -> None:
|
|
437
549
|
completed = self.run_resolver("--override", "soon")
|
|
438
550
|
assert completed.returncode == 2
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Constants for the usage-window resolver.
|
|
2
2
|
|
|
3
3
|
Groups: the OAuth usage-endpoint probe, the CLI credential file keys, the
|
|
4
|
-
session ingress token file environment variable, the
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
session ingress token file environment variable, the host entrypoint
|
|
5
|
+
detection, the usage-response field keys, the override parse patterns, the
|
|
6
|
+
wakeup stage sizing, the weekly warn threshold, the result JSON keys, the
|
|
7
|
+
source labels, and the exit codes.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
@@ -24,6 +25,9 @@ CREDENTIALS_EXPIRES_AT_KEY = "expiresAt"
|
|
|
24
25
|
SESSION_INGRESS_TOKEN_FILE_ENV_VAR = "CLAUDE_SESSION_INGRESS_TOKEN_FILE"
|
|
25
26
|
MILLISECONDS_PER_SECOND = 1000
|
|
26
27
|
|
|
28
|
+
ENTRYPOINT_ENV_VAR = "CLAUDE_CODE_ENTRYPOINT"
|
|
29
|
+
DESKTOP_ENTRYPOINT_VALUE = "claude-desktop"
|
|
30
|
+
|
|
27
31
|
FIVE_HOUR_BUCKET_KEY = "five_hour"
|
|
28
32
|
SEVEN_DAY_BUCKET_KEY = "seven_day"
|
|
29
33
|
UTILIZATION_KEY = "utilization"
|