chore: rebuild CentOS7 release package
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
__pycache__/
|
||||
sessions/
|
||||
logs/
|
||||
attachments/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"updatedAt": "2026-06-28T07:29:33.938Z",
|
||||
"updatedAt": "2026-06-30T15:42:26.421Z",
|
||||
"replies": []
|
||||
}
|
||||
Binary file not shown.
72
findings.md
Normal file
72
findings.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# cc-web Codex hooks 验证发现
|
||||
|
||||
## Initial Position
|
||||
|
||||
当前需验证的核心边界:
|
||||
|
||||
- Codex hooks 官方上属于 Codex/app-server 的生命周期能力。
|
||||
- cc-web 作为平台客户端,理论上不应硬编码执行某个 skill 的 hooks。
|
||||
- 需要分别验证官方文档、cc-web 实现、本地环境三个层面,避免把“配置发现”“运行时注入”“技能行为”混成一个问题。
|
||||
|
||||
## Findings
|
||||
|
||||
### 官方文档背景线程
|
||||
|
||||
- Codex hooks 默认开启,可用 `[features] hooks = false` 关闭。
|
||||
- hooks 从 active config layers 旁边的 `hooks.json` 或 `config.toml` 内联 `[hooks]` 发现。
|
||||
- 常见位置包括 `~/.codex/hooks.json`、`~/.codex/config.toml`、`<repo>/.codex/hooks.json`、`<repo>/.codex/config.toml`。
|
||||
- 项目级 hooks 依赖项目 `.codex/` layer trust;未信任项目时仍可加载用户/系统 hooks。
|
||||
- 非 managed command hooks 需要 review/trust;定义变化后按 hash 重新 review。
|
||||
- 未找到官方文档支持 `thread/start.config.hooks.*` 作为运行时 hooks 注入机制。
|
||||
- `externalAgentConfig/import` 的 `HOOKS` 是 external-agent artifacts 迁移导入,不是每轮运行时 hook 注入。
|
||||
|
||||
### 六项断点阶段结论
|
||||
|
||||
- 断点 1 `CODEX_HOME/HOME`:当前 local 模式下通。运行中 app-server 看到 `HOME=/home/hdzx`,`CODEX_HOME` 未设置,能按默认读取 `/home/hdzx/.codex`。custom 模式会把 `CODEX_HOME` 指向 `config/codex-runtime-home`,但当前未启用。
|
||||
- 断点 2 `thread cwd`:通。目标会话 `00a7cbc2-d0c3-457f-a262-aa5a5859fa54` 的 `cwd=/home/cc-web`;`thread/start` 和 `thread/resume` 都使用同一个 `threadParams.cwd`。
|
||||
- 断点 3 项目 `.codex` trust:通。`/home/hdzx/.codex/config.toml` 存在 `[projects."/home/cc-web"] trust_level = "trusted"`。
|
||||
- 断点 5 `[features].hooks`:未发现关闭。用户级和项目级配置、cc-web thread config、app-server 启动参数均未发现 `hooks = false`。
|
||||
- 断点 4 command hook review/trust:不通。app-server 只读 `hooks/list` 返回当前 `/home/cc-web/.codex/hooks.json` 的 7 条 command hook 全部 `trustStatus: "untrusted"`。
|
||||
- 断点 6 marker hook:不再需要作为前置验证。既然 `hooks/list` 已确认 command hooks 未 trust,按官方语义这些非 managed command hooks 会被跳过;marker 只有在 trust 后仍不触发时才需要。
|
||||
|
||||
### Command Hook Trust 明细
|
||||
|
||||
当前项目 7 条 hook 均来自 `/home/cc-web/.codex/hooks.json`,均 `enabled: true`、`source: project`,但均 `trustStatus: "untrusted"`:
|
||||
|
||||
- `preToolUse`: `sha256:a3f36079079ee92b6d39f0ca263b0d23fadafdd4bd2eae8ef3982af4e446fd8b`
|
||||
- `permissionRequest`: `sha256:db03c3a6c225bdb10010a5b2101d8fcb986d1ee5b2838746d8f003e952d2d08e`
|
||||
- `postToolUse`: `sha256:7e6a12765c74be11e44fcfcb8f4e6534f138913fc76c539993f62b6e03da6e7a`
|
||||
- `preCompact`: `sha256:73ec586017f031e8b216256e2eab41a7de8b45395643ce4461c76f212ad9a0ca`
|
||||
- `sessionStart`: `sha256:b7e002f1913670e919eb42947a8b5dd7c5a9b448b07d1b0aa86bcded841109c0`
|
||||
- `userPromptSubmit`: `sha256:cd4a71b110e2fdeae8eb47b47bae3e4786baa20b841f35032267cf6914567409`
|
||||
- `stop`: `sha256:cd543e4852e7bc63704908c599ba9cf0bfc6662b7bd8fe2fea7738cd59d641b7`
|
||||
|
||||
### cc-web Codex App 接入链路线程
|
||||
|
||||
- 当前 `local` 模式下,cc-web 不主动阻断 Codex App hooks 加载:app-server 继承 `process.env`,不剥离 `HOME` / `CODEX_HOME`;`thread/start` 和 `thread/resume` 都传入会话 `cwd`。
|
||||
- `codexAppThreadConfig()` 当前只组装 `mcp_servers.*`,没有 hooks/trust 注入;这符合“不走未文档化 `thread/start.config.hooks.*` 主路径”的判断。
|
||||
- 当前本机 `config/codex.json` 为 `mode: "local"`,所以 `custom` 模式的 `CODEX_HOME` 隔离风险未激活。
|
||||
- 风险:`custom` 模式会将 `CODEX_HOME` 指到 `config/codex-runtime-home`,该目录目前只写认证/model provider 配置,不复制 hooks 配置;如果用户级 hooks 依赖 `~/.codex/hooks.json`,custom 模式可能导致用户级 hooks 不加载。
|
||||
- cc-web 当前没有 Codex App hooks/trust 诊断 UI 或日志;已有日志能看到 app-server 初始化、collaborationMode、MCP startup,但不能直接展示 hook 是否发现、是否跳过、trustStatus 是什么。
|
||||
- 建议:增加只读 hooks 诊断能力,展示 app-server `hooks/list` 的 source/sourcePath/currentHash/trustStatus;custom 模式增加 `CODEX_HOME` 隔离提示或继承策略。
|
||||
|
||||
### hapi 对齐线程
|
||||
|
||||
- hapi 的 app-server 路径主要依赖 JSON-RPC 事件流、`thread/start.config["mcp_servers.hapi"]` 和 `turn/start.collaborationMode`,未看到 app-server 路径主动注入 hooks。
|
||||
- hapi 的 hooks 主要用于本地 Codex CLI 路径,通过 `-c hooks.SessionStart=...` 做 transcript/session 发现;这不是 app-server `thread/start.config.hooks.*` 机制。
|
||||
- hapi 因此不能作为“cc-web 应该把 hooks 注入 thread/start”的证据;它反而支持:MCP 走 `thread/start.config.mcp_servers.*`,hooks 不走未文档化的 runtime 注入。
|
||||
- hapi 也不能推翻官方 hooks 机制或本机 app-server `hooks/list` 结果;它只能说明 hapi 当前产品路径没有依赖 app-server hooks。
|
||||
- 对 cc-web 的结论应拆开:
|
||||
- 运行 hooks 的主权仍属于 Codex/app-server 原生 hooks 机制。
|
||||
- cc-web 不应模拟执行 hooks,也不应发明 `thread/start.config.hooks.*` 主路径。
|
||||
- cc-web 应补的是诊断/信任入口:展示 app-server `hooks/list` 的 trust 状态,并引导用户 review/trust。
|
||||
|
||||
### 本地 planning-with-files hooks 条件线程
|
||||
|
||||
- 本地 `planning-with-files` 脚本和配置本身可用:`.codex/hooks.json` 配置了 `SessionStart`、`UserPromptSubmit`、`PreToolUse`、`PermissionRequest`、`PostToolUse`、`PreCompact`、`Stop`。
|
||||
- 当前项目根存在 `task_plan.md`、`findings.md`、`progress.md`,因此当前时刻满足 root active plan 条件;但这些文件是本轮验证中新建的,不能倒推出目标会话发生时也存在 active plan。
|
||||
- 手动 smoke test 通过:`sh .codex/hooks/user-prompt-submit.sh` 能输出 `[planning-with-files] ACTIVE PLAN`;`pre_tool_use.py` / `post_tool_use.py` adapter 能输出预期 JSON 或 progress 提醒。
|
||||
- 无 active plan 时 hook 会静默;`.planning/sessions/` 存在但当前 session 未 attached 时也会静默。
|
||||
- 多数 `.sh` hook 脚本没有 `+x`,但 `.codex/hooks.json` 使用 `sh script` / `python3 script` 调用,因此执行位不是当前阻断点。
|
||||
- 与断点 4 合并后的结论:本地脚本没坏,当前阻断点在 Codex command hook trust,7 条项目 command hook 均为 `untrusted`,所以正常 app-server 路径会跳过它们。
|
||||
- marker 方案仍可用于 trust 后二次验证;但在 hooks 未 trust 前,marker 不出现只能证明 trust 拦截,不再是定位根因的必要步骤。
|
||||
@@ -170,13 +170,14 @@ function createCodexRolloutStore(deps) {
|
||||
return walkFiles(codexSessionsDir, []).filter((filePath) => filePath.endsWith('.jsonl')).sort().reverse();
|
||||
}
|
||||
|
||||
function getImportedCodexThreadIds() {
|
||||
function getImportedCodexThreadIds(agent = 'codex') {
|
||||
const field = agent === 'codexapp' ? 'codexAppThreadId' : 'codexThreadId';
|
||||
const imported = new Set();
|
||||
try {
|
||||
for (const f of fs.readdirSync(sessionsDir).filter((name) => name.endsWith('.json'))) {
|
||||
try {
|
||||
const session = normalizeSession(JSON.parse(fs.readFileSync(path.join(sessionsDir, f), 'utf8')));
|
||||
if (session.codexThreadId) imported.add(session.codexThreadId);
|
||||
if (session[field]) imported.add(session[field]);
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
20
progress.md
Normal file
20
progress.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# cc-web Codex hooks 验证进度
|
||||
|
||||
## Log
|
||||
|
||||
- 2026-06-30T00:00:00+08:00 使用 `planning-with-files` 建立本次验证计划。
|
||||
- 2026-06-30T00:00:00+08:00 确认根目录此前没有 `task_plan.md`、`findings.md`、`progress.md`;工作区存在用户已有未提交改动,主线程不会触碰。
|
||||
- 2026-06-30T00:00:00+08:00 先创建 4 个背景子线程:官方文档、cc-web 链路、hapi 对齐、本地 hooks 条件。
|
||||
- 2026-06-30T00:00:00+08:00 根据用户纠正,收敛到 6 个精准断点,并分别创建子线程验证。
|
||||
- 2026-06-30T00:00:00+08:00 官方文档背景线程已返回:确认 hooks 属于官方 config layer 发现机制,未确认 `thread/start.config.hooks.*` 是稳定注入路径。
|
||||
- 2026-06-30T00:00:00+08:00 断点 1、2、3、5 已有子线程/主线程证据:当前通或未关闭;断点 4、6 仍是关键未闭环点。
|
||||
- 2026-06-30T00:00:00+08:00 断点 4 已返回直接证据:app-server `hooks/list` 显示项目 7 条 command hook 全部 untrusted。marker 验证不再是定位根因的必要步骤。
|
||||
- 2026-06-30T00:00:00+08:00 cc-web 接入链路线程已纳入:local 模式不阻断 hooks;custom 模式存在 CODEX_HOME 隔离风险;当前缺少 hooks/trust 诊断入口。
|
||||
- 2026-06-30T00:00:00+08:00 hapi 对齐线程已纳入:hapi app-server 路径没用 hooks 注入,支持“不走 thread/start.config.hooks.*”;但不能否定 Codex 原生 hooks/list/trust 机制。
|
||||
- 2026-06-30T00:00:00+08:00 本地 planning-with-files 条件线程已纳入:脚本和 active-plan 检测本身可用;当前阻断点仍是 command hook untrusted。
|
||||
- 2026-06-30T00:00:00+08:00 断点 3 返回后完成总线收敛:项目根 `/home/cc-web` 已 trusted,但这不等于 `.codex/hooks.json` 里的每条 command hook 已通过 hash trust。最终结论锁定为 command hook trust 阻断。
|
||||
- 2026-06-30T00:00:00+08:00 断点 4 正式返回并确认:app-server 只读 `hooks/list` 对当前 `/home/cc-web/.codex/hooks.json` 返回 7 条 command hook,全部 `enabled=true`、`source=project`、`trustStatus=untrusted`。这是本轮最直接根因证据。
|
||||
- 2026-06-30T00:00:00+08:00 断点 5 正式返回并确认:用户级 `[features]` 未设置 `hooks=false`,项目 `.codex/config.toml` 无 `[features]`,cc-web thread config/app-server 参数/环境变量均未发现关闭 hooks 的配置。hooks feature 开关不是阻断点。
|
||||
- 2026-06-30T00:00:00+08:00 断点 6 隔离 marker 验证返回:临时 app-server 能通过 `hooks/list` 发现 `/tmp` 项目 hook,状态为 `enabled=true/source=project/trustStatus=untrusted`;`turn/start` 可触发但 marker 不出现、无 hook started/completed 通知。结论是“加载但未 trust”,不是“未加载 hooks”。
|
||||
- 2026-06-30T00:00:00+08:00 修复入口核验:本机 Codex CLI 0.140.0 帮助中没有公开 `hooks trust` 子命令,只有 `--dangerously-bypass-hook-trust`;app-server schema 暴露 `hooks/list` 和 `HookTrustStatus`,未发现公开 `hooks/trust` 请求。产品修复第一阶段应先做诊断/引导,不直接发明 trust 写入。
|
||||
- 2026-06-30T00:00:00+08:00 用户执行 Codex hook review 后复核:`/home/hdzx/.codex/config.toml` 新增 `[hooks.state]` 下 7 条 `/home/cc-web/.codex/hooks.json:<event>:0:0` 的 `trusted_hash`;按 schema 用 `hooks/list { cwds: ["/home/cc-web"] }` 复查,7 条 project hooks 均返回 `trustStatus=trusted`。
|
||||
@@ -4057,13 +4057,10 @@
|
||||
});
|
||||
}
|
||||
if (importSessionBtn) {
|
||||
if (isCodexAppAgent(currentAgent)) {
|
||||
importSessionBtn.textContent = 'Codex App 暂不支持导入';
|
||||
importSessionBtn.disabled = true;
|
||||
} else {
|
||||
importSessionBtn.textContent = currentAgent === 'codex' ? '导入本地 Codex 会话' : '导入本地 Claude 会话';
|
||||
importSessionBtn.disabled = false;
|
||||
}
|
||||
importSessionBtn.textContent = isCodexLikeAgent(currentAgent)
|
||||
? `导入本地 ${AGENT_LABELS[currentAgent]} 会话`
|
||||
: '导入本地 Claude 会话';
|
||||
importSessionBtn.disabled = false;
|
||||
}
|
||||
updateReloadMcpButtonUI();
|
||||
}
|
||||
@@ -8297,9 +8294,7 @@
|
||||
});
|
||||
importSessionBtn.addEventListener('click', () => {
|
||||
newChatDropdown.hidden = true;
|
||||
if (isCodexAppAgent(currentAgent)) {
|
||||
appendError('Codex App 模式暂不支持导入本地会话。');
|
||||
} else if (currentAgent === 'codex') {
|
||||
if (isCodexLikeAgent(currentAgent)) {
|
||||
showImportCodexSessionModal();
|
||||
} else {
|
||||
showImportSessionModal();
|
||||
@@ -9970,7 +9965,15 @@
|
||||
}
|
||||
|
||||
function showImportCodexSessionModal() {
|
||||
if (currentAgent !== 'codex') return;
|
||||
if (!isCodexLikeAgent(currentAgent)) return;
|
||||
const importAgent = currentAgent;
|
||||
const label = AGENT_LABELS[importAgent] || 'Codex';
|
||||
const contextTitle = importAgent === 'codexapp'
|
||||
? '从 Codex App rollout 历史导入'
|
||||
: '从 Codex rollout 历史导入';
|
||||
const contextCopy = importAgent === 'codexapp'
|
||||
? '读取 ~/.codex/sessions/ 下的 rollout 文件,恢复对话文本、工具调用和 token 统计,并绑定 Codex App 线程用于后续续接。'
|
||||
: '读取 ~/.codex/sessions/ 下的 rollout 文件,恢复用户消息、助手输出、函数调用和 token 统计。';
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.id = 'import-codex-session-overlay';
|
||||
@@ -9978,12 +9981,12 @@
|
||||
overlay.innerHTML = `
|
||||
<div class="modal-panel modal-panel-wide">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">导入本地 Codex 会话</span>
|
||||
<span class="modal-title">导入本地 ${escapeHtml(label)} 会话</span>
|
||||
<button class="modal-close-btn" id="ics-close-btn">✕</button>
|
||||
</div>
|
||||
<div class="modal-body" id="ics-body">
|
||||
${buildAgentContextCard('codex', '从 Codex rollout 历史导入', '读取 ~/.codex/sessions/ 下的 rollout 文件,恢复用户消息、助手输出、函数调用和 token 统计。')}
|
||||
<div class="modal-loading">正在加载 Codex 本地历史…</div>
|
||||
${buildAgentContextCard(importAgent, contextTitle, contextCopy)}
|
||||
<div class="modal-loading">正在加载 ${escapeHtml(label)} 本地历史…</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -10002,11 +10005,11 @@
|
||||
const body = overlay.querySelector('#ics-body');
|
||||
if (!body) return;
|
||||
if (!items || items.length === 0) {
|
||||
body.innerHTML = `${buildAgentContextCard('codex', '从 Codex rollout 历史导入', '读取 ~/.codex/sessions/ 下的 rollout 文件,恢复用户消息、助手输出、函数调用和 token 统计。')}<div class="modal-empty">未找到本地 Codex 会话</div>`;
|
||||
body.innerHTML = `${buildAgentContextCard(importAgent, contextTitle, contextCopy)}<div class="modal-empty">未找到本地 ${escapeHtml(label)} 会话</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
body.innerHTML = buildAgentContextCard('codex', '从 Codex rollout 历史导入', '读取 ~/.codex/sessions/ 下的 rollout 文件,恢复用户消息、助手输出、函数调用和 token 统计。');
|
||||
body.innerHTML = buildAgentContextCard(importAgent, contextTitle, contextCopy);
|
||||
items.forEach((sess) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'import-item';
|
||||
@@ -10050,11 +10053,11 @@
|
||||
btn.textContent = sess.alreadyImported ? '重新导入' : '导入';
|
||||
btn.addEventListener('click', () => {
|
||||
const confirmed = sess.alreadyImported
|
||||
? confirm('已导入过此 Codex 会话,重新导入将覆盖已有内容。确认继续?')
|
||||
: confirm('将解析本地 Codex rollout 历史并导入当前 Web 视图。确认继续?');
|
||||
? confirm(`已导入过此 ${label} 会话,重新导入将覆盖已有内容。确认继续?`)
|
||||
: confirm(`将解析本地 ${label} rollout 历史并导入当前 Web 视图。确认继续?`);
|
||||
if (!confirmed) return;
|
||||
close();
|
||||
send({ type: 'import_codex_session', threadId: sess.threadId, rolloutPath: sess.rolloutPath });
|
||||
send({ type: 'import_codex_session', agent: importAgent, threadId: sess.threadId, rolloutPath: sess.rolloutPath });
|
||||
});
|
||||
|
||||
item.appendChild(info);
|
||||
@@ -10063,7 +10066,7 @@
|
||||
});
|
||||
};
|
||||
|
||||
send({ type: 'list_codex_sessions' });
|
||||
send({ type: 'list_codex_sessions', agent: importAgent });
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
@@ -313,16 +313,22 @@ function createFakeClaudeHistory(homeDir) {
|
||||
return { sessionId, projectDir: 'tmp-project', filePath };
|
||||
}
|
||||
|
||||
function createFakeCodexHistory(homeDir) {
|
||||
function createFakeCodexHistory(homeDir, options = {}) {
|
||||
const sessionsDir = path.join(homeDir, '.codex', 'sessions', '2026', '03', '12');
|
||||
mkdirp(sessionsDir);
|
||||
const threadId = 'codex-import-thread';
|
||||
const rolloutPath = path.join(sessionsDir, 'rollout-2026-03-12T00-00-00-codex-import-thread.jsonl');
|
||||
const threadId = options.threadId || 'codex-import-thread';
|
||||
const cwd = options.cwd || '/tmp/project-b';
|
||||
const userText = options.userText || 'Codex import prompt';
|
||||
const answerText = options.answerText || 'Codex import answer';
|
||||
const source = options.source || 'exec';
|
||||
const cliVersion = options.cliVersion || '0.114.0';
|
||||
const fileStamp = options.fileStamp || '2026-03-12T00-00-00';
|
||||
const rolloutPath = path.join(sessionsDir, `rollout-${fileStamp}-${threadId}.jsonl`);
|
||||
const rolloutLines = [
|
||||
JSON.stringify({
|
||||
timestamp: '2026-03-12T00:00:00.000Z',
|
||||
type: 'session_meta',
|
||||
payload: { id: threadId, cwd: '/tmp/project-b', cli_version: '0.114.0', source: 'exec' },
|
||||
payload: { id: threadId, cwd, cli_version: cliVersion, source },
|
||||
}),
|
||||
JSON.stringify({
|
||||
timestamp: '2026-03-12T00:00:00.100Z',
|
||||
@@ -336,7 +342,7 @@ function createFakeCodexHistory(homeDir) {
|
||||
JSON.stringify({
|
||||
timestamp: '2026-03-12T00:00:01.000Z',
|
||||
type: 'event_msg',
|
||||
payload: { type: 'user_message', message: 'Codex import prompt' },
|
||||
payload: { type: 'user_message', message: userText },
|
||||
}),
|
||||
JSON.stringify({
|
||||
timestamp: '2026-03-12T00:00:02.000Z',
|
||||
@@ -344,7 +350,7 @@ function createFakeCodexHistory(homeDir) {
|
||||
payload: {
|
||||
type: 'message',
|
||||
role: 'assistant',
|
||||
content: [{ type: 'output_text', text: 'Codex import answer' }],
|
||||
content: [{ type: 'output_text', text: answerText }],
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
@@ -419,7 +425,7 @@ function createFakeCodexHistory(homeDir) {
|
||||
estimated_bytes INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
INSERT INTO threads (id, rollout_path, created_at, updated_at, source, model_provider, cwd, title, sandbox_policy, approval_mode, cli_version)
|
||||
VALUES ('${threadId}', '${rolloutPath.replace(/'/g, "''")}', 1, 2, 'exec', 'OpenAI', '/tmp/project-b', 'Codex import prompt', '{}', 'never', '0.114.0');
|
||||
VALUES ('${threadId}', '${rolloutPath.replace(/'/g, "''")}', 1, 2, '${source}', 'OpenAI', '${cwd.replace(/'/g, "''")}', '${userText.replace(/'/g, "''")}', '{}', 'never', '${cliVersion}');
|
||||
INSERT INTO logs (ts, ts_nanos, level, target, thread_id) VALUES (1, 0, 'INFO', 'test', '${threadId}');
|
||||
`);
|
||||
|
||||
@@ -500,6 +506,15 @@ function assertFrontendGenerationControlsContract() {
|
||||
!source.includes(staleDefaultApprovalWarning),
|
||||
'Frontend should not show the stale default-mode approval warning after Codex App approvals are supported'
|
||||
);
|
||||
assert(
|
||||
!source.includes('Codex App 暂不支持导入') && !source.includes('Codex App 模式暂不支持导入'),
|
||||
'Frontend should not disable Codex App native session import'
|
||||
);
|
||||
assert(
|
||||
source.includes("send({ type: 'list_codex_sessions', agent: importAgent })") &&
|
||||
source.includes("send({ type: 'import_codex_session', agent: importAgent"),
|
||||
'Frontend Codex import modal should pass the selected Codex-like agent'
|
||||
);
|
||||
}
|
||||
|
||||
function assertFrontendComposerMcpContract() {
|
||||
@@ -674,6 +689,14 @@ async function main() {
|
||||
createFakeClaudeHistory(homeDir);
|
||||
createFakeCodexConfig(homeDir);
|
||||
const codexFixture = createFakeCodexHistory(homeDir);
|
||||
const codexAppImportFixture = createFakeCodexHistory(homeDir, {
|
||||
threadId: 'codexapp-import-thread',
|
||||
cwd: '/tmp/project-c',
|
||||
userText: 'Codex App import prompt',
|
||||
answerText: 'Codex App import answer',
|
||||
source: 'vscode',
|
||||
fileStamp: '2026-03-12T00-00-10',
|
||||
});
|
||||
|
||||
const port = await getFreePort();
|
||||
const password = 'Regression!234';
|
||||
@@ -1870,6 +1893,48 @@ async function main() {
|
||||
assert(importedCodex.messages?.[0]?.content === 'Codex import prompt', 'Codex import kept wrapper instructions');
|
||||
assert(importedCodex.totalUsage?.inputTokens === 20, 'Codex import usage parse failed');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'list_codex_sessions', agent: 'codexapp' }));
|
||||
const codexAppImportSessions = await nextMessage(messages, ws, (msg) => msg.type === 'codex_sessions');
|
||||
const codexAppImportItem = codexAppImportSessions.sessions.find((item) => item.threadId === codexAppImportFixture.threadId);
|
||||
assert(codexAppImportItem, 'Codex App session listing failed');
|
||||
assert(codexAppImportItem.agent === 'codexapp', 'Codex App import listing should echo target agent');
|
||||
assert(codexAppImportItem.alreadyImported === false, 'Codex App import should not reuse old Codex imported state');
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'import_codex_session',
|
||||
agent: 'codexapp',
|
||||
threadId: codexAppImportItem.threadId,
|
||||
rolloutPath: codexAppImportItem.rolloutPath,
|
||||
}));
|
||||
const importedCodexApp = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codexapp' && msg.title === 'Codex App import prompt');
|
||||
assert(importedCodexApp.messages?.[0]?.content === 'Codex App import prompt', 'Codex App import parsed wrong first message');
|
||||
assert(importedCodexApp.totalUsage?.inputTokens === 20, 'Codex App import usage parse failed');
|
||||
const importedCodexAppPath = path.join(sessionsDir, `${importedCodexApp.sessionId}.json`);
|
||||
const storedImportedCodexApp = JSON.parse(fs.readFileSync(importedCodexAppPath, 'utf8'));
|
||||
assert(storedImportedCodexApp.agent === 'codexapp', 'Codex App import should persist codexapp agent');
|
||||
assert(storedImportedCodexApp.codexAppThreadId === codexAppImportFixture.threadId, 'Codex App import should persist codexAppThreadId');
|
||||
assert(!storedImportedCodexApp.codexThreadId, 'Codex App import should not persist legacy codexThreadId');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'list_codex_sessions', agent: 'codexapp' }));
|
||||
const codexAppImportSessionsAfter = await nextMessage(messages, ws, (msg) => msg.type === 'codex_sessions');
|
||||
const codexAppImportItemAfter = codexAppImportSessionsAfter.sessions.find((item) => item.threadId === codexAppImportFixture.threadId);
|
||||
assert(codexAppImportItemAfter?.alreadyImported === true, 'Codex App import listing should mark codexAppThreadId as imported');
|
||||
|
||||
ws.send(JSON.stringify({ type: 'delete_session', sessionId: importedCodexApp.sessionId }));
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && !msg.sessions.some((s) => s.id === importedCodexApp.sessionId));
|
||||
assert(!fs.existsSync(importedCodexAppPath), 'Deleting Codex App imported session did not remove cc-web session JSON');
|
||||
assert(fs.existsSync(codexAppImportFixture.rolloutPath), 'Deleting Codex App imported session should keep rollout history for recovery');
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'import_codex_session',
|
||||
agent: 'codexapp',
|
||||
threadId: codexAppImportFixture.threadId,
|
||||
rolloutPath: codexAppImportFixture.rolloutPath,
|
||||
}));
|
||||
const restoredCodexApp = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codexapp' && msg.title === 'Codex App import prompt');
|
||||
assert(restoredCodexApp.sessionId !== importedCodexApp.sessionId, 'Codex App deleted session should be recreated from rollout history');
|
||||
assert(restoredCodexApp.messages?.[0]?.content === 'Codex App import prompt', 'Codex App re-import should restore messages after cc-web deletion');
|
||||
|
||||
const importedSessionId = importedCodex.sessionId;
|
||||
ws.send(JSON.stringify({ type: 'delete_session', sessionId: importedSessionId }));
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && !msg.sessions.some((s) => s.id === importedSessionId));
|
||||
|
||||
23
server.js
23
server.js
@@ -6314,7 +6314,7 @@ wss.on('connection', (ws, req) => {
|
||||
handleImportNativeSession(ws, msg);
|
||||
break;
|
||||
case 'list_codex_sessions':
|
||||
handleListCodexSessions(ws);
|
||||
handleListCodexSessions(ws, msg);
|
||||
break;
|
||||
case 'import_codex_session':
|
||||
handleImportCodexSession(ws, msg);
|
||||
@@ -9878,8 +9878,13 @@ function handleImportNativeSession(ws, msg) {
|
||||
sendSessionList(ws);
|
||||
}
|
||||
|
||||
function handleListCodexSessions(ws) {
|
||||
const imported = getImportedCodexThreadIds();
|
||||
function resolveCodexImportAgent(value) {
|
||||
return value === 'codexapp' ? 'codexapp' : 'codex';
|
||||
}
|
||||
|
||||
function handleListCodexSessions(ws, msg = {}) {
|
||||
const importAgent = resolveCodexImportAgent(msg?.agent);
|
||||
const imported = getImportedCodexThreadIds(importAgent);
|
||||
const items = [];
|
||||
const seen = new Set();
|
||||
for (const filePath of getCodexRolloutFiles()) {
|
||||
@@ -9896,6 +9901,7 @@ function handleListCodexSessions(ws) {
|
||||
cliVersion: parsed.meta.cliVersion || '',
|
||||
source: parsed.meta.source || '',
|
||||
rolloutPath: filePath,
|
||||
agent: importAgent,
|
||||
alreadyImported: imported.has(parsed.meta.threadId),
|
||||
});
|
||||
}
|
||||
@@ -9904,6 +9910,7 @@ function handleListCodexSessions(ws) {
|
||||
|
||||
function handleImportCodexSession(ws, msg) {
|
||||
const threadId = String(msg?.threadId || '').trim();
|
||||
const importAgent = resolveCodexImportAgent(msg?.agent);
|
||||
if (!threadId) {
|
||||
return wsSend(ws, { type: 'error', message: '缺少 threadId' });
|
||||
}
|
||||
@@ -9932,7 +9939,8 @@ function handleImportCodexSession(ws, msg) {
|
||||
for (const f of fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith('.json'))) {
|
||||
try {
|
||||
const s = normalizeSession(JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf8')));
|
||||
if (s.codexThreadId === threadId) { existingSession = s; break; }
|
||||
const runtimeThreadId = importAgent === 'codexapp' ? s.codexAppThreadId : s.codexThreadId;
|
||||
if (runtimeThreadId === threadId) { existingSession = s; break; }
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
@@ -9944,10 +9952,11 @@ function handleImportCodexSession(ws, msg) {
|
||||
created: existingSession?.created || new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
pinnedAt: existingSession?.pinnedAt || null,
|
||||
agent: 'codex',
|
||||
agent: importAgent,
|
||||
claudeSessionId: null,
|
||||
codexThreadId: threadId,
|
||||
importedFrom: 'codex',
|
||||
codexThreadId: importAgent === 'codex' ? threadId : null,
|
||||
codexAppThreadId: importAgent === 'codexapp' ? threadId : null,
|
||||
importedFrom: importAgent,
|
||||
importedRolloutPath: parsed.filePath,
|
||||
model: existingSession?.model || null,
|
||||
permissionMode: existingSession?.permissionMode || 'yolo',
|
||||
|
||||
43
task_plan.md
Normal file
43
task_plan.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# cc-web Codex hooks 验证计划
|
||||
|
||||
## Goal
|
||||
|
||||
验证 cc-web 的 Codex App 模式下,Codex hooks 应由谁加载、当前链路是否让 app-server 发现项目/全局 hooks,以及 `planning-with-files` hook 没体现效果的真实原因。
|
||||
|
||||
## Status
|
||||
|
||||
- [x] 建立验证计划
|
||||
- [x] 官方文档验证:确认 app-server/hooks 官方支持边界
|
||||
- [x] cc-web 实现链路验证:确认 cwd、CODEX_HOME、thread params、config 注入逻辑
|
||||
- [x] 本地环境/触发条件验证:确认 hook 文件、active plan、trust/feature 开关等条件
|
||||
- [x] 汇总结论:明确是否需要 cc-web 改造,以及改造点
|
||||
|
||||
## Focused Breakpoints
|
||||
|
||||
- [x] app-server 进程看到的 `CODEX_HOME` / `HOME` 是否正确
|
||||
- [x] `thread/start` / `thread/resume` 的 `cwd` 是否为 `/home/cc-web`
|
||||
- [x] `/home/cc-web/.codex` 项目层是否被 Codex trust
|
||||
- [x] `.codex/hooks.json` 里的 command hook 是否已 review/trust
|
||||
- [x] `[features].hooks` 是否被关闭
|
||||
- [x] 最小 marker hook 是否能证明 app-server 触发 hooks
|
||||
|
||||
## Final Synthesis
|
||||
|
||||
- 当前目标会话没看到 `planning-with-files` hook 效果,主因是项目 hooks 的 command hash 未被 Codex trust:`hooks/list` 已显示 7 条 command hook 全部 `untrusted`。
|
||||
- `HOME/CODEX_HOME`、`thread cwd`、项目根 trust、`[features].hooks` 这些前置条件在当前 local 模式下都不是阻断点。
|
||||
- `planning-with-files` 脚本本身有效;手动 smoke test 可输出 active plan。它的效果被 Codex command hook trust 层挡住,而不是 skill 逻辑坏。
|
||||
- cc-web 不应自行模拟执行 hooks,也不应把 `thread/start.config.hooks.*` 当主路径;应让 Codex app-server 原生 hooks/list/trust 机制工作,并补诊断/信任入口。
|
||||
|
||||
## Questions
|
||||
|
||||
1. `thread/start.config.hooks.*` 是否有官方文档支持,还是不应作为主路径?
|
||||
2. cc-web 是否应该自行执行 hooks,还是只保证 app-server 能看到官方 config layers?
|
||||
3. 当前项目 hooks 没生效,更可能是哪一层断了?
|
||||
4. 如何用最小可复现实验证明 app-server 端是否加载并执行 hooks?
|
||||
|
||||
## Constraints
|
||||
|
||||
- 不修改业务代码。
|
||||
- 不覆盖用户已有未提交改动。
|
||||
- 代码理解优先使用 `codebase-memory-mcp`,不用 graphify。
|
||||
- 官方 Codex 行为优先参考 OpenAI 官方文档。
|
||||
Reference in New Issue
Block a user