Files
openclaw/src/agents/identity-avatar-file.ts
T
Peter SteinbergerandGitHub f07a1fb502 refactor: centralize bounded file reads in fs-safe (#111104)
* refactor: use fs-safe bounded descriptor reads

* build: update fs-safe to 0.4.2

* build: refresh root npm shrinkwrap

* fix: satisfy bounded read return paths

* fix: update fs-safe integration for latest main

* fix: adopt fs-safe overflow compatibility release

* build: complete fs-safe lockfile update

* build: update fs-safe to 0.4.4

* build: refresh plugin SDK API baseline

* test: follow fs-safe bounded read seam
2026-07-19 01:29:23 -07:00

166 lines
5.0 KiB
TypeScript

// Internal local-avatar resolution and pinned file reads.
import fs from "node:fs";
import path from "node:path";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { openRootFileSync } from "../infra/boundary-file-read.js";
import { readFileDescriptorBoundedSync } from "../infra/boundary-file-read.js";
import { isRenderableAvatarImageDataUrl } from "../shared/avatar-limits.js";
import {
AVATAR_MAX_BYTES,
hasAvatarUriScheme,
isAvatarDataUrl,
isAvatarHttpUrl,
isPathWithinRoot,
isSupportedLocalAvatarExtension,
isWindowsAbsolutePath,
resolveAvatarMime,
} from "../shared/avatar-policy.js";
import { resolveUserPath } from "../utils.js";
import { resolveAgentWorkspaceDir } from "./agent-scope.js";
type LocalAgentAvatarFailureReason =
| "missing"
| "outside_workspace"
| "too_large"
| "unreadable"
| "unsupported_extension";
export type OpenedLocalAgentAvatarFile = {
path: string;
fd: number;
};
type LocalAgentAvatarPath = {
filePath: string;
workspaceRoot: string;
};
function resolveExistingPath(value: string): string {
try {
return fs.realpathSync(value);
} catch {
return path.resolve(value);
}
}
/** Resolve one local avatar source while retaining its canonical workspace root. */
export function resolveLocalAgentAvatarPath(params: {
raw: string;
workspaceDir: string;
}):
| { ok: true; value: LocalAgentAvatarPath }
| { ok: false; reason: LocalAgentAvatarFailureReason } {
const workspaceRoot = resolveExistingPath(params.workspaceDir);
const resolved =
params.raw.startsWith("~") || path.isAbsolute(params.raw)
? resolveUserPath(params.raw)
: path.resolve(workspaceRoot, params.raw);
const filePath = resolveExistingPath(resolved);
if (!isPathWithinRoot(workspaceRoot, filePath)) {
return { ok: false, reason: "outside_workspace" };
}
if (!isSupportedLocalAvatarExtension(filePath)) {
return { ok: false, reason: "unsupported_extension" };
}
try {
const stat = fs.statSync(filePath);
if (!stat.isFile()) {
return { ok: false, reason: "missing" };
}
if (stat.size > AVATAR_MAX_BYTES) {
return { ok: false, reason: "too_large" };
}
} catch {
return { ok: false, reason: "missing" };
}
return { ok: true, value: { filePath, workspaceRoot } };
}
function openResolvedLocalAgentAvatarFile(
resolved: LocalAgentAvatarPath,
): OpenedLocalAgentAvatarFile | null {
try {
const opened = openRootFileSync({
absolutePath: resolved.filePath,
rootPath: resolved.workspaceRoot,
rootRealPath: resolved.workspaceRoot,
boundaryLabel: "agent workspace",
maxBytes: AVATAR_MAX_BYTES,
rejectHardlinks: true,
skipLexicalRootCheck: true,
});
if (!opened.ok) {
return null;
}
if (!isSupportedLocalAvatarExtension(opened.path)) {
fs.closeSync(opened.fd);
return null;
}
return { path: opened.path, fd: opened.fd };
} catch {
return null;
}
}
/**
* Open one selected local avatar under its agent workspace.
* A successful caller owns `file.fd` and must close it exactly once.
*/
export function openLocalAgentAvatarFile(params: {
cfg: OpenClawConfig;
agentId: string;
source: string;
}):
| { ok: true; file: OpenedLocalAgentAvatarFile }
| { ok: false; reason: LocalAgentAvatarFailureReason } {
const resolved = resolveLocalAgentAvatarPath({
raw: params.source,
workspaceDir: resolveAgentWorkspaceDir(params.cfg, params.agentId),
});
if (!resolved.ok) {
return resolved;
}
const file = openResolvedLocalAgentAvatarFile(resolved.value);
return file ? { ok: true, file } : { ok: false, reason: "unreadable" };
}
/** Consume a pinned local avatar descriptor into a data URL. Always closes it. */
export function readOpenedLocalAgentAvatarDataUrl(
opened: OpenedLocalAgentAvatarFile,
): string | undefined {
try {
// Keep the validated inode pinned through the read. Reopening by path
// would restore the symlink/rename race that openRootFileSync closes.
const buffer = readFileDescriptorBoundedSync(opened.fd, AVATAR_MAX_BYTES);
return `data:${resolveAvatarMime(opened.path)};base64,${buffer.toString("base64")}`;
} catch {
return undefined;
} finally {
fs.closeSync(opened.fd);
}
}
/** Resolve one configured avatar source for agent-list projections. */
export function resolveAgentAvatarUrlFromSource(
cfg: OpenClawConfig,
agentId: string,
source: string | null | undefined,
): string | undefined {
const normalized = normalizeOptionalString(source);
if (!normalized) {
return undefined;
}
if (isAvatarHttpUrl(normalized) || isRenderableAvatarImageDataUrl(normalized)) {
return normalized;
}
if (
isAvatarDataUrl(normalized) ||
(hasAvatarUriScheme(normalized) && !isWindowsAbsolutePath(normalized))
) {
return undefined;
}
const opened = openLocalAgentAvatarFile({ cfg, agentId, source: normalized });
return opened.ok ? readOpenedLocalAgentAvatarDataUrl(opened.file) : undefined;
}