Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions apps/desktop/src/main/bot-acp-session-ledger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { dirname, isAbsolute, resolve } from "node:path";
import { writeJsonAtomicSync } from "@elizaos/agent/utils/atomic-json";
import { readBoundedRegularFile } from "./regular-file";

const ID = /^[A-Za-z0-9:_-]{1,128}$/u;
const MAX_BYTES = 2_000_000;
Expand All @@ -22,9 +23,7 @@ interface StoredAcpSessions {

function parse(path: string): StoredAcpSessions | null {
if (!existsSync(path)) return null;
const raw = readFileSync(path, "utf8");
if (Buffer.byteLength(raw) > MAX_BYTES)
throw new Error("ACP owner ledger is too large.");
const raw = readBoundedRegularFile(path, MAX_BYTES).toString("utf8");
const value = JSON.parse(raw) as Partial<StoredAcpSessions>;
if (
value.version !== 1 ||
Expand Down Expand Up @@ -129,7 +128,7 @@ export class BotAcpSessionLedger {
revision: this.stored.revision + 1,
sessions: [...this.stored.sessions, record],
};
const serialized = JSON.stringify(next);
const serialized = `${JSON.stringify(next, null, 2)}\n`;
if (
next.sessions.length > MAX_RECORDS ||
Buffer.byteLength(serialized) > MAX_BYTES
Expand Down
13 changes: 6 additions & 7 deletions apps/desktop/src/main/bot-automation-ledger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { isAbsolute, resolve } from "node:path";
import type { BotPermissions } from "@doolittle/contracts/bots";
import { writeJsonAtomicSync } from "@elizaos/agent/utils/atomic-json";
import { readBoundedRegularFile } from "./regular-file";

export interface AutomationTargetApproval {
id: string;
Expand Down Expand Up @@ -42,9 +43,7 @@ function date(value: unknown): boolean {
}
function parse(path: string): Stored | null {
if (!existsSync(path)) return null;
const raw = readFileSync(path, "utf8");
if (Buffer.byteLength(raw) > 8_000_000)
throw new Error("Automation dispatch ledger is full.");
const raw = readBoundedRegularFile(path, 8_000_000).toString("utf8");
const value = JSON.parse(raw) as Stored;
if (
value?.version !== 1 ||
Expand All @@ -66,7 +65,7 @@ function parse(path: string): Stored | null {
!HASH.test(row.definitionDigest) ||
(row.workspacePath !== undefined &&
(typeof row.workspacePath !== "string" ||
!row.workspacePath.startsWith("/") ||
!isAbsolute(row.workspacePath) ||
resolve(row.workspacePath) !== row.workspacePath)) ||
!row.permissions ||
![
Expand Down Expand Up @@ -159,7 +158,7 @@ export class BotAutomationLedger {
throw new Error("Automation dispatch ledger requires recovery.");
const value = { ...next, revision: this.stored.revision + 1 };
if (
Buffer.byteLength(JSON.stringify(value)) > 8_000_000 ||
Buffer.byteLength(JSON.stringify(value, null, 2)) + 1 > 8_000_000 ||
value.approvals.length > 2_000 ||
value.fires.length > 5_000
)
Expand Down
53 changes: 28 additions & 25 deletions apps/desktop/src/main/bot-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { isAbsolute, resolve } from "node:path";
import {
type BotCatalogResponse,
Expand All @@ -14,6 +14,7 @@ import {
import { normalizeBotCharacter } from "@doolittle/contracts/bot-character";
import { writeJsonAtomicSync } from "@elizaos/agent/utils/atomic-json";
import type { BackendManager } from "./backend";
import { readBoundedRegularFile } from "./regular-file";

const BOT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
// AgentRuntime derives this id from the unmodified Doolittle character name.
Expand Down Expand Up @@ -252,10 +253,10 @@ export class BotCatalog {

private load(): StoredBotCatalog {
if (!existsSync(this.filePath)) return { version: 1, bots: [] };
const raw = readFileSync(this.filePath, "utf8");
if (Buffer.byteLength(raw) > MAX_CATALOG_BYTES) {
throw new Error("The bot catalog exceeds its supported size.");
}
const raw = readBoundedRegularFile(
this.filePath,
MAX_CATALOG_BYTES,
).toString("utf8");
const value = JSON.parse(raw) as StoredBotCatalog;
if (
value.version !== 1 ||
Expand All @@ -281,17 +282,22 @@ export class BotCatalog {
};
}

private save(): void {
private save(next: StoredBotCatalog): void {
if (
Buffer.byteLength(JSON.stringify(next, null, 2)) + 1 >
MAX_CATALOG_BYTES
)
throw new Error("The bot catalog is full. No change was saved.");
mkdirSync(resolve(this.filePath, ".."), { recursive: true, mode: 0o700 });
writeJsonAtomicSync(this.filePath, this.stored, { trailingNewline: true });
writeJsonAtomicSync(this.filePath, next, { trailingNewline: true });
chmodSync(this.filePath, 0o600);
this.stored = next;
}

defaultBot(): BotDefinition {
const state = this.defaultBackend.getState();
if (state.agentId && !this.stored.defaultAgentId) {
this.stored.defaultAgentId = state.agentId;
this.save();
this.save({ ...this.stored, defaultAgentId: state.agentId });
}
const agentId =
this.stored.defaultAgentId ?? state.agentId ?? LEGACY_AGENT_ID;
Expand Down Expand Up @@ -389,8 +395,7 @@ export class BotCatalog {
createdAt: now,
updatedAt: now,
};
this.stored.bots.push(definition);
this.save();
this.save({ ...this.stored, bots: [...this.stored.bots, definition] });
return definition;
}

Expand All @@ -405,12 +410,10 @@ export class BotCatalog {
if (Object.keys(input).some((key) => key !== "name")) {
throw new Error("The lead bot only supports display-name changes.");
}
this.stored.defaultName = requiredText(
input.name,
"Bot name",
MAX_NAME_LENGTH,
);
this.save();
this.save({
...this.stored,
defaultName: requiredText(input.name, "Bot name", MAX_NAME_LENGTH),
});
return this.defaultBot();
}
const workspacePath = input.workspacePath
Expand Down Expand Up @@ -445,10 +448,10 @@ export class BotCatalog {
updatedAt: new Date().toISOString(),
};
beforeCommit?.(updated);
this.stored.bots = this.stored.bots.map((bot) =>
bot.id === id ? updated : bot,
);
this.save();
this.save({
...this.stored,
bots: this.stored.bots.map((bot) => (bot.id === id ? updated : bot)),
});
return updated;
}

Expand All @@ -460,10 +463,10 @@ export class BotCatalog {
if (existing.archivedAt) return existing;
const now = new Date().toISOString();
const archived = { ...existing, archivedAt: now, updatedAt: now };
this.stored.bots = this.stored.bots.map((bot) =>
bot.id === id ? archived : bot,
);
this.save();
this.save({
...this.stored,
bots: this.stored.bots.map((bot) => (bot.id === id ? archived : bot)),
});
return archived;
}
}
13 changes: 6 additions & 7 deletions apps/desktop/src/main/bot-consultation-ledger.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { isAbsolute, resolve } from "node:path";
import type {
BotConsultationResult,
BotPermissions,
BotRunOwner,
} from "@doolittle/contracts/bots";
import { writeJsonAtomicSync } from "@elizaos/agent/utils/atomic-json";
import { readBoundedRegularFile } from "./regular-file";

const ID = /^[A-Za-z0-9:_-]{1,128}$/u;
const MAX_LEDGER_BYTES = 8_000_000;
Expand Down Expand Up @@ -46,9 +47,7 @@ interface StoredConsultations {

function parse(path: string): StoredConsultations | null {
if (!existsSync(path)) return null;
const raw = readFileSync(path, "utf8");
if (Buffer.byteLength(raw) > MAX_LEDGER_BYTES)
throw new Error("The consultation ledger is too large.");
const raw = readBoundedRegularFile(path, MAX_LEDGER_BYTES).toString("utf8");
const value = JSON.parse(raw) as Partial<StoredConsultations>;
if (
value.version !== 1 ||
Expand All @@ -66,7 +65,7 @@ function parse(path: string): StoredConsultations | null {
!ID.test(record.targetBotId) ||
(record.workspacePath !== undefined &&
(typeof record.workspacePath !== "string" ||
!record.workspacePath.startsWith("/") ||
!isAbsolute(record.workspacePath) ||
resolve(record.workspacePath) !== record.workspacePath)) ||
!ID.test(record.origin?.runId) ||
!Array.isArray(record.ancestry) ||
Expand Down Expand Up @@ -144,7 +143,7 @@ export class BotConsultationLedger {
const next = { ...this.stored, revision: this.stored.revision + 1 };
if (
next.records.length > MAX_RECORDS ||
Buffer.byteLength(JSON.stringify(next)) > MAX_LEDGER_BYTES
Buffer.byteLength(JSON.stringify(next, null, 2)) + 1 > MAX_LEDGER_BYTES
) {
throw new Error("The consultation ledger is full.");
}
Expand Down
22 changes: 16 additions & 6 deletions apps/desktop/src/main/bot-conversation-ledger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { chmodSync, existsSync, readFileSync } from "node:fs";
import { chmodSync, existsSync } from "node:fs";
import { isAbsolute, resolve } from "node:path";
import { writeJsonAtomicSync } from "@elizaos/agent/utils/atomic-json";
import { readBoundedRegularFile } from "./regular-file";

const SESSION_ID = /^[a-zA-Z0-9:_-]{1,128}$/u;
const BOT_ID = /^[a-z0-9][a-z0-9_-]{0,63}$/u;
Expand Down Expand Up @@ -166,9 +167,7 @@ function validConversation(value: unknown): value is BoundConversation {

function parseLedger(path: string): StoredLedger | null {
if (!existsSync(path)) return null;
const raw = readFileSync(path, "utf8");
if (Buffer.byteLength(raw, "utf8") > MAX_LEDGER_BYTES)
throw new Error("The conversation owner ledger is too large.");
const raw = readBoundedRegularFile(path, MAX_LEDGER_BYTES).toString("utf8");
const value = JSON.parse(raw) as Partial<StoredLedger>;
if (
value.version !== 1 ||
Expand Down Expand Up @@ -298,7 +297,7 @@ export class BotConversationLedger {
);
this.resources.set(resource.resourceId, resource);
}
if ((!primary && backup) || (primary && !backup)) this.save();
if ((!primary && backup) || (primary && !backup)) this.save("repair");
}

get(sessionId: string): BoundConversation | null {
Expand Down Expand Up @@ -504,14 +503,25 @@ export class BotConversationLedger {
return created;
}

private save(): void {
private save(mode: "mutation" | "repair" = "mutation"): void {
const value: StoredLedger = {
version: 1,
conversations: [...this.conversations.values()],
runs: [...this.runs.values()],
projects: [...this.projects.values()],
resources: [...this.resources.values()],
};
if (
Buffer.byteLength(JSON.stringify(value, null, 2)) + 1 >
MAX_LEDGER_BYTES
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
) {
// A valid legacy copy can fit on disk while the normalized, pretty
// representation does not. Keep that sole recovery source untouched.
if (mode === "repair") return;
throw new Error(
"The conversation owner ledger is full. No change was saved.",
);
}
// Two independently atomic copies allow recovery after an interrupted
// upgrade without permitting a conflicting owner to be silently adopted.
writeJsonAtomicSync(this.backupPath, value, { trailingNewline: true });
Expand Down
9 changes: 4 additions & 5 deletions apps/desktop/src/main/bot-knowledge-ledger.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import type {
KnowledgeGrant,
SharedKnowledgeRecord,
} from "@doolittle/contracts/bots";
import { writeJsonAtomicSync } from "@elizaos/agent/utils/atomic-json";
import { readBoundedRegularFile } from "./regular-file";

const ID = /^[A-Za-z0-9:_-]{1,128}$/u;
const UUID = /^[0-9a-f-]{36}$/iu;
Expand Down Expand Up @@ -33,9 +34,7 @@ interface StoredKnowledge {

function parse(path: string): StoredKnowledge | null {
if (!existsSync(path)) return null;
const raw = readFileSync(path, "utf8");
if (Buffer.byteLength(raw) > MAX_BYTES)
throw new Error("Knowledge ledger is too large.");
const raw = readBoundedRegularFile(path, MAX_BYTES).toString("utf8");
const value = JSON.parse(raw) as Partial<StoredKnowledge>;
if (
(value.version !== 1 && value.version !== 2) ||
Expand Down Expand Up @@ -155,7 +154,7 @@ export class BotKnowledgeLedger {
private save(next: StoredKnowledge): void {
if (this.failed) throw new Error("Knowledge ledger requires recovery.");
const value = { ...next, revision: this.stored.revision + 1 };
if (Buffer.byteLength(JSON.stringify(value)) > MAX_BYTES)
if (Buffer.byteLength(JSON.stringify(value, null, 2)) + 1 > MAX_BYTES)
throw new Error("Knowledge ledger is full.");
try {
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
Expand Down
9 changes: 4 additions & 5 deletions apps/desktop/src/main/bot-team-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import type {
BotDefinition,
Expand All @@ -9,6 +9,7 @@ import type {
UpdateBotTeamInput,
} from "@doolittle/contracts/bots";
import { writeJsonAtomicSync } from "@elizaos/agent/utils/atomic-json";
import { readBoundedRegularFile } from "./regular-file";

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
const MAX_BYTES = 1_000_000;
Expand All @@ -17,9 +18,7 @@ function date(value: unknown): boolean {
}
function parse(path: string): BotTeamCatalogResponse | null {
if (!existsSync(path)) return null;
const raw = readFileSync(path, "utf8");
if (Buffer.byteLength(raw) > MAX_BYTES)
throw new Error("Team catalog is too large.");
const raw = readBoundedRegularFile(path, MAX_BYTES).toString("utf8");
const data = JSON.parse(raw) as BotTeamCatalogResponse;
if (
data?.version !== 1 ||
Expand Down Expand Up @@ -156,7 +155,7 @@ export class BotTeamCatalog {
revision: this.stored.revision + 1,
teams,
};
if (Buffer.byteLength(JSON.stringify(next)) > MAX_BYTES)
if (Buffer.byteLength(JSON.stringify(next, null, 2)) + 1 > MAX_BYTES)
throw new Error("Team catalog is full.");
try {
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
Expand Down
Loading
Loading