codebuff-agent 2.1.74__py3-none-any.whl

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 (217) hide show
  1. codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  2. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
  3. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  4. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
  5. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
  6. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
  7. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
  8. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
  9. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
  10. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
  11. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  12. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  13. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  14. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
  15. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
  16. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
  17. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
  18. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
  19. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  20. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  21. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
  22. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
  23. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  24. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
  25. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
  26. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
  27. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
  28. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  29. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  30. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  31. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  32. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
  33. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
  34. codebuff_agent-2.1.74.dist-info/METADATA +142 -0
  35. codebuff_agent-2.1.74.dist-info/RECORD +217 -0
  36. codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
  37. codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
  38. codebuff_core/__init__.py +153 -0
  39. codebuff_core/codebuff_config_defaults.json +64 -0
  40. codebuff_core/core/__init__.py +35 -0
  41. codebuff_core/core/code_validator.py +1238 -0
  42. codebuff_core/core/error_classifier.py +579 -0
  43. codebuff_core/core/exceptions.py +49 -0
  44. codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
  45. codebuff_core/directory_policy.py +32 -0
  46. codebuff_core/factory.py +155 -0
  47. codebuff_core/interfaces.py +197 -0
  48. codebuff_core/knowledge/__init__.py +31 -0
  49. codebuff_core/knowledge/chunking.py +375 -0
  50. codebuff_core/knowledge/loader.py +339 -0
  51. codebuff_core/knowledge/watchdog_service.py +354 -0
  52. codebuff_core/legacy_paths.py +69 -0
  53. codebuff_core/llm/__init__.py +13 -0
  54. codebuff_core/llm/service.py +407 -0
  55. codebuff_core/managers/__init__.py +22 -0
  56. codebuff_core/managers/config_manager.py +690 -0
  57. codebuff_core/managers/remote_config.py +673 -0
  58. codebuff_core/managers/remote_skills.py +539 -0
  59. codebuff_core/managers/session_manager.py +255 -0
  60. codebuff_core/middleware/__init__.py +3 -0
  61. codebuff_core/middleware/error_recovery.py +394 -0
  62. codebuff_core/middleware/logging_utils.py +33 -0
  63. codebuff_core/models/__init__.py +111 -0
  64. codebuff_core/models/agent.py +306 -0
  65. codebuff_core/models/chat.py +57 -0
  66. codebuff_core/models/common.py +82 -0
  67. codebuff_core/models/rag.py +300 -0
  68. codebuff_core/platform_rules.py +127 -0
  69. codebuff_core/prompts/__init__.py +44 -0
  70. codebuff_core/prompts/auto_agent_prompts.py +1278 -0
  71. codebuff_core/services/__init__.py +30 -0
  72. codebuff_core/services/agent_service.py +345 -0
  73. codebuff_core/services/analytics_aggregate.py +837 -0
  74. codebuff_core/services/chat_service.py +898 -0
  75. codebuff_core/services/context_condenser.py +306 -0
  76. codebuff_core/services/pii_guardrail.py +393 -0
  77. codebuff_core/services/rag_chat_agent.py +2993 -0
  78. codebuff_core/services/rag_collections.py +2740 -0
  79. codebuff_core/services/rag_manager.py +890 -0
  80. codebuff_core/services/rag_service.py +145 -0
  81. codebuff_core/services/retriever.py +260 -0
  82. codebuff_core/services/vllm_embedding_service.py +290 -0
  83. codebuff_core/tests/__init__.py +1 -0
  84. codebuff_core/tests/conftest.py +81 -0
  85. codebuff_core/tests/test_error_recovery.py +647 -0
  86. codebuff_core/tests/test_factory.py +135 -0
  87. codebuff_core/tests/test_services.py +150 -0
  88. jupyter_ext/__init__.py +266 -0
  89. jupyter_ext/_version.py +4 -0
  90. jupyter_ext/agent/__init__.py +49 -0
  91. jupyter_ext/agent/agent_factory.py +2270 -0
  92. jupyter_ext/agent/ast_tools.py +451 -0
  93. jupyter_ext/agent/autofix_middleware.py +481 -0
  94. jupyter_ext/agent/background_procs.py +372 -0
  95. jupyter_ext/agent/code_history_middleware.py +211 -0
  96. jupyter_ext/agent/codex_autofix.py +160 -0
  97. jupyter_ext/agent/codex_bridge.py +3339 -0
  98. jupyter_ext/agent/codex_hooks.py +320 -0
  99. jupyter_ext/agent/codex_slash.py +685 -0
  100. jupyter_ext/agent/codex_threads.py +114 -0
  101. jupyter_ext/agent/content_injection.py +377 -0
  102. jupyter_ext/agent/continuation_middleware.py +548 -0
  103. jupyter_ext/agent/custom_skills.py +335 -0
  104. jupyter_ext/agent/file_diffs.py +99 -0
  105. jupyter_ext/agent/git_history.py +188 -0
  106. jupyter_ext/agent/guardrail_middleware.py +421 -0
  107. jupyter_ext/agent/json_repair_utils.py +54 -0
  108. jupyter_ext/agent/langchain_tools.py +2673 -0
  109. jupyter_ext/agent/mermaid_fixer.py +289 -0
  110. jupyter_ext/agent/mermaid_middleware.py +135 -0
  111. jupyter_ext/agent/middleware.py +359 -0
  112. jupyter_ext/agent/middleware_prompts.py +53 -0
  113. jupyter_ext/agent/observability.py +334 -0
  114. jupyter_ext/agent/patch_tool_calls.py +57 -0
  115. jupyter_ext/agent/prompts.py +112 -0
  116. jupyter_ext/agent/skill_middleware.py +257 -0
  117. jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
  118. jupyter_ext/agent/skills/anomaly_detection.md +12 -0
  119. jupyter_ext/agent/skills/athena_sql.md +156 -0
  120. jupyter_ext/agent/skills/bdp_table_search.md +33 -0
  121. jupyter_ext/agent/skills/bigquery.md +65 -0
  122. jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
  123. jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
  124. jupyter_ext/agent/skills/classification_evaluation.md +11 -0
  125. jupyter_ext/agent/skills/clustering_analysis.md +12 -0
  126. jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
  127. jupyter_ext/agent/skills/data_analysis.md +236 -0
  128. jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
  129. jupyter_ext/agent/skills/data_loading.md +158 -0
  130. jupyter_ext/agent/skills/data_quality_report.md +13 -0
  131. jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
  132. jupyter_ext/agent/skills/emr_script_development.md +17 -0
  133. jupyter_ext/agent/skills/experiment_tracking.md +13 -0
  134. jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
  135. jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
  136. jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
  137. jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
  138. jupyter_ext/agent/skills/inference.md +392 -0
  139. jupyter_ext/agent/skills/lambda_function_development.md +19 -0
  140. jupyter_ext/agent/skills/model_comparison.md +11 -0
  141. jupyter_ext/agent/skills/model_explainability.md +12 -0
  142. jupyter_ext/agent/skills/model_training.md +318 -0
  143. jupyter_ext/agent/skills/pyspark.md +352 -0
  144. jupyter_ext/agent/skills/python_code_quality.md +35 -0
  145. jupyter_ext/agent/skills/regression_evaluation.md +12 -0
  146. jupyter_ext/agent/skills/spring_java_development.md +35 -0
  147. jupyter_ext/agent/skills/text_preprocessing.md +12 -0
  148. jupyter_ext/agent/skills/time_series_analysis.md +13 -0
  149. jupyter_ext/agent/skills/unit_testing.md +203 -0
  150. jupyter_ext/agent/todo_prompts.py +57 -0
  151. jupyter_ext/agent/tool_report.py +45 -0
  152. jupyter_ext/analytics_ingest.py +599 -0
  153. jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  154. jupyter_ext/handlers/__init__.py +8 -0
  155. jupyter_ext/handlers/analytics_handler.py +170 -0
  156. jupyter_ext/handlers/benchmark_handler.py +794 -0
  157. jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
  158. jupyter_ext/handlers/rag_admin_handler.py +689 -0
  159. jupyter_ext/handlers/websocket.py +497 -0
  160. jupyter_ext/http_handlers.py +8744 -0
  161. jupyter_ext/idle_monitor.py +346 -0
  162. jupyter_ext/labextension/package.json +157 -0
  163. jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  164. jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
  165. jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
  166. jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
  167. jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
  168. jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
  169. jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
  170. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
  171. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  172. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  173. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  174. jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
  175. jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
  176. jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
  177. jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
  178. jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
  179. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  180. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  181. jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
  182. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
  183. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  184. jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
  185. jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
  186. jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
  187. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
  188. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  189. jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  190. jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  191. jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  192. jupyter_ext/labextension/static/style.js +4 -0
  193. jupyter_ext/labextension/static/third-party-licenses.json +724 -0
  194. jupyter_ext/log_paths.py +113 -0
  195. jupyter_ext/resource_usage.py +519 -0
  196. jupyter_ext/secrets_bootstrap.py +116 -0
  197. jupyter_ext/team_access.py +105 -0
  198. jupyter_ext/tests/__init__.py +1 -0
  199. jupyter_ext/tests/test_context.py +234 -0
  200. jupyter_ext/tests/test_factory.py +563 -0
  201. jupyter_ext/tests/test_file_finder.py +139 -0
  202. jupyter_ext/tests/test_handlers.py +58 -0
  203. jupyter_ext/tests/test_mcp_selftest.py +204 -0
  204. jupyter_ext/tests/test_mermaid_fixer.py +272 -0
  205. jupyter_ext/tests/test_resource_tiers.py +298 -0
  206. jupyter_ext/tools/__init__.py +15 -0
  207. jupyter_ext/tools/base.py +90 -0
  208. jupyter_ext/tools/context.py +268 -0
  209. jupyter_ext/tools/dir_complete.py +86 -0
  210. jupyter_ext/tools/file_finder.py +303 -0
  211. jupyter_ext/tools/file_tools.py +244 -0
  212. jupyter_ext/tools/jupyter_tools.py +296 -0
  213. jupyter_ext/tools/registry.py +159 -0
  214. jupyter_ext/tools/search_tools.py +230 -0
  215. jupyter_ext/tools/security_tools.py +657 -0
  216. jupyter_ext/tools/shell_tools.py +109 -0
  217. jupyter_ext/tools/user_tools.py +132 -0
