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.
Files changed (66) hide show
  1. package/.agents/skills/pr-lifecycle/reference/review-loop-before-merge.md +2 -2
  2. package/bin/install.settings-defaults.test.mjs +27 -2
  3. package/bin/merge_settings_defaults.mjs +39 -21
  4. package/bin/merge_settings_defaults.test.mjs +42 -0
  5. package/docs/account-broker.md +1 -1
  6. package/docs/codex-accounts.md +6 -5
  7. package/hooks/blocking/cloud_graphql_gate.py +213 -0
  8. package/hooks/blocking/headless_claude_broker_gate.py +1 -8
  9. package/hooks/blocking/merge_review_gate.py +65 -3
  10. package/hooks/blocking/pr_lifecycle_skill_gate.py +19 -10
  11. package/hooks/blocking/pull_request_proof.py +42 -13
  12. package/hooks/blocking/pull_request_proof_edit_gate.py +180 -0
  13. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +5 -1
  14. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  15. package/hooks/blocking/test_cloud_graphql_gate.py +210 -0
  16. package/hooks/blocking/test_headless_claude_broker_gate.py +12 -7
  17. package/hooks/blocking/test_merge_review_gate.py +63 -0
  18. package/hooks/blocking/test_pr_lifecycle_skill_gate.py +11 -0
  19. package/hooks/blocking/test_pre_tool_use_dispatcher.py +1 -0
  20. package/hooks/blocking/test_pull_request_proof.py +7 -0
  21. package/hooks/blocking/test_pull_request_proof_edit_gate.py +199 -0
  22. package/hooks/conftest.py +1 -1
  23. package/hooks/features/bash-dispatchers.md +3 -0
  24. package/hooks/features/conduct-gates.md +2 -2
  25. package/hooks/features/write-edit-blocking.md +4 -1
  26. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -0
  27. package/hooks/hooks_constants/cloud_graphql_gate_constants.py +116 -0
  28. package/hooks/hooks_constants/cloud_session.py +17 -0
  29. package/hooks/hooks_constants/cloud_session_constants.py +11 -0
  30. package/hooks/hooks_constants/gh_command_words.py +52 -0
  31. package/hooks/hooks_constants/gh_command_words_constants.py +64 -0
  32. package/hooks/hooks_constants/headless_claude_broker_gate_constants.py +1 -7
  33. package/hooks/hooks_constants/hook_block_logger.py +21 -3
  34. package/hooks/hooks_constants/merge_review_gate_constants.py +14 -3
  35. package/hooks/hooks_constants/mod_handoff.py +2 -3
  36. package/hooks/hooks_constants/mod_handoff_constants.py +0 -4
  37. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +27 -2
  38. package/hooks/hooks_constants/pr_lifecycle_skill_gate_constants.py +0 -1
  39. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  40. package/hooks/hooks_constants/pull_request_proof_constants.py +22 -3
  41. package/hooks/hooks_constants/shell_command_evaluation_constants.py +67 -0
  42. package/hooks/hooks_constants/shell_command_substitution_constants.py +25 -0
  43. package/hooks/hooks_constants/shell_command_wrappers.py +327 -12
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -1
  45. package/hooks/hooks_constants/test_cloud_session.py +27 -0
  46. package/hooks/hooks_constants/test_gh_command_words.py +45 -0
  47. package/hooks/hooks_constants/test_hook_block_logger.py +61 -0
  48. package/hooks/hooks_constants/test_mod_handoff.py +3 -2
  49. package/hooks/hooks_constants/test_piped_pytest_blocker_constants.py +11 -0
  50. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +9 -6
  51. package/hooks/hooks_constants/test_shell_command_wrappers.py +125 -0
  52. package/hooks/test_conftest.py +1 -1
  53. package/package.json +1 -1
  54. package/scripts/account_broker_support.py +10 -11
  55. package/scripts/codex_account_meters.py +30 -43
  56. package/scripts/dev_env_scripts_constants/account_broker_constants.py +34 -25
  57. package/scripts/dev_env_scripts_constants/claude_account_constants.py +0 -9
  58. package/scripts/dev_env_scripts_constants/codex_account_constants.py +0 -18
  59. package/scripts/dev_env_scripts_constants/test_account_broker_constants.py +42 -0
  60. package/scripts/dev_env_scripts_constants/test_codex_account_constants.py +119 -0
  61. package/scripts/merge_review_receipt.py +56 -12
  62. package/scripts/test_account_broker.py +12 -0
  63. package/scripts/test_account_broker_support.py +32 -6
  64. package/scripts/test_codex_account_meters.py +26 -14
  65. package/scripts/test_merge_review_receipt.py +86 -0
  66. 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. It also names the environment variable a Claude Code
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 characters before writing.
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[:_MAX_PREVIEW_LENGTH]
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. Record that pass with `"
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> 0`, then merge pinned to that head."
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, record it, then merge."
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 os.environ.get(CLOUD_SESSION_ENV_VAR, "").strip().lower() == CLOUD_SESSION_ENV_TRUE_VALUE:
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 ``timeout`` take their own options and then the
23
- command, and ``ALL_WRAPPER_OPTION_GRAMMARS_BY_NAME`` names each one's
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. Run the create call"
63
- " from a checkout of the head branch with origin fetched, or put a screenshot"
64
- " link in the 'Proof in practice' section. Read " + PROOF_GUIDE_PATH + " for the full rule."
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})