claude-dev-env 8.79.0 → 8.81.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/.agents/skills/pr-lifecycle/reference/review-loop-before-merge.md +2 -2
- package/bin/install.settings-defaults.test.mjs +27 -2
- package/bin/merge_settings_defaults.mjs +39 -21
- package/bin/merge_settings_defaults.test.mjs +42 -0
- package/docs/account-broker.md +1 -1
- package/docs/codex-accounts.md +6 -5
- package/hooks/blocking/cloud_graphql_gate.py +213 -0
- package/hooks/blocking/headless_claude_broker_gate.py +1 -8
- package/hooks/blocking/merge_review_gate.py +65 -3
- package/hooks/blocking/pr_lifecycle_skill_gate.py +19 -10
- package/hooks/blocking/pull_request_proof.py +42 -13
- package/hooks/blocking/pull_request_proof_edit_gate.py +180 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +5 -1
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_cloud_graphql_gate.py +210 -0
- package/hooks/blocking/test_headless_claude_broker_gate.py +12 -7
- package/hooks/blocking/test_merge_review_gate.py +63 -0
- package/hooks/blocking/test_pr_lifecycle_skill_gate.py +11 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +1 -0
- package/hooks/blocking/test_pull_request_proof.py +7 -0
- package/hooks/blocking/test_pull_request_proof_edit_gate.py +199 -0
- package/hooks/conftest.py +1 -1
- package/hooks/features/bash-dispatchers.md +3 -0
- package/hooks/features/conduct-gates.md +2 -2
- package/hooks/features/write-edit-blocking.md +4 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/cloud_graphql_gate_constants.py +116 -0
- package/hooks/hooks_constants/cloud_session.py +17 -0
- package/hooks/hooks_constants/cloud_session_constants.py +11 -0
- package/hooks/hooks_constants/gh_command_words.py +52 -0
- package/hooks/hooks_constants/gh_command_words_constants.py +64 -0
- package/hooks/hooks_constants/headless_claude_broker_gate_constants.py +1 -7
- package/hooks/hooks_constants/hook_block_logger.py +21 -3
- package/hooks/hooks_constants/merge_review_gate_constants.py +14 -3
- package/hooks/hooks_constants/mod_handoff.py +2 -3
- package/hooks/hooks_constants/mod_handoff_constants.py +0 -4
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +27 -2
- package/hooks/hooks_constants/pr_lifecycle_skill_gate_constants.py +0 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pull_request_proof_constants.py +22 -3
- package/hooks/hooks_constants/shell_command_evaluation_constants.py +67 -0
- package/hooks/hooks_constants/shell_command_substitution_constants.py +25 -0
- package/hooks/hooks_constants/shell_command_wrappers.py +327 -12
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -1
- package/hooks/hooks_constants/test_cloud_session.py +27 -0
- package/hooks/hooks_constants/test_gh_command_words.py +45 -0
- package/hooks/hooks_constants/test_hook_block_logger.py +61 -0
- package/hooks/hooks_constants/test_mod_handoff.py +3 -2
- package/hooks/hooks_constants/test_piped_pytest_blocker_constants.py +11 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +9 -6
- package/hooks/hooks_constants/test_shell_command_wrappers.py +125 -0
- package/hooks/test_conftest.py +1 -1
- package/package.json +1 -1
- package/scripts/account_broker_support.py +10 -11
- package/scripts/codex_account_meters.py +30 -43
- package/scripts/dev_env_scripts_constants/account_broker_constants.py +34 -25
- package/scripts/dev_env_scripts_constants/claude_account_constants.py +0 -9
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +0 -18
- package/scripts/dev_env_scripts_constants/test_account_broker_constants.py +42 -0
- package/scripts/dev_env_scripts_constants/test_codex_account_constants.py +119 -0
- package/scripts/merge_review_receipt.py +56 -12
- package/scripts/test_account_broker.py +12 -0
- package/scripts/test_account_broker_support.py +32 -6
- package/scripts/test_codex_account_meters.py +26 -14
- package/scripts/test_merge_review_receipt.py +86 -0
- package/settings.json +3 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Names the ``gh`` options that take a separate value, and the option prefix and value separator.
|
|
2
|
+
|
|
3
|
+
A reader of a ``gh`` command skips the token after each of these options, so
|
|
4
|
+
``gh -R o/r pr list`` reads as ``pr list`` and ``gh pr merge 12 --body -h``
|
|
5
|
+
reads ``-h`` as the body text. ``-f``, ``-F``, ``-H``, ``-p`` and ``-t`` take a value in
|
|
6
|
+
``gh api``, ``gh workflow run`` and ``gh pr create`` alike.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ALL_GH_OPTIONS_WITH_A_VALUE",
|
|
13
|
+
"GH_OPTION_PREFIX",
|
|
14
|
+
"GH_OPTION_VALUE_SEPARATOR",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
ALL_GH_OPTIONS_WITH_A_VALUE: frozenset[str] = frozenset(
|
|
18
|
+
{
|
|
19
|
+
"-R",
|
|
20
|
+
"--repo",
|
|
21
|
+
"--hostname",
|
|
22
|
+
"--config",
|
|
23
|
+
"-H",
|
|
24
|
+
"--header",
|
|
25
|
+
"--head",
|
|
26
|
+
"-X",
|
|
27
|
+
"--method",
|
|
28
|
+
"-F",
|
|
29
|
+
"--field",
|
|
30
|
+
"--body-file",
|
|
31
|
+
"-f",
|
|
32
|
+
"--raw-field",
|
|
33
|
+
"--input",
|
|
34
|
+
"-q",
|
|
35
|
+
"--jq",
|
|
36
|
+
"-t",
|
|
37
|
+
"--template",
|
|
38
|
+
"-p",
|
|
39
|
+
"--preview",
|
|
40
|
+
"--title",
|
|
41
|
+
"--cache",
|
|
42
|
+
"--json",
|
|
43
|
+
"-b",
|
|
44
|
+
"--body",
|
|
45
|
+
"-B",
|
|
46
|
+
"--base",
|
|
47
|
+
"-S",
|
|
48
|
+
"--search",
|
|
49
|
+
"-L",
|
|
50
|
+
"--limit",
|
|
51
|
+
"-s",
|
|
52
|
+
"--state",
|
|
53
|
+
"-l",
|
|
54
|
+
"--label",
|
|
55
|
+
"-a",
|
|
56
|
+
"--assignee",
|
|
57
|
+
"-A",
|
|
58
|
+
"--author",
|
|
59
|
+
"-m",
|
|
60
|
+
"--milestone",
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
GH_OPTION_PREFIX: str = "-"
|
|
64
|
+
GH_OPTION_VALUE_SEPARATOR: str = "="
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
Holds the Claude program name and its profile-launcher prefix, the Windows
|
|
4
4
|
launcher suffixes stripped before the name check, the two print-mode flags, the
|
|
5
5
|
hook name the block log records, and the deny message that names the broker
|
|
6
|
-
command to run instead.
|
|
7
|
-
cloud session sets, where the gate stands aside because the session has one
|
|
8
|
-
account.
|
|
6
|
+
command to run instead.
|
|
9
7
|
"""
|
|
10
8
|
|
|
11
9
|
from __future__ import annotations
|
|
@@ -18,8 +16,6 @@ __all__ = [
|
|
|
18
16
|
"PRINT_MODE_LONG_FLAG_WITH_VALUE_PREFIX",
|
|
19
17
|
"GATE_HOOK_NAME",
|
|
20
18
|
"BROKER_COMMAND_DENY_REASON",
|
|
21
|
-
"CLOUD_SESSION_ENV_VAR",
|
|
22
|
-
"CLOUD_SESSION_ENV_TRUE_VALUE",
|
|
23
19
|
]
|
|
24
20
|
|
|
25
21
|
CLAUDE_PROGRAM_NAME: str = "claude"
|
|
@@ -28,8 +24,6 @@ ALL_WINDOWS_LAUNCHER_SUFFIXES: tuple[str, ...] = (".cmd", ".exe", ".ps1", ".bat"
|
|
|
28
24
|
ALL_PRINT_MODE_FLAGS: frozenset[str] = frozenset({"-p", "--print"})
|
|
29
25
|
PRINT_MODE_LONG_FLAG_WITH_VALUE_PREFIX: str = "--print="
|
|
30
26
|
GATE_HOOK_NAME: str = "headless_claude_broker_gate.py"
|
|
31
|
-
CLOUD_SESSION_ENV_VAR: str = "CLAUDE_CODE_REMOTE"
|
|
32
|
-
CLOUD_SESSION_ENV_TRUE_VALUE: str = "true"
|
|
33
27
|
BROKER_COMMAND_DENY_REASON: str = (
|
|
34
28
|
"BLOCKED: Start a headless Claude session through the account broker. "
|
|
35
29
|
"Run: python \"$HOME/.claude/scripts/account_broker.py\" run --product claude "
|
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
"""Shared fail-safe logger for hook block events.
|
|
2
2
|
|
|
3
3
|
Every blocking hook calls log_hook_block at the moment it decides to block,
|
|
4
|
-
so the user has a single log showing what tripped and why.
|
|
4
|
+
so the user has a single log showing what tripped and why. A GitHub token or
|
|
5
|
+
an Authorization header value in the preview is replaced before it is written.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
import datetime
|
|
8
9
|
import json
|
|
10
|
+
import re
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
11
13
|
_HOOK_BLOCKS_LOG_RELATIVE_PATH = ".claude/logs/hook-blocks.log"
|
|
12
14
|
_MAX_PREVIEW_LENGTH = 500
|
|
15
|
+
_REDACTION_MARKER = "[REDACTED]"
|
|
16
|
+
_GITHUB_TOKEN_PATTERN = re.compile(r"\b(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]+")
|
|
17
|
+
_AUTHORIZATION_VALUE_PATTERN = re.compile(
|
|
18
|
+
r"(?P<header>authorization['\"]?\s*:\s*['\"]?)[^'\"\r\n]+", re.IGNORECASE
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _preview_without_secrets(offending_input_preview: str) -> str:
|
|
23
|
+
"""Return the preview with GitHub tokens and Authorization header values replaced."""
|
|
24
|
+
without_tokens = _GITHUB_TOKEN_PATTERN.sub(_REDACTION_MARKER, offending_input_preview)
|
|
25
|
+
return _AUTHORIZATION_VALUE_PATTERN.sub(
|
|
26
|
+
lambda each_match: each_match.group("header") + _REDACTION_MARKER, without_tokens
|
|
27
|
+
)
|
|
13
28
|
|
|
14
29
|
|
|
15
30
|
def log_hook_block(
|
|
@@ -31,7 +46,8 @@ def log_hook_block(
|
|
|
31
46
|
block_reason: The human-readable reason the hook is blocking.
|
|
32
47
|
tool_name: The Claude tool name when available, e.g. ``Bash``.
|
|
33
48
|
offending_input_preview: A short excerpt of the input that triggered
|
|
34
|
-
the block; truncated to 500
|
|
49
|
+
the block; secrets are replaced, then it is truncated to 500
|
|
50
|
+
characters before writing.
|
|
35
51
|
"""
|
|
36
52
|
try:
|
|
37
53
|
home_directory = Path.home()
|
|
@@ -51,7 +67,9 @@ def log_hook_block(
|
|
|
51
67
|
if tool_name is not None:
|
|
52
68
|
log_record["tool"] = tool_name
|
|
53
69
|
if offending_input_preview is not None:
|
|
54
|
-
log_record["preview"] = offending_input_preview[
|
|
70
|
+
log_record["preview"] = _preview_without_secrets(offending_input_preview)[
|
|
71
|
+
:_MAX_PREVIEW_LENGTH
|
|
72
|
+
]
|
|
55
73
|
|
|
56
74
|
with log_path.open("a", encoding="utf-8") as log_file:
|
|
57
75
|
log_file.write(json.dumps(log_record) + "\n")
|
|
@@ -25,6 +25,10 @@ PR_NUMBER_PATTERN = re.compile(r"(?:^|\s)#?(?P<number>\d+)(?:\s|$)")
|
|
|
25
25
|
PR_URL_PATTERN = re.compile(
|
|
26
26
|
r"github\.com/(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)/pull/(?P<number>\d+)"
|
|
27
27
|
)
|
|
28
|
+
RULINGS_LINK_PATTERN = re.compile(
|
|
29
|
+
r"https://github\.com/(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)/pull/(?P<number>\d+)"
|
|
30
|
+
r"#issuecomment-\d+"
|
|
31
|
+
)
|
|
28
32
|
REPO_OPTION_PATTERN = re.compile(r"(?:-R|--repo)[\s=](?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)")
|
|
29
33
|
MATCH_HEAD_PATTERN = re.compile(r"--match-head-commit[\s=](?P<sha>[0-9a-fA-F]{7,40})")
|
|
30
34
|
API_SHA_FIELD_PATTERN = re.compile(r"(?:-f|-F|--field|--raw-field)[\s=]sha=(?P<sha>[0-9a-fA-F]{7,40})")
|
|
@@ -43,15 +47,21 @@ RECEIPT_FILE_NAME_TEMPLATE = "{owner}__{repo}__{number}.json"
|
|
|
43
47
|
REPOSITORY_SEPARATOR = ","
|
|
44
48
|
RECEIPT_HEAD_KEY = "head"
|
|
45
49
|
RECEIPT_FINDINGS_KEY = "findings"
|
|
50
|
+
RECEIPT_RULED_OUT_KEY = "ruled_out"
|
|
51
|
+
RECEIPT_RULINGS_KEY = "rulings"
|
|
46
52
|
RECORD_COMMAND = "python3 ~/.claude/scripts/merge_review_receipt.py"
|
|
47
53
|
|
|
48
54
|
NO_CLEAN_REVIEW_REASON_TEMPLATE = (
|
|
49
55
|
"{slug}#{number} has no clean review on the head you are merging. Run the review "
|
|
50
56
|
"loop: review the pull request head with `{review_command}`, fix and push every "
|
|
51
57
|
"finding, and review again "
|
|
52
|
-
"until a pass reports zero findings
|
|
58
|
+
"until a pass reports zero findings or every finding is ruled out with file:line "
|
|
59
|
+
"evidence. Record that pass with `"
|
|
53
60
|
+ RECORD_COMMAND
|
|
54
|
-
+ " {slug} {number} <head sha>
|
|
61
|
+
+ " {slug} {number} <head sha> <finding count>`, adding `--ruled-out <finding count> "
|
|
62
|
+
"--rulings <link to the rulings comment on this pull request>` when every finding is "
|
|
63
|
+
"ruled out, then merge "
|
|
64
|
+
"pinned to that head."
|
|
55
65
|
)
|
|
56
66
|
UNPINNED_MERGE_REASON_TEMPLATE = (
|
|
57
67
|
"{slug}#{number} has a clean review for {head}, but this merge names no head. "
|
|
@@ -60,7 +70,8 @@ UNPINNED_MERGE_REASON_TEMPLATE = (
|
|
|
60
70
|
)
|
|
61
71
|
STALE_REVIEW_REASON_TEMPLATE = (
|
|
62
72
|
"{slug}#{number} was reviewed clean at {head}, and this merge names {pinned}. "
|
|
63
|
-
"Review the new head until a pass reports zero findings
|
|
73
|
+
"Review the new head until a pass reports zero findings or every finding is ruled "
|
|
74
|
+
"out, record it, then merge."
|
|
64
75
|
)
|
|
65
76
|
AUTO_MERGE_REASON_TEMPLATE = (
|
|
66
77
|
"Auto-merge on {slug}#{number} names no head, so it could merge a head no review "
|
|
@@ -13,10 +13,9 @@ import json
|
|
|
13
13
|
import os
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
|
+
from hooks_constants.cloud_session import is_cloud_session
|
|
16
17
|
from hooks_constants.mod_handoff_constants import (
|
|
17
18
|
ALL_SETTINGS_FILE_NAMES_IN_PRECEDENCE_ORDER,
|
|
18
|
-
CLOUD_SESSION_ENV_TRUE_VALUE,
|
|
19
|
-
CLOUD_SESSION_ENV_VAR,
|
|
20
19
|
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
21
20
|
CLAUDE_PROJECT_DIR_ENV_VAR,
|
|
22
21
|
DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME,
|
|
@@ -81,7 +80,7 @@ def is_mod_plugin_enabled(plugin_name: str) -> bool:
|
|
|
81
80
|
Args:
|
|
82
81
|
plugin_name: The plugin that holds the mod replacing the calling hook.
|
|
83
82
|
"""
|
|
84
|
-
if
|
|
83
|
+
if is_cloud_session():
|
|
85
84
|
return False
|
|
86
85
|
is_enabled = False
|
|
87
86
|
for each_path in all_settings_paths_in_precedence_order():
|
|
@@ -8,8 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"ALL_SETTINGS_FILE_NAMES_IN_PRECEDENCE_ORDER",
|
|
11
|
-
"CLOUD_SESSION_ENV_TRUE_VALUE",
|
|
12
|
-
"CLOUD_SESSION_ENV_VAR",
|
|
13
11
|
"CLAUDE_CONFIG_DIR_ENV_VAR",
|
|
14
12
|
"CLAUDE_PROJECT_DIR_ENV_VAR",
|
|
15
13
|
"DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME",
|
|
@@ -22,8 +20,6 @@ __all__ = [
|
|
|
22
20
|
]
|
|
23
21
|
|
|
24
22
|
ENABLED_PLUGINS_SETTINGS_KEY = "enabledPlugins"
|
|
25
|
-
CLOUD_SESSION_ENV_VAR = "CLAUDE_CODE_REMOTE"
|
|
26
|
-
CLOUD_SESSION_ENV_TRUE_VALUE = "true"
|
|
27
23
|
PLUGIN_MARKETPLACE_SEPARATOR = "@"
|
|
28
24
|
SHELL_GUARDS_PLUGIN_NAME = "shell-guards"
|
|
29
25
|
SESSION_PROMPTS_PLUGIN_NAME = "session-prompts"
|
|
@@ -19,8 +19,9 @@ that consumes these program, wrapper, and option constants lives in
|
|
|
19
19
|
``pytest_invocation``.
|
|
20
20
|
|
|
21
21
|
Two wrapper shapes reach the program behind them differently. ``sudo``,
|
|
22
|
-
``uvx``, ``xargs``, ``env``, and ``
|
|
23
|
-
command, and
|
|
22
|
+
``uvx``, ``xargs``, ``env``, ``timeout``, ``command``, and ``exec`` take their own
|
|
23
|
+
options and then the command, and a shell keyword such as ``if``, ``then``,
|
|
24
|
+
``do``, or ``!`` runs the command after it, and ``ALL_WRAPPER_OPTION_GRAMMARS_BY_NAME`` names each one's
|
|
24
25
|
value-taking options, its flag-only options, and the operands it reads before
|
|
25
26
|
the command, so ``xargs -I {} rm {}`` and ``timeout --signal KILL 5 git push``
|
|
26
27
|
both reach the program. ``uv``, ``poetry``, ``pipenv``, ``pdm``, ``hatch``, ``rye``, and
|
|
@@ -149,6 +150,8 @@ __all__ = [
|
|
|
149
150
|
"WrapperOptionGrammar",
|
|
150
151
|
"WINDOWS_EXECUTABLE_SUFFIX",
|
|
151
152
|
"RUN_SUBCOMMAND_OPTION_GRAMMAR",
|
|
153
|
+
"SHELL_KEYWORD_OPTION_GRAMMAR",
|
|
154
|
+
"ALL_SHELL_KEYWORDS_BEFORE_A_COMMAND",
|
|
152
155
|
"ALL_WRAPPER_OPTION_GRAMMARS_BY_NAME",
|
|
153
156
|
"ALL_RUN_SUBCOMMAND_WRAPPER_COMMANDS",
|
|
154
157
|
"RUN_SUBCOMMAND_NAME",
|
|
@@ -312,6 +315,14 @@ RUN_SUBCOMMAND_OPTION_GRAMMAR = WrapperOptionGrammar(
|
|
|
312
315
|
all_flag_options=frozenset(),
|
|
313
316
|
leading_operand_count=0,
|
|
314
317
|
)
|
|
318
|
+
SHELL_KEYWORD_OPTION_GRAMMAR = WrapperOptionGrammar(
|
|
319
|
+
all_value_taking_options=frozenset(),
|
|
320
|
+
all_flag_options=frozenset(),
|
|
321
|
+
leading_operand_count=0,
|
|
322
|
+
)
|
|
323
|
+
ALL_SHELL_KEYWORDS_BEFORE_A_COMMAND: frozenset[str] = frozenset(
|
|
324
|
+
{"if", "then", "elif", "else", "while", "until", "do", "!", "{"}
|
|
325
|
+
)
|
|
315
326
|
ALL_WRAPPER_OPTION_GRAMMARS_BY_NAME: dict[str, WrapperOptionGrammar] = {
|
|
316
327
|
"sudo": WrapperOptionGrammar(
|
|
317
328
|
all_value_taking_options=frozenset(
|
|
@@ -365,6 +376,20 @@ ALL_WRAPPER_OPTION_GRAMMARS_BY_NAME: dict[str, WrapperOptionGrammar] = {
|
|
|
365
376
|
all_flag_options=frozenset({"-v", "--verbose", "--preserve-status", "--foreground"}),
|
|
366
377
|
leading_operand_count=1,
|
|
367
378
|
),
|
|
379
|
+
"command": WrapperOptionGrammar(
|
|
380
|
+
all_value_taking_options=frozenset(),
|
|
381
|
+
all_flag_options=frozenset({"-p"}),
|
|
382
|
+
leading_operand_count=0,
|
|
383
|
+
),
|
|
384
|
+
"exec": WrapperOptionGrammar(
|
|
385
|
+
all_value_taking_options=frozenset({"-a"}),
|
|
386
|
+
all_flag_options=frozenset({"-c", "-l"}),
|
|
387
|
+
leading_operand_count=0,
|
|
388
|
+
),
|
|
389
|
+
**{
|
|
390
|
+
each_keyword: SHELL_KEYWORD_OPTION_GRAMMAR
|
|
391
|
+
for each_keyword in ALL_SHELL_KEYWORDS_BEFORE_A_COMMAND
|
|
392
|
+
},
|
|
368
393
|
}
|
|
369
394
|
ALL_RUN_SUBCOMMAND_WRAPPER_COMMANDS: frozenset[str] = frozenset(
|
|
370
395
|
{
|
|
@@ -25,7 +25,6 @@ COMMAND_SEPARATORS = ";|&\n()`"
|
|
|
25
25
|
COMMAND_WHITESPACE = " \t\r"
|
|
26
26
|
OPTION_AND_VALUE_WORD_COUNT = 2
|
|
27
27
|
GIT_GLOBAL_OPTIONS_WITH_VALUE = frozenset({"-C", "-c", "--git-dir", "--work-tree", "--namespace", "--config-env"})
|
|
28
|
-
GH_OPTIONS_WITH_VALUE = frozenset({"-R", "--repo", "--hostname", "--config", "-H", "--header", "-X", "--method", "-F", "--field", "-f", "--raw-field", "--input", "--jq", "-q", "--template", "-t", "--cache"})
|
|
29
28
|
GIT_ACTIONS = frozenset({"commit", "push"})
|
|
30
29
|
GH_PULL_REQUEST_COMMAND = "pr"
|
|
31
30
|
GH_API_COMMAND = "api"
|
|
@@ -87,4 +87,8 @@ ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
|
|
|
87
87
|
script_relative_path="blocking/context_budget_blocker.py",
|
|
88
88
|
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_APPLY_PATCH_TOOL_NAMES,
|
|
89
89
|
),
|
|
90
|
+
HostedHookEntry(
|
|
91
|
+
script_relative_path="blocking/pull_request_proof_edit_gate.py",
|
|
92
|
+
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_APPLY_PATCH_TOOL_NAMES,
|
|
93
|
+
),
|
|
90
94
|
)
|
|
@@ -59,9 +59,10 @@ MISSING_LOOK_REASON = (
|
|
|
59
59
|
CHANGED_FILES_SEPARATOR = ", "
|
|
60
60
|
UNREADABLE_CHANGES_REASON = (
|
|
61
61
|
"Git in this directory cannot list the files this pull request changes, so the"
|
|
62
|
-
" picture check cannot tell whether people see the change.
|
|
63
|
-
"
|
|
64
|
-
"
|
|
62
|
+
" picture check cannot tell whether people see the change. Put a screenshot or"
|
|
63
|
+
" Artifact link in the 'Proof in practice' section and run the create call again,"
|
|
64
|
+
" or run it from a checkout of the head branch with origin fetched. Take one of"
|
|
65
|
+
" these routes now, before any change to this gate. Read " + PROOF_GUIDE_PATH + " for the full rule."
|
|
65
66
|
)
|
|
66
67
|
FEATURE_TITLE_PATTERN = r"^\s*feat(\([^)]*\))?!?:"
|
|
67
68
|
EVAL_HEADING_PATTERN = r"^(#{1,6})[ \t]+evals?[ \t]*#*[ \t]*$"
|
|
@@ -83,3 +84,21 @@ MISSING_BUILD_EVAL_REASON = (
|
|
|
83
84
|
+ EVAL_GUIDE_PATH
|
|
84
85
|
+ " for the full rule."
|
|
85
86
|
)
|
|
87
|
+
ALL_PROOF_GATE_DENY_MARKERS = (PROOF_GUIDE_PATH, EXISTING_WORK_GUIDE_PATH, EVAL_GUIDE_PATH)
|
|
88
|
+
ALL_PROOF_GATE_FILE_NAMES = frozenset(
|
|
89
|
+
{
|
|
90
|
+
"pull_request_proof.py",
|
|
91
|
+
"pull_request_proof_constants.py",
|
|
92
|
+
"pull_request_proof_edit_gate.py",
|
|
93
|
+
"pre_tool_use_dispatcher_constants.py",
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
GATE_EDIT_BEFORE_ROUTE_REASON = (
|
|
97
|
+
"The pull request proof gate denied a create call in this session, and its deny"
|
|
98
|
+
" named the way through. Take that route now. Add what the deny asked for, such as"
|
|
99
|
+
" a screenshot or Artifact link in the 'Proof in practice' section, then run the"
|
|
100
|
+
" create call again. The deny text is the instruction. Edit this gate only"
|
|
101
|
+
" after a create call passes it. Read " + PROOF_GUIDE_PATH + " for the full rule."
|
|
102
|
+
)
|
|
103
|
+
WINDOWS_PATH_SEPARATOR = "\\"
|
|
104
|
+
POSIX_PATH_SEPARATOR = "/"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Names the programs that run a command held in their arguments rather than as a shell wrapper.
|
|
2
|
+
|
|
3
|
+
``eval`` and PowerShell's ``Invoke-Expression`` run their joined arguments,
|
|
4
|
+
``find -exec`` runs the tokens up to its terminator, and PowerShell's
|
|
5
|
+
``-EncodedCommand`` runs a base64 string of UTF-16LE text. PowerShell accepts
|
|
6
|
+
any prefix of ``-EncodedCommand`` from ``-e`` on, plus the ``-ec`` alias.
|
|
7
|
+
``Start-Process`` runs its file path with its argument list, which may be
|
|
8
|
+
comma-separated, and a parameter may carry its value after a colon, as in
|
|
9
|
+
``-ArgumentList:'pr list'``; the values of its other parameters are not part
|
|
10
|
+
of the command. Unwrapping stops past ``MAXIMUM_UNWRAP_DEPTH`` nested levels, so a
|
|
11
|
+
deeply nested command cannot exhaust the parser.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ALL_STRING_EVALUATING_PROGRAM_NAMES",
|
|
18
|
+
"FIND_PROGRAM_NAME",
|
|
19
|
+
"ALL_FIND_EXEC_ACTIONS",
|
|
20
|
+
"ALL_FIND_EXEC_TERMINATORS",
|
|
21
|
+
"FIND_EXEC_COMMAND_SEPARATOR",
|
|
22
|
+
"ALL_POWERSHELL_PROGRAM_NAMES",
|
|
23
|
+
"ENCODED_COMMAND_OPTION",
|
|
24
|
+
"ENCODED_COMMAND_SHORT_ALIAS",
|
|
25
|
+
"MINIMUM_ENCODED_COMMAND_PREFIX_LENGTH",
|
|
26
|
+
"ENCODED_COMMAND_TEXT_ENCODING",
|
|
27
|
+
"ALL_START_PROCESS_PROGRAM_NAMES",
|
|
28
|
+
"ALL_START_PROCESS_NON_COMMAND_PARAMETERS",
|
|
29
|
+
"START_PROCESS_PARAMETER_PREFIX",
|
|
30
|
+
"START_PROCESS_ARGUMENT_SEPARATOR",
|
|
31
|
+
"START_PROCESS_PARAMETER_VALUE_SEPARATOR",
|
|
32
|
+
"MAXIMUM_UNWRAP_DEPTH",
|
|
33
|
+
"ALL_LINE_BREAK_CHARACTERS",
|
|
34
|
+
"QUOTED_LINE_BREAK_REPLACEMENT",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
ALL_STRING_EVALUATING_PROGRAM_NAMES: frozenset[str] = frozenset(
|
|
38
|
+
{"eval", "invoke-expression", "iex"}
|
|
39
|
+
)
|
|
40
|
+
FIND_PROGRAM_NAME: str = "find"
|
|
41
|
+
ALL_FIND_EXEC_ACTIONS: frozenset[str] = frozenset({"-exec", "-execdir", "-ok", "-okdir"})
|
|
42
|
+
ALL_FIND_EXEC_TERMINATORS: frozenset[str] = frozenset({";", "\\;", "+"})
|
|
43
|
+
FIND_EXEC_COMMAND_SEPARATOR: str = " ; "
|
|
44
|
+
ALL_POWERSHELL_PROGRAM_NAMES: frozenset[str] = frozenset({"pwsh", "powershell"})
|
|
45
|
+
ENCODED_COMMAND_OPTION: str = "-encodedcommand"
|
|
46
|
+
ENCODED_COMMAND_SHORT_ALIAS: str = "-ec"
|
|
47
|
+
MINIMUM_ENCODED_COMMAND_PREFIX_LENGTH: int = 2
|
|
48
|
+
ENCODED_COMMAND_TEXT_ENCODING: str = "utf-16-le"
|
|
49
|
+
ALL_START_PROCESS_PROGRAM_NAMES: frozenset[str] = frozenset({"start-process", "saps", "start"})
|
|
50
|
+
ALL_START_PROCESS_NON_COMMAND_PARAMETERS: frozenset[str] = frozenset(
|
|
51
|
+
{
|
|
52
|
+
"-workingdirectory",
|
|
53
|
+
"-verb",
|
|
54
|
+
"-windowstyle",
|
|
55
|
+
"-credential",
|
|
56
|
+
"-redirectstandardinput",
|
|
57
|
+
"-redirectstandardoutput",
|
|
58
|
+
"-redirectstandarderror",
|
|
59
|
+
"-environment",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
START_PROCESS_PARAMETER_PREFIX: str = "-"
|
|
63
|
+
START_PROCESS_ARGUMENT_SEPARATOR: str = ","
|
|
64
|
+
START_PROCESS_PARAMETER_VALUE_SEPARATOR: str = ":"
|
|
65
|
+
MAXIMUM_UNWRAP_DEPTH: int = 16
|
|
66
|
+
ALL_LINE_BREAK_CHARACTERS: frozenset[str] = frozenset({"\n", "\r"})
|
|
67
|
+
QUOTED_LINE_BREAK_REPLACEMENT: str = " "
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Characters the command substitution scan reads in a shell command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"COMMAND_SUBSTITUTION_OPENER",
|
|
7
|
+
"BACKTICK",
|
|
8
|
+
"SINGLE_QUOTE",
|
|
9
|
+
"DOUBLE_QUOTE",
|
|
10
|
+
"SHELL_ESCAPE_CHARACTER",
|
|
11
|
+
"OPENING_PARENTHESIS",
|
|
12
|
+
"CLOSING_PARENTHESIS",
|
|
13
|
+
"ALL_PARENTHESIS_DEPTH_CHANGES",
|
|
14
|
+
"ALL_BACKTICK_ESCAPED_CHARACTERS",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
COMMAND_SUBSTITUTION_OPENER: str = "$("
|
|
18
|
+
BACKTICK: str = "`"
|
|
19
|
+
SINGLE_QUOTE: str = "'"
|
|
20
|
+
DOUBLE_QUOTE: str = '"'
|
|
21
|
+
SHELL_ESCAPE_CHARACTER: str = "\\"
|
|
22
|
+
OPENING_PARENTHESIS: str = "("
|
|
23
|
+
CLOSING_PARENTHESIS: str = ")"
|
|
24
|
+
ALL_PARENTHESIS_DEPTH_CHANGES: dict[str, int] = {OPENING_PARENTHESIS: 1, CLOSING_PARENTHESIS: -1}
|
|
25
|
+
ALL_BACKTICK_ESCAPED_CHARACTERS: frozenset[str] = frozenset({"$", BACKTICK, SHELL_ESCAPE_CHARACTER})
|