claude-dev-env 7.0.0 → 7.1.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.
Files changed (210) hide show
  1. package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
  2. package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
  3. package/.agents/skills/session-continuity/AGENTS.md +1 -0
  4. package/.agents/skills/session-continuity/CLAUDE.md +1 -0
  5. package/.agents/skills/session-continuity/README.md +189 -0
  6. package/.agents/skills/session-continuity/SKILL.md +114 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +375 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
  10. package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
  11. package/bin/cde.mjs +36 -8
  12. package/bin/cde.test.mjs +48 -0
  13. package/bin/ever-shipped-skills.mjs +2 -20
  14. package/bin/ever-shipped-skills.test.mjs +8 -0
  15. package/bin/expand_home_directory_tokens.mjs +4 -5
  16. package/bin/expand_home_directory_tokens.test.mjs +128 -0
  17. package/bin/git_hooks_installer.mjs +22 -3
  18. package/bin/git_hooks_installer.test.mjs +159 -5
  19. package/bin/install-session-continuity.mjs +78 -0
  20. package/bin/install-session-continuity.test.mjs +42 -0
  21. package/bin/install.codex-rules.test.mjs +5 -2
  22. package/bin/install.cursor-rules.test.mjs +5 -2
  23. package/bin/install.mjs +478 -63
  24. package/bin/install.policy-lint.test.mjs +56 -4
  25. package/bin/install.profiles.test.mjs +10 -0
  26. package/bin/install.prune.test.mjs +227 -17
  27. package/bin/install.test.mjs +500 -120
  28. package/bin/install.transaction.test.mjs +3 -3
  29. package/docs/CODE_RULES.md +1 -1
  30. package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
  31. package/hooks/blocking/precommit_code_rules_gate.py +5 -8
  32. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
  33. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
  34. package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
  36. package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
  37. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
  38. package/hooks/git-hooks/post_commit.py +33 -0
  39. package/hooks/git-hooks/pre_commit.py +18 -90
  40. package/hooks/git-hooks/pre_push.py +45 -541
  41. package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
  42. package/hooks/git-hooks/test_native_hook_support.py +129 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +39 -6
  44. package/hooks/git-hooks/test_post_commit.py +43 -0
  45. package/hooks/git-hooks/test_pre_commit.py +36 -392
  46. package/hooks/git-hooks/test_pre_push.py +48 -1631
  47. package/hooks/git-hooks/test_verification_notice.py +60 -0
  48. package/hooks/git-hooks/test_verification_start.py +173 -0
  49. package/hooks/git-hooks/verification_notice.py +192 -0
  50. package/hooks/git-hooks/verification_notice_context.py +253 -0
  51. package/hooks/git-hooks/verification_notice_state.py +237 -0
  52. package/hooks/git-hooks/verification_start.py +100 -0
  53. package/hooks/hooks.json +0 -119
  54. package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
  55. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
  56. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
  57. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
  58. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
  59. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
  60. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
  61. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
  62. package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
  63. package/hooks/validators/run_all_validators.py +5 -1
  64. package/hooks/validators/security_checks.py +57 -2
  65. package/hooks/validators/test_hook_timing_harness.py +2 -5
  66. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
  67. package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
  68. package/hooks/validators/test_security_checks.py +57 -0
  69. package/package.json +1 -1
  70. package/rules/confirm-implementation-forks.md +2 -0
  71. package/rules/git-workflow.md +12 -0
  72. package/rules/pstack-host-mapping.md +48 -0
  73. package/rules/vault-context.md +3 -5
  74. package/rules/verify-runtime-state.md +7 -0
  75. package/scripts/audit-hooks-cli.mjs +5 -0
  76. package/scripts/audit-hooks.mjs +72 -16
  77. package/scripts/automatic_advisory/__init__.py +1 -0
  78. package/scripts/automatic_advisory/checkout.py +51 -0
  79. package/scripts/automatic_advisory/cli.py +334 -0
  80. package/scripts/automatic_advisory/config/__init__.py +1 -0
  81. package/scripts/automatic_advisory/config/constants.py +93 -0
  82. package/scripts/automatic_advisory/config/timing.py +4 -0
  83. package/scripts/automatic_advisory/configuration.py +197 -0
  84. package/scripts/automatic_advisory/execution.py +350 -0
  85. package/scripts/automatic_advisory/git.py +69 -0
  86. package/scripts/automatic_advisory/model.py +107 -0
  87. package/scripts/automatic_advisory/process_host.py +32 -0
  88. package/scripts/automatic_advisory/publisher.py +84 -0
  89. package/scripts/automatic_advisory/runner.py +399 -0
  90. package/scripts/automatic_advisory/state.py +116 -0
  91. package/scripts/automatic_advisory/window_flags.py +41 -0
  92. package/scripts/automatic_advisory/windows_job.py +122 -0
  93. package/scripts/codex_compat_materializer.py +99 -13
  94. package/scripts/durable_post_lint.py +30 -2
  95. package/scripts/durable_post_lint_config/config/constants.py +10 -0
  96. package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
  97. package/scripts/local_report_cli.py +184 -0
  98. package/scripts/local_report_core.py +247 -0
  99. package/scripts/local_report_publisher.py +12 -0
  100. package/scripts/local_report_validation.py +357 -0
  101. package/scripts/local_verification/__init__.py +23 -0
  102. package/scripts/local_verification/check_runner.py +364 -0
  103. package/scripts/local_verification/check_support.py +99 -0
  104. package/scripts/local_verification/cli.py +158 -0
  105. package/scripts/local_verification/command_runner.py +84 -0
  106. package/scripts/local_verification/config/__init__.py +1 -0
  107. package/scripts/local_verification/config/constants.py +89 -0
  108. package/scripts/local_verification/config/timing.py +1 -0
  109. package/scripts/local_verification/git_state.py +146 -0
  110. package/scripts/local_verification/manifest.py +224 -0
  111. package/scripts/local_verification/model.py +174 -0
  112. package/scripts/local_verification/runner.py +234 -0
  113. package/scripts/policy_lint/adapter_pairing.py +58 -3
  114. package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
  115. package/scripts/pr_verification/__init__.py +1 -0
  116. package/scripts/pr_verification/branch_refs.py +114 -0
  117. package/scripts/pr_verification/config/__init__.py +1 -0
  118. package/scripts/pr_verification/config/constants.py +84 -0
  119. package/scripts/pr_verification/config/timing.py +5 -0
  120. package/scripts/pr_verification/github.py +137 -0
  121. package/scripts/pr_verification/github_api.py +376 -0
  122. package/scripts/pr_verification/github_labels.py +88 -0
  123. package/scripts/pr_verification/github_parsing.py +182 -0
  124. package/scripts/pr_verification/github_transport.py +91 -0
  125. package/scripts/pr_verification/lock.py +122 -0
  126. package/scripts/pr_verification/model.py +43 -0
  127. package/scripts/pr_verification/requirements.txt +1 -0
  128. package/scripts/test_durable_post_lint.py +97 -0
  129. package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
  130. package/scripts/tests/test-audit-hooks.mjs +94 -2
  131. package/scripts/tests/test_automatic_advisory.py +376 -0
  132. package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
  133. package/scripts/tests/test_automatic_advisory_polling.py +216 -0
  134. package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
  135. package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
  136. package/scripts/tests/test_automatic_advisory_settings.py +48 -0
  137. package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
  138. package/scripts/tests/test_closed_pr_label.py +157 -0
  139. package/scripts/tests/test_codex_compat_materializer.py +189 -6
  140. package/scripts/tests/test_github_labels.py +101 -0
  141. package/scripts/tests/test_github_transport.py +125 -0
  142. package/scripts/tests/test_local_report_publisher.py +396 -0
  143. package/scripts/tests/test_local_report_publisher_cli.py +85 -0
  144. package/scripts/tests/test_local_verification.py +348 -0
  145. package/scripts/tests/test_local_verification_cli.py +98 -0
  146. package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
  147. package/scripts/tests/test_pr_verification_github.py +392 -0
  148. package/scripts/tests/test_pr_verification_lock.py +168 -0
  149. package/scripts/tests/test_verification_notice.py +347 -0
  150. package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
  151. package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
  152. package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
  153. package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
  154. package/hooks/blocking/destructive_command_blocker.py +0 -2073
  155. package/hooks/blocking/fable_spawn_gate.py +0 -187
  156. package/hooks/blocking/luna_fast_mode_gate.py +0 -160
  157. package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
  158. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
  159. package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
  160. package/hooks/blocking/pii_prevention_blocker.py +0 -365
  161. package/hooks/blocking/piped_pytest_blocker.py +0 -1223
  162. package/hooks/blocking/question_to_user_enforcer.py +0 -130
  163. package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
  164. package/hooks/blocking/sensitive_file_protector.py +0 -167
  165. package/hooks/blocking/session_edit_stage_gate.py +0 -684
  166. package/hooks/blocking/session_handoff_blocker.py +0 -182
  167. package/hooks/blocking/shell_substitution_blocker.py +0 -350
  168. package/hooks/blocking/test_ask_user_question_shape.py +0 -69
  169. package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
  170. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
  171. package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
  172. package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
  173. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
  174. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
  175. package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
  176. package/hooks/blocking/test_fable_spawn_gate.py +0 -381
  177. package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
  178. package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
  179. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
  180. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
  181. package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
  182. package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
  183. package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
  184. package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
  185. package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
  186. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
  187. package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
  188. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
  189. package/hooks/blocking/test_sensitive_file_protector.py +0 -365
  190. package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
  191. package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
  192. package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
  193. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
  194. package/hooks/blocking/test_session_handoff_blocker.py +0 -318
  195. package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
  196. package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
  197. package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
  198. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
  199. package/hooks/blocking/unscoped_search_blocker.py +0 -391
  200. package/hooks/blocking/write_existing_file_blocker.py +0 -124
  201. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
  202. package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
  203. package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
  204. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
  205. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
  206. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
  207. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
  208. package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
  209. package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
  210. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.parse
