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.
- codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
- codebuff_agent-2.1.74.dist-info/METADATA +142 -0
- codebuff_agent-2.1.74.dist-info/RECORD +217 -0
- codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
- codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
- codebuff_core/__init__.py +153 -0
- codebuff_core/codebuff_config_defaults.json +64 -0
- codebuff_core/core/__init__.py +35 -0
- codebuff_core/core/code_validator.py +1238 -0
- codebuff_core/core/error_classifier.py +579 -0
- codebuff_core/core/exceptions.py +49 -0
- codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
- codebuff_core/directory_policy.py +32 -0
- codebuff_core/factory.py +155 -0
- codebuff_core/interfaces.py +197 -0
- codebuff_core/knowledge/__init__.py +31 -0
- codebuff_core/knowledge/chunking.py +375 -0
- codebuff_core/knowledge/loader.py +339 -0
- codebuff_core/knowledge/watchdog_service.py +354 -0
- codebuff_core/legacy_paths.py +69 -0
- codebuff_core/llm/__init__.py +13 -0
- codebuff_core/llm/service.py +407 -0
- codebuff_core/managers/__init__.py +22 -0
- codebuff_core/managers/config_manager.py +690 -0
- codebuff_core/managers/remote_config.py +673 -0
- codebuff_core/managers/remote_skills.py +539 -0
- codebuff_core/managers/session_manager.py +255 -0
- codebuff_core/middleware/__init__.py +3 -0
- codebuff_core/middleware/error_recovery.py +394 -0
- codebuff_core/middleware/logging_utils.py +33 -0
- codebuff_core/models/__init__.py +111 -0
- codebuff_core/models/agent.py +306 -0
- codebuff_core/models/chat.py +57 -0
- codebuff_core/models/common.py +82 -0
- codebuff_core/models/rag.py +300 -0
- codebuff_core/platform_rules.py +127 -0
- codebuff_core/prompts/__init__.py +44 -0
- codebuff_core/prompts/auto_agent_prompts.py +1278 -0
- codebuff_core/services/__init__.py +30 -0
- codebuff_core/services/agent_service.py +345 -0
- codebuff_core/services/analytics_aggregate.py +837 -0
- codebuff_core/services/chat_service.py +898 -0
- codebuff_core/services/context_condenser.py +306 -0
- codebuff_core/services/pii_guardrail.py +393 -0
- codebuff_core/services/rag_chat_agent.py +2993 -0
- codebuff_core/services/rag_collections.py +2740 -0
- codebuff_core/services/rag_manager.py +890 -0
- codebuff_core/services/rag_service.py +145 -0
- codebuff_core/services/retriever.py +260 -0
- codebuff_core/services/vllm_embedding_service.py +290 -0
- codebuff_core/tests/__init__.py +1 -0
- codebuff_core/tests/conftest.py +81 -0
- codebuff_core/tests/test_error_recovery.py +647 -0
- codebuff_core/tests/test_factory.py +135 -0
- codebuff_core/tests/test_services.py +150 -0
- jupyter_ext/__init__.py +266 -0
- jupyter_ext/_version.py +4 -0
- jupyter_ext/agent/__init__.py +49 -0
- jupyter_ext/agent/agent_factory.py +2270 -0
- jupyter_ext/agent/ast_tools.py +451 -0
- jupyter_ext/agent/autofix_middleware.py +481 -0
- jupyter_ext/agent/background_procs.py +372 -0
- jupyter_ext/agent/code_history_middleware.py +211 -0
- jupyter_ext/agent/codex_autofix.py +160 -0
- jupyter_ext/agent/codex_bridge.py +3339 -0
- jupyter_ext/agent/codex_hooks.py +320 -0
- jupyter_ext/agent/codex_slash.py +685 -0
- jupyter_ext/agent/codex_threads.py +114 -0
- jupyter_ext/agent/content_injection.py +377 -0
- jupyter_ext/agent/continuation_middleware.py +548 -0
- jupyter_ext/agent/custom_skills.py +335 -0
- jupyter_ext/agent/file_diffs.py +99 -0
- jupyter_ext/agent/git_history.py +188 -0
- jupyter_ext/agent/guardrail_middleware.py +421 -0
- jupyter_ext/agent/json_repair_utils.py +54 -0
- jupyter_ext/agent/langchain_tools.py +2673 -0
- jupyter_ext/agent/mermaid_fixer.py +289 -0
- jupyter_ext/agent/mermaid_middleware.py +135 -0
- jupyter_ext/agent/middleware.py +359 -0
- jupyter_ext/agent/middleware_prompts.py +53 -0
- jupyter_ext/agent/observability.py +334 -0
- jupyter_ext/agent/patch_tool_calls.py +57 -0
- jupyter_ext/agent/prompts.py +112 -0
- jupyter_ext/agent/skill_middleware.py +257 -0
- jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
- jupyter_ext/agent/skills/anomaly_detection.md +12 -0
- jupyter_ext/agent/skills/athena_sql.md +156 -0
- jupyter_ext/agent/skills/bdp_table_search.md +33 -0
- jupyter_ext/agent/skills/bigquery.md +65 -0
- jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
- jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
- jupyter_ext/agent/skills/classification_evaluation.md +11 -0
- jupyter_ext/agent/skills/clustering_analysis.md +12 -0
- jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
- jupyter_ext/agent/skills/data_analysis.md +236 -0
- jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
- jupyter_ext/agent/skills/data_loading.md +158 -0
- jupyter_ext/agent/skills/data_quality_report.md +13 -0
- jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
- jupyter_ext/agent/skills/emr_script_development.md +17 -0
- jupyter_ext/agent/skills/experiment_tracking.md +13 -0
- jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
- jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
- jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
- jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
- jupyter_ext/agent/skills/inference.md +392 -0
- jupyter_ext/agent/skills/lambda_function_development.md +19 -0
- jupyter_ext/agent/skills/model_comparison.md +11 -0
- jupyter_ext/agent/skills/model_explainability.md +12 -0
- jupyter_ext/agent/skills/model_training.md +318 -0
- jupyter_ext/agent/skills/pyspark.md +352 -0
- jupyter_ext/agent/skills/python_code_quality.md +35 -0
- jupyter_ext/agent/skills/regression_evaluation.md +12 -0
- jupyter_ext/agent/skills/spring_java_development.md +35 -0
- jupyter_ext/agent/skills/text_preprocessing.md +12 -0
- jupyter_ext/agent/skills/time_series_analysis.md +13 -0
- jupyter_ext/agent/skills/unit_testing.md +203 -0
- jupyter_ext/agent/todo_prompts.py +57 -0
- jupyter_ext/agent/tool_report.py +45 -0
- jupyter_ext/analytics_ingest.py +599 -0
- jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- jupyter_ext/handlers/__init__.py +8 -0
- jupyter_ext/handlers/analytics_handler.py +170 -0
- jupyter_ext/handlers/benchmark_handler.py +794 -0
- jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
- jupyter_ext/handlers/rag_admin_handler.py +689 -0
- jupyter_ext/handlers/websocket.py +497 -0
- jupyter_ext/http_handlers.py +8744 -0
- jupyter_ext/idle_monitor.py +346 -0
- jupyter_ext/labextension/package.json +157 -0
- jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
- jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
- jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
- jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
- jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
- jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
- jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
- jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
- jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
- jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
- jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
- jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- jupyter_ext/labextension/static/style.js +4 -0
- jupyter_ext/labextension/static/third-party-licenses.json +724 -0
- jupyter_ext/log_paths.py +113 -0
- jupyter_ext/resource_usage.py +519 -0
- jupyter_ext/secrets_bootstrap.py +116 -0
- jupyter_ext/team_access.py +105 -0
- jupyter_ext/tests/__init__.py +1 -0
- jupyter_ext/tests/test_context.py +234 -0
- jupyter_ext/tests/test_factory.py +563 -0
- jupyter_ext/tests/test_file_finder.py +139 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- jupyter_ext/tests/test_mcp_selftest.py +204 -0
- jupyter_ext/tests/test_mermaid_fixer.py +272 -0
- jupyter_ext/tests/test_resource_tiers.py +298 -0
- jupyter_ext/tools/__init__.py +15 -0
- jupyter_ext/tools/base.py +90 -0
- jupyter_ext/tools/context.py +268 -0
- jupyter_ext/tools/dir_complete.py +86 -0
- jupyter_ext/tools/file_finder.py +303 -0
- jupyter_ext/tools/file_tools.py +244 -0
- jupyter_ext/tools/jupyter_tools.py +296 -0
- jupyter_ext/tools/registry.py +159 -0
- jupyter_ext/tools/search_tools.py +230 -0
- jupyter_ext/tools/security_tools.py +657 -0
- jupyter_ext/tools/shell_tools.py +109 -0
- 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
|
+
)
|