From 062f71eac35263427d37318508798a975c6ded33 Mon Sep 17 00:00:00 2001 From: Donovan! Date: Mon, 20 Jul 2026 01:48:32 -0400 Subject: [PATCH 1/7] chore(ci): reproduce current master regressions --- .github/ci-regression-probe.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/ci-regression-probe.md diff --git a/.github/ci-regression-probe.md b/.github/ci-regression-probe.md new file mode 100644 index 0000000..0b2ed7b --- /dev/null +++ b/.github/ci-regression-probe.md @@ -0,0 +1,3 @@ +# CI regression probe + +Temporary branch-only marker used to reproduce the current `master` failures through pull-request-triggered workflows. It will be removed before merge. From d6b7085a3e05d7ab7ce5b5928dda8ea022a14a78 Mon Sep 17 00:00:00 2001 From: Donovan! Date: Mon, 20 Jul 2026 01:54:14 -0400 Subject: [PATCH 2/7] fix(embeddings): preserve configured provider failures --- src/embeddings.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/embeddings.ts b/src/embeddings.ts index 1ccdf9d..cd33175 100644 --- a/src/embeddings.ts +++ b/src/embeddings.ts @@ -1,4 +1,4 @@ -import type { PluginConfig } from './types.js'; +import type { PluginConfig } from './types.js'; import { requestRemoteEmbedding } from './embedding-provider-client.js'; import { averageEmbeddings, @@ -58,21 +58,17 @@ export class EmbeddingGenerator { return results; } - private async generateChunk(chunk: EmbeddingChunk): Promise { + private generateChunk(chunk: EmbeddingChunk): Promise { if (this.provider === 'hash') { - return hashEmbedding(chunk.content, this.dimensions); - } - try { - return await requestRemoteEmbedding({ - provider: this.provider, - model: this.config.embeddingModel, - dimensions: this.dimensions, - apiKey: this.config.embeddingApiKey, - apiUrl: this.config.embeddingApiUrl, - }, chunk.content); - } catch { - return hashEmbedding(chunk.content, this.dimensions); + return Promise.resolve(hashEmbedding(chunk.content, this.dimensions)); } + return requestRemoteEmbedding({ + provider: this.provider, + model: this.config.embeddingModel, + dimensions: this.dimensions, + apiKey: this.config.embeddingApiKey, + apiUrl: this.config.embeddingApiUrl, + }, chunk.content); } } From 2e0228f1a3c47e342618fa1a2bd3d887d4f3eaa0 Mon Sep 17 00:00:00 2001 From: Donovan! Date: Mon, 20 Jul 2026 01:54:56 -0400 Subject: [PATCH 3/7] fix(wiki): enforce host-independent manifest containment --- src/wiki-export-files.ts | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/wiki-export-files.ts b/src/wiki-export-files.ts index 7a7efdf..e46c2e1 100644 --- a/src/wiki-export-files.ts +++ b/src/wiki-export-files.ts @@ -1,6 +1,6 @@ import { randomUUID } from 'node:crypto'; import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'; -import { dirname, isAbsolute, relative, resolve } from 'node:path'; +import { dirname, posix, win32 } from 'node:path'; import { renderLogEntry, type LogEntry, type RenderedNote } from './wiki-note-renderer.js'; import type { WikiExportManifest } from './wiki-export-types.js'; @@ -55,18 +55,40 @@ export function pruneOwnedFiles(outputDir: string, paths: string[]): number { } export function resolveOwnedPath(outputDir: string, manifestPath: string): string { - if (!manifestPath || isAbsolute(manifestPath)) { + const windowsRoot = isWindowsPath(outputDir) + || (!posix.isAbsolute(outputDir) && process.platform === 'win32'); + const pathApi = windowsRoot ? win32 : posix; + const containsTraversal = manifestPath.split(/[\\/]/u).includes('..'); + + if ( + !manifestPath + || manifestPath.includes('\0') + || containsTraversal + || /^[A-Za-z]:/u.test(manifestPath) + || win32.isAbsolute(manifestPath) + || posix.isAbsolute(manifestPath) + ) { throw new Error(`Unsafe wiki manifest path: ${manifestPath}`); } - const root = resolve(outputDir); - const candidate = resolve(root, manifestPath); - const fromRoot = relative(root, candidate); - if (!fromRoot || fromRoot.startsWith('..') || isAbsolute(fromRoot)) { + + const root = pathApi.resolve(outputDir); + const candidate = pathApi.resolve(root, manifestPath); + const fromRoot = pathApi.relative(root, candidate); + if ( + !fromRoot + || fromRoot === '..' + || fromRoot.startsWith(`..${pathApi.sep}`) + || pathApi.isAbsolute(fromRoot) + ) { throw new Error(`Unsafe wiki manifest path: ${manifestPath}`); } return candidate; } +function isWindowsPath(value: string): boolean { + return /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith('\\\\'); +} + export function appendExportLog(outputDir: string, entry: LogEntry): void { const logPath = resolveOwnedPath(outputDir, 'log.md'); const existing = existsSync(logPath) ? readFileSync(logPath, 'utf8') : ''; From 982260d365063151d034951b3124de72857a04c2 Mon Sep 17 00:00:00 2001 From: Donovan! Date: Mon, 20 Jul 2026 01:56:21 -0400 Subject: [PATCH 4/7] fix(redaction): protect standalone paths containing spaces --- src/redactor.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/redactor.ts b/src/redactor.ts index fb2378a..f81b0f4 100644 --- a/src/redactor.ts +++ b/src/redactor.ts @@ -140,6 +140,10 @@ export class Redactor { } try { + const trimmed = text.trim(); + if (this.config.categories.path !== 'off' && looksLikeStandalonePath(trimmed)) { + return this.redactPath(text); + } return this.redactUnsafe(text); } catch { // Fail-closed: redact everything we safely can @@ -418,6 +422,16 @@ function isAbsolutePath(value: string): boolean { return pathStyle(value) === 'windows' ? win32.isAbsolute(value) : posix.isAbsolute(value); } +function looksLikeStandalonePath(value: string): boolean { + if (!isAbsolutePath(value) || /\s--?[A-Za-z0-9]/u.test(value)) return false; + const absolutePathStarts = value.match( + /(?:^|\s)(?:[A-Za-z]:[\\/]|\\\\|\/(?:home|Users|root|var|opt|etc|tmp|usr|mnt|srv|data|www|private|Volumes|Library|Applications)(?:\/|$))/gu, + ); + if ((absolutePathStarts?.length ?? 0) > 1) return false; + if (/^[A-Za-z]:[\\/]/u.test(value) || value.startsWith('\\\\')) return true; + return /^\/(?:home|Users|root|var|opt|etc|tmp|usr|mnt|srv|data|www|private|Volumes|Library|Applications)(?:\/.+)+$/u.test(value); +} + /** * Convenience function: redact a string with default config. */ From d837ed03973b6aaf2895861bb4c3a465ca1902b3 Mon Sep 17 00:00:00 2001 From: Donovan! Date: Mon, 20 Jul 2026 01:57:13 -0400 Subject: [PATCH 5/7] test(embeddings): make backfill telemetry hermetic --- test/backfill-recall-telemetry.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/backfill-recall-telemetry.test.ts b/test/backfill-recall-telemetry.test.ts index ce57278..b7b1f87 100644 --- a/test/backfill-recall-telemetry.test.ts +++ b/test/backfill-recall-telemetry.test.ts @@ -35,8 +35,8 @@ describe('Phase 19b — backfill and recall telemetry', () => { databaseUrl: buildTempDbUrl(BASE_DB_URL, tempDbName), databaseProvider: 'postgres', sqlitePath: '.data/csm-memory.db', - embeddingModel: 'nomic-embed-text', - embeddingApiUrl: process.env.OLLAMA_URL ?? 'http://localhost:11434', + embeddingModel: 'test-hash', + embeddingDimensions: 768, } as PluginConfig; let db: Database; From 2f43e087b45fbe7bbbbc7b9140c5e8014d7ff460 Mon Sep 17 00:00:00 2001 From: Donovan! Date: Mon, 20 Jul 2026 01:57:50 -0400 Subject: [PATCH 6/7] test(search): remove live embedding dependency from CI --- test/hybrid-search.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/hybrid-search.test.ts b/test/hybrid-search.test.ts index 86d173b..53d5fb7 100644 --- a/test/hybrid-search.test.ts +++ b/test/hybrid-search.test.ts @@ -60,8 +60,8 @@ async function cleanupDatabase( databaseUrl: databaseUrl(databaseName), databaseProvider: 'postgres', sqlitePath: '.data/csm-memory.db', - embeddingModel: 'nomic-embed-text', - embeddingApiUrl: process.env.OLLAMA_URL ?? 'http://localhost:11434', + embeddingModel: 'test-hash', + embeddingDimensions: 768, } as PluginConfig; let db: Database | undefined; let embeddings: EmbeddingGenerator; From e03ce10e0e098837c06206b86c1cda9a5596bf01 Mon Sep 17 00:00:00 2001 From: Donovan! Date: Mon, 20 Jul 2026 01:58:01 -0400 Subject: [PATCH 7/7] chore(ci): remove diagnostic probe --- .github/ci-regression-probe.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .github/ci-regression-probe.md diff --git a/.github/ci-regression-probe.md b/.github/ci-regression-probe.md deleted file mode 100644 index 0b2ed7b..0000000 --- a/.github/ci-regression-probe.md +++ /dev/null @@ -1,3 +0,0 @@ -# CI regression probe - -Temporary branch-only marker used to reproduce the current `master` failures through pull-request-triggered workflows. It will be removed before merge.