4
+ from collections.abc import Callable
5
+ from dataclasses import replace
6
+
7
+ from .config.constants import (
8
+ GIT_BRANCH_REF_SAFE_CHARACTERS,
9
+ GIT_BRANCH_REFERENCE_ENDPOINT_TEMPLATE,
10
+ GIT_REFERENCE_OBJECT_KEY,
11
+ GIT_REFERENCE_RESOURCE_NAME,
12
+ PULL_SHA_KEY,
13
+ )
14
+ from .github_parsing import require_mapping, require_text
15
+ from .model import PullRequestCandidate, RepositorySettings
16
+
17
+ GetJson = Callable[[str], object]
18
+
19
+
20
+ def _read_branch_sha(
21
+ repository: RepositorySettings,
22
+ base_ref: str,
23
+ get_json: GetJson,
24
+ ) -> str:
25
+ endpoint = GIT_BRANCH_REFERENCE_ENDPOINT_TEMPLATE.format(
26
+ repository=repository.slug,
27
+ base_ref=urllib.parse.quote(
28
+ base_ref,
29
+ safe=GIT_BRANCH_REF_SAFE_CHARACTERS,
30
+ ),
31
+ )
32
+ reference = require_mapping(get_json(endpoint), GIT_REFERENCE_RESOURCE_NAME)
33
+ target = require_mapping(
34
+ reference.get(GIT_REFERENCE_OBJECT_KEY),
35
+ GIT_REFERENCE_RESOURCE_NAME,
36
+ )
37
+ return require_text(target, PULL_SHA_KEY, GIT_REFERENCE_RESOURCE_NAME)
38
+
39
+
40
+ def replace_candidate_base_sha(
41
+ candidate: PullRequestCandidate,
42
+ repository: RepositorySettings,
43
+ get_json: GetJson,
44
+ ) -> PullRequestCandidate:
45
+ """Replace one pull request base SHA with its current branch tip.
46
+
47
+ Args:
48
+ candidate: Pull request candidate from the pulls API.
49
+ repository: Repository containing the target branch.
50
+ get_json: Authenticated GitHub JSON reader.
51
+
52
+ Returns:
53
+ Candidate carrying the current target branch SHA.
54
+ """
55
+ current_base_sha = _read_branch_sha(repository, candidate.base_ref, get_json)
56
+ return replace(candidate, base_sha=current_base_sha)
57
+
58
+
59
+ def replace_candidate_base_shas(
60
+ all_candidates: tuple[PullRequestCandidate, ...],
61
+ repository: RepositorySettings,
62
+ get_json: GetJson,
63
+ ) -> tuple[PullRequestCandidate, ...]:
64
+ """Replace pull request base SHAs with current unique branch tips.
65
+
66
+ Args:
67
+ all_candidates: Pull request candidates from the pulls API.
68
+ repository: Repository containing the target branches.
69
+ get_json: Authenticated GitHub JSON reader.
70
+
71
+ Returns:
72
+ Candidates carrying current target branch SHAs.
73
+ """
74
+ base_sha_by_ref = _read_unique_branch_shas(
75
+ all_candidates,
76
+ repository,
77
+ get_json,
78
+ )
79
+ return tuple(
80
+ replace(
81
+ each_candidate,
82
+ base_sha=base_sha_by_ref[each_candidate.base_ref],
83
+ )
84
+ for each_candidate in all_candidates
85
+ )
86
+
87
+
88
+ def _resolve_candidate_base_shas(
89
+ all_candidates: list[PullRequestCandidate],
90
+ repository: RepositorySettings,
91
+ get_json: GetJson,
92
+ should_require_merge_commit: bool,
93
+ ) -> tuple[PullRequestCandidate, ...]:
94
+ candidates = tuple(all_candidates)
95
+ if should_require_merge_commit:
96
+ return candidates
97
+ return replace_candidate_base_shas(candidates, repository, get_json)
98
+
99
+
100
+ def _read_unique_branch_shas(
101
+ all_candidates: tuple[PullRequestCandidate, ...],
102
+ repository: RepositorySettings,
103
+ get_json: GetJson,
104
+ ) -> dict[str, str]:
105
+ base_sha_by_ref: dict[str, str] = {}
106
+ for each_candidate in all_candidates:
107
+ if each_candidate.base_ref in base_sha_by_ref:
108
+ continue
109
+ base_sha_by_ref[each_candidate.base_ref] = _read_branch_sha(
110
+ repository,
111
+ each_candidate.base_ref,
112
+ get_json,
113
+ )
114
+ return base_sha_by_ref
@@ -0,0 +1 @@
1
+ """Configuration constants for pull request verification."""
@@ -0,0 +1,84 @@
1
+ import re
2
+
3
+ SUCCESS_EXIT_CODE = 0
4
+ INCOMPLETE_EXIT_CODE = 3
5
+ HTTP_OK = 200
6
+ HTTP_CREATED = 201
7
+ HTTP_NO_CONTENT = 204
8
+ GITHUB_API_URL = "https://api.github.com"
9
+ GITHUB_PAGE_SIZE = 100
10
+ UTF8_ENCODING = "utf-8"
11
+ JSON_CONTENT_TYPE = "application/json"
12
+ GITHUB_ACCEPT_TYPE = "application/vnd.github+json"
13
+ GITHUB_API_VERSION = "2022-11-28"
14
+ AUTHORIZATION_HEADER = "Authorization"
15
+ ACCEPT_HEADER = "Accept"
16
+ CONTENT_TYPE_HEADER = "Content-Type"
17
+ API_VERSION_HEADER = "X-GitHub-Api-Version"
18
+ BEARER_PREFIX = "Bearer "
19
+ APP_JWT_ALGORITHM = "RS256"
20
+ APP_ISSUER_CLAIM = "iss"
21
+ APP_ISSUED_AT_CLAIM = "iat"
22
+ APP_EXPIRATION_CLAIM = "exp"
23
+ PULL_NUMBER_KEY = "number"
24
+ PULL_DRAFT_KEY = "draft"
25
+ PULL_MERGE_SHA_KEY = "merge_commit_sha"
26
+ PULL_BASE_KEY = "base"
27
+ PULL_HEAD_KEY = "head"
28
+ PULL_REF_KEY = "ref"
29
+ PULL_SHA_KEY = "sha"
30
+ COMMIT_PARENTS_KEY = "parents"
31
+ TOKEN_KEY = "token"
32
+ REPOSITORIES_KEY = "repositories"
33
+ PERMISSIONS_KEY = "permissions"
34
+ CONTENTS_PERMISSION_KEY = "contents"
35
+ PULL_REQUESTS_PERMISSION_KEY = "pull_requests"
36
+ STATUSES_PERMISSION_KEY = "statuses"
37
+ ISSUES_PERMISSION_KEY = "issues"
38
+ READ_PERMISSION = "read"
39
+ WRITE_PERMISSION = "write"
40
+ STATUS_STATE_KEY = "state"
41
+ STATUS_CONTEXT_KEY = "context"
42
+ STATUS_DESCRIPTION_KEY = "description"
43
+ STATUS_PENDING = "pending"
44
+ STATUS_ERROR = "error"
45
+ STATUS_FAILURE = "failure"
46
+ STATUS_SUCCESS = "success"
47
+ LOCAL_CHECKS_CONTEXT = "local-checks"
48
+ LOCAL_CHECKS_PASSED_LABEL = "local-checks:passed"
49
+ PULLS_ENDPOINT_TEMPLATE = (
50
+ "/repos/{repository}/pulls?state=open&per_page={page_size}&page={page}"
51
+ )
52
+ PULL_ENDPOINT_TEMPLATE = "/repos/{repository}/pulls/{pull_number}"
53
+ GIT_BRANCH_REFERENCE_ENDPOINT_TEMPLATE = "/repos/{repository}/git/ref/heads/{base_ref}"
54
+ GIT_BRANCH_REF_SAFE_CHARACTERS = "/"
55
+ GIT_REFERENCE_OBJECT_KEY = "object"
56
+ GIT_REFERENCE_RESOURCE_NAME = "git reference"
57
+ COMMIT_ENDPOINT_TEMPLATE = "/repos/{repository}/git/commits/{commit_sha}"
58
+ STATUS_ENDPOINT_TEMPLATE = "/repos/{repository}/statuses/{commit_sha}"
59
+ ISSUE_LABELS_ENDPOINT_TEMPLATE = "/repos/{repository}/issues/{pull_number}/labels"
60
+ ISSUE_LABELS_PAGE_ENDPOINT_TEMPLATE = (
61
+ "/repos/{repository}/issues/{pull_number}/labels?per_page={page_size}&page={page}"
62
+ )
63
+ ISSUE_LABEL_ENDPOINT_TEMPLATE = (
64
+ "/repos/{repository}/issues/{pull_number}/labels/{label}"
65
+ )
66
+ ISSUE_LABELS_KEY = "labels"
67
+ INSTALLATION_TOKEN_ENDPOINT_TEMPLATE = (
68
+ "/app/installations/{installation_id}/access_tokens"
69
+ )
70
+ GITHUB_REQUEST_ERROR_TEMPLATE = "GitHub {method} {url} returned {status_code}"
71
+ GITHUB_JSON_ERROR_TEMPLATE = "GitHub returned invalid JSON from {url}"
72
+ GITHUB_SHAPE_ERROR_TEMPLATE = "GitHub returned an invalid {resource} payload"
73
+ PULL_RESOURCE_NAME = "pull request"
74
+ PULL_LIST_RESOURCE_NAME = "pull request list"
75
+ TOKEN_RESOURCE_NAME = "installation token"
76
+ PENDING_DESCRIPTION = "Local verification is running"
77
+ FAILURE_DESCRIPTION = "A required local check failed"
78
+ ERROR_DESCRIPTION = "Local verification did not complete"
79
+ GIT_BARE_SUFFIX = ".git"
80
+ SUPERVISOR_LOCK_FILENAME = "supervisor.lock"
81
+ SUPERVISOR_LOCK_ERROR = "Another verification supervisor owns this cache root"
82
+ SUPERVISOR_LOCK_IO_ERROR = "Verification supervisor lock file is unavailable"
83
+ SUPERVISOR_LOCK_FILE_MODE = 0o600
84
+ REPOSITORY_SLUG_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
@@ -0,0 +1,5 @@
1
+ APP_JWT_LIFETIME_SECONDS = 540
2
+ APP_JWT_CLOCK_SKEW_SECONDS = 60
3
+ HTTP_TIMEOUT_SECONDS = 30
4
+ DOCKER_CLEANUP_TIMEOUT_SECONDS = 30
5
+ CHECK_TIMEOUT_GRACE_SECONDS = 15
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+
8
+ from . import github_api, github_transport
9
+ from .config.constants import (
10
+ APP_EXPIRATION_CLAIM,
11
+ APP_ISSUED_AT_CLAIM,
12
+ APP_ISSUER_CLAIM,
13
+ APP_JWT_ALGORITHM,
14
+ CONTENTS_PERMISSION_KEY,
15
+ HTTP_CREATED,
16
+ INSTALLATION_TOKEN_ENDPOINT_TEMPLATE,
17
+ ISSUES_PERMISSION_KEY,
18
+ PERMISSIONS_KEY,
19
+ PULL_REQUESTS_PERMISSION_KEY,
20
+ READ_PERMISSION,
21
+ REPOSITORIES_KEY,
22
+ STATUSES_PERMISSION_KEY,
23
+ TOKEN_KEY,
24
+ TOKEN_RESOURCE_NAME,
25
+ UTF8_ENCODING,
26
+ WRITE_PERMISSION,
27
+ )
28
+ from .config.timing import (
29
+ APP_JWT_CLOCK_SKEW_SECONDS,
30
+ APP_JWT_LIFETIME_SECONDS,
31
+ )
32
+ from .github_api import authorized_headers, request_json
33
+ from .github_parsing import require_mapping, require_text
34
+ from .github_transport import (
35
+ HttpRequester,
36
+ JwtEncoder,
37
+ encode_app_jwt,
38
+ request_http,
39
+ )
40
+ from .model import RepositorySettings
41
+
42
+ GitHubApi = github_api.GitHubApi
43
+ GitHubError = github_api.GitHubError
44
+ HttpReply = github_transport.HttpReply
45
+
46
+
47
+ def _build_installation_token_body(
48
+ repository: RepositorySettings, should_write_issue_labels: bool
49
+ ) -> bytes:
50
+ all_permissions = {
51
+ CONTENTS_PERMISSION_KEY: READ_PERMISSION,
52
+ PULL_REQUESTS_PERMISSION_KEY: READ_PERMISSION,
53
+ STATUSES_PERMISSION_KEY: WRITE_PERMISSION,
54
+ }
55
+ if should_write_issue_labels:
56
+ all_permissions[ISSUES_PERMISSION_KEY] = WRITE_PERMISSION
57
+ all_permissions[PULL_REQUESTS_PERMISSION_KEY] = WRITE_PERMISSION
58
+ token_request = {
59
+ REPOSITORIES_KEY: [repository.name],
60
+ PERMISSIONS_KEY: all_permissions,
61
+ }
62
+ return json.dumps(token_request).encode(UTF8_ENCODING)
63
+
64
+
65
+ class GitHubAppAuthenticator:
66
+ def __init__(
67
+ self,
68
+ api_url: str,
69
+ app_id: int,
70
+ installation_id: int,
71
+ private_key_path: Path,
72
+ requester: HttpRequester = request_http,
73
+ jwt_encoder: JwtEncoder = encode_app_jwt,
74
+ clock: Callable[[], float] = time.time,
75
+ ) -> None:
76
+ self.api_url = api_url
77
+ self.app_id = app_id
78
+ self.installation_id = installation_id
79
+ self.private_key_path = private_key_path
80
+ self.requester = requester
81
+ self.jwt_encoder = jwt_encoder
82
+ self.clock = clock
83
+
84
+ def issue_repository_api(
85
+ self,
86
+ repository: RepositorySettings,
87
+ *,
88
+ should_write_issue_labels: bool = False,
89
+ ) -> GitHubApi:
90
+ """Create a repository-scoped API client.
91
+
92
+ Args:
93
+ repository: Repository to scope.
94
+
95
+ Returns:
96
+ Authenticated API client.
97
+ """
98
+ installation_token = self._request_repository_token(
99
+ repository, should_write_issue_labels
100
+ )
101
+ return GitHubApi(self.api_url, installation_token, self.requester)
102
+
103
+ def _request_repository_token(
104
+ self, repository: RepositorySettings, should_write_issue_labels: bool
105
+ ) -> str:
106
+ app_token = self.create_app_token()
107
+ endpoint = INSTALLATION_TOKEN_ENDPOINT_TEMPLATE.format(
108
+ installation_id=self.installation_id
109
+ )
110
+ request_body = _build_installation_token_body(
111
+ repository, should_write_issue_labels
112
+ )
113
+ token_payload = request_json(
114
+ self.requester,
115
+ "POST",
116
+ self.api_url + endpoint,
117
+ authorized_headers(app_token),
118
+ request_body,
119
+ HTTP_CREATED,
120
+ )
121
+ token_mapping = require_mapping(token_payload, TOKEN_RESOURCE_NAME)
122
+ return require_text(token_mapping, TOKEN_KEY, TOKEN_RESOURCE_NAME)
123
+
124
+ def create_app_token(self) -> str:
125
+ """Create an app JWT for token exchange.
126
+
127
+ Returns:
128
+ The encoded app token.
129
+ """
130
+ current_time = int(self.clock())
131
+ all_claims: dict[str, int | str] = {
132
+ APP_ISSUER_CLAIM: str(self.app_id),
133
+ APP_ISSUED_AT_CLAIM: current_time - APP_JWT_CLOCK_SKEW_SECONDS,
134
+ APP_EXPIRATION_CLAIM: current_time + APP_JWT_LIFETIME_SECONDS,
135
+ }
136
+ private_key = self.private_key_path.read_text(encoding=UTF8_ENCODING)
137
+ return self.jwt_encoder(all_claims, private_key, APP_JWT_ALGORITHM)