@@ -0,0 +1,320 @@
1
+ """codex hook 을 pod 가 대신 돌린다 (이슈 #145).
2
+
3
+ **codex 네이티브 hook 은 쓸 수 없다.** hook 명령은 codex 프로세스가 도는 기계에서
4
+ 세션 cwd 로 실행되는데, codex 는 centri 에서 돌고 사용자 파일은 pod 에 있다. 그래서
5
+ pod 가 codex 와 같은 규격으로 hook 을 찾고 돌린다 — 물리 위치만 다르고 메커니즘은
6
+ 같다. `custom_skills` (#134) 가 스킬에 대해 한 것과 같은 방식이다.
7
+ 규격: https://learn.chatgpt.com/docs/hooks
8
+
9
+ | 층 | 위치 | 모드 |
10
+ |---|---|---|
11
+ | `builtin` | 이 모듈의 `BUILTIN_HOOKS` — ruff/biome autofix | 두 모드 다 |
12
+ | `repo` | 워크스페이스부터 git 루트까지 `.codex/hooks.json` | `bypass` 에서만 |
13
+ | `user` | `$CODEX_HOME/hooks.json` (없으면 `~/.codex/hooks.json`) | 두 모드 다 |
14
+
15
+ - codex 의 hook 별 trust 검토는 대행하지 않고 모드로 대신한다 — pod 에는 검토 화면이
16
+ 없다. repo 층을 `ask` 에서 안 돌리는 이유는, 클론한 저장소의 hooks.json 이 승인받은
17
+ 편집 한 번에 임의 명령을 승인 없이 돌리면 `ask` 의 승인 계약이 무너져서다.
18
+ - 이벤트는 `PostToolUse` 하나다. config.toml 의 `[hooks]`·requirements·플러그인 층과
19
+ `async` 는 대행하지 않는다 (`async` hook 은 동기로 돈다).
20
+ - 도구 이름은 codex 이름으로 바꿔 matcher 에 댄다 — 파일 편집은 `apply_patch`
21
+ (별칭 `Edit`·`Write`), 셸은 `Bash`. 그래야 CLI 용으로 쓴 hooks.json 이 그대로 돈다.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ import json
28
+ import logging
29
+ import os
30
+ import re
31
+ import sys
32
+ from dataclasses import dataclass, field
33
+ from pathlib import Path
34
+ from typing import Any, Dict, List, Optional
35
+
36
+ from .custom_skills import _repo_dirs
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ EVENT = "PostToolUse"
41
+ #: codex 의 기본값이다 (SessionEnd·Interrupt 만 1초)
42
+ DEFAULT_TIMEOUT = 600
43
+ #: 내장 autofix 전체 한도 — ruff check·format 두 단계(`codex_autofix.STEP_TIMEOUT`)가 들어간다
44
+ BUILTIN_TIMEOUT = 60
45
+
46
+ _CODEX_TOOL_NAMES = {
47
+ "write_file": "apply_patch",
48
+ "edit_file": "apply_patch",
49
+ "execute_command": "Bash",
50
+ }
51
+ _ALIASES = {"apply_patch": ("Edit", "Write")}
52
+ _PERMISSION_MODES = {"bypass": "bypassPermissions", "ask": "default"}
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class Hook:
57
+ matcher: Optional[str]
58
+ command: str
59
+ timeout: float
60
+ status_message: str
61
+ scope: str
62
+ source: str
63
+
64
+
65
+ BUILTIN_HOOKS = (
66
+ Hook(
67
+ matcher="apply_patch",
68
+ # `-I`: hook 은 워크스페이스를 cwd 로 돈다. `-m` 은 cwd 를 sys.path 앞에 두므로
69
+ # 워크스페이스의 `json.py`·`jupyter_ext/` 가 표준 라이브러리·패키지를 가린다
70
+ command=f'"{sys.executable}" -I -m jupyter_ext.agent.codex_autofix',
71
+ timeout=BUILTIN_TIMEOUT,
72
+ status_message="",
73
+ scope="builtin",
74
+ source="ruff/biome autofix",
75
+ ),
76
+ )
77
+
78
+
79
+ @dataclass
80
+ class Outcome:
81
+ """hook 들의 결과를 모은 것. 브리지가 도구 결과와 패널에 반영한다."""
82
+
83
+ context: List[str] = field(default_factory=list)
84
+ block_reason: Optional[str] = None
85
+ #: 실패·시간 초과 — 매번 띄운다. 첫 번째만 보이면 두 번째 멈춤이 무음이 된다
86
+ messages: List[str] = field(default_factory=list)
87
+ #: `systemMessage` — 같은 문장은 세션당 한 번만 띄운다 (도구 없음 알림 등)
88
+ notices: List[str] = field(default_factory=list)
89
+
90
+
91
+ def codex_tool_name(tool: str) -> str:
92
+ return _CODEX_TOOL_NAMES.get(tool, tool)
93
+
94
+
95
+ def matches(matcher: Optional[str], tool_name: str) -> bool:
96
+ """codex 처럼 matcher 를 정규식으로 본다. `*`·빈 값·생략은 전체다."""
97
+ if not matcher or matcher == "*":
98
+ return True
99
+ names = (tool_name, *_ALIASES.get(tool_name, ()))
100
+ try:
101
+ return any(re.fullmatch(matcher, n) for n in names)
102
+ except re.error:
103
+ return False
104
+
105
+
106
+ def _user_hooks_file() -> Path:
107
+ home = Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex")
108
+ return home / "hooks.json"
109
+
110
+
111
+ def hook_files(workspace_root: str) -> List[tuple]:
112
+ """(경로, 층) — 실제로 있는 hooks.json 만.
113
+
114
+ 워크스페이스가 홈이면 `~/.codex/hooks.json` 이 repo 층에도 잡힌다. 같은 파일은
115
+ 한 번만 싣고 user 층으로 본다 — 두 번 돌거나 ask 모드에서 거짓 "건너뜀" 이 뜨면 안 된다.
116
+ """
117
+ user = _user_hooks_file()
118
+ found = [(d / ".codex" / "hooks.json", "repo") for d in _repo_dirs(workspace_root)]
119
+ found.append((user, "user"))
120
+ out: List[tuple] = []
121
+ seen = set()
122
+ for path, scope in found:
123
+ # 권한 없는 상위 디렉터리는 `is_file()` 도 예외를 던진다 (custom_skills 와 같다)
124
+ try:
125
+ if not path.is_file():
126
+ continue
127
+ key = path.resolve()
128
+ if scope == "repo" and user.is_file() and key == user.resolve():
129
+ continue
130
+ except OSError as e:
131
+ logger.warning("hooks.json 을 확인하지 못함 (%s): %s", path, e)
132
+ continue
133
+ if key not in seen:
134
+ seen.add(key)
135
+ out.append((path, scope))
136
+ return out
137
+
138
+
139
+ def _parse(path: Path, scope: str, errors: List[str]) -> List[Hook]:
140
+ """hook 하나가 규격 밖이면 그 hook 만 빼고 알린다 — 파일째 버리지 않는다."""
141
+ data = json.loads(path.read_text(encoding="utf-8"))
142
+ hooks: List[Hook] = []
143
+ for group in (data.get("hooks") or {}).get(EVENT) or []:
144
+ matcher = group.get("matcher")
145
+ for h in group.get("hooks") or []:
146
+ if h.get("type", "command") != "command" or not h.get("command"):
147
+ continue
148
+ try:
149
+ if matcher is not None and not isinstance(matcher, str):
150
+ raise ValueError(f"matcher 는 정규식 문자열이다: {matcher!r}")
151
+ timeout = float(h.get("timeout") or DEFAULT_TIMEOUT)
152
+ except (TypeError, ValueError) as e:
153
+ errors.append(f"hooks.json 의 hook 하나를 뺐습니다: {path} ({e})")
154
+ continue
155
+ hooks.append(
156
+ Hook(
157
+ matcher=matcher,
158
+ command=str(h["command"]),
159
+ timeout=timeout,
160
+ status_message=str(h.get("statusMessage") or ""),
161
+ scope=scope,
162
+ source=str(path),
163
+ )
164
+ )
165
+ return hooks
166
+
167
+
168
+ def discover(workspace_root: str, errors: Optional[List[str]] = None) -> List[Hook]:
169
+ """내장 → repo → user 순. 여러 층을 모두 싣는다 (codex 도 덮어쓰지 않는다)."""
170
+ if errors is None:
171
+ errors = []
172
+ hooks: List[Hook] = list(BUILTIN_HOOKS)
173
+ for path, scope in hook_files(workspace_root):
174
+ try:
175
+ hooks.extend(_parse(path, scope, errors))
176
+ except (OSError, ValueError, AttributeError, TypeError) as e:
177
+ logger.warning("hooks.json 을 읽지 못함 (%s): %s", path, e)
178
+ errors.append(f"hooks.json 을 읽지 못했습니다: {path} ({e})")
179
+ return hooks
180
+
181
+
182
+ def patch_envelope(path: str, is_new: bool) -> str:
183
+ """codex `apply_patch` 의 `tool_input.command` 꼴. CLI 용 hook 이 경로를 여기서 읽는다."""
184
+ head = "Add File" if is_new else "Update File"
185
+ return f"*** Begin Patch\n*** {head}: {path}\n*** End Patch\n"
186
+
187
+
188
+ def build_payload(
189
+ *,
190
+ tool: str,
191
+ args: Dict[str, Any],
192
+ is_new_file: bool,
193
+ tool_response: str,
194
+ tool_use_id: str,
195
+ cwd: str,
196
+ session_id: str,
197
+ turn_id: str,
198
+ model: str,
199
+ permission_mode: str,
200
+ ) -> Dict[str, Any]:
201
+ name = codex_tool_name(tool)
202
+ if name == "apply_patch":
203
+ path = str(args.get("path") or "")
204
+ tool_input: Dict[str, Any] = {
205
+ "command": patch_envelope(path, is_new_file),
206
+ "path": path,
207
+ "codebuff_tool": tool,
208
+ }
209
+ else:
210
+ tool_input = dict(args)
211
+ return {
212
+ "session_id": session_id,
213
+ "turn_id": turn_id,
214
+ "transcript_path": None,
215
+ "cwd": cwd,
216
+ "hook_event_name": EVENT,
217
+ "model": model,
218
+ "permission_mode": _PERMISSION_MODES.get(permission_mode, "default"),
219
+ "tool_name": name,
220
+ "tool_input": tool_input,
221
+ "tool_response": tool_response,
222
+ "tool_use_id": tool_use_id,
223
+ }
224
+
225
+
226
+ @dataclass
227
+ class Run:
228
+ returncode: Optional[int]
229
+ stdout: str
230
+ stderr: str
231
+ timed_out: bool = False
232
+
233
+
234
+ async def run_hook(
235
+ hook: Hook, payload: Dict[str, Any], cwd: str, env: Dict[str, str]
236
+ ) -> Run:
237
+ """hook 명령을 셸로 돌린다. 시간이 넘거나 취소되면 프로세스 그룹째 끝낸다."""
238
+ proc = await asyncio.create_subprocess_shell(
239
+ hook.command,
240
+ cwd=cwd,
241
+ env=env,
242
+ stdin=asyncio.subprocess.PIPE,
243
+ stdout=asyncio.subprocess.PIPE,
244
+ stderr=asyncio.subprocess.PIPE,
245
+ start_new_session=True,
246
+ )
247
+ data = json.dumps(payload, ensure_ascii=False).encode()
248
+ try:
249
+ out, err = await asyncio.wait_for(proc.communicate(data), hook.timeout)
250
+ except asyncio.TimeoutError:
251
+ _kill_group(proc)
252
+ await proc.wait()
253
+ return Run(None, "", "", timed_out=True)
254
+ except BaseException:
255
+ _kill_group(proc)
256
+ raise
257
+ return Run(
258
+ proc.returncode,
259
+ out.decode("utf-8", errors="replace"),
260
+ err.decode("utf-8", errors="replace"),
261
+ )
262
+
263
+
264
+ def _kill_group(proc: Any) -> None:
265
+ try:
266
+ os.killpg(proc.pid, 9)
267
+ except (ProcessLookupError, PermissionError):
268
+ try:
269
+ proc.kill()
270
+ except ProcessLookupError:
271
+ pass
272
+
273
+
274
+ def _label(hook: Hook) -> str:
275
+ """패널에 띄우는 hook 이름. 명령줄은 싣지 않는다 — 토큰이 박혀 있을 수 있다."""
276
+ return hook.source
277
+
278
+
279
+ def interpret(hook: Hook, run: Run, outcome: Outcome) -> None:
280
+ """codex `PostToolUse` 출력 규칙대로 결과를 모은다.
281
+
282
+ - exit 0: stdout 이 JSON 이면 해석한다. 평문은 PostToolUse 에서 무시한다.
283
+ - exit 2: 차단 — stderr 가 이유다. JSON 의 `decision: "block"`·`continue: false` 도 같다.
284
+ 실행은 되돌리지 않고 결과를 이유로 바꾼다. 여럿이면 마지막 것이 남는다.
285
+ - 그 밖·시간 초과: hook 실패로 알리고 계속 간다.
286
+ """
287
+ if run.timed_out:
288
+ outcome.messages.append(f"hook 시간 초과 ({hook.timeout:g}초): {_label(hook)}")
289
+ return
290
+ if run.returncode == 2:
291
+ outcome.block_reason = (
292
+ run.stderr.strip() or f"hook 이 막았습니다: {_label(hook)}"
293
+ )
294
+ return
295
+ if run.returncode != 0:
296
+ outcome.messages.append(
297
+ f"hook 실패 (종료 코드 {run.returncode}): {_label(hook)}"
298
+ )
299
+ return
300
+ try:
301
+ out = json.loads(run.stdout) if run.stdout.strip() else {}
302
+ except ValueError:
303
+ return
304
+ if not isinstance(out, dict):
305
+ return
306
+ if out.get("systemMessage"):
307
+ outcome.notices.append(str(out["systemMessage"]))
308
+ specific = out.get("hookSpecificOutput") or {}
309
+ if isinstance(specific, dict) and specific.get("additionalContext"):
310
+ outcome.context.append(str(specific["additionalContext"]))
311
+ if out.get("decision") == "block":
312
+ outcome.block_reason = str(
313
+ out.get("reason") or f"hook 이 막았습니다: {_label(hook)}"
314
+ )
315
+ if out.get("continue") is False:
316
+ outcome.block_reason = str(
317
+ out.get("stopReason")
318
+ or out.get("reason")
319
+ or f"hook 이 멈췄습니다: {_label(hook)}"
320
+ )