mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:40:43 +00:00
Refactor file access to use fs-safe primitives (#78255)
* refactor: use fs-safe primitives across file access * fix: preserve invalid managed npm manifests * fix: keep fs seams for startup metadata
This commit is contained in:
committed by
GitHub
parent
0d73f174a9
commit
b85b1c68d1
@@ -2,6 +2,7 @@ import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
import { resolveAcpxPluginRoot } from "./config.js";
|
||||
import type { ResolvedAcpxPluginConfig } from "./config.js";
|
||||
|
||||
@@ -113,7 +114,10 @@ async function resolveInstalledAcpPackageBinPath(
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const packageJsonPath = requireFromHere.resolve(`${packageName}/package.json`);
|
||||
const manifest = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as PackageManifest;
|
||||
const { value: manifest } = await readJsonFileWithFallback<PackageManifest>(
|
||||
packageJsonPath,
|
||||
{},
|
||||
);
|
||||
if (manifest.name !== packageName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
||||
@@ -10,24 +11,14 @@ function decoratedMarkerPath(userDataDir: string) {
|
||||
}
|
||||
|
||||
function safeReadJson(filePath: string): Record<string, unknown> | null {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||
return null;
|
||||
}
|
||||
return parsed as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const parsed = loadJsonFile(filePath);
|
||||
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
||||
? (parsed as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function safeWriteJson(filePath: string, data: Record<string, unknown>) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
saveJsonFile(filePath, data);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
|
||||
|
||||
export async function exists(filePath: string): Promise<boolean> {
|
||||
@@ -47,10 +48,8 @@ export async function readJsonObject(
|
||||
if (!filePath) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
const { value: parsed } = await readJsonFileWithFallback<unknown>(filePath, {});
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
||||
? (parsed as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import type { ThemeRegistrationResolved } from "@pierre/diffs";
|
||||
import { RegisteredCustomThemes, ResolvedThemes, ResolvingThemes } from "@pierre/diffs";
|
||||
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
|
||||
type PierreThemeName = "pierre-dark" | "pierre-light";
|
||||
const themeRequire = createRequire(import.meta.url);
|
||||
@@ -20,8 +20,9 @@ function createThemeLoader(
|
||||
return cachedTheme;
|
||||
}
|
||||
const themePath = themeRequire.resolve(themeSpecifier);
|
||||
const { value: theme } = await readJsonFileWithFallback<Record<string, unknown>>(themePath, {});
|
||||
cachedTheme = {
|
||||
...(JSON.parse(await fs.readFile(themePath, "utf8")) as Record<string, unknown>),
|
||||
...theme,
|
||||
name: themeName,
|
||||
} as ThemeRegistrationResolved;
|
||||
return cachedTheme;
|
||||
|
||||
@@ -174,13 +174,13 @@ export class DiffArtifactStore {
|
||||
}
|
||||
|
||||
async cleanupExpired(): Promise<void> {
|
||||
await this.ensureRoot();
|
||||
const entries = await fs.readdir(this.rootDir, { withFileTypes: true }).catch(() => []);
|
||||
const root = await this.artifactRoot();
|
||||
const entries = await root.list("", { withFileTypes: true }).catch(() => []);
|
||||
const now = Date.now();
|
||||
|
||||
await Promise.all(
|
||||
entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.filter((entry) => entry.isDirectory)
|
||||
.map(async (entry) => {
|
||||
const id = entry.name;
|
||||
const meta = await this.readMeta(id);
|
||||
@@ -199,12 +199,7 @@ export class DiffArtifactStore {
|
||||
return;
|
||||
}
|
||||
|
||||
const artifactPath = this.artifactDir(id);
|
||||
const stat = await fs.stat(artifactPath).catch(() => null);
|
||||
if (!stat) {
|
||||
return;
|
||||
}
|
||||
if (now - stat.mtimeMs > SWEEP_FALLBACK_AGE_MS) {
|
||||
if (now - entry.mtimeMs > SWEEP_FALLBACK_AGE_MS) {
|
||||
await this.deleteArtifact(id);
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,10 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { writeJsonFileAtomically as writeJsonFileAtomicallyImpl } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
loadJsonFile,
|
||||
writeJsonFileAtomically as writeJsonFileAtomicallyImpl,
|
||||
} from "openclaw/plugin-sdk/json-store";
|
||||
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
||||
import { resolveConfiguredMatrixAccountIds } from "./account-selection.js";
|
||||
import { isMatrixLegacyCryptoInspectorAvailable } from "./legacy-crypto-inspector-availability.js";
|
||||
@@ -208,20 +211,13 @@ function resolveLegacyMatrixFlatStorePlan(params: {
|
||||
function loadLegacyBotSdkMetadata(cryptoRootDir: string): MatrixLegacyBotSdkMetadata {
|
||||
const metadataPath = path.join(cryptoRootDir, "bot-sdk.json");
|
||||
const fallback: MatrixLegacyBotSdkMetadata = { deviceId: null };
|
||||
try {
|
||||
if (!fs.existsSync(metadataPath)) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = JSON.parse(fs.readFileSync(metadataPath, "utf8")) as {
|
||||
deviceId?: unknown;
|
||||
};
|
||||
return {
|
||||
deviceId:
|
||||
typeof parsed.deviceId === "string" && parsed.deviceId.trim() ? parsed.deviceId : null,
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = loadJsonFile<{ deviceId?: unknown }>(metadataPath);
|
||||
return {
|
||||
deviceId:
|
||||
typeof parsed?.deviceId === "string" && parsed.deviceId.trim()
|
||||
? parsed.deviceId
|
||||
: fallback.deviceId,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMatrixLegacyCryptoPlans(params: {
|
||||
@@ -288,25 +284,11 @@ function resolveMatrixLegacyCryptoPlans(params: {
|
||||
}
|
||||
|
||||
function loadStoredRecoveryKey(filePath: string): MatrixStoredRecoveryKey | null {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")) as MatrixStoredRecoveryKey;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return loadJsonFile<MatrixStoredRecoveryKey>(filePath) ?? null;
|
||||
}
|
||||
|
||||
function loadLegacyCryptoMigrationState(filePath: string): MatrixLegacyCryptoMigrationState | null {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")) as MatrixLegacyCryptoMigrationState;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return loadJsonFile<MatrixLegacyCryptoMigrationState>(filePath) ?? null;
|
||||
}
|
||||
|
||||
async function persistLegacyMigrationState(params: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
requiresExplicitMatrixDefaultAccount,
|
||||
resolveMatrixDefaultOrOnlyAccountId,
|
||||
@@ -105,10 +106,10 @@ function resolveStorageRootMtimeMs(rootDir: string): number {
|
||||
function readStoredRootMetadata(rootDir: string): StoredRootMetadata {
|
||||
const metadata: StoredRootMetadata = {};
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
fs.readFileSync(path.join(rootDir, STORAGE_META_FILENAME), "utf8"),
|
||||
) as Partial<StoredRootMetadata>;
|
||||
const parsed = loadJsonFile<Partial<StoredRootMetadata>>(
|
||||
path.join(rootDir, STORAGE_META_FILENAME),
|
||||
);
|
||||
if (parsed) {
|
||||
if (typeof parsed.homeserver === "string" && parsed.homeserver.trim()) {
|
||||
metadata.homeserver = parsed.homeserver.trim();
|
||||
}
|
||||
@@ -130,19 +131,17 @@ function readStoredRootMetadata(rootDir: string): StoredRootMetadata {
|
||||
if (typeof parsed.createdAt === "string" && parsed.createdAt.trim()) {
|
||||
metadata.createdAt = parsed.createdAt.trim();
|
||||
}
|
||||
} catch {
|
||||
// ignore missing or malformed storage metadata
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
fs.readFileSync(path.join(rootDir, STARTUP_VERIFICATION_FILENAME), "utf8"),
|
||||
) as { deviceId?: unknown };
|
||||
if (!metadata.deviceId && typeof parsed.deviceId === "string" && parsed.deviceId.trim()) {
|
||||
metadata.deviceId = parsed.deviceId.trim();
|
||||
}
|
||||
} catch {
|
||||
// ignore missing or malformed verification state
|
||||
const verification = loadJsonFile<{ deviceId?: unknown }>(
|
||||
path.join(rootDir, STARTUP_VERIFICATION_FILENAME),
|
||||
);
|
||||
if (
|
||||
!metadata.deviceId &&
|
||||
typeof verification?.deviceId === "string" &&
|
||||
verification.deviceId.trim()
|
||||
) {
|
||||
metadata.deviceId = verification.deviceId.trim();
|
||||
}
|
||||
|
||||
return metadata;
|
||||
@@ -473,8 +472,7 @@ function writeStoredRootMetadata(
|
||||
},
|
||||
): boolean {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(metaPath), { recursive: true });
|
||||
fs.writeFileSync(metaPath, JSON.stringify(payload, null, 2), "utf-8");
|
||||
saveJsonFile(metaPath, payload);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { decodeRecoveryKey } from "matrix-js-sdk/lib/crypto-api/recovery-key.js";
|
||||
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import { formatMatrixErrorMessage, formatMatrixErrorReason } from "../errors.js";
|
||||
import { LogService } from "./logger.js";
|
||||
import type {
|
||||
@@ -399,13 +398,9 @@ export class MatrixRecoveryKeyStore {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (!fs.existsSync(this.recoveryKeyPath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = fs.readFileSync(this.recoveryKeyPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as Partial<MatrixStoredRecoveryKey>;
|
||||
const parsed = loadJsonFile<Partial<MatrixStoredRecoveryKey>>(this.recoveryKeyPath);
|
||||
if (
|
||||
parsed.version !== 1 ||
|
||||
parsed?.version !== 1 ||
|
||||
typeof parsed.createdAt !== "string" ||
|
||||
typeof parsed.privateKeyBase64 !== "string" || // pragma: allowlist secret
|
||||
!parsed.privateKeyBase64.trim()
|
||||
@@ -450,9 +445,7 @@ export class MatrixRecoveryKeyStore {
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
fs.mkdirSync(path.dirname(this.recoveryKeyPath), { recursive: true });
|
||||
fs.writeFileSync(this.recoveryKeyPath, JSON.stringify(payload, null, 2), "utf8");
|
||||
fs.chmodSync(this.recoveryKeyPath, 0o600);
|
||||
saveJsonFile(this.recoveryKeyPath, payload);
|
||||
} catch (err) {
|
||||
LogService.warn("MatrixClientLite", "Failed to persist recovery key:", err);
|
||||
}
|
||||
|
||||
@@ -396,10 +396,6 @@ type DailyIngestionState = {
|
||||
files: Record<string, DailyIngestionFileState>;
|
||||
};
|
||||
|
||||
function resolveDailyIngestionStatePath(workspaceDir: string): string {
|
||||
return path.join(workspaceDir, DAILY_INGESTION_STATE_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
function normalizeDailyIngestionState(raw: unknown): DailyIngestionState {
|
||||
const record = asRecord(raw);
|
||||
const filesRaw = asRecord(record?.files);
|
||||
@@ -442,10 +438,9 @@ function normalizeMemoryDay(value: unknown): string | undefined {
|
||||
}
|
||||
|
||||
async function readDailyIngestionState(workspaceDir: string): Promise<DailyIngestionState> {
|
||||
const statePath = resolveDailyIngestionStatePath(workspaceDir);
|
||||
try {
|
||||
return normalizeDailyIngestionState(
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(path.relative(workspaceDir, statePath)),
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(DAILY_INGESTION_STATE_RELATIVE_PATH),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
@@ -459,8 +454,7 @@ async function writeDailyIngestionState(
|
||||
workspaceDir: string,
|
||||
state: DailyIngestionState,
|
||||
): Promise<void> {
|
||||
const statePath = resolveDailyIngestionStatePath(workspaceDir);
|
||||
await privateFileStore(workspaceDir).writeJson(path.relative(workspaceDir, statePath), state, {
|
||||
await privateFileStore(workspaceDir).writeJson(DAILY_INGESTION_STATE_RELATIVE_PATH, state, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
@@ -496,10 +490,6 @@ function normalizeWorkspaceKey(workspaceDir: string): string {
|
||||
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
||||
}
|
||||
|
||||
function resolveSessionIngestionStatePath(workspaceDir: string): string {
|
||||
return path.join(workspaceDir, SESSION_INGESTION_STATE_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
function normalizeSessionIngestionState(raw: unknown): SessionIngestionState {
|
||||
const record = asRecord(raw);
|
||||
const filesRaw = asRecord(record?.files);
|
||||
@@ -554,10 +544,9 @@ function normalizeSessionIngestionState(raw: unknown): SessionIngestionState {
|
||||
}
|
||||
|
||||
async function readSessionIngestionState(workspaceDir: string): Promise<SessionIngestionState> {
|
||||
const statePath = resolveSessionIngestionStatePath(workspaceDir);
|
||||
try {
|
||||
return normalizeSessionIngestionState(
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(path.relative(workspaceDir, statePath)),
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(SESSION_INGESTION_STATE_RELATIVE_PATH),
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
@@ -571,8 +560,7 @@ async function writeSessionIngestionState(
|
||||
workspaceDir: string,
|
||||
state: SessionIngestionState,
|
||||
): Promise<void> {
|
||||
const statePath = resolveSessionIngestionStatePath(workspaceDir);
|
||||
await privateFileStore(workspaceDir).writeJson(path.relative(workspaceDir, statePath), state, {
|
||||
await privateFileStore(workspaceDir).writeJson(SESSION_INGESTION_STATE_RELATIVE_PATH, state, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -757,10 +757,9 @@ async function withShortTermLock<T>(workspaceDir: string, task: () => Promise<T>
|
||||
}
|
||||
|
||||
async function readStore(workspaceDir: string, nowIso: string): Promise<ShortTermRecallStore> {
|
||||
const storePath = resolveStorePath(workspaceDir);
|
||||
try {
|
||||
return normalizeStore(
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(path.relative(workspaceDir, storePath)),
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(SHORT_TERM_STORE_RELATIVE_PATH),
|
||||
nowIso,
|
||||
);
|
||||
} catch (err) {
|
||||
@@ -830,12 +829,9 @@ async function readPhaseSignalStore(
|
||||
workspaceDir: string,
|
||||
nowIso: string,
|
||||
): Promise<ShortTermPhaseSignalStore> {
|
||||
const phaseSignalPath = resolvePhaseSignalPath(workspaceDir);
|
||||
try {
|
||||
return normalizePhaseSignalStore(
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(
|
||||
path.relative(workspaceDir, phaseSignalPath),
|
||||
),
|
||||
await privateFileStore(workspaceDir).readJsonIfExists(SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH),
|
||||
nowIso,
|
||||
);
|
||||
} catch {
|
||||
@@ -847,21 +843,15 @@ async function writePhaseSignalStore(
|
||||
workspaceDir: string,
|
||||
store: ShortTermPhaseSignalStore,
|
||||
): Promise<void> {
|
||||
const phaseSignalPath = resolvePhaseSignalPath(workspaceDir);
|
||||
await ensureShortTermArtifactsDir(workspaceDir);
|
||||
await privateFileStore(workspaceDir).writeJson(
|
||||
path.relative(workspaceDir, phaseSignalPath),
|
||||
store,
|
||||
{
|
||||
trailingNewline: true,
|
||||
},
|
||||
);
|
||||
await privateFileStore(workspaceDir).writeJson(SHORT_TERM_PHASE_SIGNAL_RELATIVE_PATH, store, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function writeStore(workspaceDir: string, store: ShortTermRecallStore): Promise<void> {
|
||||
const storePath = resolveStorePath(workspaceDir);
|
||||
await ensureShortTermArtifactsDir(workspaceDir);
|
||||
await privateFileStore(workspaceDir).writeJson(path.relative(workspaceDir, storePath), store, {
|
||||
await privateFileStore(workspaceDir).writeJson(SHORT_TERM_STORE_RELATIVE_PATH, store, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
replaceManagedMarkdownBlock,
|
||||
withTrailingNewline,
|
||||
@@ -679,8 +680,7 @@ async function writeImportRunRecord(
|
||||
record: ChatGptImportRunRecord,
|
||||
): Promise<void> {
|
||||
const recordPath = resolveImportRunPath(vaultRoot, record.runId);
|
||||
await fs.mkdir(path.dirname(recordPath), { recursive: true });
|
||||
await fs.writeFile(recordPath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
||||
await writeJsonFileAtomically(recordPath, record);
|
||||
}
|
||||
|
||||
async function readImportRunRecord(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
|
||||
export type MemoryWikiImportedSourceGroup = "bridge" | "unsafe-local";
|
||||
|
||||
@@ -30,30 +31,14 @@ export async function readMemoryWikiSourceSyncState(
|
||||
vaultRoot: string,
|
||||
): Promise<MemoryWikiImportedSourceState> {
|
||||
const statePath = resolveMemoryWikiSourceSyncStatePath(vaultRoot);
|
||||
const raw = await fs.readFile(statePath, "utf8").catch((err: unknown) => {
|
||||
if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
|
||||
return "";
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
if (!raw.trim()) {
|
||||
return {
|
||||
version: EMPTY_STATE.version,
|
||||
entries: {},
|
||||
};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<MemoryWikiImportedSourceState>;
|
||||
return {
|
||||
version: 1,
|
||||
entries: { ...parsed.entries },
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
version: EMPTY_STATE.version,
|
||||
entries: {},
|
||||
};
|
||||
}
|
||||
const { value: parsed } = await readJsonFileWithFallback<Partial<MemoryWikiImportedSourceState>>(
|
||||
statePath,
|
||||
EMPTY_STATE,
|
||||
);
|
||||
return {
|
||||
version: 1,
|
||||
entries: { ...parsed.entries },
|
||||
};
|
||||
}
|
||||
|
||||
export async function writeMemoryWikiSourceSyncState(
|
||||
@@ -61,8 +46,7 @@ export async function writeMemoryWikiSourceSyncState(
|
||||
state: MemoryWikiImportedSourceState,
|
||||
): Promise<void> {
|
||||
const statePath = resolveMemoryWikiSourceSyncStatePath(vaultRoot);
|
||||
await fs.mkdir(path.dirname(statePath), { recursive: true });
|
||||
await fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
||||
await writeJsonFileAtomically(statePath, state);
|
||||
}
|
||||
|
||||
export async function shouldSkipImportedSourceWrite(params: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { replaceFileAtomic } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
|
||||
/** Default cooldown between reflections per session (5 minutes). */
|
||||
export const DEFAULT_COOLDOWN_MS = 300_000;
|
||||
@@ -93,11 +93,7 @@ export async function storeSessionLearning(params: {
|
||||
learnings = learnings.slice(-10);
|
||||
}
|
||||
|
||||
await replaceFileAtomic({
|
||||
filePath: learningsFile,
|
||||
content: JSON.stringify(learnings, null, 2),
|
||||
tempPrefix: ".msteams-learnings",
|
||||
});
|
||||
await writeJsonFileAtomically(learningsFile, learnings);
|
||||
if (!exists && legacyLearningsFile !== learningsFile) {
|
||||
await fs.rm(legacyLearningsFile, { force: true }).catch(() => undefined);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import {
|
||||
buildExecRemoteCommand,
|
||||
createSshSandboxSessionFromConfigText,
|
||||
@@ -37,11 +37,11 @@ function resolveBundledOpenShellCommand(): string | null {
|
||||
}
|
||||
try {
|
||||
const packageJsonPath = require.resolve("openshell/package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
||||
const packageJson = loadJsonFile<{
|
||||
bin?: string | Record<string, string>;
|
||||
};
|
||||
}>(packageJsonPath);
|
||||
const relativeBin =
|
||||
typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.openshell;
|
||||
typeof packageJson?.bin === "string" ? packageJson.bin : packageJson?.bin?.openshell;
|
||||
cachedBundledOpenShellCommand = relativeBin
|
||||
? path.resolve(path.dirname(packageJsonPath), relativeBin)
|
||||
: null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import { getHomeDir, getQQBotDataDir, isWindows } from "../../utils/platform.js";
|
||||
import type { SlashCommandResult } from "../slash-commands.js";
|
||||
|
||||
@@ -10,10 +11,7 @@ function getConfiguredLogFiles(): string[] {
|
||||
for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
|
||||
try {
|
||||
const cfgPath = path.join(homeDir, `.${cli}`, `${cli}.json`);
|
||||
if (!fs.existsSync(cfgPath)) {
|
||||
continue;
|
||||
}
|
||||
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
||||
const cfg = loadJsonFile<{ logging?: { file?: unknown } }>(cfgPath);
|
||||
const logFile = cfg?.logging?.file;
|
||||
if (logFile && typeof logFile === "string") {
|
||||
files.push(path.resolve(logFile));
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
|
||||
import { replaceFileAtomicSync } from "openclaw/plugin-sdk/security-runtime";
|
||||
import { getCredentialBackupFile, getLegacyCredentialBackupFile } from "../utils/data-paths.js";
|
||||
|
||||
@@ -70,17 +71,15 @@ export function loadCredentialBackup(accountId?: string): CredentialBackup | nul
|
||||
try {
|
||||
if (accountId) {
|
||||
const newPath = getCredentialBackupFile(accountId);
|
||||
if (fs.existsSync(newPath)) {
|
||||
const data = JSON.parse(fs.readFileSync(newPath, "utf8")) as CredentialBackup;
|
||||
if (data?.appId && data.clientSecret) {
|
||||
return data;
|
||||
}
|
||||
const data = loadJsonFile<CredentialBackup>(newPath);
|
||||
if (data?.appId && data.clientSecret) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const legacy = getLegacyCredentialBackupFile();
|
||||
if (fs.existsSync(legacy)) {
|
||||
const data = JSON.parse(fs.readFileSync(legacy, "utf8")) as CredentialBackup;
|
||||
const data = loadJsonFile<CredentialBackup>(legacy);
|
||||
if (data) {
|
||||
if (!data?.appId || !data?.clientSecret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -42,10 +42,8 @@ async function withLock<T>(key: string, task: () => Promise<T>): Promise<T> {
|
||||
}
|
||||
}
|
||||
|
||||
async function readJson(rootDir: string, filePath: string): Promise<StoreFile> {
|
||||
const parsed = await privateFileStore(rootDir).readJsonIfExists<StoreFile>(
|
||||
path.relative(rootDir, filePath),
|
||||
);
|
||||
async function readJson(rootDir: string, relativePath: string): Promise<StoreFile> {
|
||||
const parsed = await privateFileStore(rootDir).readJsonIfExists<StoreFile>(relativePath);
|
||||
if (!parsed) {
|
||||
return { version: 1, proposals: [] };
|
||||
}
|
||||
@@ -77,8 +75,12 @@ function normalizeReviewState(
|
||||
};
|
||||
}
|
||||
|
||||
async function atomicWriteJson(rootDir: string, filePath: string, data: StoreFile): Promise<void> {
|
||||
await privateFileStore(rootDir).writeJson(path.relative(rootDir, filePath), data, {
|
||||
async function atomicWriteJson(
|
||||
rootDir: string,
|
||||
relativePath: string,
|
||||
data: StoreFile,
|
||||
): Promise<void> {
|
||||
await privateFileStore(rootDir).writeJson(relativePath, data, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
@@ -86,18 +88,16 @@ async function atomicWriteJson(rootDir: string, filePath: string, data: StoreFil
|
||||
export class SkillWorkshopStore {
|
||||
readonly stateDir: string;
|
||||
readonly filePath: string;
|
||||
private readonly relativePath: string;
|
||||
|
||||
constructor(params: { stateDir: string; workspaceDir: string }) {
|
||||
this.stateDir = path.resolve(params.stateDir);
|
||||
this.filePath = path.join(
|
||||
this.stateDir,
|
||||
"skill-workshop",
|
||||
`${workspaceKey(params.workspaceDir)}.json`,
|
||||
);
|
||||
this.relativePath = path.join("skill-workshop", `${workspaceKey(params.workspaceDir)}.json`);
|
||||
this.filePath = path.join(this.stateDir, this.relativePath);
|
||||
}
|
||||
|
||||
async list(status?: SkillWorkshopStatus): Promise<SkillProposal[]> {
|
||||
const file = await readJson(this.stateDir, this.filePath);
|
||||
const file = await readJson(this.stateDir, this.relativePath);
|
||||
const proposals = status
|
||||
? file.proposals.filter((proposal) => proposal.status === status)
|
||||
: file.proposals;
|
||||
@@ -110,7 +110,7 @@ export class SkillWorkshopStore {
|
||||
|
||||
async add(proposal: SkillProposal, maxPending: number): Promise<SkillProposal> {
|
||||
return await withLock(this.filePath, async () => {
|
||||
const file = await readJson(this.stateDir, this.filePath);
|
||||
const file = await readJson(this.stateDir, this.relativePath);
|
||||
const duplicate = file.proposals.find(
|
||||
(item) =>
|
||||
(item.status === "pending" || item.status === "quarantined") &&
|
||||
@@ -132,7 +132,7 @@ export class SkillWorkshopStore {
|
||||
).length <= maxPending
|
||||
);
|
||||
});
|
||||
await atomicWriteJson(this.stateDir, this.filePath, {
|
||||
await atomicWriteJson(this.stateDir, this.relativePath, {
|
||||
...file,
|
||||
version: 1,
|
||||
proposals: nextProposals,
|
||||
@@ -143,41 +143,41 @@ export class SkillWorkshopStore {
|
||||
|
||||
async updateStatus(id: string, status: SkillWorkshopStatus): Promise<SkillProposal> {
|
||||
return await withLock(this.filePath, async () => {
|
||||
const file = await readJson(this.stateDir, this.filePath);
|
||||
const file = await readJson(this.stateDir, this.relativePath);
|
||||
const index = file.proposals.findIndex((proposal) => proposal.id === id);
|
||||
if (index < 0) {
|
||||
throw new Error(`proposal not found: ${id}`);
|
||||
}
|
||||
const updated = { ...file.proposals[index], status, updatedAt: Date.now() };
|
||||
file.proposals[index] = updated;
|
||||
await atomicWriteJson(this.stateDir, this.filePath, file);
|
||||
await atomicWriteJson(this.stateDir, this.relativePath, file);
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
async recordReviewTurn(toolCalls: number): Promise<SkillWorkshopReviewState> {
|
||||
return await withLock(this.filePath, async () => {
|
||||
const file = await readJson(this.stateDir, this.filePath);
|
||||
const file = await readJson(this.stateDir, this.relativePath);
|
||||
const current = normalizeReviewState(file.review);
|
||||
const next = {
|
||||
...current,
|
||||
turnsSinceReview: current.turnsSinceReview + 1,
|
||||
toolCallsSinceReview: current.toolCallsSinceReview + Math.max(0, Math.trunc(toolCalls)),
|
||||
};
|
||||
await atomicWriteJson(this.stateDir, this.filePath, { ...file, review: next });
|
||||
await atomicWriteJson(this.stateDir, this.relativePath, { ...file, review: next });
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
async markReviewed(): Promise<SkillWorkshopReviewState> {
|
||||
return await withLock(this.filePath, async () => {
|
||||
const file = await readJson(this.stateDir, this.filePath);
|
||||
const file = await readJson(this.stateDir, this.relativePath);
|
||||
const next = {
|
||||
turnsSinceReview: 0,
|
||||
toolCallsSinceReview: 0,
|
||||
lastReviewAt: Date.now(),
|
||||
};
|
||||
await atomicWriteJson(this.stateDir, this.filePath, { ...file, review: next });
|
||||
await atomicWriteJson(this.stateDir, this.relativePath, { ...file, review: next });
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
|
||||
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
||||
|
||||
const STORE_VERSION = 2;
|
||||
@@ -45,30 +45,30 @@ function extractBotIdFromToken(token?: string): string | null {
|
||||
return rawBotId;
|
||||
}
|
||||
|
||||
function safeParseState(raw: string): TelegramUpdateOffsetState | null {
|
||||
function safeParseState(parsed: unknown): TelegramUpdateOffsetState | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as {
|
||||
const state = parsed as {
|
||||
version?: number;
|
||||
lastUpdateId?: number | null;
|
||||
botId?: string | null;
|
||||
};
|
||||
if (parsed?.version !== STORE_VERSION && parsed?.version !== 1) {
|
||||
if (state?.version !== STORE_VERSION && state?.version !== 1) {
|
||||
return null;
|
||||
}
|
||||
if (parsed.lastUpdateId !== null && !isValidUpdateId(parsed.lastUpdateId)) {
|
||||
if (state.lastUpdateId !== null && !isValidUpdateId(state.lastUpdateId)) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
parsed.version === STORE_VERSION &&
|
||||
parsed.botId !== null &&
|
||||
typeof parsed.botId !== "string"
|
||||
state.version === STORE_VERSION &&
|
||||
state.botId !== null &&
|
||||
typeof state.botId !== "string"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
version: STORE_VERSION,
|
||||
lastUpdateId: parsed.lastUpdateId ?? null,
|
||||
botId: parsed.version === STORE_VERSION ? (parsed.botId ?? null) : null,
|
||||
lastUpdateId: state.lastUpdateId ?? null,
|
||||
botId: state.version === STORE_VERSION ? (state.botId ?? null) : null,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -81,24 +81,16 @@ export async function readTelegramUpdateOffset(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<number | null> {
|
||||
const filePath = resolveTelegramUpdateOffsetPath(params.accountId, params.env);
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, "utf-8");
|
||||
const parsed = safeParseState(raw);
|
||||
const expectedBotId = extractBotIdFromToken(params.botToken);
|
||||
if (expectedBotId && parsed?.botId && parsed.botId !== expectedBotId) {
|
||||
return null;
|
||||
}
|
||||
if (expectedBotId && parsed?.botId === null) {
|
||||
return null;
|
||||
}
|
||||
return parsed?.lastUpdateId ?? null;
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
const { value } = await readJsonFileWithFallback<unknown>(filePath, null);
|
||||
const parsed = safeParseState(value);
|
||||
const expectedBotId = extractBotIdFromToken(params.botToken);
|
||||
if (expectedBotId && parsed?.botId && parsed.botId !== expectedBotId) {
|
||||
return null;
|
||||
}
|
||||
if (expectedBotId && parsed?.botId === null) {
|
||||
return null;
|
||||
}
|
||||
return parsed?.lastUpdateId ?? null;
|
||||
}
|
||||
|
||||
export async function writeTelegramUpdateOffset(params: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { root } from "openclaw/plugin-sdk/security-runtime";
|
||||
import type { VoiceCallConfig } from "./config.js";
|
||||
import type { CoreAgentDeps, CoreConfig } from "./core-bridge.js";
|
||||
|
||||
@@ -42,15 +41,6 @@ function resolveAgentSystemPromptOverride(cfg: CoreConfig, agentId: string): str
|
||||
);
|
||||
}
|
||||
|
||||
function isSafeWorkspaceRelativeFile(file: string): boolean {
|
||||
if (!file.trim() || path.isAbsolute(file)) {
|
||||
return false;
|
||||
}
|
||||
const normalized = path.normalize(file);
|
||||
const parts = normalized.split(/[\\/]+/);
|
||||
return normalized !== "." && !parts.includes("..") && !normalized.includes("\0");
|
||||
}
|
||||
|
||||
function limitText(text: string, maxChars: number): string {
|
||||
if (text.length <= maxChars) {
|
||||
return text;
|
||||
@@ -65,12 +55,15 @@ async function readWorkspaceVoiceContextFiles(params: {
|
||||
}): Promise<string[]> {
|
||||
const sections: string[] = [];
|
||||
let remaining = params.maxChars;
|
||||
const workspaceRoot = await root(params.workspaceDir).catch(() => null);
|
||||
if (!workspaceRoot) {
|
||||
return sections;
|
||||
}
|
||||
for (const file of params.files) {
|
||||
if (remaining <= 0 || !isSafeWorkspaceRelativeFile(file)) {
|
||||
if (remaining <= 0) {
|
||||
continue;
|
||||
}
|
||||
const fullPath = path.join(params.workspaceDir, path.normalize(file));
|
||||
const content = await readFile(fullPath, "utf8").catch(() => undefined);
|
||||
const content = await workspaceRoot.readText(file).catch(() => undefined);
|
||||
const trimmed = content?.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { configureFsSafePython } from "@openclaw/fs-safe/config";
|
||||
export { root } from "@openclaw/fs-safe/root";
|
||||
export { isPathInside } from "@openclaw/fs-safe/path";
|
||||
export {
|
||||
readRegularFile,
|
||||
@@ -21,6 +22,7 @@ export function isFileMissingError(
|
||||
err &&
|
||||
typeof err === "object" &&
|
||||
"code" in err &&
|
||||
(err as Partial<NodeJS.ErrnoException>).code === "ENOENT",
|
||||
((err as Partial<NodeJS.ErrnoException>).code === "ENOENT" ||
|
||||
(err as { code?: unknown }).code === "not-found"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,13 @@ import {
|
||||
resolveMemorySearchConfig,
|
||||
type OpenClawConfig,
|
||||
} from "./config-utils.js";
|
||||
import { isFileMissingError, isPathInside, readRegularFile, statRegularFile } from "./fs-utils.js";
|
||||
import {
|
||||
isFileMissingError,
|
||||
isPathInside,
|
||||
readRegularFile,
|
||||
root,
|
||||
statRegularFile,
|
||||
} from "./fs-utils.js";
|
||||
import { isMemoryPath, normalizeExtraMemoryPaths } from "./internal.js";
|
||||
import {
|
||||
buildMemoryReadResult,
|
||||
@@ -66,6 +72,17 @@ export async function readMemoryFile(params: {
|
||||
if (!absPath.endsWith(".md")) {
|
||||
throw new Error("path required");
|
||||
}
|
||||
if (allowedWorkspace) {
|
||||
try {
|
||||
const workspaceRoot = await root(params.workspaceDir);
|
||||
await workspaceRoot.resolve(relPath);
|
||||
} catch (err) {
|
||||
if (isFileMissingError(err)) {
|
||||
return { text: "", path: relPath };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const statResult = await statRegularFile(absPath);
|
||||
if (statResult.missing) {
|
||||
return { text: "", path: relPath };
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { applyMergePatch } from "../../config/merge-patch.js";
|
||||
import { tryReadJson, writeJson } from "../../infra/json-files.js";
|
||||
import type { BundleMcpConfig, BundleMcpServerConfig } from "../../plugins/bundle-mcp.js";
|
||||
import {
|
||||
applyCommonServerConfig,
|
||||
@@ -11,14 +12,10 @@ import {
|
||||
} from "./bundle-mcp-adapter-shared.js";
|
||||
|
||||
async function readJsonObject(filePath: string): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const raw = JSON.parse(await fs.readFile(filePath, "utf-8")) as unknown;
|
||||
return raw && typeof raw === "object" && !Array.isArray(raw)
|
||||
? ({ ...raw } as Record<string, unknown>)
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
const raw = await tryReadJson<unknown>(filePath);
|
||||
return raw && typeof raw === "object" && !Array.isArray(raw)
|
||||
? ({ ...raw } as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function resolveEnvPlaceholder(
|
||||
@@ -86,7 +83,7 @@ export async function writeGeminiSystemSettings(
|
||||
if (!isRecord(settings.mcp) || !isRecord(settings.mcpServers)) {
|
||||
throw new Error("Gemini MCP settings merge produced an invalid object");
|
||||
}
|
||||
await fs.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
||||
await writeJson(settingsPath, settings, { trailingNewline: true });
|
||||
return {
|
||||
env: {
|
||||
...inheritedEnv,
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
import { applyMergePatch } from "../../config/merge-patch.js";
|
||||
import type { CliBackendConfig } from "../../config/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { tryReadJson } from "../../infra/json-files.js";
|
||||
import { extractMcpServerMap, type BundleMcpConfig } from "../../plugins/bundle-mcp.js";
|
||||
import type { CliBundleMcpMode } from "../../plugins/types.js";
|
||||
import { loadMergedBundleMcpConfig, toCliBundleMcpServerConfig } from "../bundle-mcp-config.js";
|
||||
@@ -26,12 +27,7 @@ function resolveBundleMcpMode(mode: CliBundleMcpMode | undefined): CliBundleMcpM
|
||||
}
|
||||
|
||||
async function readExternalMcpConfig(configPath: string): Promise<BundleMcpConfig> {
|
||||
try {
|
||||
const raw = JSON.parse(await fs.readFile(configPath, "utf-8")) as unknown;
|
||||
return { mcpServers: extractMcpServerMap(raw) };
|
||||
} catch {
|
||||
return { mcpServers: {} };
|
||||
}
|
||||
return { mcpServers: extractMcpServerMap(await tryReadJson<unknown>(configPath)) };
|
||||
}
|
||||
|
||||
function sortJsonValue(value: unknown): unknown {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import { replaceFileAtomicSync } from "../infra/replace-file.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import {
|
||||
listProviderEnvAuthLookupKeys,
|
||||
@@ -63,12 +65,7 @@ export function scrubLegacyStaticAuthJsonEntriesForDiscovery(pathname: string):
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(pathname, "utf8")) as unknown;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const parsed = tryReadJsonSync(pathname);
|
||||
if (!isRecord(parsed)) {
|
||||
return;
|
||||
}
|
||||
@@ -94,6 +91,11 @@ export function scrubLegacyStaticAuthJsonEntriesForDiscovery(pathname: string):
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(pathname, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
||||
fs.chmodSync(pathname, 0o600);
|
||||
replaceFileAtomicSync({
|
||||
filePath: pathname,
|
||||
content: `${JSON.stringify(parsed, null, 2)}\n`,
|
||||
dirMode: 0o700,
|
||||
mode: 0o600,
|
||||
tempPrefix: ".pi-auth",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
downloadClawHubSkillArchive,
|
||||
@@ -13,6 +12,7 @@ import { pathExists } from "../infra/fs-safe.js";
|
||||
import { withExtractedArchiveRoot } from "../infra/install-flow.js";
|
||||
import { installPackageDir } from "../infra/install-package-dir.js";
|
||||
import { resolveSafeInstallDir } from "../infra/install-safe-path.js";
|
||||
import { tryReadJson, writeJson } from "../infra/json-files.js";
|
||||
|
||||
const DOT_DIR = ".clawhub";
|
||||
const LEGACY_DOT_DIR = ".clawdhub";
|
||||
@@ -147,10 +147,8 @@ async function readClawHubSkillsLockfile(workspaceDir: string): Promise<ClawHubS
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const raw = JSON.parse(
|
||||
await fs.readFile(candidate, "utf8"),
|
||||
) as Partial<ClawHubSkillsLockfile>;
|
||||
if (raw.version === 1 && raw.skills && typeof raw.skills === "object") {
|
||||
const raw = await tryReadJson<Partial<ClawHubSkillsLockfile>>(candidate);
|
||||
if (raw?.version === 1 && raw.skills && typeof raw.skills === "object") {
|
||||
return {
|
||||
version: 1,
|
||||
skills: raw.skills,
|
||||
@@ -168,8 +166,7 @@ async function writeClawHubSkillsLockfile(
|
||||
lockfile: ClawHubSkillsLockfile,
|
||||
): Promise<void> {
|
||||
const targetPath = path.join(workspaceDir, DOT_DIR, "lock.json");
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await fs.writeFile(targetPath, `${JSON.stringify(lockfile, null, 2)}\n`, "utf8");
|
||||
await writeJson(targetPath, lockfile, { trailingNewline: true });
|
||||
}
|
||||
|
||||
async function readClawHubSkillOrigin(skillDir: string): Promise<ClawHubSkillOrigin | null> {
|
||||
@@ -179,9 +176,9 @@ async function readClawHubSkillOrigin(skillDir: string): Promise<ClawHubSkillOri
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const raw = JSON.parse(await fs.readFile(candidate, "utf8")) as Partial<ClawHubSkillOrigin>;
|
||||
const raw = await tryReadJson<Partial<ClawHubSkillOrigin>>(candidate);
|
||||
if (
|
||||
raw.version === 1 &&
|
||||
raw?.version === 1 &&
|
||||
typeof raw.registry === "string" &&
|
||||
typeof raw.slug === "string" &&
|
||||
typeof raw.installedVersion === "string" &&
|
||||
@@ -201,8 +198,7 @@ async function writeClawHubSkillOrigin(
|
||||
origin: ClawHubSkillOrigin,
|
||||
): Promise<void> {
|
||||
const targetPath = path.join(skillDir, SKILL_ORIGIN_RELATIVE_PATH);
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
||||
await fs.writeFile(targetPath, `${JSON.stringify(origin, null, 2)}\n`, "utf8");
|
||||
await writeJson(targetPath, origin, { trailingNewline: true });
|
||||
}
|
||||
|
||||
export async function searchSkillsFromClawHub(params: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import { listChannelCatalogEntries } from "../plugins/channel-catalog-registry.js";
|
||||
import type { PluginPackageChannel } from "../plugins/manifest.js";
|
||||
@@ -50,19 +51,15 @@ function readOfficialCatalogFileSync(): ChannelCatalogEntryLike[] {
|
||||
officialCatalogFileCache.set(candidate, null);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(fs.readFileSync(candidate, "utf8")) as {
|
||||
entries?: unknown;
|
||||
};
|
||||
const payload = tryReadJsonSync<{ entries?: unknown }>(candidate);
|
||||
if (payload) {
|
||||
const entries = Array.isArray(payload.entries)
|
||||
? (payload.entries as ChannelCatalogEntryLike[])
|
||||
: [];
|
||||
officialCatalogFileCache.set(candidate, entries);
|
||||
return entries;
|
||||
} catch {
|
||||
officialCatalogFileCache.set(candidate, null);
|
||||
continue;
|
||||
}
|
||||
officialCatalogFileCache.set(candidate, null);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { MANIFEST_KEY } from "../../compat/legacy-names.js";
|
||||
import { tryReadJsonSync } from "../../infra/json-files.js";
|
||||
import { isPrereleaseSemverVersion, parseRegistryNpmSpec } from "../../infra/npm-registry-spec.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
|
||||
import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
|
||||
@@ -135,15 +135,11 @@ function loadExternalCatalogEntries(options: CatalogOptions): ExternalCatalogEnt
|
||||
function loadCatalogEntriesFromPaths(paths: Iterable<string>): ExternalCatalogEntry[] {
|
||||
const entries: ExternalCatalogEntry[] = [];
|
||||
for (const resolvedPath of paths) {
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
const payload = tryReadJsonSync(resolvedPath);
|
||||
if (payload === null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")) as unknown;
|
||||
entries.push(...parseCatalogEntries(payload));
|
||||
} catch {
|
||||
// Ignore invalid catalog files.
|
||||
}
|
||||
entries.push(...parseCatalogEntries(payload));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
@@ -158,18 +154,14 @@ function loadOfficialCatalogEntriesFromPaths(paths: Iterable<string>): ExternalC
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
const payload = tryReadJsonSync(resolvedPath);
|
||||
if (payload === null) {
|
||||
officialCatalogEntriesByPath.set(resolvedPath, null);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")) as unknown;
|
||||
const parsed = parseCatalogEntries(payload);
|
||||
officialCatalogEntriesByPath.set(resolvedPath, parsed);
|
||||
entries.push(...parsed);
|
||||
} catch {
|
||||
officialCatalogEntriesByPath.set(resolvedPath, null);
|
||||
}
|
||||
const parsed = parseCatalogEntries(payload);
|
||||
officialCatalogEntriesByPath.set(resolvedPath, parsed);
|
||||
entries.push(...parsed);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Command } from "commander";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import { findBundledPluginSource } from "../plugins/bundled-sources.js";
|
||||
import { loadPluginManifest } from "../plugins/manifest.js";
|
||||
import {
|
||||
@@ -40,24 +41,17 @@ function readBundledInstallRecoveryMetadata(rootDir: string): {
|
||||
}
|
||||
const manifest = loadPluginManifest(rootDir, false);
|
||||
const pluginId = manifest.ok ? manifest.manifest.id : undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
||||
openclaw?: {
|
||||
install?: {
|
||||
allowInvalidConfigRecovery?: boolean;
|
||||
};
|
||||
const parsed = tryReadJsonSync<{
|
||||
openclaw?: {
|
||||
install?: {
|
||||
allowInvalidConfigRecovery?: boolean;
|
||||
};
|
||||
};
|
||||
return {
|
||||
...(pluginId ? { pluginId } : {}),
|
||||
allowInvalidConfigRecovery: parsed.openclaw?.install?.allowInvalidConfigRecovery === true,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
...(pluginId ? { pluginId } : {}),
|
||||
allowInvalidConfigRecovery: false,
|
||||
};
|
||||
}
|
||||
}>(packageJsonPath);
|
||||
return {
|
||||
...(pluginId ? { pluginId } : {}),
|
||||
allowInvalidConfigRecovery: parsed?.openclaw?.install?.allowInvalidConfigRecovery === true,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBundledInstallRecoveryMetadata(
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "../../daemon/service.js";
|
||||
import { createLowDiskSpaceWarning } from "../../infra/disk-space.js";
|
||||
import { pathExists } from "../../infra/fs-safe.js";
|
||||
import { readJsonIfExists, writeJson } from "../../infra/json-files.js";
|
||||
import { runGlobalPackageUpdateSteps } from "../../infra/package-update-steps.js";
|
||||
import { getSelfAndAncestorPidsSync } from "../../infra/restart-stale-pids.js";
|
||||
import { nodeVersionSatisfiesEngine } from "../../infra/runtime-guard.js";
|
||||
@@ -1702,15 +1703,14 @@ async function writePostCorePluginUpdateResultFile(
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
await fs.writeFile(filePath, `${JSON.stringify(result)}\n`, "utf-8");
|
||||
await writeJson(filePath, result, { trailingNewline: true });
|
||||
}
|
||||
|
||||
async function readPostCorePluginUpdateResultFile(
|
||||
filePath: string,
|
||||
): Promise<PostCorePluginUpdateResult | undefined> {
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as PostCorePluginUpdateResult;
|
||||
const parsed = await readJsonIfExists<PostCorePluginUpdateResult>(filePath);
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === "object" &&
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
type DevicePairingPendingRequest,
|
||||
type PairedDevice,
|
||||
} from "../infra/device-pairing.js";
|
||||
import { JsonFileReadError } from "../infra/json-files.js";
|
||||
import { JsonFileReadError, tryReadJsonSync } from "../infra/json-files.js";
|
||||
import type { DeviceAuthStore } from "../shared/device-auth.js";
|
||||
import { normalizeDeviceAuthScopes } from "../shared/device-auth.js";
|
||||
import { roleScopesAllow } from "../shared/operator-scope-compat.js";
|
||||
@@ -391,14 +390,7 @@ function collectPairedRecordIssues(snapshot: DoctorPairingSnapshot): string[] {
|
||||
}
|
||||
|
||||
function readJsonFile(filePath: string): unknown {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return tryReadJsonSync(filePath);
|
||||
}
|
||||
|
||||
function readLocalIdentity(env: NodeJS.ProcessEnv = process.env): StoredDeviceIdentity | null {
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { saveJsonFile } from "../infra/json-file.js";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import { resolveDefaultPluginNpmDir } from "../plugins/install-paths.js";
|
||||
import type { InstalledPluginIndexRecordStoreOptions } from "../plugins/installed-plugin-index-records.js";
|
||||
import { loadInstalledPluginIndex } from "../plugins/installed-plugin-index.js";
|
||||
@@ -36,12 +37,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
}
|
||||
|
||||
function readJsonObject(filePath: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
|
||||
return isRecord(parsed) ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const parsed = tryReadJsonSync(filePath);
|
||||
return isRecord(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function readStringMap(value: unknown): Record<string, string> {
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import type { CommandContext } from "../auto-reply/reply/commands-types.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { tryReadJson, writeJson } from "../infra/json-files.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
executeCrestodianOperation,
|
||||
@@ -81,7 +82,10 @@ async function readPending(
|
||||
now = new Date(),
|
||||
): Promise<RescuePendingOperation | null> {
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(pendingPath, "utf8")) as RescuePendingOperation;
|
||||
const parsed = await tryReadJson<RescuePendingOperation>(pendingPath);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
if (Date.parse(parsed.expiresAt) <= now.getTime()) {
|
||||
await fs.rm(pendingPath, { force: true });
|
||||
return null;
|
||||
@@ -93,13 +97,10 @@ async function readPending(
|
||||
}
|
||||
|
||||
async function writePending(pendingPath: string, pending: RescuePendingOperation): Promise<void> {
|
||||
await fs.mkdir(path.dirname(pendingPath), { recursive: true });
|
||||
await fs.writeFile(pendingPath, `${JSON.stringify(pending, null, 2)}\n`, {
|
||||
encoding: "utf8",
|
||||
await writeJson(pendingPath, pending, {
|
||||
dirMode: 0o700,
|
||||
mode: 0o600,
|
||||
});
|
||||
await fs.chmod(pendingPath, 0o600).catch(() => {
|
||||
// Best-effort on platforms/filesystems without POSIX modes.
|
||||
trailingNewline: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "node:path";
|
||||
import { getLatestSubagentRunByChildSessionKey } from "../agents/subagent-registry.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { readLocalFileSafely } from "../infra/fs-safe.js";
|
||||
import { tryReadJson, writeJson } from "../infra/json-files.js";
|
||||
import { safeFileURLToPath } from "../infra/local-file-access.js";
|
||||
import {
|
||||
getImageMetadata,
|
||||
@@ -381,8 +382,7 @@ async function getVariantStats(filePath: string) {
|
||||
|
||||
async function writeManagedImageRecord(record: ManagedImageRecord, stateDir = resolveStateDir()) {
|
||||
const recordPath = resolveOutgoingRecordPath(record.attachmentId, stateDir);
|
||||
await fs.mkdir(path.dirname(recordPath), { recursive: true });
|
||||
await fs.writeFile(recordPath, JSON.stringify(record, null, 2), "utf-8");
|
||||
await writeJson(recordPath, record, { trailingNewline: true });
|
||||
}
|
||||
|
||||
async function deleteManagedImageRecordArtifacts(
|
||||
@@ -479,10 +479,8 @@ export async function cleanupManagedOutgoingImageRecords(params?: {
|
||||
continue;
|
||||
}
|
||||
const recordPath = path.join(recordsDir, name);
|
||||
let record: ManagedImageRecord;
|
||||
try {
|
||||
record = JSON.parse(await fs.readFile(recordPath, "utf-8")) as ManagedImageRecord;
|
||||
} catch {
|
||||
const record = await tryReadJson<ManagedImageRecord>(recordPath);
|
||||
if (!record) {
|
||||
try {
|
||||
await fs.rm(recordPath, { force: true });
|
||||
} catch {
|
||||
|
||||
@@ -49,6 +49,13 @@ const mocks = vi.hoisted(() => ({
|
||||
realPath: "/workspace/test-agent/AGENTS.md",
|
||||
stat: { size: 0, mtimeMs: 0 },
|
||||
})),
|
||||
rootStat: vi.fn(async (_params?: unknown) => ({
|
||||
isFile: true,
|
||||
isSymbolicLink: false,
|
||||
mtimeMs: 0,
|
||||
nlink: 1,
|
||||
size: 0,
|
||||
})),
|
||||
rootWrite: vi.fn(async (_params?: unknown) => {}),
|
||||
}));
|
||||
|
||||
@@ -119,6 +126,7 @@ vi.mock("../../infra/fs-safe.js", async () => {
|
||||
root: vi.fn(async (rootDir: string) => ({
|
||||
open: async (relativePath: string, options?: Record<string, unknown>) =>
|
||||
await mocks.rootOpen({ rootDir, relativePath, ...options }),
|
||||
stat: async (relativePath: string) => await mocks.rootStat({ rootDir, relativePath }),
|
||||
read: async (relativePath: string, options?: Record<string, unknown>) =>
|
||||
await mocks.rootRead({ rootDir, relativePath, ...options }),
|
||||
write: async (
|
||||
@@ -190,18 +198,28 @@ beforeEach(() => {
|
||||
realPath: "/workspace/test-agent/AGENTS.md",
|
||||
stat: { size: 0, mtimeMs: 0 },
|
||||
});
|
||||
mocks.rootStat.mockResolvedValue({
|
||||
isFile: true,
|
||||
isSymbolicLink: false,
|
||||
mtimeMs: 0,
|
||||
nlink: 1,
|
||||
size: 0,
|
||||
});
|
||||
mocks.rootWrite.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
function makeRootForTest(overrides?: {
|
||||
open?: (params: Record<string, unknown>) => Promise<unknown>;
|
||||
read?: (params: Record<string, unknown>) => Promise<unknown>;
|
||||
stat?: (params: Record<string, unknown>) => Promise<unknown>;
|
||||
write?: (params: Record<string, unknown>) => Promise<unknown>;
|
||||
}) {
|
||||
return async (rootDir: string) =>
|
||||
({
|
||||
open: async (relativePath: string, options?: Record<string, unknown>) =>
|
||||
await (overrides?.open ?? mocks.rootOpen)({ rootDir, relativePath, ...options }),
|
||||
stat: async (relativePath: string) =>
|
||||
await (overrides?.stat ?? mocks.rootStat)({ rootDir, relativePath }),
|
||||
read: async (relativePath: string, options?: Record<string, unknown>) =>
|
||||
await (overrides?.read ?? mocks.rootRead)({ rootDir, relativePath, ...options }),
|
||||
write: async (
|
||||
@@ -1156,19 +1174,19 @@ describe("agents.files.list", () => {
|
||||
const rootOpen = vi.fn(async () => {
|
||||
throw createErrnoError("EACCES");
|
||||
});
|
||||
agentsTesting.setDepsForTests({ root: makeRootForTest({ open: rootOpen }) });
|
||||
mocks.fsLstat.mockImplementation(async (...args: unknown[]) => {
|
||||
if (args[0] === "/workspace/main/AGENTS.md") {
|
||||
return makeFileStat({ size: 17, mtimeMs: 4567 });
|
||||
}
|
||||
throw createEnoentError();
|
||||
});
|
||||
mocks.fsStat.mockImplementation(async (...args: unknown[]) => {
|
||||
if (args[0] === "/workspace/main/AGENTS.md") {
|
||||
return makeFileStat({ size: 17, mtimeMs: 4567 });
|
||||
const rootStat = vi.fn(async ({ relativePath }: Record<string, unknown>) => {
|
||||
if (relativePath === "AGENTS.md") {
|
||||
return {
|
||||
isFile: true,
|
||||
isSymbolicLink: false,
|
||||
mtimeMs: 4567,
|
||||
nlink: 1,
|
||||
size: 17,
|
||||
};
|
||||
}
|
||||
throw createEnoentError();
|
||||
});
|
||||
agentsTesting.setDepsForTests({ root: makeRootForTest({ open: rootOpen, stat: rootStat }) });
|
||||
|
||||
const { respond, promise } = makeCall("agents.files.list", { agentId: "main" });
|
||||
await promise;
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
} from "../../config/sessions.js";
|
||||
import type { IdentityConfig } from "../../config/types.base.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { sameFileIdentity } from "../../infra/fs-safe-advanced.js";
|
||||
import { root, FsSafeError, type ReadResult } from "../../infra/fs-safe.js";
|
||||
import { movePathToTrash } from "../../plugin-sdk/browser-maintenance.js";
|
||||
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js";
|
||||
@@ -131,46 +130,34 @@ type FileMeta = {
|
||||
updatedAtMs: number;
|
||||
};
|
||||
|
||||
function isPathInsideDirectory(rootDir: string, candidatePath: string): boolean {
|
||||
const relative = path.relative(rootDir, candidatePath);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
type WorkspaceRoot = Awaited<ReturnType<typeof root>>;
|
||||
|
||||
async function statWorkspaceFileSafely(
|
||||
workspaceDir: string,
|
||||
workspaceRoot: WorkspaceRoot,
|
||||
name: string,
|
||||
): Promise<FileMeta | null> {
|
||||
try {
|
||||
const workspaceReal = await fs.realpath(workspaceDir);
|
||||
const candidatePath = path.resolve(workspaceReal, name);
|
||||
if (!isPathInsideDirectory(workspaceReal, candidatePath)) {
|
||||
const stat = await workspaceRoot.stat(name);
|
||||
if (!stat.isFile || stat.isSymbolicLink || stat.nlink > 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathStat = await fs.lstat(candidatePath);
|
||||
if (!pathStat.isFile() || pathStat.nlink > 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const realPath = await fs.realpath(candidatePath);
|
||||
if (!isPathInsideDirectory(workspaceReal, realPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const realStat = await fs.stat(realPath);
|
||||
if (!realStat.isFile() || realStat.nlink > 1 || !sameFileIdentity(pathStat, realStat)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
size: realStat.size,
|
||||
updatedAtMs: Math.floor(realStat.mtimeMs),
|
||||
size: stat.size,
|
||||
updatedAtMs: Math.floor(stat.mtimeMs),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function openWorkspaceRootSafely(workspaceDir: string): Promise<WorkspaceRoot | null> {
|
||||
try {
|
||||
return await agentsHandlerDeps.root(workspaceDir);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function listAgentFiles(workspaceDir: string, options?: { hideBootstrap?: boolean }) {
|
||||
const files: Array<{
|
||||
name: string;
|
||||
@@ -180,12 +167,25 @@ async function listAgentFiles(workspaceDir: string, options?: { hideBootstrap?:
|
||||
updatedAtMs?: number;
|
||||
}> = [];
|
||||
|
||||
const workspaceRoot = await openWorkspaceRootSafely(workspaceDir);
|
||||
if (!workspaceRoot) {
|
||||
const missingNames = [
|
||||
...(options?.hideBootstrap ? BOOTSTRAP_FILE_NAMES_POST_ONBOARDING : BOOTSTRAP_FILE_NAMES),
|
||||
DEFAULT_MEMORY_FILENAME,
|
||||
];
|
||||
return missingNames.map((name) => ({
|
||||
name,
|
||||
path: path.join(workspaceDir, name),
|
||||
missing: true,
|
||||
}));
|
||||
}
|
||||
|
||||
const bootstrapFileNames = options?.hideBootstrap
|
||||
? BOOTSTRAP_FILE_NAMES_POST_ONBOARDING
|
||||
: BOOTSTRAP_FILE_NAMES;
|
||||
for (const name of bootstrapFileNames) {
|
||||
const filePath = path.join(workspaceDir, name);
|
||||
const meta = await statWorkspaceFileSafely(workspaceDir, name);
|
||||
const meta = await statWorkspaceFileSafely(workspaceRoot, name);
|
||||
if (meta) {
|
||||
files.push({
|
||||
name,
|
||||
@@ -199,7 +199,7 @@ async function listAgentFiles(workspaceDir: string, options?: { hideBootstrap?:
|
||||
}
|
||||
}
|
||||
|
||||
const primaryMeta = await statWorkspaceFileSafely(workspaceDir, DEFAULT_MEMORY_FILENAME);
|
||||
const primaryMeta = await statWorkspaceFileSafely(workspaceRoot, DEFAULT_MEMORY_FILENAME);
|
||||
if (primaryMeta) {
|
||||
files.push({
|
||||
name: DEFAULT_MEMORY_FILENAME,
|
||||
@@ -757,8 +757,9 @@ export const agentsHandlers: GatewayRequestHandlers = {
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const filePath = path.join(workspaceDir, name);
|
||||
const content = params.content;
|
||||
let workspaceRoot: WorkspaceRoot;
|
||||
try {
|
||||
const workspaceRoot = await agentsHandlerDeps.root(workspaceDir);
|
||||
workspaceRoot = await agentsHandlerDeps.root(workspaceDir);
|
||||
await workspaceRoot.write(name, content, { encoding: "utf8" });
|
||||
} catch (err) {
|
||||
if (!(err instanceof FsSafeError)) {
|
||||
@@ -767,7 +768,7 @@ export const agentsHandlers: GatewayRequestHandlers = {
|
||||
respondWorkspaceFileUnsafe(respond, name);
|
||||
return;
|
||||
}
|
||||
const meta = await statWorkspaceFileSafely(workspaceDir, name);
|
||||
const meta = await statWorkspaceFileSafely(workspaceRoot, name);
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { isPathWithin } from "../commands/cleanup-utils.js";
|
||||
import { resolveHomeDir, resolveUserPath } from "../utils.js";
|
||||
import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
import { writeJson } from "./json-files.js";
|
||||
|
||||
type TarRuntime = typeof import("tar");
|
||||
|
||||
@@ -366,7 +367,7 @@ export async function createBackupArchive(
|
||||
oauthDir: plan.oauthDir,
|
||||
workspaceDirs: plan.workspaceDirs,
|
||||
});
|
||||
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
||||
await writeJson(manifestPath, manifest, { trailingNewline: true });
|
||||
|
||||
const tar = await loadTarRuntime();
|
||||
const stateAsset = result.assets.find((asset) => asset.kind === "state");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
storeDeviceAuthTokenInStore,
|
||||
} from "../shared/device-auth-store.js";
|
||||
import type { DeviceAuthStore } from "../shared/device-auth.js";
|
||||
import { safeParseJsonWithSchema } from "../utils/zod-parse.js";
|
||||
import { privateFileStoreSync } from "./private-file-store.js";
|
||||
|
||||
const DEVICE_AUTH_FILE = "device-auth.json";
|
||||
@@ -25,11 +23,11 @@ function resolveDeviceAuthPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
|
||||
function readStore(filePath: string): DeviceAuthStore | null {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
return safeParseJsonWithSchema(DeviceAuthStoreSchema, raw);
|
||||
const parsed = privateFileStoreSync(path.dirname(filePath)).readJsonIfExists(
|
||||
path.basename(filePath),
|
||||
);
|
||||
const store = DeviceAuthStoreSchema.safeParse(parsed);
|
||||
return store.success ? store.data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { privateFileStoreSync } from "./private-file-store.js";
|
||||
@@ -63,36 +62,35 @@ export function loadOrCreateDeviceIdentity(
|
||||
filePath: string = resolveDefaultIdentityPath(),
|
||||
): DeviceIdentity {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as StoredIdentity;
|
||||
if (
|
||||
parsed?.version === 1 &&
|
||||
typeof parsed.deviceId === "string" &&
|
||||
typeof parsed.publicKeyPem === "string" &&
|
||||
typeof parsed.privateKeyPem === "string"
|
||||
) {
|
||||
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
|
||||
if (derivedId && derivedId !== parsed.deviceId) {
|
||||
const updated: StoredIdentity = {
|
||||
...parsed,
|
||||
deviceId: derivedId,
|
||||
};
|
||||
privateFileStoreSync(path.dirname(filePath)).writeJson(path.basename(filePath), updated, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
return {
|
||||
deviceId: derivedId,
|
||||
publicKeyPem: parsed.publicKeyPem,
|
||||
privateKeyPem: parsed.privateKeyPem,
|
||||
};
|
||||
}
|
||||
const parsed = privateFileStoreSync(path.dirname(filePath)).readJsonIfExists<StoredIdentity>(
|
||||
path.basename(filePath),
|
||||
);
|
||||
if (
|
||||
parsed?.version === 1 &&
|
||||
typeof parsed.deviceId === "string" &&
|
||||
typeof parsed.publicKeyPem === "string" &&
|
||||
typeof parsed.privateKeyPem === "string"
|
||||
) {
|
||||
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
|
||||
if (derivedId && derivedId !== parsed.deviceId) {
|
||||
const updated: StoredIdentity = {
|
||||
...parsed,
|
||||
deviceId: derivedId,
|
||||
};
|
||||
privateFileStoreSync(path.dirname(filePath)).writeJson(path.basename(filePath), updated, {
|
||||
trailingNewline: true,
|
||||
});
|
||||
return {
|
||||
deviceId: parsed.deviceId,
|
||||
deviceId: derivedId,
|
||||
publicKeyPem: parsed.publicKeyPem,
|
||||
privateKeyPem: parsed.privateKeyPem,
|
||||
};
|
||||
}
|
||||
return {
|
||||
deviceId: parsed.deviceId,
|
||||
publicKeyPem: parsed.publicKeyPem,
|
||||
privateKeyPem: parsed.privateKeyPem,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// fall through to regenerate
|
||||
@@ -116,13 +114,12 @@ export function loadDeviceIdentityIfPresent(
|
||||
filePath: string = resolveDefaultIdentityPath(),
|
||||
): DeviceIdentity | null {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as StoredIdentity;
|
||||
const parsed = privateFileStoreSync(path.dirname(filePath)).readJsonIfExists<StoredIdentity>(
|
||||
path.basename(filePath),
|
||||
);
|
||||
if (
|
||||
parsed?.version !== 1 ||
|
||||
!parsed ||
|
||||
parsed.version !== 1 ||
|
||||
typeof parsed.deviceId !== "string" ||
|
||||
typeof parsed.publicKeyPem !== "string" ||
|
||||
typeof parsed.privateKeyPem !== "string"
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { pathExists } from "./fs-safe.js";
|
||||
import { assertCanonicalPathWithinBase } from "./install-safe-path.js";
|
||||
import { tryReadJson, writeJson } from "./json-files.js";
|
||||
import { createSafeNpmInstallArgs, createSafeNpmInstallEnv } from "./safe-package-install.js";
|
||||
|
||||
const INSTALL_BASE_CHANGED_ERROR_MESSAGE = "install base directory changed during install";
|
||||
@@ -25,23 +26,11 @@ function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
async function sanitizeManifestForNpmInstall(targetDir: string): Promise<void> {
|
||||
const manifestPath = path.join(targetDir, "package.json");
|
||||
let manifestRaw = "";
|
||||
try {
|
||||
manifestRaw = await fs.readFile(manifestPath, "utf-8");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
let manifest: Record<string, unknown>;
|
||||
try {
|
||||
const parsed = JSON.parse(manifestRaw) as unknown;
|
||||
if (!isObjectRecord(parsed)) {
|
||||
return;
|
||||
}
|
||||
manifest = parsed;
|
||||
} catch {
|
||||
const parsed = await tryReadJson<unknown>(manifestPath);
|
||||
if (!isObjectRecord(parsed)) {
|
||||
return;
|
||||
}
|
||||
const manifest = parsed;
|
||||
|
||||
const devDependencies = manifest.devDependencies;
|
||||
if (!isObjectRecord(devDependencies)) {
|
||||
@@ -61,7 +50,7 @@ async function sanitizeManifestForNpmInstall(targetDir: string): Promise<void> {
|
||||
} else {
|
||||
manifest.devDependencies = Object.fromEntries(filteredEntries);
|
||||
}
|
||||
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
||||
await writeJson(manifestPath, manifest, { trailingNewline: true });
|
||||
}
|
||||
|
||||
async function hideProjectNpmConfigForInstall(targetDir: string): Promise<HiddenProjectConfigFile> {
|
||||
|
||||
@@ -61,6 +61,22 @@ describe("managed npm root", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not overwrite a present malformed package manifest", async () => {
|
||||
const npmRoot = await makeTempRoot();
|
||||
const manifestPath = path.join(npmRoot, "package.json");
|
||||
await fs.writeFile(manifestPath, "{not-json", "utf8");
|
||||
|
||||
await expect(
|
||||
upsertManagedNpmRootDependency({
|
||||
npmRoot,
|
||||
packageName: "@openclaw/feishu",
|
||||
dependencySpec: "2026.5.2",
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
|
||||
await expect(fs.readFile(manifestPath, "utf8")).resolves.toBe("{not-json");
|
||||
});
|
||||
|
||||
it("pins managed dependencies to the resolved version", () => {
|
||||
expect(
|
||||
resolveManagedNpmRootDependencySpec({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { NpmSpecResolution } from "./install-source-utils.js";
|
||||
import { readJson, readJsonIfExists, writeJson } from "./json-files.js";
|
||||
import type { ParsedRegistryNpmSpec } from "./npm-registry-spec.js";
|
||||
|
||||
type ManagedNpmRootManifest = {
|
||||
@@ -37,15 +38,8 @@ function readDependencyRecord(value: unknown): Record<string, string> {
|
||||
}
|
||||
|
||||
async function readManagedNpmRootManifest(filePath: string): Promise<ManagedNpmRootManifest> {
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(filePath, "utf8")) as unknown;
|
||||
return isRecord(parsed) ? { ...parsed } : {};
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return {};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const parsed = await readJsonIfExists<unknown>(filePath);
|
||||
return isRecord(parsed) ? { ...parsed } : {};
|
||||
}
|
||||
|
||||
export function resolveManagedNpmRootDependencySpec(params: {
|
||||
@@ -72,7 +66,7 @@ export async function upsertManagedNpmRootDependency(params: {
|
||||
[params.packageName]: params.dependencySpec,
|
||||
},
|
||||
};
|
||||
await fs.writeFile(manifestPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
||||
await writeJson(manifestPath, next, { trailingNewline: true });
|
||||
}
|
||||
|
||||
export async function readManagedNpmRootInstalledDependency(params: {
|
||||
@@ -80,7 +74,7 @@ export async function readManagedNpmRootInstalledDependency(params: {
|
||||
packageName: string;
|
||||
}): Promise<ManagedNpmRootInstalledDependency | null> {
|
||||
const lockPath = path.join(params.npmRoot, "package-lock.json");
|
||||
const parsed = JSON.parse(await fs.readFile(lockPath, "utf8")) as unknown;
|
||||
const parsed = await readJson<unknown>(lockPath);
|
||||
if (!isRecord(parsed) || !isRecord(parsed.packages)) {
|
||||
return null;
|
||||
}
|
||||
@@ -111,5 +105,5 @@ export async function removeManagedNpmRootDependency(params: {
|
||||
private: true,
|
||||
dependencies: nextDependencies,
|
||||
};
|
||||
await fs.writeFile(manifestPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
||||
await writeJson(manifestPath, next, { trailingNewline: true });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { isLocalBuildMetadataDistPath } from "../../scripts/lib/local-build-metadata-paths.mjs";
|
||||
import { readJsonIfExists, writeJson } from "./json-files.js";
|
||||
|
||||
export { LOCAL_BUILD_METADATA_DIST_PATHS } from "../../scripts/lib/local-build-metadata-paths.mjs";
|
||||
|
||||
@@ -117,15 +118,7 @@ async function collectExternalizedBundledExtensionIds(
|
||||
packageRoot: string,
|
||||
): Promise<ExternalizedBundledExtensionIds> {
|
||||
const packageJsonPath = path.join(packageRoot, "package.json");
|
||||
try {
|
||||
const parsed = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as unknown;
|
||||
return collectExcludedPackagedExtensionDirs(parsed);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return new Set();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return collectExcludedPackagedExtensionDirs(await readJsonIfExists<unknown>(packageJsonPath));
|
||||
}
|
||||
|
||||
function isPackagedDistPath(
|
||||
@@ -321,15 +314,16 @@ export async function writePackageDistInventory(packageRoot: string): Promise<st
|
||||
(left, right) => left.localeCompare(right),
|
||||
);
|
||||
const inventoryPath = path.join(packageRoot, PACKAGE_DIST_INVENTORY_RELATIVE_PATH);
|
||||
await fs.mkdir(path.dirname(inventoryPath), { recursive: true });
|
||||
await fs.writeFile(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`, "utf8");
|
||||
await writeJson(inventoryPath, inventory, { trailingNewline: true });
|
||||
return inventory;
|
||||
}
|
||||
|
||||
async function readPackageDistInventory(packageRoot: string): Promise<string[]> {
|
||||
async function readPackageDistInventoryOptional(packageRoot: string): Promise<string[] | null> {
|
||||
const inventoryPath = path.join(packageRoot, PACKAGE_DIST_INVENTORY_RELATIVE_PATH);
|
||||
const raw = await fs.readFile(inventoryPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
const parsed = await readJsonIfExists<unknown>(inventoryPath);
|
||||
if (parsed === null) {
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
|
||||
throw new Error(`Invalid package dist inventory at ${PACKAGE_DIST_INVENTORY_RELATIVE_PATH}`);
|
||||
}
|
||||
@@ -341,14 +335,7 @@ async function readPackageDistInventory(packageRoot: string): Promise<string[]>
|
||||
export async function readPackageDistInventoryIfPresent(
|
||||
packageRoot: string,
|
||||
): Promise<string[] | null> {
|
||||
try {
|
||||
return await readPackageDistInventory(packageRoot);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return await readPackageDistInventoryOptional(packageRoot);
|
||||
}
|
||||
|
||||
export async function collectPackageDistInventoryErrors(packageRoot: string): Promise<string[]> {
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import { resolveRequiredHomeDir } from "./home-dir.js";
|
||||
import { tryReadJsonSync } from "./json-files.js";
|
||||
import type { UsageProviderId } from "./provider-usage.types.js";
|
||||
|
||||
export const DEFAULT_TIMEOUT_MS = 5000;
|
||||
@@ -84,12 +85,9 @@ export function resolveLegacyPiAgentAccessToken(
|
||||
if (!fs.existsSync(authPath)) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = JSON.parse(fs.readFileSync(authPath, "utf8")) as Record<
|
||||
string,
|
||||
{ access?: string }
|
||||
>;
|
||||
const parsed = tryReadJsonSync<Record<string, { access?: string }>>(authPath);
|
||||
for (const providerId of providerIds) {
|
||||
const token = parsed[providerId]?.access;
|
||||
const token = parsed?.[providerId]?.access;
|
||||
if (typeof token === "string" && token.trim()) {
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import { collectBundledChannelConfigs } from "./bundled-channel-config-metadata.js";
|
||||
import {
|
||||
collectBundledPluginPublicSurfaceArtifacts,
|
||||
@@ -51,14 +52,7 @@ export type BundledPluginMetadata = {
|
||||
|
||||
function readPackageManifest(pluginDir: string): PackageManifest | undefined {
|
||||
const packagePath = path.join(pluginDir, "package.json");
|
||||
if (!fs.existsSync(packagePath)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(packagePath, "utf-8")) as PackageManifest;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return tryReadJsonSync<PackageManifest>(packagePath) ?? undefined;
|
||||
}
|
||||
|
||||
function resolveBundledPluginMetadataScanDir(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { tryReadJsonSync } from "../../../infra/json-files.js";
|
||||
import {
|
||||
normalizeBundledPluginStringList,
|
||||
resolveBundledPluginScanDir,
|
||||
@@ -59,14 +60,10 @@ export type BundledCapabilityManifest = Pick<
|
||||
>;
|
||||
|
||||
function readJsonRecord(filePath: string): Record<string, unknown> | undefined {
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
||||
return raw && typeof raw === "object" && !Array.isArray(raw)
|
||||
? (raw as Record<string, unknown>)
|
||||
: undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const raw = tryReadJsonSync(filePath);
|
||||
return raw && typeof raw === "object" && !Array.isArray(raw)
|
||||
? (raw as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function readBundledCapabilityManifest(pluginDir: string): BundledCapabilityManifest | undefined {
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { openRootFileSync } from "../infra/boundary-file-read.js";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
@@ -436,11 +437,7 @@ function readPackageManifest(
|
||||
}
|
||||
|
||||
function readTrustedPackageManifest(dir: string): PackageManifest | null {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")) as PackageManifest;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return tryReadJsonSync<PackageManifest>(path.join(dir, "package.json"));
|
||||
}
|
||||
|
||||
function readCandidatePackageManifest(params: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { tryReadJson } from "../infra/json-files.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import { extensionUsesSkippedScannerPath, isPathInside } from "../security/scan-paths.js";
|
||||
import { scanDirectoryWithSummary } from "../security/skill-scanner.js";
|
||||
@@ -496,10 +497,8 @@ async function scanManifestDependencyDenylist(params: {
|
||||
});
|
||||
const packageManifestPaths = traversalResult.packageManifestPaths;
|
||||
for (const manifestPath of packageManifestPaths) {
|
||||
let manifest: PackageManifest;
|
||||
try {
|
||||
manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")) as PackageManifest;
|
||||
} catch {
|
||||
const manifest = await tryReadJson<PackageManifest>(manifestPath);
|
||||
if (!manifest) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,12 +34,8 @@ function readRecordMap(value: unknown): Record<string, PluginInstallRecord> | nu
|
||||
}
|
||||
|
||||
function readJsonObjectFileSync(filePath: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
|
||||
return isRecord(parsed) ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const parsed = tryReadJsonSync(filePath);
|
||||
return isRecord(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function readStringRecord(value: unknown): Record<string, string> {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
||||
import { openRootFileSync } from "../infra/boundary-file-read.js";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import {
|
||||
DEFAULT_MEMORY_DREAMING_PLUGIN_ID,
|
||||
@@ -662,16 +663,12 @@ function resolveBundledPackageRootForCache(stockRoot?: string): string | undefin
|
||||
}
|
||||
|
||||
function readPackageVersionForCache(packageJsonPath: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as unknown;
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return "unknown";
|
||||
}
|
||||
const version = (parsed as { version?: unknown }).version;
|
||||
return typeof version === "string" && version.trim() ? version.trim() : "unknown";
|
||||
} catch {
|
||||
const parsed = tryReadJsonSync(packageJsonPath);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return "unknown";
|
||||
}
|
||||
const version = (parsed as { version?: unknown }).version;
|
||||
return typeof version === "string" && version.trim() ? version.trim() : "unknown";
|
||||
}
|
||||
|
||||
const bundledPackageCacheIdentityByStockRoot = new Map<
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import type { PluginCandidate } from "./discovery.js";
|
||||
import { hashJson } from "./installed-plugin-index-hash.js";
|
||||
import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
|
||||
@@ -129,8 +130,8 @@ function resolveInstalledPackageMetadata(record: InstalledPluginIndexRecord): {
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
return fallbackPackageManifest ? { packageManifest: fallbackPackageManifest } : {};
|
||||
}
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as PackageManifest;
|
||||
const packageJson = tryReadJsonSync<PackageManifest>(packageJsonPath);
|
||||
if (packageJson) {
|
||||
const packageManifest = getPackageManifestMetadata(packageJson);
|
||||
const dependencies = normalizePluginDependencySpecs({
|
||||
dependencies: packageJson.dependencies,
|
||||
@@ -158,9 +159,8 @@ function resolveInstalledPackageMetadata(record: InstalledPluginIndexRecord): {
|
||||
packageDependencies: dependencies.dependencies,
|
||||
packageOptionalDependencies: dependencies.optionalDependencies,
|
||||
};
|
||||
} catch {
|
||||
return fallbackPackageManifest ? { packageManifest: fallbackPackageManifest } : {};
|
||||
}
|
||||
return fallbackPackageManifest ? { packageManifest: fallbackPackageManifest } : {};
|
||||
}
|
||||
|
||||
function toPluginCandidate(record: InstalledPluginIndexRecord): PluginCandidate {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { resolveArchiveKind } from "../infra/archive.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { pathExists } from "../infra/fs-safe.js";
|
||||
import { resolveOsHomeRelativePath } from "../infra/home-dir.js";
|
||||
import { tryReadJson } from "../infra/json-files.js";
|
||||
import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
||||
import { isPathInside } from "../infra/path-guards.js";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
@@ -315,12 +316,7 @@ async function readClaudeKnownMarketplaces(): Promise<Record<string, KnownMarket
|
||||
return {};
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(await fs.readFile(knownPath, "utf-8"));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
const parsed = await tryReadJson<unknown>(knownPath);
|
||||
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return {};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { writeJsonSync } from "../infra/json-files.js";
|
||||
|
||||
function writeRuntimeJsonFile(targetPath: string, value: unknown): void {
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.writeFileSync(targetPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
writeJsonSync(targetPath, value);
|
||||
}
|
||||
|
||||
function writeRuntimeModuleWrapper(sourcePath: string, targetPath: string): void {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import { listBundledPluginMetadata } from "./bundled-plugin-metadata.js";
|
||||
|
||||
const NON_PACKAGED_RUNTIME_SIDECAR_PLUGIN_DIRS = new Set(["qa-channel", "qa-lab", "qa-matrix"]);
|
||||
@@ -13,10 +14,8 @@ function collectRootPackageExcludedRuntimeSidecarPluginDirs(rootDir: string): Se
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return new Set();
|
||||
}
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
||||
files?: unknown;
|
||||
};
|
||||
if (!Array.isArray(packageJson.files)) {
|
||||
const packageJson = tryReadJsonSync<{ files?: unknown }>(packageJsonPath);
|
||||
if (!Array.isArray(packageJson?.files)) {
|
||||
return new Set();
|
||||
}
|
||||
const excluded = new Set<string>();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { PluginLruCache } from "./plugin-cache-primitives.js";
|
||||
@@ -37,15 +38,13 @@ function readPluginSdkPackageJson(packageRoot: string): PluginSdkPackageJson | n
|
||||
if (pluginSdkPackageJsonByRoot.has(cacheKey)) {
|
||||
return pluginSdkPackageJsonByRoot.get(cacheKey) ?? null;
|
||||
}
|
||||
try {
|
||||
const pkgRaw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8");
|
||||
const parsed = JSON.parse(pkgRaw) as PluginSdkPackageJson;
|
||||
pluginSdkPackageJsonByRoot.set(cacheKey, parsed);
|
||||
return parsed;
|
||||
} catch {
|
||||
const parsed = tryReadJsonSync<PluginSdkPackageJson>(path.join(packageRoot, "package.json"));
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
pluginSdkPackageJsonByRoot.set(cacheKey, null);
|
||||
return null;
|
||||
}
|
||||
pluginSdkPackageJsonByRoot.set(cacheKey, parsed);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function isSafePluginSdkSubpathSegment(subpath: string): boolean {
|
||||
@@ -306,29 +305,19 @@ function isUsableDistPluginSdkArtifact(candidate: string): boolean {
|
||||
}
|
||||
|
||||
function readPrivateLocalOnlyPluginSdkSubpaths(packageRoot: string): string[] {
|
||||
try {
|
||||
const raw = fs.readFileSync(
|
||||
path.join(packageRoot, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"),
|
||||
"utf-8",
|
||||
);
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed.filter((subpath): subpath is string => isSafePluginSdkSubpathSegment(subpath));
|
||||
} catch {
|
||||
const parsed = tryReadJsonSync(
|
||||
path.join(packageRoot, "scripts", "lib", "plugin-sdk-private-local-only-subpaths.json"),
|
||||
);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed.filter((subpath): subpath is string => isSafePluginSdkSubpathSegment(subpath));
|
||||
}
|
||||
|
||||
function readBundledPluginPackageName(packageJsonPath: string): string | null {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { name?: unknown };
|
||||
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
||||
return name.startsWith("@openclaw/") ? name : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const parsed = tryReadJsonSync<{ name?: unknown }>(packageJsonPath);
|
||||
const name = typeof parsed?.name === "string" ? parsed.name.trim() : "";
|
||||
return name.startsWith("@openclaw/") ? name : null;
|
||||
}
|
||||
|
||||
function isBundledPluginPublicSurfaceSourceBasename(params: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import type { TtsAutoMode, TtsConfig, TtsProvider } from "../config/types.tts.js";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
@@ -87,14 +87,7 @@ function resolveTtsPrefsPathValue(prefsPath: string | undefined): string {
|
||||
}
|
||||
|
||||
function readPrefs(prefsPath: string): TtsUserPrefs {
|
||||
try {
|
||||
if (!fs.existsSync(prefsPath)) {
|
||||
return {};
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(prefsPath, "utf8")) as TtsUserPrefs;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
return tryReadJsonSync<TtsUserPrefs>(prefsPath) ?? {};
|
||||
}
|
||||
|
||||
function resolveTtsAutoModeFromPrefs(prefs: TtsUserPrefs): TtsAutoMode | undefined {
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ModelProviderConfig } from "../config/types.models.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { getGatewayModelPricingCacheFingerprint } from "../gateway/model-pricing-cache-state.js";
|
||||
import { getCachedGatewayModelPricing } from "../gateway/model-pricing-cache.js";
|
||||
import { tryReadJsonSync } from "../infra/json-files.js";
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
|
||||
/**
|
||||
@@ -212,13 +213,13 @@ function loadModelsJsonCostIndex(options?: {
|
||||
modelsJsonCostCache.path !== modelsPath ||
|
||||
modelsJsonCostCache.mtimeMs !== stat.mtimeMs
|
||||
) {
|
||||
const parsed = JSON.parse(fs.readFileSync(modelsPath, "utf8")) as {
|
||||
const parsed = tryReadJsonSync<{
|
||||
providers?: Record<string, ModelProviderConfig>;
|
||||
};
|
||||
}>(modelsPath);
|
||||
modelsJsonCostCache = {
|
||||
path: modelsPath,
|
||||
mtimeMs: stat.mtimeMs,
|
||||
providers: parsed.providers,
|
||||
providers: parsed?.providers,
|
||||
normalizedEntries: null,
|
||||
rawEntries: null,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user