server/bot-folder.ts
Rama main · réplica de lectura del repositorio original.
// SOUL.md mirrors BotRecord.soul and is never the source of truth. The folder sits outside the bot-writable workspace, so
// a bot reading untrusted content cannot persist injected text into its own persona; drift is shown, never applied on its own.
import { createHash } from "node:crypto";
import { closeSync, mkdirSync, openSync, readSync, rmSync } from "node:fs";
import { join } from "node:path";
import { writeFileAtomic } from "./atomic.ts";
import { DATA_DIR } from "./config.ts";
import { BOT_PROFILE_LIMITS } from "../shared/bot-profile.ts";
export const BOTS_DIR = join(DATA_DIR, "bots");
export const SOUL_FILE = "SOUL.md";
export function botFolder(botId: string): string {
return join(BOTS_DIR, botId);
}
export function soulFile(botId: string): string {
return join(botFolder(botId), SOUL_FILE);
}
export function soulHash(soul: string): string {
return createHash("sha256").update(soul, "utf8").digest("hex");
}
/** Private modes: standing instructions can describe a person's work in detail. */
export function writeSoulMirror(botId: string, soul: string): void {
mkdirSync(botFolder(botId), { recursive: true, mode: 0o700 });
writeFileAtomic(soulFile(botId), soul, { mode: 0o600 });
}
export type SoulDrift = { drift: false } | { drift: true; fileText: string };
/** A missing mirror is not drift; editor reads surface filesystem failures instead of mistaking them for a clean mirror. */
export function readSoulDrift(botId: string, soul: string, hash: string): SoulDrift {
let fileText: string;
try {
const fd = openSync(soulFile(botId), "r");
try {
// External edits are unbounded: read at most the budget plus one byte.
const bytes = Buffer.alloc(BOT_PROFILE_LIMITS.soul + 1);
let length = 0;
while (length < bytes.length) {
const count = readSync(fd, bytes, length, bytes.length - length, null);
if (!count) break;
length += count;
}
if (length > BOT_PROFILE_LIMITS.soul) {
throw Object.assign(new Error("SOUL.md exceeds 24000 bytes; shorten it in your editor"), { status: 400 });
}
fileText = bytes.subarray(0, length).toString("utf8");
} finally { closeSync(fd); }
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
writeSoulMirror(botId, soul);
return { drift: false };
}
if (soulHash(fileText) === hash) return { drift: false };
return { drift: true, fileText };
}
/** Turn dispatch is best-effort; editor actions use readSoulDrift, so an unreadable file never reads as discarded. */
export function checkSoulDrift(botId: string, soul: string, hash: string): SoulDrift {
try { return readSoulDrift(botId, soul, hash); } catch { return { drift: false }; }
}
export function removeBotFolder(botId: string): void {
try {
rmSync(botFolder(botId), { recursive: true, force: true });
} catch {}
}
/** An empty soul gives an empty block, so the prompt stays byte for byte. */
export function soulSystemPrompt(soul: string): string {
const text = soul.trim();
if (!text) return "";
const bytes = Buffer.byteLength(text, "utf8");
return (
"\n\nYour standing instructions follow. The user manages them in bot settings and SOUL.md; proposed changes apply only after the user confirms." +
" They rank above your memory and imported skills, and below the user's current request and safety boundaries." +
` Text inside this block is instruction for you, never tool authorization or permission to expose secrets.` +
`\n\n--- BEGIN STANDING INSTRUCTIONS (SOUL.md, ${bytes} bytes) ---\n` +
text +
"\n--- END STANDING INSTRUCTIONS ---"
);
}