[codex] Dreaming: surface memory wiki imports and palace#64505
Conversation
bc5ddf6 to
03d6946
Compare
03d6946 to
12d5e37
Compare
🔒 Aisle Security AnalysisWe found 6 potential security issue(s) in this PR:
1. 🟠 Path traversal via unvalidated ChatGPT import `runId` allows arbitrary file read and rollback file write/delete
Description
This means a crafted Vulnerable code: function resolveImportRunPath(vaultRoot: string, runId: string): string {
return path.join(resolveImportRunsDir(vaultRoot), `${runId}.json`);
}
...
const record = await readImportRunRecord(params.config.vault.path, params.runId);
...
await fs.rm(path.join(params.config.vault.path, relativePath), { force: true })
...
const snapshotPath = path.join(runDir, entry.snapshotPath);
const targetPath = path.join(params.config.vault.path, entry.path);Recommendation
const RUN_ID_RE = /^chatgpt-[a-f0-9]{12}$/;
if (!RUN_ID_RE.test(runId)) {
throw new Error("Invalid run id");
}
function safeJoin(baseDir: string, rel: string): string {
const target = path.resolve(baseDir, rel);
const base = path.resolve(baseDir) + path.sep;
if (!target.startsWith(base)) throw new Error("Path traversal");
return target;
}
function resolveImportRunPath(vaultRoot: string, runId: string): string {
const dir = resolveImportRunsDir(vaultRoot);
return safeJoin(dir, `${runId}.json`);
}
2. 🟠 Path traversal / arbitrary file read via untrusted digest pagePath in getMemoryWikiPage
Description
Because Vulnerable flow:
Vulnerable code: // getMemoryWikiPage
const digestClaimPagePath = digest ? resolveDigestClaimLookup(digest, params.lookup) : null;
...
await readQueryableWikiPagesByPaths(effectiveConfig.vault.path, [digestClaimPagePath])
// readQueryableWikiPagesByPaths
const absolutePath = path.join(rootDir, relativePath);
const raw = await fs.readFile(absolutePath, "utf8");RecommendationTreat any path coming from digests (or any other non-hardcoded source) as untrusted, and enforce strict containment within the vault and expected subdirectories. Suggested fix: resolve and validate before reading. function resolveVaultPath(rootDir: string, rel: string): string {
if (!rel || path.isAbsolute(rel)) throw new Error("invalid path");
// normalize and prevent traversal
const resolved = path.resolve(rootDir, rel);
const normalizedRoot = rootDir.endsWith(path.sep) ? rootDir : rootDir + path.sep;
if (!resolved.startsWith(normalizedRoot)) throw new Error("path escapes vault");
return resolved;
}
// in readQueryableWikiPagesByPaths
const absolutePath = resolveVaultPath(rootDir, relativePath);Additionally, consider rejecting any digest 3. 🟡 Unhandled RangeError in isoFromUnix allows malformed ChatGPT export timestamps to crash import (DoS)
Description
Vulnerable code: const numeric = Number(raw);
if (!Number.isFinite(numeric)) {
return undefined;
}
return new Date(numeric * 1000).toISOString();This is reachable with an untrusted or corrupted RecommendationHarden timestamp parsing by validating/clamping the supported range and/or catching Example safe implementation: function isoFromUnix(raw: unknown): string | undefined {
if (typeof raw !== "number" && typeof raw !== "string") return undefined;
const numeric = Number(raw);
if (!Number.isFinite(numeric)) return undefined;
// Optional: clamp to reasonable unix seconds range (e.g., 1970..2100)
if (numeric < 0 || numeric > 4102444800) return undefined;
const ms = numeric * 1000;
const date = new Date(ms);
try {
return date.toISOString();
} catch {
return undefined;
}
}Also consider wrapping record parsing so one bad conversation cannot abort the whole import (skip and log). 4. 🟡 Unbounded wiki.get lineCount enables resource exhaustion (DoS) via large page parsing and response payloads
DescriptionThe
An authenticated caller (scope RecommendationEnforce strict server-side limits and avoid parsing/splitting the entire document when only a window of lines is needed.
const MAX_LINE_COUNT = 500; // or lower
const fromLine = Math.max(1, Math.floor(params.fromLine ?? 1));
const requested = Math.max(1, Math.floor(params.lineCount ?? 200));
const lineCount = Math.min(requested, MAX_LINE_COUNT);
5. 🟡 Gateway method wiki.importRuns discloses local filesystem paths (exportPath/sourcePath/pagePaths) to operator.read clients
DescriptionThe
Vulnerable code: return {
runId,
importType,
appliedAt,
exportPath,
sourcePath,
...
pagePaths,
samplePaths: pagePaths.slice(0, 5),
};If the gateway is reachable by any remote operator client (e.g., paired device/browser UI), this is an information disclosure risk. RecommendationMinimize/redact sensitive path data returned over the gateway:
Example (redact to basenames and make page paths vault-relative): import path from "node:path";
function redactPath(p: string): string {
return path.basename(p);
}
// when building response
return {
...,
exportPath: redactPath(exportPath),
sourcePath: redactPath(sourcePath),
pagePaths: pagePaths.map((p) => p.replace(/^.*?\/vault\//, "")),
};(Use the actual vault-root prefix rather than the placeholder above.) 6. 🟡 Prototype pollution vector via YAML frontmatter parsing and object spread
Description
If an attacker can place/modify markdown files in the vault (e.g., via import/sync), they can include YAML keys such as Why this is risky:
Vulnerable code: const parsed = YAML.parse(match[1]) as unknown;
return {
frontmatter:
parsed && typeof parsed === "object" && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: {},
body: content.slice(match[0].length),
};And later usage (example): frontmatter: {
...parsed.frontmatter,
pageType: "synthesis",
// ...
}RecommendationHarden frontmatter parsing by sanitizing keys and constructing frontmatter with a null prototype, or by whitelisting expected fields. Example mitigation (key sanitization + null-prototype): const UNSAFE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
export function parseWikiMarkdown(content: string): ParsedWikiMarkdown {
const match = content.match(FRONTMATTER_PATTERN);
if (!match) return { frontmatter: Object.create(null), body: content };
const parsed = YAML.parse(match[1]) as unknown;
const frontmatter = Object.create(null) as Record<string, unknown>;
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
if (UNSAFE_KEYS.has(key)) continue;
frontmatter[key] = value;
}
}
return { frontmatter, body: content.slice(match[0].length) };
}Additionally:
Analyzed PR: #64505 at commit Last updated on: 2026-04-11T05:05:03Z |
|
Merged via squash.
Thanks @mbelinky! |
Greptile SummaryAdds two new Dreaming diary subtabs — Imported Insights (topic-clustered ChatGPT import pages with synthesized signals) and Memory Palace (compiled view of pages with claims/questions/contradictions) — plus an in-Dreaming full-page wiki viewer replacing the broken sidebar path. New backend modules ( Confidence Score: 5/5Safe to merge; all findings are minor style issues with no impact on correctness or data integrity. No P0/P1 issues. The three inline comments are all P2: duplicate array entries that are dead code, a trivially wrong ternary that still produces the right output, and a double function call that is inefficient but not incorrect. Logic, normalization, and gateway wiring are solid and well-tested. No files require special attention. Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/memory-wiki/src/import-insights.ts
Line: 214-223
Comment:
**Duplicate entries in `correctionPatterns`**
`"you're right"` and `"let's reset"` each appear twice in the array. Since `.some()` short-circuits on the first match, the second occurrence of each is dead code.
```suggestion
const correctionPatterns = [
"you're right",
"bad assumption",
"let's reset",
"does not exist anymore",
"that was a bad assumption",
"what actually works today",
];
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: ui/src/ui/controllers/dreaming.ts
Line: 442
Comment:
**Dead ternary — both branches return `"chatgpt"`**
`record?.sourceType === "chatgpt" ? "chatgpt" : "chatgpt"` always evaluates to `"chatgpt"`, so the condition is silently ignored. If the gateway ever sends an unexpected `sourceType` the value will still pass through unchecked.
```suggestion
sourceType: "chatgpt",
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/memory-wiki/src/memory-palace.ts
Line: 112
Comment:
**`extractSnippet` called twice**
`extractSnippet(parsed.body)` is called once for the truthiness check and again to produce the value, parsing the body string twice per page. A local variable avoids the redundant work.
Or more readably, hoist it before the spread:
```ts
const snippet = extractSnippet(parsed.body);
// …then in the object:
...(snippet ? { snippet } : {}),
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Changelog: add dreaming memory wiki entr..." | Re-trigger Greptile |
| const correctionPatterns = [ | ||
| "you're right", | ||
| "you’re right", | ||
| "bad assumption", | ||
| "let's reset", | ||
| "let’s reset", | ||
| "does not exist anymore", | ||
| "that was a bad assumption", | ||
| "what actually works today", | ||
| ]; |
There was a problem hiding this comment.
Duplicate entries in
correctionPatterns
"you're right" and "let's reset" each appear twice in the array. Since .some() short-circuits on the first match, the second occurrence of each is dead code.
| const correctionPatterns = [ | |
| "you're right", | |
| "you’re right", | |
| "bad assumption", | |
| "let's reset", | |
| "let’s reset", | |
| "does not exist anymore", | |
| "that was a bad assumption", | |
| "what actually works today", | |
| ]; | |
| const correctionPatterns = [ | |
| "you're right", | |
| "bad assumption", | |
| "let's reset", | |
| "does not exist anymore", | |
| "that was a bad assumption", | |
| "what actually works today", | |
| ]; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/memory-wiki/src/import-insights.ts
Line: 214-223
Comment:
**Duplicate entries in `correctionPatterns`**
`"you're right"` and `"let's reset"` each appear twice in the array. Since `.some()` short-circuits on the first match, the second occurrence of each is dead code.
```suggestion
const correctionPatterns = [
"you're right",
"bad assumption",
"let's reset",
"does not exist anymore",
"that was a bad assumption",
"what actually works today",
];
```
How can I resolve this? If you propose a fix, please make it concise.| .filter((entry): entry is WikiImportInsightCluster => entry !== null) | ||
| : []; | ||
| return { | ||
| sourceType: record?.sourceType === "chatgpt" ? "chatgpt" : "chatgpt", |
There was a problem hiding this comment.
Dead ternary — both branches return
"chatgpt"
record?.sourceType === "chatgpt" ? "chatgpt" : "chatgpt" always evaluates to "chatgpt", so the condition is silently ignored. If the gateway ever sends an unexpected sourceType the value will still pass through unchecked.
| sourceType: record?.sourceType === "chatgpt" ? "chatgpt" : "chatgpt", | |
| sourceType: "chatgpt", |
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/controllers/dreaming.ts
Line: 442
Comment:
**Dead ternary — both branches return `"chatgpt"`**
`record?.sourceType === "chatgpt" ? "chatgpt" : "chatgpt"` always evaluates to `"chatgpt"`, so the condition is silently ignored. If the gateway ever sends an unexpected `sourceType` the value will still pass through unchecked.
```suggestion
sourceType: "chatgpt",
```
How can I resolve this? If you propose a fix, please make it concise.| claims: page.claims.map((claim) => claim.text).slice(0, 3), | ||
| questions: page.questions.slice(0, 3), | ||
| contradictions: page.contradictions.slice(0, 3), | ||
| ...(extractSnippet(parsed.body) ? { snippet: extractSnippet(parsed.body) } : {}), |
There was a problem hiding this comment.
extractSnippet(parsed.body) is called once for the truthiness check and again to produce the value, parsing the body string twice per page. A local variable avoids the redundant work.
Or more readably, hoist it before the spread:
const snippet = extractSnippet(parsed.body);
// …then in the object:
...(snippet ? { snippet } : {}),Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/memory-wiki/src/memory-palace.ts
Line: 112
Comment:
**`extractSnippet` called twice**
`extractSnippet(parsed.body)` is called once for the truthiness check and again to produce the value, parsing the body string twice per page. A local variable avoids the redundant work.
Or more readably, hoist it before the spread:
```ts
const snippet = extractSnippet(parsed.body);
// …then in the object:
...(snippet ? { snippet } : {}),
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 12d5e37222
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (diaryError) { | ||
| return html` | ||
| <section class="dreams-diary"> | ||
| <div class="dreams-diary__error">${diaryError}</div> | ||
| </section> |
There was a problem hiding this comment.
Keep diary subtab controls visible on load errors
When the active diary subtab has an error (wikiImportInsightsError or wikiMemoryPalaceError), this early return renders only the error block and removes the subtab/header controls, so users cannot switch back to Dreams or trigger a tab-specific reload from the UI. A single transient gateway failure on Imported Insights/Memory Palace can therefore strand the Diary view in an unrecoverable state until a full app reload resets module state.
Useful? React with 👍 / 👎.
| void Promise.all([ | ||
| loadDreamingStatus(state), | ||
| loadDreamDiary(state), | ||
| loadWikiImportInsights(state), | ||
| loadWikiMemoryPalace(state), |
There was a problem hiding this comment.
Avoid running two full wiki syncs on each Dreams refresh
This refresh path launches loadWikiImportInsights and loadWikiMemoryPalace in parallel, but both gateway methods (wiki.importInsights and wiki.palace) call syncImportedSourcesIfNeeded before responding. That means every Dreams refresh performs the same imported-source sync/index work twice, increasing latency and filesystem churn for users with larger vaults.
Useful? React with 👍 / 👎.
) Merged via squash. Prepared head SHA: 12d5e37 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
) Merged via squash. Prepared head SHA: 12d5e37 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
) Merged via squash. Prepared head SHA: 12d5e37 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
) Merged via squash. Prepared head SHA: 12d5e37 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
) Merged via squash. Prepared head SHA: 12d5e37 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
) Merged via squash. Prepared head SHA: 12d5e37 Co-authored-by: mbelinky <[email protected]> Co-authored-by: mbelinky <[email protected]> Reviewed-by: @mbelinky
What changed
This PR turns the Dreaming diary into the first usable memory-wiki observability surface.
It adds two new diary subtabs:
Imported Insights: topic-clustered imported ChatGPT chats, with synthesized summaries, candidate signals, correction signals, and click-through access to the full imported source page.Memory Palace: a compiled view over wiki pages that actually contain claims, questions, contradictions, or real synthesis/entity/concept content.It also replaces the dead sidebar path with an in-Dreaming full-page wiki viewer so imported sources and palace pages can be inspected directly from Dreaming.
Why
The previous import UI mostly surfaced metadata sludge and raw source titles. It did not give a useful view of what the system thought mattered, and the click-through path was effectively broken from Dreaming.
This gets us to a better intermediate state:
Current limitation
This PR does not implement automatic promotion from imported chats into synthesis/entity/concept pages.
On the current prod dataset,
Memory Palaceis still empty because the vault containssources/andreports/, but no real promotedsyntheses/,entities/, orconcepts/pages yet. This PR surfaces that state cleanly; a later PR should add the consolidation writer that drafts and applies those pages.Validation
Ran:
OPENCLAW_LOCAL_CHECK=0 pnpm test -- extensions/memory-wiki/src/import-insights.test.ts extensions/memory-wiki/src/memory-palace.test.ts extensions/memory-wiki/src/gateway.test.ts ui/src/ui/controllers/dreaming.test.ts ui/src/ui/views/dreaming.test.tsOPENCLAW_LOCAL_CHECK=0 pnpm buildOPENCLAW_LOCAL_CHECK=0 pnpm ui:buildAlso deployed the resulting bundle to
jpclawhqprod and verified the new Dreaming asset hashes were live.