From 98b1921ca671e941945d5f9139265621e5cac5b7 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Tue, 8 Sep 2026 16:41:46 +0700 Subject: [PATCH] fix(codex): keep commented TOML fields, follow symlinked config surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit readTomlMeta was dropping any field with a trailing `# comment` (common in real agents/*.toml). readSkills/readAgents/readRules used bare isDirectory()/isFile(), which miss symlinked entries per cc-discovery.js's own documented Dirent quirk — reused its isDirLike/isFileLike instead (exporting isDirLike from cc-discovery.js, which wasn't previously public). Also swapped the "never walks outside" test for one that actually exercises a symlink escape, and dropped the unnecessary isUnder re-export. --- server/__tests__/codex-discovery.test.js | 23 ++++++++++++++++--- server/lib/cc-discovery.js | 1 + server/lib/codex-discovery.js | 29 ++++++++++++++++-------- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/server/__tests__/codex-discovery.test.js b/server/__tests__/codex-discovery.test.js index d5f606b5..75643b9f 100644 --- a/server/__tests__/codex-discovery.test.js +++ b/server/__tests__/codex-discovery.test.js @@ -85,6 +85,14 @@ describe("codex-discovery", () => { assert.equal(meta.model, "gpt-5.3-codex-spark"); }); + it("keeps a field with a trailing inline comment, and keeps a '#' that is inside the string", () => { + const meta = cx.readTomlMeta( + 'model = "gpt-5.4" # default\n' + 'description = "uses # for headings"\n' + ); + assert.equal(meta.model, "gpt-5.4"); + assert.equal(meta.description, "uses # for headings"); + }); + it("reads a rule file's preview", () => { write( path.join(TMP, "rules", "no-force-push.rules"), @@ -119,11 +127,20 @@ describe("codex-discovery", () => { assert.equal(project.exists, false); }); - it("never walks outside the codex roots", () => { - write(path.join(TMP, "skills", "esc", "SKILL.md"), "x"); - for (const row of cx.readSkills({ cwd: REPO })) { + it("follows a symlinked skill dir (a supported install pattern) without ever reporting a path outside the codex roots", () => { + const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-disc-outside-")); + write(path.join(outside, "SKILL.md"), "---\nname: linked\n---\nbody"); + fs.symlinkSync(outside, path.join(TMP, "skills", "linked"), "dir"); + + const rows = cx.readSkills({ cwd: REPO }); + const linked = rows.find((r) => r.name === "linked"); + assert.ok(linked, "a symlinked skill directory must still be discovered"); + assert.equal(linked.frontmatter.name, "linked"); + for (const row of rows) { assert.ok(row.file.startsWith(TMP) || row.file.startsWith(REPO)); + assert.ok(!row.file.startsWith(outside)); } + fs.rmSync(outside, { recursive: true, force: true }); }); it("summarizes counts and roots in the overview", () => { diff --git a/server/lib/cc-discovery.js b/server/lib/cc-discovery.js index efdc7a94..bfdf7514 100644 --- a/server/lib/cc-discovery.js +++ b/server/lib/cc-discovery.js @@ -925,6 +925,7 @@ module.exports = { parseFrontmatter, redactSettings, isUnder, + isDirLike, isFileLike, MAX_FILE_BYTES, HOOK_EVENT_TYPES, diff --git a/server/lib/codex-discovery.js b/server/lib/codex-discovery.js index 7fe21f3c..2e78428b 100644 --- a/server/lib/codex-discovery.js +++ b/server/lib/codex-discovery.js @@ -15,7 +15,13 @@ const fs = require("node:fs"); const path = require("node:path"); const { getCodexHome, getProjectCodexDir, getProjectRoot } = require("./codex-home"); -const { parseFrontmatter, isUnder, MAX_FILE_BYTES } = require("./cc-discovery"); +const { + parseFrontmatter, + isUnder, + isDirLike, + isFileLike, + MAX_FILE_BYTES, +} = require("./cc-discovery"); function safeReadText(absPath) { try { @@ -55,7 +61,7 @@ function readSkillsAt(scope, codexDir) { const skills = []; for (const ent of listDir(dir)) { const skillDir = path.join(dir, ent.name); - if (!ent.isDirectory() || !isUnder(codexDir, skillDir)) continue; + if (!isDirLike(ent, skillDir) || !isUnder(codexDir, skillDir)) continue; const skillFile = path.join(skillDir, "SKILL.md"); const read = safeReadText(skillFile); if (!read) continue; @@ -116,10 +122,16 @@ function readTomlMeta(text) { (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); - } else if (value === "true" || value === "false") { - value = value === "true"; - } else if (/^-?\d+(\.\d+)?$/.test(value)) { - value = Number(value); + } else if (value.startsWith('"') || value.startsWith("'")) { + // A quoted value with a trailing `# comment` outside the string — + // e.g. model = "gpt-5.4" # default. Strip from the closing quote on. + const quote = value[0]; + const closeIdx = value.indexOf(quote, 1); + if (closeIdx > 0) value = value.slice(1, closeIdx); + else continue; + } else if (/^(true|false|-?\d+(\.\d+)?)\s*(#.*)?$/.test(value)) { + value = value.replace(/\s*#.*$/, ""); + value = value === "true" ? true : value === "false" ? false : Number(value); } else { continue; // arrays, inline tables, etc. — not one of the shown fields } @@ -134,7 +146,7 @@ function readAgentsAt(scope, codexDir) { for (const ent of listDir(dir)) { if (!ent.name.endsWith(".toml")) continue; const file = path.join(dir, ent.name); - if (!ent.isFile() || !isUnder(codexDir, file)) continue; + if (!isFileLike(ent, file) || !isUnder(codexDir, file)) continue; const read = safeReadText(file); if (!read) continue; agents.push({ @@ -165,7 +177,7 @@ function readRulesAt(scope, codexDir) { for (const ent of listDir(dir)) { if (!ent.name.endsWith(".rules")) continue; const file = path.join(dir, ent.name); - if (!ent.isFile() || !isUnder(codexDir, file)) continue; + if (!isFileLike(ent, file) || !isUnder(codexDir, file)) continue; const read = safeReadText(file); if (!read) continue; rules.push({ @@ -268,5 +280,4 @@ module.exports = { readConfig, readOverview, readTomlMeta, - isUnder, };