@triflux/core 10.1.0 → 10.25.1
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/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +183 -23
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +254 -33
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +831 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +184 -51
- package/hud/renderers.mjs +413 -123
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +48 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +70 -10
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
package/hooks/safety-guard.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
// hooks/safety-guard.mjs — PreToolUse:Bash 훅
|
|
3
4
|
//
|
|
4
5
|
// 위험한 Bash 명령을 사전 차단(exit 2)하거나 경고(additionalContext)한다.
|
|
@@ -8,23 +9,71 @@
|
|
|
8
9
|
// BLOCK (exit 2) — 복구 불가능한 파괴적 명령
|
|
9
10
|
// WARN (allow + context) — 주의가 필요한 명령
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
14
|
import { join } from "node:path";
|
|
15
|
+
import { readHosts } from "../hub/lib/hosts-compat.mjs";
|
|
16
|
+
|
|
17
|
+
// ── 로컬 우회 플래그 ──────────────────────────────────────────
|
|
18
|
+
// LOCAL ONLY — Issue #89 대안 API 구현 전까지 psmux kill 시리즈 우회용.
|
|
19
|
+
// 활성 조건 (OR):
|
|
20
|
+
// 1) env: TFX_CLEANUP_BYPASS=1
|
|
21
|
+
// 2) 파일: .claude/cleanup-bypass 존재 (repo root)
|
|
22
|
+
// 파일 방식은 Claude Code Bash 도구가 훅에 env 전달 못하는 경우에도 동작.
|
|
23
|
+
// 정식 해결: hub/team/psmux.mjs 에 listSessions/killSessionByTitle/pruneStale 노출.
|
|
24
|
+
const CLEANUP_BYPASS = (() => {
|
|
25
|
+
if (process.env.TFX_CLEANUP_BYPASS === "1") return true;
|
|
26
|
+
try {
|
|
27
|
+
return existsSync(join(process.cwd(), ".claude", "cleanup-bypass"));
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
13
32
|
|
|
14
33
|
// ── 차단 규칙 ──────────────────────────────────────────────
|
|
15
34
|
const BLOCK_RULES = [
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
35
|
+
{
|
|
36
|
+
pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!tmp\b)(?!\S*node_modules)/i,
|
|
37
|
+
reason: "루트/홈 디렉토리 rm -rf 차단",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
pattern: /\brm\s+(-[^\s]*)?-rf?\s+\.\s*$/i,
|
|
41
|
+
reason: "현재 디렉토리 rm -rf . 차단",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
pattern: /\bgit\s+push\s+.*--force\s+.*\b(main|master)\b/i,
|
|
45
|
+
reason: "main/master force push 차단",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
pattern: /\bgit\s+push\s+--force\s*$/i,
|
|
49
|
+
reason: "대상 미지정 force push 차단",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
pattern: /\bgit\s+reset\s+--hard\s+origin\//i,
|
|
53
|
+
reason: "remote reset --hard 차단 — 로컬 작업 소실 위험",
|
|
54
|
+
},
|
|
21
55
|
{ pattern: /\bdrop\s+(table|database|schema)\b/i, reason: "SQL DROP 차단" },
|
|
22
56
|
{ pattern: /\btruncate\s+table\b/i, reason: "SQL TRUNCATE 차단" },
|
|
23
57
|
{ pattern: /\bformat\s+[a-z]:/i, reason: "디스크 포맷 차단" },
|
|
24
58
|
{ pattern: /\b(del|rmdir)\s+\/[sq]\b/i, reason: "Windows 재귀 삭제 차단" },
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
59
|
+
{
|
|
60
|
+
pattern: /\bgit\s+clean\s+.*-fd/i,
|
|
61
|
+
reason: "git clean -fd 차단 — 추적되지 않은 파일 소실 위험",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
pattern: /\bpsmux\s+kill-session\b/i,
|
|
65
|
+
reason:
|
|
66
|
+
"raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 대안: listSessions()/killSessionByTitle()/pruneStale() 또는 node hub/team/psmux.mjs --internal kill-by-title <prefix|/regex/> (또는 TFX_CLEANUP_BYPASS=1/.claude/cleanup-bypass)",
|
|
67
|
+
skipIfGit: true,
|
|
68
|
+
cleanupBypass: true,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
pattern: /\bpsmux\s+kill-server\b/i,
|
|
72
|
+
reason:
|
|
73
|
+
"psmux kill-server 차단 — 모든 세션이 즉시 종료됩니다. node hub/team/psmux.mjs kill-swarm 사용 (또는 TFX_CLEANUP_BYPASS=1)",
|
|
74
|
+
skipIfGit: true,
|
|
75
|
+
cleanupBypass: true,
|
|
76
|
+
},
|
|
28
77
|
];
|
|
29
78
|
|
|
30
79
|
const WT_DIRECT_PATTERNS = [
|
|
@@ -37,47 +86,122 @@ const WT_DIRECT_PATTERNS = [
|
|
|
37
86
|
];
|
|
38
87
|
|
|
39
88
|
const WT_DIRECT_BLOCK_MESSAGE =
|
|
40
|
-
"[safety-guard] wt.exe 직접 호출
|
|
89
|
+
"[safety-guard] wt.exe 직접 호출 차단됨.\n" +
|
|
90
|
+
"→ hub/team/wt-manager.mjs의 createWtManager() 팩토리 사용:\n" +
|
|
91
|
+
" wt.createTab({ title, command, profile, cwd }) — 새 탭\n" +
|
|
92
|
+
" wt.splitPane({ direction: 'H'|'V', title, command }) — 패인 분할\n" +
|
|
93
|
+
" wt.applySplitLayout([{ title, command, direction }]) — 다중 배치\n" +
|
|
94
|
+
"사용법: node -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); wt.createTab({ title: '제목', command: 'pwsh' }); })\"";
|
|
95
|
+
|
|
96
|
+
const PSMUX_INTERNAL_WRAPPER_PATTERNS = [
|
|
97
|
+
/node(?:\.exe)?\s+.*hub[\\/]+team[\\/]+psmux\.mjs\s+--internal\s+(?:list|kill-by-title|prune-stale)\b/i,
|
|
98
|
+
];
|
|
41
99
|
|
|
42
100
|
// ── SSH+PowerShell bash 문법 차단 ────────────────────────────
|
|
43
101
|
// 원격 기본 셸이 PowerShell인 호스트에 bash redirect/glob을 보내면 오동작
|
|
102
|
+
// macOS/Linux 대상 SSH에는 bash 문법이 정상이므로 hosts.json OS를 확인한다.
|
|
44
103
|
const BASH_SYNTAX_IN_SSH = [
|
|
45
|
-
/2>\/dev\/null/,
|
|
46
|
-
/>\s*\/dev\/null/,
|
|
47
|
-
/&>\s*\/dev\/null/,
|
|
48
|
-
/\$\(/,
|
|
49
|
-
/\bsource\s+/,
|
|
50
|
-
/\bexport\s+\w+=/,
|
|
104
|
+
/2>\/dev\/null/, // 2>/dev/null → PowerShell에서 Out-File C:\dev\null
|
|
105
|
+
/>\s*\/dev\/null/, // >/dev/null
|
|
106
|
+
/&>\s*\/dev\/null/, // &>/dev/null
|
|
107
|
+
/\$\(/, // $(cmd) → PowerShell에서 다른 의미
|
|
108
|
+
/\bsource\s+/, // source → PowerShell에 없음
|
|
109
|
+
/\bexport\s+\w+=/, // export VAR= → PowerShell에 없음
|
|
51
110
|
];
|
|
52
111
|
|
|
53
112
|
const SSH_POWERSHELL_HINT =
|
|
54
113
|
"원격 셸이 PowerShell입니다. bash 문법 직접 전달 금지. scp + pwsh -File 패턴 사용. " +
|
|
55
114
|
"2>/dev/null → 2>$null, $() → $(), export → $env:, source → . (dot-source)";
|
|
56
115
|
|
|
116
|
+
/** hosts.json에서 Windows 호스트 식별자 집합을 구축한다. */
|
|
117
|
+
function getWindowsHostIds() {
|
|
118
|
+
const ids = new Set();
|
|
119
|
+
try {
|
|
120
|
+
const registry = readHosts(process.cwd());
|
|
121
|
+
for (const [name, cfg] of Object.entries(registry.hosts || {})) {
|
|
122
|
+
if (cfg.os !== "windows") continue;
|
|
123
|
+
ids.add(name);
|
|
124
|
+
if (cfg.tailscale?.ip) ids.add(cfg.tailscale.ip);
|
|
125
|
+
if (cfg.tailscale?.dns) ids.add(cfg.tailscale.dns);
|
|
126
|
+
if (cfg.ssh?.host) ids.add(cfg.ssh.host);
|
|
127
|
+
if (cfg.ssh?.user) {
|
|
128
|
+
ids.add(`${cfg.ssh.user}@${name}`);
|
|
129
|
+
if (cfg.tailscale?.ip) ids.add(`${cfg.ssh.user}@${cfg.tailscale.ip}`);
|
|
130
|
+
if (cfg.tailscale?.dns) ids.add(`${cfg.ssh.user}@${cfg.tailscale.dns}`);
|
|
131
|
+
if (cfg.ssh?.host) ids.add(`${cfg.ssh.user}@${cfg.ssh.host}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// hosts.json 로드 실패 시 빈 집합 → 차단 안 함 (POSIX 기본)
|
|
136
|
+
}
|
|
137
|
+
return ids;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** SSH 명령의 대상이 Windows 호스트인지 판별한다. */
|
|
141
|
+
function isSshTargetWindows(command) {
|
|
142
|
+
const winIds = getWindowsHostIds();
|
|
143
|
+
if (winIds.size === 0) return false; // Windows 호스트 없으면 POSIX 가정
|
|
144
|
+
for (const id of winIds) {
|
|
145
|
+
if (command.includes(id)) return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
57
150
|
// ── 경고 규칙 ──────────────────────────────────────────────
|
|
58
151
|
const WARN_RULES = [
|
|
59
|
-
{
|
|
60
|
-
|
|
152
|
+
{
|
|
153
|
+
pattern: /\bgit\s+push\b(?!.*--force)/i,
|
|
154
|
+
warn: "git push 감지. 원격 저장소에 반영됩니다.",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
pattern: /\bgit\s+rebase\b/i,
|
|
158
|
+
warn: "git rebase 감지. 커밋 히스토리가 변경됩니다.",
|
|
159
|
+
},
|
|
61
160
|
{ pattern: /\bgit\s+branch\s+-[dD]\b/i, warn: "브랜치 삭제 감지." },
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
161
|
+
{
|
|
162
|
+
pattern: /\bnpm\s+publish\b/i,
|
|
163
|
+
warn: "npm publish 감지. 공개 레지스트리에 배포됩니다.",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
pattern: /\brm\s+(-[^\s]*)?-rf?\s/i,
|
|
167
|
+
warn: "재귀 삭제 감지. 대상을 확인하세요.",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
pattern: /--no-verify\b/i,
|
|
171
|
+
warn: "--no-verify 감지. 훅 건너뛰기는 권장하지 않습니다.",
|
|
172
|
+
},
|
|
65
173
|
{ pattern: /\bchmod\s+777\b/i, warn: "chmod 777 감지. 보안 위험." },
|
|
66
|
-
{
|
|
174
|
+
{
|
|
175
|
+
pattern: /\bcurl\s.*\|\s*(bash|sh)\b/i,
|
|
176
|
+
warn: "curl | sh 감지. 원격 스크립트 실행 주의.",
|
|
177
|
+
},
|
|
67
178
|
];
|
|
68
179
|
|
|
69
180
|
// ── reflexion 적응형 패널티 로드 ──────────────────────────────
|
|
70
181
|
function loadReflexionPenalties() {
|
|
71
182
|
try {
|
|
72
183
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
73
|
-
const penaltyFile = join(
|
|
184
|
+
const penaltyFile = join(
|
|
185
|
+
home,
|
|
186
|
+
".triflux",
|
|
187
|
+
"reflexion",
|
|
188
|
+
"pending-penalties.jsonl",
|
|
189
|
+
);
|
|
74
190
|
if (!existsSync(penaltyFile)) return [];
|
|
75
191
|
return readFileSync(penaltyFile, "utf8")
|
|
76
192
|
.split("\n")
|
|
77
193
|
.filter(Boolean)
|
|
78
|
-
.map(
|
|
194
|
+
.map((line) => {
|
|
195
|
+
try {
|
|
196
|
+
return JSON.parse(line);
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
})
|
|
79
201
|
.filter(Boolean);
|
|
80
|
-
} catch {
|
|
202
|
+
} catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
81
205
|
}
|
|
82
206
|
|
|
83
207
|
function readStdin() {
|
|
@@ -89,7 +213,11 @@ function readStdin() {
|
|
|
89
213
|
}
|
|
90
214
|
|
|
91
215
|
function shouldSkipSegment(segment) {
|
|
92
|
-
return
|
|
216
|
+
return (
|
|
217
|
+
!segment ||
|
|
218
|
+
segment.startsWith("#") ||
|
|
219
|
+
/^\s*(echo|printf|grep|git\s+commit)\b/i.test(segment)
|
|
220
|
+
);
|
|
93
221
|
}
|
|
94
222
|
|
|
95
223
|
function hasSegmentInvocation(cmd, patterns) {
|
|
@@ -118,11 +246,65 @@ function hasSegmentInvocation(cmd, patterns) {
|
|
|
118
246
|
});
|
|
119
247
|
}
|
|
120
248
|
|
|
249
|
+
function isGitCommitInvocation(command) {
|
|
250
|
+
const lines = command.split(/\n/);
|
|
251
|
+
let heredocDelimiter = null;
|
|
252
|
+
return lines.some((line) => {
|
|
253
|
+
if (heredocDelimiter !== null) {
|
|
254
|
+
if (line.trim() === heredocDelimiter) heredocDelimiter = null;
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const heredocMatch = line.match(/<<['"]?(\w+)['"]?/);
|
|
259
|
+
if (heredocMatch) {
|
|
260
|
+
heredocDelimiter = heredocMatch[1];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return line
|
|
264
|
+
.split(/\s*(?:&&|;|\|\|)\s*/)
|
|
265
|
+
.some((segment) => /^\s*git\s+commit\b/i.test(segment.trim()));
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function resolveHookCwd(input) {
|
|
270
|
+
return String(
|
|
271
|
+
input?.cwd ||
|
|
272
|
+
input?.tool_input?.cwd ||
|
|
273
|
+
process.env.CLAUDE_CWD ||
|
|
274
|
+
process.cwd() ||
|
|
275
|
+
process.env.PWD,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getCurrentGitBranch(cwd) {
|
|
280
|
+
try {
|
|
281
|
+
const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
282
|
+
cwd,
|
|
283
|
+
encoding: "utf8",
|
|
284
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
285
|
+
});
|
|
286
|
+
if (result.status !== 0) return "";
|
|
287
|
+
return String(result.stdout || "").trim();
|
|
288
|
+
} catch {
|
|
289
|
+
return "";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isProtectedMainBranch(branch) {
|
|
294
|
+
return /^(main|master)$/i.test(String(branch || "").trim());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isSwarmWorktreeCwd(cwd) {
|
|
298
|
+
return /(?:^|\/)\.codex-swarm\/wt-[^/]+(?:\/|$)/.test(
|
|
299
|
+
String(cwd || "").replace(/\\/g, "/"),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
121
303
|
function blockCommand(message, command) {
|
|
122
304
|
process.stderr.write(
|
|
123
305
|
`${message}\n` +
|
|
124
306
|
`명령어: ${command.slice(0, 120)}${command.length > 120 ? "..." : ""}\n` +
|
|
125
|
-
"이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요."
|
|
307
|
+
"이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요.",
|
|
126
308
|
);
|
|
127
309
|
process.exit(2);
|
|
128
310
|
}
|
|
@@ -142,6 +324,7 @@ function main() {
|
|
|
142
324
|
|
|
143
325
|
const command = (input.tool_input?.command || "").trim();
|
|
144
326
|
if (!command) process.exit(0);
|
|
327
|
+
const hookCwd = resolveHookCwd(input);
|
|
145
328
|
|
|
146
329
|
// psmux 명령이 실제 CLI 호출인지 판별 (오탐 방지)
|
|
147
330
|
// git commit 메시지, echo, grep, cat, heredoc 안의 텍스트는 무시
|
|
@@ -149,6 +332,10 @@ function main() {
|
|
|
149
332
|
return hasSegmentInvocation(cmd, [/\bpsmux\s+kill-(session|server)\b/i]);
|
|
150
333
|
}
|
|
151
334
|
|
|
335
|
+
function isAllowedPsmuxWrapperInvocation(cmd) {
|
|
336
|
+
return hasSegmentInvocation(cmd, PSMUX_INTERNAL_WRAPPER_PATTERNS);
|
|
337
|
+
}
|
|
338
|
+
|
|
152
339
|
function isWtDirectInvocation(cmd) {
|
|
153
340
|
return hasSegmentInvocation(cmd, WT_DIRECT_PATTERNS);
|
|
154
341
|
}
|
|
@@ -157,11 +344,37 @@ function main() {
|
|
|
157
344
|
blockCommand(WT_DIRECT_BLOCK_MESSAGE, command);
|
|
158
345
|
}
|
|
159
346
|
|
|
347
|
+
if (isAllowedPsmuxWrapperInvocation(command)) {
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const codexPrdActive = process.env.CODEX_PRD_ACTIVE === "1";
|
|
352
|
+
if (codexPrdActive && isGitCommitInvocation(command)) {
|
|
353
|
+
const branch = getCurrentGitBranch(hookCwd);
|
|
354
|
+
if (isProtectedMainBranch(branch)) {
|
|
355
|
+
const locationHint = isSwarmWorktreeCwd(hookCwd)
|
|
356
|
+
? "swarm worktree 내부에서도 main/master 직접 commit은 금지됩니다."
|
|
357
|
+
: "현재 PWD가 swarm worktree가 아닙니다.";
|
|
358
|
+
blockCommand(
|
|
359
|
+
`[safety-guard] Codex PRD 실행 중 ${branch} 직접 commit 차단됨. ${locationHint}`,
|
|
360
|
+
command,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
160
365
|
// 0.1. reflexion 적응형 패널티 — 이전 세션에서 차단된 패턴 사전 경고
|
|
161
366
|
const penalties = loadReflexionPenalties();
|
|
162
367
|
if (penalties.length > 0) {
|
|
163
368
|
for (const penalty of penalties) {
|
|
164
|
-
if (
|
|
369
|
+
if (
|
|
370
|
+
penalty.error_pattern &&
|
|
371
|
+
new RegExp(
|
|
372
|
+
penalty.error_pattern
|
|
373
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
374
|
+
.slice(0, 80),
|
|
375
|
+
"i",
|
|
376
|
+
).test(command)
|
|
377
|
+
) {
|
|
165
378
|
const output = {
|
|
166
379
|
hookSpecificOutput: {
|
|
167
380
|
hookEventName: "PreToolUse",
|
|
@@ -177,16 +390,18 @@ function main() {
|
|
|
177
390
|
}
|
|
178
391
|
}
|
|
179
392
|
|
|
180
|
-
// 0.5. SSH → PowerShell
|
|
181
|
-
//
|
|
182
|
-
if (
|
|
183
|
-
|
|
393
|
+
// 0.5. SSH → Windows(PowerShell) 호스트에만 bash 문법 전달 차단
|
|
394
|
+
// macOS/Linux 대상은 bash/zsh이므로 허용. hosts.json OS로 판별.
|
|
395
|
+
if (
|
|
396
|
+
hasSegmentInvocation(command, [/^\s*ssh\s+/i]) &&
|
|
397
|
+
isSshTargetWindows(command)
|
|
398
|
+
) {
|
|
184
399
|
const segments = command.split(/\s*(?:&&|;|\|\||\|)\s*/);
|
|
185
400
|
for (const seg of segments) {
|
|
186
401
|
const sshMatch = seg.trim().match(/^ssh\s+\S+\s+(.*)/s);
|
|
187
402
|
if (!sshMatch) continue;
|
|
188
403
|
const sshPayload = sshMatch[1];
|
|
189
|
-
const bashSyntax = BASH_SYNTAX_IN_SSH.find(p => p.test(sshPayload));
|
|
404
|
+
const bashSyntax = BASH_SYNTAX_IN_SSH.find((p) => p.test(sshPayload));
|
|
190
405
|
if (bashSyntax) {
|
|
191
406
|
blockCommand(
|
|
192
407
|
`[safety-guard] SSH 명령에 bash 전용 문법 감지: ${bashSyntax}. ${SSH_POWERSHELL_HINT}`,
|
|
@@ -198,12 +413,18 @@ function main() {
|
|
|
198
413
|
|
|
199
414
|
// 1. BLOCK 체크 — exit 2로 차단
|
|
200
415
|
for (const rule of BLOCK_RULES) {
|
|
416
|
+
if (rule.cleanupBypass && CLEANUP_BYPASS) continue;
|
|
201
417
|
if (rule.skipIfGit && !isPsmuxInvocation(command)) continue;
|
|
202
418
|
if (rule.pattern.test(command)) {
|
|
203
419
|
blockCommand(`[triflux safety-guard] BLOCKED: ${rule.reason}`, command);
|
|
204
420
|
}
|
|
205
421
|
}
|
|
206
422
|
|
|
423
|
+
// wt 정리 명령 우회 (TFX_CLEANUP_BYPASS=1 한정). new-tab/split-pane만 차단 유지하려면 아래 조건 세분화.
|
|
424
|
+
if (CLEANUP_BYPASS) {
|
|
425
|
+
// bypass 모드에서는 아래 wt 검사를 이미 통과한 상태. 추가 작업 없음.
|
|
426
|
+
}
|
|
427
|
+
|
|
207
428
|
// 2. WARN 체크 — allow + additionalContext
|
|
208
429
|
const warnings = [];
|
|
209
430
|
for (const rule of WARN_RULES) {
|