mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-06 12:52:04 +00:00
fix(semantic): delete indexes written before secrets stopped being indexed
Fixing the writer does not rewrite files already on disk, so a profile that built an index before today still has one holding stored secrets in plaintext. Reporting a version string in a status field and expecting the user to notice is not a remedy for that. Both implementations now check the manifest's model_id on every read — status, search, and the API equivalents — and delete records + manifest when it predates the fix. Reading a stale index would also mean serving results derived from the secrets it should never have held, so search purges before it reads rather than after. Deleting is safe: the index is a derived cache, rebuilt from the vault in milliseconds, and the user is told to rebuild. A manifest that cannot be parsed is deliberately NOT treated as stale. "Cannot tell" must not mean "delete it", or every corrupt-manifest read would throw away a working index; that case is tested on both sides. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
This commit is contained in:
1 parent
fdbdb12596
commit
885cbe5665
8 files changed
+228
-23
No files matched your search
@@ -30,6 +30,7 @@ import {
|
||||
searchSemanticRecords,
|
||||
semanticManifest,
|
||||
semanticStatus,
|
||||
isStaleSemanticIndex,
|
||||
type SemanticRecord,
|
||||
generateFingerprint,
|
||||
assertValidMnemonic,
|
||||
@@ -75,7 +76,7 @@ import {
|
||||
sshPublicKeyOpenSsh,
|
||||
utf8,
|
||||
} from "@seedpass/core";
|
||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { readFile, writeFile, mkdir, rm } from "node:fs/promises";
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { basename, dirname, extname, join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
@@ -1690,6 +1691,40 @@ export function buildProgram(io: ProgramIo = defaultIo): Command {
|
||||
return { dir, records: join(dir, "records.json"), manifest: join(dir, "manifest.json") };
|
||||
}
|
||||
|
||||
async function readManifest(path: string): Promise<Record<string, unknown>> {
|
||||
if (!existsSync(path)) return {};
|
||||
try {
|
||||
return JSON.parse(await readFile(path, "utf8")) as Record<string, unknown>;
|
||||
} catch {
|
||||
// A corrupt manifest reads as "not built"; the fix is a rebuild, which
|
||||
// is what status is meant to prompt.
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an index written before secrets stopped being indexed.
|
||||
*
|
||||
* Such a file holds stored secrets in the clear, and fixing the writer does
|
||||
* not rewrite files already on disk. Deleting is safe — the index is a
|
||||
* derived cache, rebuilt from the vault in milliseconds — and leaving it is
|
||||
* leaving plaintext secrets next to an encrypted vault.
|
||||
*/
|
||||
async function purgeStaleSemantic(paths: {
|
||||
records: string;
|
||||
manifest: string;
|
||||
}): Promise<boolean> {
|
||||
if (!isStaleSemanticIndex(await readManifest(paths.manifest))) return false;
|
||||
for (const path of [paths.records, paths.manifest]) {
|
||||
if (existsSync(path)) await rm(path, { force: true });
|
||||
}
|
||||
io.err(
|
||||
"Removed a semantic index built by an older version: it stored entry " +
|
||||
"secrets in plaintext. Run 'semantic build' to search again.",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function readRecords(path: string): Promise<SemanticRecord[]> {
|
||||
if (!existsSync(path)) return [];
|
||||
try {
|
||||
@@ -1744,15 +1779,8 @@ export function buildProgram(io: ProgramIo = defaultIo): Command {
|
||||
.action(async () => {
|
||||
const opts = program.opts() as GlobalOpts;
|
||||
const paths = await semanticPaths(opts);
|
||||
let manifest: Record<string, unknown> = {};
|
||||
if (existsSync(paths.manifest)) {
|
||||
try {
|
||||
manifest = JSON.parse(await readFile(paths.manifest, "utf8"));
|
||||
} catch {
|
||||
// A corrupt manifest reports as "not built" rather than failing:
|
||||
// the fix is to rebuild, which the status is meant to prompt.
|
||||
}
|
||||
}
|
||||
await purgeStaleSemantic(paths);
|
||||
const manifest = await readManifest(paths.manifest);
|
||||
const records = await readRecords(paths.records);
|
||||
io.out(JSON.stringify(semanticStatus(manifest, records.length), null, 2));
|
||||
});
|
||||
@@ -1765,6 +1793,9 @@ export function buildProgram(io: ProgramIo = defaultIo): Command {
|
||||
.action(async (query: string, o: { limit: string; kind?: string }) => {
|
||||
const opts = program.opts() as GlobalOpts;
|
||||
const paths = await semanticPaths(opts);
|
||||
// Reading a stale index would serve results derived from the secrets it
|
||||
// should never have held.
|
||||
await purgeStaleSemantic(paths);
|
||||
const records = await readRecords(paths.records);
|
||||
if (records.length === 0) {
|
||||
throw new Error("no semantic index; run 'semantic build' first");
|
||||
|
||||
Reference in new issue
Block a user