@triflux/core 10.1.0 → 10.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +191 -31
- 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 +262 -34
- 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 +1051 -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 +22 -21
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/schema/delegator-tools.schema.json +1 -1
- package/hub/delegator/service.mjs +93 -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 +82 -108
- 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 +78 -30
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +23 -17
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +190 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +157 -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 +210 -52
- package/hud/renderers.mjs +440 -127
- 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 +37 -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 +49 -20
- 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 +117 -12
- 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/hub/codex-adapter.mjs
CHANGED
|
@@ -1,46 +1,55 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { runPreflight } from './codex-preflight.mjs';
|
|
6
|
-
import { withRetry } from './workers/worker-utils.mjs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
7
4
|
import {
|
|
8
|
-
createCircuitBreaker,
|
|
9
|
-
createResult,
|
|
10
|
-
appendWarnings,
|
|
11
5
|
buildExecCommand,
|
|
6
|
+
createResult,
|
|
7
|
+
executeWithCircuitBroker,
|
|
12
8
|
normalizePathForShell,
|
|
13
|
-
shellQuote,
|
|
14
9
|
runProcess,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
shellQuote,
|
|
11
|
+
} from "./cli-adapter-base.mjs";
|
|
12
|
+
import { runPreflight } from "./codex-preflight.mjs";
|
|
18
13
|
|
|
19
14
|
// ── Codex-specific stall inference ──────────────────────────────
|
|
20
15
|
|
|
21
16
|
function inferStallMode(stdout, stderr) {
|
|
22
17
|
const text = `${stdout}\n${stderr}`.toLowerCase();
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
if (
|
|
19
|
+
/(rate.?limit|quota|throttl|too.many.requests|429|usage.limit)/u.test(text)
|
|
20
|
+
)
|
|
21
|
+
return "rate_limited";
|
|
22
|
+
if (/(approval|approve|permission|sandbox|bypass)/u.test(text))
|
|
23
|
+
return "approval_stall";
|
|
24
|
+
if (
|
|
25
|
+
/\bmcp\b|context7|playwright|tavily|exa|brave|sequential|server/u.test(text)
|
|
26
|
+
)
|
|
27
|
+
return "mcp_stall";
|
|
28
|
+
return "timeout";
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
// ── Codex command building ──────────────────────────────────────
|
|
30
32
|
|
|
31
33
|
function commandWithOverrides(command, prompt, codexPath, overrides = []) {
|
|
32
|
-
const next = codexPath
|
|
34
|
+
const next = codexPath
|
|
35
|
+
? command.replace(/^codex\b/u, shellQuote(codexPath))
|
|
36
|
+
: command;
|
|
33
37
|
if (!overrides.length) return next;
|
|
34
38
|
const promptArg = JSON.stringify(prompt);
|
|
35
|
-
const flags = overrides
|
|
39
|
+
const flags = overrides
|
|
40
|
+
.flatMap((value) => ["-c", shellQuote(value)])
|
|
41
|
+
.join(" ");
|
|
36
42
|
return next.endsWith(promptArg)
|
|
37
43
|
? `${next.slice(0, -promptArg.length)}${flags} ${promptArg}`
|
|
38
44
|
: `${next} ${flags}`;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
function buildOverrides(requested, excluded) {
|
|
42
|
-
return [
|
|
43
|
-
|
|
48
|
+
return [
|
|
49
|
+
...new Set(
|
|
50
|
+
(requested || []).filter((name) => (excluded || []).includes(name)),
|
|
51
|
+
),
|
|
52
|
+
].map((name) => `mcp_servers.${name}.enabled=false`);
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
function buildAttempts(opts, preflight) {
|
|
@@ -63,49 +72,58 @@ function buildAttempts(opts, preflight) {
|
|
|
63
72
|
// ── Launch script ───────────────────────────────────────────────
|
|
64
73
|
|
|
65
74
|
function createLaunchScriptText(opts) {
|
|
66
|
-
const parts = [
|
|
67
|
-
if (opts.profile) parts.push(
|
|
75
|
+
const parts = ["codex"];
|
|
76
|
+
if (opts.profile) parts.push("--profile", shellQuote(opts.profile));
|
|
68
77
|
parts.push(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
"exec",
|
|
79
|
+
"--dangerously-bypass-approvals-and-sandbox",
|
|
80
|
+
"--skip-git-repo-check",
|
|
72
81
|
'$(cat "$PROMPT_FILE")',
|
|
73
82
|
);
|
|
74
83
|
return [
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
"#!/usr/bin/env bash",
|
|
85
|
+
"set -euo pipefail",
|
|
77
86
|
`cd ${shellQuote(normalizePathForShell(opts.workdir))}`,
|
|
78
87
|
`PROMPT_FILE=${shellQuote(normalizePathForShell(opts.promptFile))}`,
|
|
79
|
-
`TFX_CODEX_TIMEOUT_MS=${shellQuote(String(opts.timeout ??
|
|
80
|
-
parts.join(
|
|
81
|
-
|
|
82
|
-
].join(
|
|
88
|
+
`TFX_CODEX_TIMEOUT_MS=${shellQuote(String(opts.timeout ?? ""))}`,
|
|
89
|
+
parts.join(" "),
|
|
90
|
+
"",
|
|
91
|
+
].join("\n");
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
export function buildLaunchScript(opts = {}) {
|
|
86
|
-
const dir = join(tmpdir(),
|
|
95
|
+
const dir = join(tmpdir(), "triflux-codex-launch");
|
|
87
96
|
mkdirSync(dir, { recursive: true });
|
|
88
|
-
const path = join(dir, `${String(opts.id ||
|
|
89
|
-
writeFileSync(path, createLaunchScriptText(opts),
|
|
97
|
+
const path = join(dir, `${String(opts.id || "launch")}.sh`);
|
|
98
|
+
writeFileSync(path, createLaunchScriptText(opts), "utf8");
|
|
90
99
|
return path;
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
// ── Exec args builder ───────────────────────────────────────────
|
|
94
103
|
|
|
95
104
|
export function buildExecArgs(opts = {}) {
|
|
96
|
-
const prompt = typeof opts.prompt ===
|
|
105
|
+
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
97
106
|
const command = buildExecCommand(prompt, opts.resultFile || null, {
|
|
98
107
|
profile: opts.profile,
|
|
99
108
|
skipGitRepoCheck: true,
|
|
100
109
|
sandboxBypass: true,
|
|
101
110
|
cwd: opts.cwd,
|
|
111
|
+
// stdinPrompt 가 명시되면 buildExecCommand 로 forward.
|
|
112
|
+
// headless 워커 spawn (backend.mjs) 은 default (stdin on, macOS hook fix).
|
|
113
|
+
// launcher path (launcher-template.mjs) 는 결정론 위해 false 전달.
|
|
114
|
+
stdinPrompt: opts.stdinPrompt,
|
|
102
115
|
});
|
|
103
116
|
|
|
104
|
-
if (!prompt) return command.replace(/\s+""$/u,
|
|
117
|
+
if (!prompt) return command.replace(/\s+""$/u, "");
|
|
105
118
|
|
|
106
119
|
let result;
|
|
107
120
|
const quotedPrompt = JSON.stringify(prompt);
|
|
108
|
-
|
|
121
|
+
// PowerShell: (Get-Content -Raw '...'), bash: "$(cat '...')"
|
|
122
|
+
if (
|
|
123
|
+
(/^\(Get-Content\b[\s\S]*\)$/u.test(prompt) ||
|
|
124
|
+
/^"\$\(cat\b[\s\S]*\)"$/u.test(prompt)) &&
|
|
125
|
+
command.endsWith(quotedPrompt)
|
|
126
|
+
) {
|
|
109
127
|
result = `${command.slice(0, -quotedPrompt.length)}${prompt}`;
|
|
110
128
|
} else {
|
|
111
129
|
result = command;
|
|
@@ -120,13 +138,16 @@ export function buildExecArgs(opts = {}) {
|
|
|
120
138
|
|
|
121
139
|
// ── Codex execution ─────────────────────────────────────────────
|
|
122
140
|
|
|
123
|
-
async function runCodex(prompt, workdir, preflight, attempt) {
|
|
124
|
-
const dir = join(tmpdir(),
|
|
141
|
+
async function runCodex(prompt, workdir, preflight, attempt, lease) {
|
|
142
|
+
const dir = join(tmpdir(), "triflux-codex-exec");
|
|
125
143
|
mkdirSync(dir, { recursive: true });
|
|
126
|
-
const resultFile = join(
|
|
144
|
+
const resultFile = join(
|
|
145
|
+
dir,
|
|
146
|
+
`codex-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`,
|
|
147
|
+
);
|
|
127
148
|
const command = commandWithOverrides(
|
|
128
149
|
buildExecCommand(prompt, resultFile, {
|
|
129
|
-
profile: attempt.profile,
|
|
150
|
+
profile: lease?.profile ?? attempt.profile,
|
|
130
151
|
skipGitRepoCheck: true,
|
|
131
152
|
sandboxBypass: attempt.forceBypass,
|
|
132
153
|
}),
|
|
@@ -134,66 +155,131 @@ async function runCodex(prompt, workdir, preflight, attempt) {
|
|
|
134
155
|
preflight.codexPath,
|
|
135
156
|
buildOverrides(attempt.requested, attempt.excluded),
|
|
136
157
|
);
|
|
137
|
-
|
|
158
|
+
// PRD A1 — lease 메타데이터를 spawn env 에 적용한다. lease.authFile 이 있으면
|
|
159
|
+
// 해당 파일이 위치한 디렉토리를 CODEX_HOME 으로 export 해서 codex CLI 가 그
|
|
160
|
+
// account 의 auth.json 을 사용하게 한다. lease 가 null 이면 default ~/.codex
|
|
161
|
+
// 동작 유지 (회귀 없음). lease.env 는 추가 환경변수 (provider 고정 등) 주입용.
|
|
162
|
+
const spawnEnv = lease ? buildLeaseSpawnEnv(lease) : undefined;
|
|
163
|
+
const authCheck = validateLeaseAuth(lease, spawnEnv);
|
|
164
|
+
if (!authCheck.ok) {
|
|
165
|
+
return createResult(false, {
|
|
166
|
+
stderr: authCheck.error,
|
|
167
|
+
failureMode: "auth_sync",
|
|
168
|
+
skipCircuit: true,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return runProcess(command, workdir, attempt.timeout, {
|
|
172
|
+
resultFile,
|
|
173
|
+
inferStallMode,
|
|
174
|
+
spawnEnv,
|
|
175
|
+
});
|
|
138
176
|
}
|
|
139
177
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
178
|
+
function buildLeaseSpawnEnv(lease) {
|
|
179
|
+
const extra = {};
|
|
180
|
+
if (lease.authFile) {
|
|
181
|
+
// lease.authFile 은 보통 ~/.claude/cache/tfx-hub/codex-auth-<account>.json 형식.
|
|
182
|
+
// codex CLI 는 CODEX_HOME 을 통해 auth.json 위치를 결정하므로, 이 cache 파일을
|
|
183
|
+
// 직접 CODEX_HOME 후보 디렉토리로 사용한다. cache 파일 자체가 auth.json 이름이
|
|
184
|
+
// 아니라면 후속 PR 에서 isolated dir + symlink/복사 처리한다 (PRD Open Question).
|
|
185
|
+
// 현재는 lease.authFile 의 dirname 을 export 하되, 그 dirname 안에 auth.json 이
|
|
186
|
+
// 실제로 있을 때만 적용한다 (false-positive 회피).
|
|
187
|
+
try {
|
|
188
|
+
const dir = dirnameOf(lease.authFile);
|
|
189
|
+
if (dir) extra.CODEX_HOME = dir;
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
if (lease.env && typeof lease.env === "object") {
|
|
193
|
+
Object.assign(extra, lease.env);
|
|
194
|
+
}
|
|
195
|
+
return Object.keys(extra).length ? { ...process.env, ...extra } : undefined;
|
|
144
196
|
}
|
|
145
197
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
return
|
|
198
|
+
function validateLeaseAuth(lease, spawnEnv) {
|
|
199
|
+
if (!lease?.authFile) return { ok: true };
|
|
200
|
+
if (basename(lease.authFile) !== "auth.json") {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: `lease authFile must resolve to auth.json for account ${lease.id}`,
|
|
204
|
+
};
|
|
150
205
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
206
|
+
if (!existsSync(lease.authFile)) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
error: `lease authFile missing for account ${lease.id}: ${lease.authFile}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (dirnameOf(lease.authFile) !== spawnEnv?.CODEX_HOME) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
// This is currently expected to match because buildLeaseSpawnEnv derives
|
|
216
|
+
// CODEX_HOME from authFile. Keep the guard so future lease.env overrides
|
|
217
|
+
// cannot silently point Codex at a different account home.
|
|
218
|
+
error: `CODEX_HOME mismatch for account ${lease.id}`,
|
|
219
|
+
};
|
|
161
220
|
}
|
|
162
|
-
|
|
163
|
-
const attempts = buildAttempts(opts, preflight);
|
|
164
|
-
let attemptIndex = 0;
|
|
165
|
-
let lastResult = createResult(false);
|
|
166
|
-
|
|
167
221
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
222
|
+
const parsed = JSON.parse(readFileSync(lease.authFile, "utf8"));
|
|
223
|
+
const accountId =
|
|
224
|
+
parsed?.tokens?.account_id ??
|
|
225
|
+
parsed?.account_id ??
|
|
226
|
+
parsed?.accountId ??
|
|
227
|
+
null;
|
|
228
|
+
if (!accountId) {
|
|
229
|
+
return {
|
|
230
|
+
ok: false,
|
|
231
|
+
error: `lease authFile has no account_id for account ${lease.id}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (accountId !== lease.id) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
error: `lease authFile account mismatch: expected ${lease.id}, got ${accountId}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
184
240
|
} catch (error) {
|
|
185
|
-
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: `lease authFile unreadable for account ${lease.id}: ${error?.message || error}`,
|
|
244
|
+
};
|
|
186
245
|
}
|
|
246
|
+
return { ok: true };
|
|
247
|
+
}
|
|
187
248
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
249
|
+
function dirnameOf(filePath) {
|
|
250
|
+
if (typeof filePath !== "string" || !filePath) return null;
|
|
251
|
+
const lastSep = Math.max(
|
|
252
|
+
filePath.lastIndexOf("/"),
|
|
253
|
+
filePath.lastIndexOf("\\"),
|
|
254
|
+
);
|
|
255
|
+
if (lastSep < 0) return null;
|
|
256
|
+
return filePath.slice(0, lastSep);
|
|
257
|
+
}
|
|
192
258
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
259
|
+
// ── Public API ──────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
export async function getCircuitState() {
|
|
262
|
+
const brokerMod = await import("./account-broker.mjs");
|
|
263
|
+
if (!brokerMod.broker) return { state: "closed", failures: [] };
|
|
264
|
+
const snap = brokerMod.broker
|
|
265
|
+
.snapshot()
|
|
266
|
+
.filter((a) => a.provider === "codex");
|
|
267
|
+
return snap.length
|
|
268
|
+
? { state: snap[0].circuitState, accounts: snap }
|
|
269
|
+
: { state: "closed", failures: [] };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function execute(opts = {}) {
|
|
273
|
+
return executeWithCircuitBroker({
|
|
274
|
+
provider: "codex",
|
|
275
|
+
runFn: runCodex,
|
|
276
|
+
preflightFn: (o) =>
|
|
277
|
+
runPreflight({
|
|
278
|
+
mcpServers: o.mcpServers,
|
|
279
|
+
subcommand: "exec",
|
|
280
|
+
workdir: o.workdir,
|
|
281
|
+
}),
|
|
282
|
+
buildAttemptsFn: buildAttempts,
|
|
283
|
+
opts,
|
|
284
|
+
});
|
|
199
285
|
}
|
package/hub/codex-compat.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// hub/codex-compat.mjs — backward-compatible facade for legacy imports
|
|
2
2
|
|
|
3
|
-
/** @experimental */
|
|
3
|
+
/** @experimental 런타임 미연결 — 향후 통합 예정 */
|
|
4
4
|
export {
|
|
5
|
+
buildExecCommand,
|
|
5
6
|
CODEX_MCP_EXECUTION_EXIT_CODE,
|
|
6
7
|
CODEX_MCP_TRANSPORT_EXIT_CODE,
|
|
7
|
-
FEATURES,
|
|
8
|
-
buildExecCommand,
|
|
9
8
|
escapePwshSingleQuoted,
|
|
9
|
+
FEATURES,
|
|
10
10
|
getCodexVersion,
|
|
11
11
|
gte,
|
|
12
|
-
} from
|
|
12
|
+
} from "./cli-adapter-base.mjs";
|
package/hub/codex-preflight.mjs
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { homedir } from
|
|
4
|
-
import {
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
import { whichCommandAsync } from
|
|
6
|
+
import { whichCommandAsync } from "./platform.mjs";
|
|
7
7
|
|
|
8
8
|
const MIN_RECOMMENDED_MINOR = 118;
|
|
9
9
|
let _cachedVersion = null;
|
|
10
10
|
|
|
11
11
|
function escapeRegExp(value) {
|
|
12
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g,
|
|
12
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function readConfigText(configPath = join(homedir(),
|
|
16
|
-
if (!existsSync(configPath)) return
|
|
15
|
+
function readConfigText(configPath = join(homedir(), ".codex", "config.toml")) {
|
|
16
|
+
if (!existsSync(configPath)) return "";
|
|
17
17
|
try {
|
|
18
|
-
return readFileSync(configPath,
|
|
18
|
+
return readFileSync(configPath, "utf8");
|
|
19
19
|
} catch {
|
|
20
|
-
return
|
|
20
|
+
return "";
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function readTomlString(text, key) {
|
|
25
|
-
const match = String(text).match(
|
|
25
|
+
const match = String(text).match(
|
|
26
|
+
new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "mu"),
|
|
27
|
+
);
|
|
26
28
|
return match?.[1] ?? null;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -30,13 +32,13 @@ function readSection(text, name) {
|
|
|
30
32
|
const lines = String(text).split(/\r?\n/u);
|
|
31
33
|
const header = `[${name}]`;
|
|
32
34
|
const start = lines.findIndex((line) => line.trim() === header);
|
|
33
|
-
if (start < 0) return
|
|
35
|
+
if (start < 0) return "";
|
|
34
36
|
const body = [];
|
|
35
37
|
for (const line of lines.slice(start + 1)) {
|
|
36
38
|
if (/^\s*\[[^\]]+\]\s*$/u.test(line)) break;
|
|
37
39
|
body.push(line);
|
|
38
40
|
}
|
|
39
|
-
return body.join(
|
|
41
|
+
return body.join("\n");
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/**
|
|
@@ -47,7 +49,10 @@ function readSection(text, name) {
|
|
|
47
49
|
export function getCodexVersion() {
|
|
48
50
|
if (_cachedVersion !== null) return _cachedVersion;
|
|
49
51
|
try {
|
|
50
|
-
const out = execSync(
|
|
52
|
+
const out = execSync("codex --version", {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
timeout: 5000,
|
|
55
|
+
}).trim();
|
|
51
56
|
// "codex 0.117.0" 또는 "0.117.0" 형식 대응
|
|
52
57
|
const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
53
58
|
_cachedVersion = match ? parseInt(match[2], 10) : 0;
|
|
@@ -58,29 +63,34 @@ export function getCodexVersion() {
|
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
async function checkCodexInstalled() {
|
|
61
|
-
const codexPath = await whichCommandAsync(
|
|
66
|
+
const codexPath = await whichCommandAsync("codex");
|
|
62
67
|
if (codexPath) return { codexPath, ok: true, warnings: [] };
|
|
63
68
|
return {
|
|
64
69
|
codexPath: null,
|
|
65
70
|
ok: false,
|
|
66
|
-
warnings: [
|
|
71
|
+
warnings: [
|
|
72
|
+
"Codex CLI not found. Install Codex and ensure `codex` is available on PATH.",
|
|
73
|
+
],
|
|
67
74
|
};
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
function checkCodexVersion() {
|
|
71
78
|
const version = getCodexVersion();
|
|
72
|
-
const warnings =
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
const warnings =
|
|
80
|
+
version >= MIN_RECOMMENDED_MINOR
|
|
81
|
+
? []
|
|
82
|
+
: [
|
|
83
|
+
`Codex CLI 0.${version}.x detected; 0.${MIN_RECOMMENDED_MINOR}.x or newer is recommended.`,
|
|
84
|
+
];
|
|
75
85
|
return { version, warnings };
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
function checkApprovalMode(configText, opts = {}) {
|
|
79
|
-
const approvalMode = readTomlString(configText,
|
|
80
|
-
const sandbox = readTomlString(configText,
|
|
81
|
-
const subcommand = opts.subcommand ||
|
|
89
|
+
const approvalMode = readTomlString(configText, "approval_mode");
|
|
90
|
+
const sandbox = readTomlString(configText, "sandbox");
|
|
91
|
+
const subcommand = opts.subcommand || "exec";
|
|
82
92
|
return {
|
|
83
|
-
needsBypass: subcommand ===
|
|
93
|
+
needsBypass: subcommand === "exec" || approvalMode !== "full-auto",
|
|
84
94
|
approvalMode,
|
|
85
95
|
sandbox,
|
|
86
96
|
};
|
|
@@ -88,28 +98,41 @@ function checkApprovalMode(configText, opts = {}) {
|
|
|
88
98
|
|
|
89
99
|
async function verifyServerHealth(name, configText) {
|
|
90
100
|
const section = readSection(configText, `mcp_servers.${name}`);
|
|
91
|
-
if (!section)
|
|
101
|
+
if (!section)
|
|
102
|
+
return { ok: false, warning: `MCP server '${name}' is not configured.` };
|
|
92
103
|
if (/^\s*enabled\s*=\s*false\s*$/mu.test(section)) {
|
|
93
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
warning: `MCP server '${name}' is disabled in config.toml.`,
|
|
107
|
+
};
|
|
94
108
|
}
|
|
95
109
|
|
|
96
|
-
const command = readTomlString(section,
|
|
110
|
+
const command = readTomlString(section, "command");
|
|
97
111
|
if (command) {
|
|
98
112
|
const resolved = await whichCommandAsync(command);
|
|
99
113
|
return resolved
|
|
100
|
-
? { ok: true, warning:
|
|
101
|
-
: {
|
|
114
|
+
? { ok: true, warning: "" }
|
|
115
|
+
: {
|
|
116
|
+
ok: false,
|
|
117
|
+
warning: `MCP server '${name}' command not found: ${command}`,
|
|
118
|
+
};
|
|
102
119
|
}
|
|
103
120
|
|
|
104
|
-
const url = readTomlString(section,
|
|
105
|
-
if (!url || !/^https?:\/\//u.test(url)) return { ok: true, warning:
|
|
121
|
+
const url = readTomlString(section, "url");
|
|
122
|
+
if (!url || !/^https?:\/\//u.test(url)) return { ok: true, warning: "" };
|
|
106
123
|
try {
|
|
107
124
|
const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
108
125
|
return response.status < 500
|
|
109
|
-
? { ok: true, warning:
|
|
110
|
-
: {
|
|
126
|
+
? { ok: true, warning: "" }
|
|
127
|
+
: {
|
|
128
|
+
ok: false,
|
|
129
|
+
warning: `MCP server '${name}' returned HTTP ${response.status}.`,
|
|
130
|
+
};
|
|
111
131
|
} catch {
|
|
112
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
warning: `MCP server '${name}' is unreachable at ${url}.`,
|
|
135
|
+
};
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
138
|
|
|
@@ -118,7 +141,7 @@ async function checkMcpHealth(mcpServers, configText) {
|
|
|
118
141
|
const warnings = [];
|
|
119
142
|
|
|
120
143
|
for (const name of Array.isArray(mcpServers) ? mcpServers : []) {
|
|
121
|
-
const server = String(name ??
|
|
144
|
+
const server = String(name ?? "").trim();
|
|
122
145
|
if (!server) continue;
|
|
123
146
|
const result = await verifyServerHealth(server, configText);
|
|
124
147
|
if (!result.ok) excludeMcpServers.push(server);
|
|
@@ -128,6 +151,53 @@ async function checkMcpHealth(mcpServers, configText) {
|
|
|
128
151
|
return { excludeMcpServers, warnings };
|
|
129
152
|
}
|
|
130
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Detects codex config drift between global (~/.codex/config.toml) and the
|
|
156
|
+
* target workdir. Covers:
|
|
157
|
+
* 1. workdir-local `.codex/config.toml` overriding global approval_mode/sandbox
|
|
158
|
+
* 2. instruction-file asymmetry (AGENTS.md vs CLAUDE.md)
|
|
159
|
+
*
|
|
160
|
+
* @param {string} [workdir] absolute path to the invocation workdir
|
|
161
|
+
* @param {string} [globalConfigText] already-read ~/.codex/config.toml content
|
|
162
|
+
* @returns {{ warnings: string[] }}
|
|
163
|
+
*/
|
|
164
|
+
export function detectWorkdirDrift(workdir, globalConfigText = "") {
|
|
165
|
+
if (!workdir || typeof workdir !== "string") return { warnings: [] };
|
|
166
|
+
const warnings = [];
|
|
167
|
+
|
|
168
|
+
const localCodexConfig = join(workdir, ".codex", "config.toml");
|
|
169
|
+
if (existsSync(localCodexConfig)) {
|
|
170
|
+
let localText = "";
|
|
171
|
+
try {
|
|
172
|
+
localText = readFileSync(localCodexConfig, "utf8");
|
|
173
|
+
} catch {
|
|
174
|
+
localText = "";
|
|
175
|
+
}
|
|
176
|
+
const keys = ["approval_mode", "sandbox", "model"];
|
|
177
|
+
for (const key of keys) {
|
|
178
|
+
const local = readTomlString(localText, key);
|
|
179
|
+
const global = readTomlString(globalConfigText, key);
|
|
180
|
+
if (local && local !== global) {
|
|
181
|
+
warnings.push(
|
|
182
|
+
`workdir '.codex/config.toml' overrides ${key}='${local}' (global='${global || "unset"}').`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const hasAgentsMd = existsSync(join(workdir, "AGENTS.md"));
|
|
189
|
+
const hasClaudeMd = existsSync(join(workdir, "CLAUDE.md"));
|
|
190
|
+
if (hasAgentsMd !== hasClaudeMd) {
|
|
191
|
+
const present = hasAgentsMd ? "AGENTS.md" : "CLAUDE.md";
|
|
192
|
+
const missing = hasAgentsMd ? "CLAUDE.md" : "AGENTS.md";
|
|
193
|
+
warnings.push(
|
|
194
|
+
`workdir has ${present} but not ${missing}; codex and other CLIs may read different instruction sources.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { warnings };
|
|
199
|
+
}
|
|
200
|
+
|
|
131
201
|
export async function runPreflight(opts = {}) {
|
|
132
202
|
const install = await checkCodexInstalled();
|
|
133
203
|
if (!install.ok) {
|
|
@@ -147,14 +217,20 @@ export async function runPreflight(opts = {}) {
|
|
|
147
217
|
|
|
148
218
|
const configText = readConfigText(opts.configPath);
|
|
149
219
|
const approval = checkApprovalMode(configText, opts);
|
|
150
|
-
if (approval.approvalMode !==
|
|
151
|
-
warnings.push(
|
|
220
|
+
if (approval.approvalMode !== "full-auto") {
|
|
221
|
+
warnings.push(
|
|
222
|
+
`approval_mode is '${approval.approvalMode || "unset"}'; bypass flag will be used.`,
|
|
223
|
+
);
|
|
152
224
|
}
|
|
153
|
-
if (approval.sandbox)
|
|
225
|
+
if (approval.sandbox)
|
|
226
|
+
warnings.push(`sandbox mode from config.toml: ${approval.sandbox}`);
|
|
154
227
|
|
|
155
228
|
const mcp = await checkMcpHealth(opts.mcpServers, configText);
|
|
156
229
|
warnings.push(...mcp.warnings);
|
|
157
230
|
|
|
231
|
+
const drift = detectWorkdirDrift(opts.workdir, configText);
|
|
232
|
+
warnings.push(...drift.warnings);
|
|
233
|
+
|
|
158
234
|
return {
|
|
159
235
|
codexPath: install.codexPath,
|
|
160
236
|
version,
|