refactor(plugin-sdk): promote shared runtime primitives (#126193)

* refactor(plugin-sdk): promote shared runtime primitives

* test(codex): keep one attempt tools owner
This commit is contained in:
Peter Steinberger
2026-08-19 01:10:18 -07:00
committed by GitHub
parent 629f37e841
commit 3378e07d50
47 changed files with 168 additions and 415 deletions
+6 -8
View File
@@ -101,7 +101,7 @@ extensions/browser/src/browser/routes/agent.act.shared.ts 3
extensions/browser/src/browser/routes/agent.act.ts 2
extensions/browser/src/browser/routes/agent.shared.ts 2
extensions/browser/src/browser/routes/agent.snapshot.ts 8
extensions/browser/src/browser/routes/agent.storage.ts 3
extensions/browser/src/browser/routes/agent.storage.ts 2
extensions/browser/src/browser/routes/basic.ts 6
extensions/browser/src/browser/routes/permissions.ts 1
extensions/browser/src/browser/routes/tabs.ts 8
@@ -250,7 +250,6 @@ extensions/codex/src/app-server/transcript-mirror-attestation.ts 2
extensions/codex/src/app-server/transcript-mirror.ts 4
extensions/codex/src/app-server/transport-stdio.ts 1
extensions/codex/src/app-server/upstream-fork-boundary.ts 2
extensions/codex/src/app-server/upstream-prompt-provenance.ts 4
extensions/codex/src/app-server/usage-limit-error.ts 2
extensions/codex/src/app-server/user-prompt-message.ts 1
extensions/codex/src/command-handlers.ts 3
@@ -513,7 +512,7 @@ extensions/file-transfer/src/node-host/dir-list.ts 1
extensions/file-transfer/src/node-host/file-fetch.ts 1
extensions/file-transfer/src/node-host/file-write.ts 2
extensions/file-transfer/src/shared/node-invoke-policy.ts 3
extensions/file-transfer/src/shared/policy.ts 12
extensions/file-transfer/src/shared/policy.ts 7
extensions/file-transfer/src/tools/dir-fetch-tool.ts 1
extensions/file-transfer/src/tools/dir-list-tool.ts 2
extensions/file-transfer/src/tools/file-fetch-tool.ts 2
@@ -549,13 +548,13 @@ extensions/google/cli-backend-auth.runtime.ts 1
extensions/google/cli-backend-isolated-auth.runtime.ts 5
extensions/google/cli-backend.ts 1
extensions/google/embedding-batch.ts 3
extensions/google/index.ts 4
extensions/google/index.ts 1
extensions/google/music-generation-provider.ts 1
extensions/google/oauth-token-shared.ts 1
extensions/google/onboard.ts 3
extensions/google/provider-catalog.ts 2
extensions/google/provider-registration.ts 2
extensions/google/realtime-voice-provider.ts 9
extensions/google/realtime-voice-provider.ts 6
extensions/google/src/gemini-web-search-provider.runtime.ts 1
extensions/google/transport-stream.ts 15
extensions/google/vertex-adc.ts 6
@@ -1017,7 +1016,7 @@ extensions/qa-lab/src/crabline-transport.ts 6
extensions/qa-lab/src/cron-run-wait.ts 1
extensions/qa-lab/src/discovery-eval.ts 1
extensions/qa-lab/src/docker-up.runtime.ts 1
extensions/qa-lab/src/evidence-gallery.ts 3
extensions/qa-lab/src/evidence-gallery.ts 2
extensions/qa-lab/src/evidence-summary.ts 1
extensions/qa-lab/src/execution-identity-storage-inspection.ts 1
extensions/qa-lab/src/fixture-utils.ts 10
@@ -1086,7 +1085,7 @@ extensions/qa-lab/src/providers/index.ts 1
extensions/qa-lab/src/providers/live-config.ts 1
extensions/qa-lab/src/providers/mock-openai/mock-anthropic-wire.ts 3
extensions/qa-lab/src/providers/mock-openai/mock-openai-assistant-text.ts 3
extensions/qa-lab/src/providers/mock-openai/mock-openai-contracts.ts 3
extensions/qa-lab/src/providers/mock-openai/mock-openai-contracts.ts 2
extensions/qa-lab/src/providers/mock-openai/mock-openai-directives.ts 4
extensions/qa-lab/src/providers/mock-openai/mock-openai-events.ts 4
extensions/qa-lab/src/providers/mock-openai/mock-openai-input.ts 11
@@ -1396,7 +1395,6 @@ extensions/voice-call/src/runtime.ts 3
extensions/voice-call/src/tts-provider-voice.ts 1
extensions/voice-call/src/webhook.ts 11
extensions/voice-call/src/webhook/realtime-handler.ts 3
extensions/voice-call/src/webhook/stream-frame-adapter.ts 2
extensions/volcengine/tts.ts 2
extensions/voyage/embedding-batch.ts 1
extensions/vydra/shared.ts 2
-1
View File
@@ -154,7 +154,6 @@ extensions/memory-lancedb/index.test.ts
extensions/memory-wiki/src/chatgpt-import.ts
extensions/memory-wiki/src/cli.ts
extensions/memory-wiki/src/compile.ts
extensions/memory-wiki/src/markdown.ts
extensions/memory-wiki/src/query.test.ts
extensions/memory-wiki/src/query.ts
extensions/microsoft-foundry/index.test.ts
+2 -2
View File
@@ -301,7 +301,7 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It
| `plugin-sdk/concurrency-runtime` | Private-local after July 2026; Bounded async task concurrency helper |
| `plugin-sdk/dedupe-runtime` | In-memory and persistent-backed dedupe cache helpers |
| `plugin-sdk/delivery-queue-runtime` | Private-local after July 2026; Outbound pending-delivery drain helper |
| `plugin-sdk/file-access-runtime` | Private-local after July 2026; Safe local-file, temp-root, media-source path, and directory-durability helpers |
| `plugin-sdk/file-access-runtime` | Private-local after July 2026; Safe local-file, path-containment, temp-root, media-source path, and directory-durability helpers |
| `plugin-sdk/heartbeat-runtime` | Private-local after July 2026; Heartbeat wake, event, and visibility helpers |
| `plugin-sdk/expect-runtime` | Private-local after July 2026; Required-value assertion helper for provable runtime invariants |
| `plugin-sdk/number-runtime` | Private-local after July 2026; Numeric coercion helper |
@@ -322,7 +322,7 @@ Use `isLoopbackHost(host)` when a plugin must accept only the local machine. It
| `plugin-sdk/context-visibility-runtime` | Private-local after July 2026; Context visibility resolution and supplemental context filtering without broad config/security imports |
| `plugin-sdk/string-coerce-runtime` | Narrow primitive record/string coercion and normalization helpers without markdown/logging imports |
| `plugin-sdk/html-entity-runtime` | Private-local after July 2026; Single-pass semicolon-terminated HTML5 entity decoding without broad text utilities |
| `plugin-sdk/text-utility-runtime` | Private-local after July 2026; Low-level text and path helpers, including five-entity HTML escaping |
| `plugin-sdk/text-utility-runtime` | Private-local after July 2026; Low-level text and path helpers, including UTF-8 prefix truncation and five-entity HTML escaping |
| `plugin-sdk/widget-html` | Complete-document detection, size validation, and tool input errors for self-contained HTML widgets |
| `plugin-sdk/host-runtime` | Private-local after July 2026; Hostname and SCP host normalization helpers |
| `plugin-sdk/retry-runtime` | Private-local after July 2026; Retry config and retry runner helpers |
+2 -11
View File
@@ -1,6 +1,7 @@
import { createReadStream } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { isPathStrictlyInside } from "openclaw/plugin-sdk/file-access-runtime";
import type { SessionCatalogSession } from "openclaw/plugin-sdk/session-catalog";
import {
isRecord,
@@ -172,7 +173,7 @@ async function scanPiFileCandidates(env: NodeJS.ProcessEnv): Promise<PiFileCandi
identity: `${String(stats.dev)}:${String(stats.ino)}:${String(stats.birthtimeMs)}`,
mtimeMs: stats.mtimeMs,
size: stats.size,
resumable: acpRoot ? pathIsWithin(acpRoot, file) : false,
resumable: acpRoot ? isPathStrictlyInside(acpRoot, file) : false,
}
: undefined;
} catch {
@@ -216,16 +217,6 @@ async function piFileCandidates(env: NodeJS.ProcessEnv): Promise<PiFileCandidate
}
}
function pathIsWithin(root: string, candidate: string): boolean {
const relative = path.relative(root, candidate);
return (
relative !== "" &&
relative !== ".." &&
!relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative)
);
}
function parsePiJsonLines(content: string): Record<string, unknown>[] {
return content.split(/\r?\n/u).flatMap((line) => {
if (!line.trim()) {
+3 -7
View File
@@ -2,6 +2,7 @@ import type { Dirent, Stats } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
const MAX_CATALOG_JSON_CACHE_ENTRIES = 4_000;
export const CLAUDE_CATALOG_IO_CONCURRENCY = 32;
@@ -72,23 +73,18 @@ export function setBoundedCache<K, V>(
}
}
function isWithin(root: string, candidate: string): boolean {
const relative = path.relative(path.resolve(root), path.resolve(candidate));
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
async function safeSessionFile(
root: string,
resolvedRoot: string,
candidate: string,
sessionId: string,
): Promise<SafeSessionFile> {
if (!isWithin(root, candidate) || path.basename(candidate) !== `${sessionId}.jsonl`) {
if (!isPathInside(root, candidate) || path.basename(candidate) !== `${sessionId}.jsonl`) {
return undefined;
}
try {
const resolvedCandidate = await fs.realpath(candidate);
if (!isWithin(resolvedRoot, resolvedCandidate)) {
if (!isPathInside(resolvedRoot, resolvedCandidate)) {
return undefined;
}
const stat = await fs.stat(resolvedCandidate);
+2 -15
View File
@@ -8,6 +8,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf8Prefix } from "openclaw/plugin-sdk/text-utility-runtime";
import {
assertBrowserProxyFileBytesWithinLimits,
assertBrowserProxyFileCountWithinLimit,
@@ -150,20 +151,6 @@ export async function prepareBrowserProxyUploadRequest(params: {
return { body: bodyWithoutPaths, upload };
}
function truncateUtf8(value: string, maxBytes: number): string {
let result = "";
let bytes = 0;
for (const character of value) {
const nextBytes = Buffer.byteLength(character, "utf8");
if (bytes + nextBytes > maxBytes) {
break;
}
result += character;
bytes += nextBytes;
}
return result;
}
function sanitizeUploadName(name: string): string {
const basename = path.posix.basename(name.replaceAll("\\", "/"));
const cleaned = Array.from(basename, (character) => {
@@ -177,7 +164,7 @@ function sanitizeUploadName(name: string): string {
.replace(/[. ]+$/u, "");
const portable = WINDOWS_RESERVED_NAME.test(cleaned) ? `_${cleaned}` : cleaned;
const safe = portable && portable !== "." && portable !== ".." ? portable : "upload";
return truncateUtf8(safe, MAX_STAGED_NAME_BYTES) || "upload";
return truncateUtf8Prefix(safe, MAX_STAGED_NAME_BYTES) || "upload";
}
function decodedBase64Size(value: string): number {
@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
import { asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
@@ -99,7 +100,7 @@ async function validateNativeManifest(params: {
"browser",
"native-messaging",
);
if (launcherPath !== managedRoot && !launcherPath.startsWith(`${managedRoot}${path.sep}`)) {
if (!isPathInside(managedRoot, launcherPath)) {
throw new Error("launcher is outside the managed root");
}
const parsed: unknown = JSON.parse(await fs.readFile(manifestPath, "utf8"));
@@ -5,6 +5,7 @@
* and related browser-context mutations for the selected profile/tab.
*/
import {
asNullableRecord,
normalizeOptionalString,
readStringValue,
} from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -200,10 +201,7 @@ export function registerBrowserAgentStorageRoutes(
app.post("/cookies/set", async (req, res) => {
const body = readBody(req);
const targetId = resolveTargetIdFromBody(body);
const cookie =
body.cookie && typeof body.cookie === "object" && !Array.isArray(body.cookie)
? (body.cookie as Record<string, unknown>)
: null;
const cookie = asNullableRecord(body.cookie);
if (!cookie) {
return jsonError(res, 400, "cookie is required");
}
+2 -11
View File
@@ -5,6 +5,7 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import {
asNullableRecord,
normalizeOptionalString,
@@ -126,16 +127,6 @@ function resolveManagedBrowserUserDataDir(configDir: string, profileName: string
return path.join(resolveManagedBrowserProfileDir(configDir, profileName), "user-data");
}
function normalizeComparablePath(targetPath: string): string {
return path.resolve(targetPath);
}
function isSameOrChildPath(candidatePath: string, parentPath: string): boolean {
const candidate = normalizeComparablePath(candidatePath);
const parent = normalizeComparablePath(parentPath);
return candidate === parent || candidate.startsWith(`${parent}${path.sep}`);
}
function isLegacyClawdProfileConfigured(cfg: OpenClawConfig, legacyProfileDir: string): boolean {
const browser = asNullableRecord(cfg.browser);
if (!browser) {
@@ -156,7 +147,7 @@ function isLegacyClawdProfileConfigured(cfg: OpenClawConfig, legacyProfileDir: s
for (const rawProfile of Object.values(configuredProfiles)) {
const profile = asNullableRecord(rawProfile);
const userDataDir = normalizeOptionalString(profile?.userDataDir);
if (userDataDir && isSameOrChildPath(resolveUserPath(userDataDir), legacyProfileDir)) {
if (userDataDir && isPathInside(legacyProfileDir, resolveUserPath(userDataDir))) {
return true;
}
}
+4 -11
View File
@@ -1,4 +1,5 @@
import type { Event, Filter, Relay } from "nostr-tools";
import { chunkItems } from "openclaw/plugin-sdk/text-chunking";
import {
BUZZ_PROFILE_KIND,
BUZZ_PROFILE_QUERY_CHUNK_SIZE,
@@ -23,14 +24,6 @@ type ProfileSubscriptionGeneration = {
readyTimeout?: ReturnType<typeof setTimeout>;
};
function chunkValues<T>(values: readonly T[], size: number): T[][] {
const chunks: T[][] = [];
for (let index = 0; index < values.length; index += size) {
chunks.push(values.slice(index, index + size));
}
return chunks;
}
async function queryBuzzDirectoryBatch(params: {
relay: Relay;
filter: Filter;
@@ -112,7 +105,7 @@ export async function queryBuzzDirectoryProfiles(params: {
onTimeout?: (error: Error) => void;
signal?: AbortSignal;
}): Promise<void> {
for (const authors of chunkValues(params.publicKeys, BUZZ_PROFILE_QUERY_CHUNK_SIZE)) {
for (const authors of chunkItems(params.publicKeys, BUZZ_PROFILE_QUERY_CHUNK_SIZE)) {
await queryBuzzDirectoryBatch({
relay: params.relay,
filter: {
@@ -138,7 +131,7 @@ export async function queryBuzzDirectoryRooms(params: {
onTimeout?: (error: Error) => void;
signal?: AbortSignal;
}): Promise<void> {
for (const roomIds of chunkValues(params.channelIds, BUZZ_ROOM_QUERY_CHUNK_SIZE)) {
for (const roomIds of chunkItems(params.channelIds, BUZZ_ROOM_QUERY_CHUNK_SIZE)) {
await queryBuzzDirectoryBatch({
relay: params.relay,
filter: {
@@ -228,7 +221,7 @@ export function startBuzzDirectoryRelay(params: {
const publicKeys = queuedProfilePublicKeys;
queuedProfilePublicKeys = undefined;
closeProfileGeneration(PROFILE_SUBSCRIPTION_REPLACED_REASON);
const authorChunks = chunkValues(publicKeys, BUZZ_PROFILE_QUERY_CHUNK_SIZE);
const authorChunks = chunkItems(publicKeys, BUZZ_PROFILE_QUERY_CHUNK_SIZE);
if (authorChunks.length === 0) {
return;
}
+2 -8
View File
@@ -15,6 +15,7 @@ import {
jsonResult,
readStringParam,
} from "openclaw/plugin-sdk/channel-actions";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
import {
addTimerTimeoutGraceMs,
@@ -122,13 +123,6 @@ function wrapCanvasEvalResult(result: unknown): string {
return wrappedText;
}
function isPathInsideRoot(root: string, candidate: string): boolean {
const relative = path.relative(root, candidate);
return (
relative === "" || (relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative))
);
}
async function readJsonlFromPath(jsonlPath: string, workspaceDir?: string): Promise<string> {
const trimmed = jsonlPath.trim();
if (!trimmed) {
@@ -140,7 +134,7 @@ async function readJsonlFromPath(jsonlPath: string, workspaceDir?: string): Prom
fs.realpath(workspaceRoot),
fs.realpath(resolved),
]);
if (!isPathInsideRoot(workspaceReal, resolvedReal)) {
if (!isPathInside(workspaceReal, resolvedReal)) {
throw new Error("jsonlPath outside workspace");
}
return (
@@ -5,6 +5,7 @@ import type {
import { projectAgentHarnessTranscriptMessageForDisplay } from "openclaw/plugin-sdk/agent-harness-runtime";
import type { AssistantMessage } from "openclaw/plugin-sdk/llm";
import { asDateTimestampMs } from "openclaw/plugin-sdk/number-runtime";
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { attachCodexMirrorIdentity } from "./upstream-prompt-provenance.js";
import { promptSnapshot } from "./user-prompt-message.js";
@@ -13,9 +14,7 @@ const CODEX_META_KEY = "__openclaw";
function readTurnTaintMetadata(message: AgentMessage): TurnTaintMetadata | undefined {
const metadata = CODEX_META_KEY in message ? message[CODEX_META_KEY] : undefined;
return metadata && typeof metadata === "object" && !Array.isArray(metadata)
? (metadata as TurnTaintMetadata)
: undefined;
return asOptionalRecord(metadata) as TurnTaintMetadata | undefined;
}
function applyStickyTurnTaint(messages: readonly AgentMessage[]): AgentMessage[] {
@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import { resolveRequiredHomeDir, resolveStateDir } from "openclaw/plugin-sdk/state-paths";
import type { CodexAppServerClient } from "./client.js";
import type { JsonObject, JsonValue } from "./protocol.js";
@@ -27,14 +28,6 @@ function isMissingPathError(error: unknown): boolean {
return (error as NodeJS.ErrnoException).code === "ENOENT";
}
function isPathWithin(root: string, candidate: string): boolean {
const relative = path.relative(path.resolve(root), path.resolve(candidate));
return (
relative === "" ||
(!path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== "..")
);
}
async function canonicalizeExistingPath(candidate: string): Promise<string> {
try {
return await fs.realpath(candidate);
@@ -72,13 +65,13 @@ async function collectPersonalSkillRealPaths(
const realDefaultCodexHome = await canonicalizeExistingPath(defaultCodexHome);
roots.push({
dir: path.join(defaultCodexHome, "skills"),
onlyEscapedStateTargets: isPathWithin(realStateDir, realDefaultCodexHome),
onlyEscapedStateTargets: isPathInside(realStateDir, realDefaultCodexHome),
});
}
const configuredCodexHome = codexHome?.trim() || process.env.CODEX_HOME?.trim();
if (configuredCodexHome) {
const realCodexHome = await canonicalizeExistingPath(configuredCodexHome);
const stateOwned = isPathWithin(realStateDir, realCodexHome);
const stateOwned = isPathInside(realStateDir, realCodexHome);
roots.push({
dir: path.join(configuredCodexHome, "skills"),
// Direct descendants of a state-owned Codex home belong to this isolated instance.
@@ -98,7 +91,7 @@ async function collectPersonalSkillRealPaths(
const recordSkillFile = async (filePath: string, onlyEscapedStateTargets: boolean) => {
try {
const skillRealPath = await fs.realpath(filePath);
if (!onlyEscapedStateTargets || !isPathWithin(realStateDir, skillRealPath)) {
if (!onlyEscapedStateTargets || !isPathInside(realStateDir, skillRealPath)) {
skillPaths.add(skillRealPath);
}
} catch (error) {
@@ -1,6 +1,7 @@
import { Buffer } from "node:buffer";
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
import type { AgentPlanStep } from "openclaw/plugin-sdk/channel-outbound";
import { truncateUtf8Prefix } from "openclaw/plugin-sdk/text-utility-runtime";
import { stripInvisibleUnicode } from "openclaw/plugin-sdk/web-content-extractor";
import type { CodexAppServerClient } from "./client.js";
import { isJsonObject } from "./protocol.js";
@@ -131,16 +132,11 @@ function serializePlan(plan: StoredPlan): string {
}
function truncateUtf8(value: string, maxBytes: number): string {
const bytes = Buffer.from(value);
if (bytes.byteLength <= maxBytes) {
if (Buffer.byteLength(value, "utf8") <= maxBytes) {
return value;
}
const suffixBytes = Buffer.byteLength(RESTORED_PLAN_TRUNCATION_SUFFIX, "utf8");
let end = Math.max(0, maxBytes - suffixBytes);
while (end > 0 && (bytes[end] ?? 0) >> 6 === 0b10) {
end -= 1;
}
return `${bytes.subarray(0, end).toString("utf8")}${RESTORED_PLAN_TRUNCATION_SUFFIX}`;
return `${truncateUtf8Prefix(value, maxBytes - suffixBytes)}${RESTORED_PLAN_TRUNCATION_SUFFIX}`;
}
function readPlanSteps(value: unknown): AgentPlanStep[] {
@@ -1,5 +1,5 @@
import path from "node:path";
import { root } from "openclaw/plugin-sdk/file-access-runtime";
import { isPathStrictlyInside, root } from "openclaw/plugin-sdk/file-access-runtime";
import { getMediaDir } from "openclaw/plugin-sdk/media-runtime";
import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
import type { CodexCommandExecParams, CodexCommandExecResponse } from "./command-exec-protocol.js";
@@ -236,7 +236,7 @@ export async function prepareCodexRemoteWorkspaceMessageMedia(params: {
if (typeof value !== "string") {
return value;
}
if (isGatewayManagedMediaPath(value, gatewayMediaRoot)) {
if (path.isAbsolute(value) && isPathStrictlyInside(gatewayMediaRoot, value)) {
attachmentEntries += 1;
gatewayManagedPaths.add(value);
return value;
@@ -374,19 +374,6 @@ export async function prepareCodexRemoteWorkspaceMessageMedia(params: {
return mapMessageMediaValues(mappedArgs, (value) => stagedPaths.get(value) ?? value);
}
function isGatewayManagedMediaPath(value: string, mediaRoot: string): boolean {
if (!path.isAbsolute(value)) {
return false;
}
const relativePath = path.relative(mediaRoot, value);
return Boolean(
relativePath &&
relativePath !== ".." &&
!relativePath.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relativePath),
);
}
async function assertGatewayManagedMediaPath(value: string, mediaRoot: string): Promise<void> {
const media = await root(mediaRoot, { symlinks: "reject" });
const opened = await media.open(path.relative(mediaRoot, value), { symlinks: "reject" });
@@ -4,6 +4,7 @@ import { constants as fsConstants } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import type { AgentHarnessRuntimeArtifactBinding } from "openclaw/plugin-sdk/agent-harness-runtime";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import {
resolveWindowsExecutablePath,
resolveWindowsSpawnProgram,
@@ -223,14 +224,6 @@ async function listPackageFiles(params: {
return files;
}
function pathIsWithin(rootPath: string, candidatePath: string): boolean {
const relative = path.relative(rootPath, candidatePath);
return (
relative === "" ||
(!path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== "..")
);
}
function assertSafeNodeOptions(env: NodeJS.ProcessEnv): void {
for (const [rawKey, value] of Object.entries(env)) {
const key = rawKey.toUpperCase();
@@ -376,7 +369,7 @@ async function hashSelectedArtifactFiles(
}
}
const externalFiles = [...new Set(allFiles)]
.filter((filePath) => !packageRoot || !pathIsWithin(packageRoot, filePath))
.filter((filePath) => !packageRoot || !isPathInside(packageRoot, filePath))
.toSorted(compareArtifactNames);
const budget: ArtifactHashBudget = { fileCount: 0, totalBytes: 0n };
const hash = createHash("sha256");
@@ -517,7 +510,7 @@ async function resolvePackageRoot(nativePath: string): Promise<string | undefine
return undefined;
}
const root = await fs.realpath(candidate);
return pathIsWithin(root, nativePath) ? root : undefined;
return isPathInside(root, nativePath) ? root : undefined;
} catch {
return undefined;
}
@@ -9,7 +9,10 @@ import {
embeddedAgentLog,
type EmbeddedRunAttemptParamsV2 as EmbeddedRunAttemptParams,
} from "openclaw/plugin-sdk/agent-harness-runtime";
import { root as openSafeFilesystemRoot } from "openclaw/plugin-sdk/file-access-runtime";
import {
isPathStrictlyInside,
root as openSafeFilesystemRoot,
} from "openclaw/plugin-sdk/file-access-runtime";
import { parseSqliteSessionFileMarker } from "openclaw/plugin-sdk/session-store-runtime";
import { resolveCodexAppServerHomeDir } from "./auth-bridge.js";
import { isJsonObject, type JsonValue } from "./protocol.js";
@@ -96,15 +99,7 @@ async function listCodexAppServerRolloutFilesForThread(
path.join(path.dirname(resolvedAgentDir), "codex-home", "sessions"),
];
const rolloutRoot = rolloutPath
? roots.find((root) => {
const relativePath = path.relative(root, rolloutPath);
return (
relativePath !== "" &&
relativePath !== ".." &&
!relativePath.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relativePath)
);
})
? roots.find((root) => isPathStrictlyInside(root, rolloutPath))
: undefined;
if (
rolloutPath &&
@@ -3,6 +3,7 @@ import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
import type { AssistantMessage, Usage } from "openclaw/plugin-sdk/llm";
import type { SessionTranscriptMessageEntry } from "openclaw/plugin-sdk/session-transcript-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf8Prefix } from "openclaw/plugin-sdk/text-utility-runtime";
import type { CodexThread, JsonValue } from "./protocol.js";
import { attachCodexMirrorIdentity } from "./upstream-prompt-provenance.js";
@@ -38,22 +39,6 @@ type ProjectedCodexHistoryMessage = {
textBytes: number;
};
function isUtf8ContinuationByte(byte: number | undefined): boolean {
return byte !== undefined && (byte & 0xc0) === 0x80;
}
function truncateUtf8Prefix(value: string, maxBytes: number): string {
const bytes = Buffer.from(value);
if (bytes.byteLength <= maxBytes) {
return value;
}
let end = Math.max(0, maxBytes);
while (end > 0 && isUtf8ContinuationByte(bytes[end])) {
end -= 1;
}
return bytes.subarray(0, end).toString("utf8");
}
function normalizeImportedHistoryText(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
@@ -1,4 +1,5 @@
import type { AgentMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
const UPSTREAM_USER_TEXT_META_KEY = "upstreamUserText" as const;
const MIRROR_IDENTITY_META_KEY = "mirrorIdentity" as const;
@@ -6,10 +7,7 @@ const CODEX_META_KEY = "__openclaw";
export function attachCodexMirrorIdentity<T extends AgentMessage>(message: T, identity: string): T {
const existing = CODEX_META_KEY in message ? message[CODEX_META_KEY] : undefined;
const baseMeta =
existing && typeof existing === "object" && !Array.isArray(existing)
? (existing as Record<string, unknown>)
: {};
const baseMeta = asOptionalRecord(existing) ?? {};
return {
...message,
__openclaw: { ...baseMeta, [MIRROR_IDENTITY_META_KEY]: identity },
@@ -18,19 +16,17 @@ export function attachCodexMirrorIdentity<T extends AgentMessage>(message: T, id
export function readMirrorIdentity(message: AgentMessage): string | undefined {
const meta = CODEX_META_KEY in message ? message[CODEX_META_KEY] : undefined;
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
const record = asOptionalRecord(meta);
if (!record) {
return undefined;
}
const id = (meta as Record<string, unknown>)[MIRROR_IDENTITY_META_KEY];
const id = record[MIRROR_IDENTITY_META_KEY];
return typeof id === "string" && id ? id : undefined;
}
export function attachUpstreamUserText<T extends AgentMessage>(message: T, text: string): T {
const existing = CODEX_META_KEY in message ? message[CODEX_META_KEY] : undefined;
const baseMeta =
existing && typeof existing === "object" && !Array.isArray(existing)
? (existing as Record<string, unknown>)
: {};
const baseMeta = asOptionalRecord(existing) ?? {};
return {
...message,
__openclaw: { ...baseMeta, [UPSTREAM_USER_TEXT_META_KEY]: text },
@@ -39,9 +35,10 @@ export function attachUpstreamUserText<T extends AgentMessage>(message: T, text:
export function readUpstreamUserText(message: AgentMessage | undefined): string | undefined {
const meta = message && CODEX_META_KEY in message ? message[CODEX_META_KEY] : undefined;
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
const record = asOptionalRecord(meta);
if (!record) {
return undefined;
}
const text = (meta as Record<string, unknown>)[UPSTREAM_USER_TEXT_META_KEY];
const text = record[UPSTREAM_USER_TEXT_META_KEY];
return typeof text === "string" && text ? text : undefined;
}
+3 -9
View File
@@ -7,6 +7,7 @@ import type {
ChannelDoctorSequenceResult,
} from "openclaw/plugin-sdk/channel-contract";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { isPathStrictlyInside } from "openclaw/plugin-sdk/file-access-runtime";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import {
isValidAgentHarnessSessionStoreEntry,
@@ -133,13 +134,6 @@ function safeReadDir(dir: string): fs.Dirent[] {
}
}
function isPathWithinRoot(targetPath: string, rootPath: string): boolean {
const resolvedTarget = path.resolve(targetPath);
const resolvedRoot = path.resolve(rootPath);
const relative = path.relative(resolvedRoot, resolvedTarget);
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
}
function formatDisplayPath(filePath: string): string {
const home = os.homedir();
const resolved = path.resolve(filePath);
@@ -327,8 +321,8 @@ function resolveSessionTranscriptCandidates(params: {
const resolved = path.isAbsolute(candidate)
? path.resolve(candidate)
: path.resolve(sessionsDir, candidate);
const isStoreCandidate = isPathWithinRoot(resolved, sessionsDir);
const isAgentSessionCandidate = isPathWithinRoot(resolved, agentSessionsDir);
const isStoreCandidate = isPathStrictlyInside(sessionsDir, resolved);
const isAgentSessionCandidate = isPathStrictlyInside(agentSessionsDir, resolved);
if (
resolved === sessionsDir ||
resolved === agentSessionsDir ||
+14 -13
View File
@@ -50,7 +50,10 @@ import path from "node:path";
import { minimatch } from "minimatch";
import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
import { asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asNullableRecord,
asOptionalObjectRecord,
} from "openclaw/plugin-sdk/string-coerce-runtime";
export type FilePolicyKind = "read" | "write";
type FilePolicyAskMode = "off" | "on-miss" | "always";
@@ -90,31 +93,29 @@ function asFilePolicyConfig(value: unknown): FilePolicyConfig | null {
}
function readFilePolicyConfigFromPluginConfig(pluginConfig: unknown): FilePolicyConfig | null {
if (!pluginConfig || typeof pluginConfig !== "object" || Array.isArray(pluginConfig)) {
const pluginRecord = asNullableRecord(pluginConfig);
if (!pluginRecord) {
return null;
}
const nodes = (pluginConfig as { nodes?: unknown }).nodes;
const nodes = pluginRecord.nodes;
return asFilePolicyConfig(nodes);
}
function readPluginConfigFromRuntimeConfig(): Record<string, unknown> | null {
const cfg = getRuntimeConfig();
const plugins = (cfg as { plugins?: unknown }).plugins;
if (!plugins || typeof plugins !== "object") {
const plugins = asOptionalObjectRecord((cfg as { plugins?: unknown }).plugins);
if (!plugins) {
return null;
}
const entries = (plugins as { entries?: unknown }).entries;
if (!entries || typeof entries !== "object") {
const entries = asOptionalObjectRecord(plugins.entries);
if (!entries) {
return null;
}
const entry = (entries as Record<string, unknown>)["file-transfer"];
if (!entry || typeof entry !== "object") {
const entry = asOptionalObjectRecord(entries["file-transfer"]);
if (!entry) {
return null;
}
const pluginConfig = (entry as { config?: unknown }).config;
return pluginConfig && typeof pluginConfig === "object" && !Array.isArray(pluginConfig)
? (pluginConfig as Record<string, unknown>)
: null;
return asNullableRecord(entry.config);
}
function readFilePolicyConfig(pluginConfig?: Record<string, unknown>): FilePolicyConfig | null {
+6 -15
View File
@@ -12,7 +12,10 @@ import type {
} from "openclaw/plugin-sdk/realtime-voice";
import { createRealtimeVoiceAudioQueue } from "openclaw/plugin-sdk/realtime-voice-audio-queue";
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
asOptionalRecord,
normalizeOptionalString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import type { VideoGenerationProvider } from "openclaw/plugin-sdk/video-generation";
import { buildGoogleGeminiCliBackend } from "./cli-backend.js";
import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js";
@@ -161,21 +164,9 @@ function resolveGoogleRealtimeProviderConfig(
rawConfig: RealtimeVoiceProviderConfig,
cfg?: { models?: { providers?: { google?: { apiKey?: unknown } } } },
): RealtimeVoiceProviderConfig {
const providers =
typeof rawConfig.providers === "object" &&
rawConfig.providers !== null &&
!Array.isArray(rawConfig.providers)
? (rawConfig.providers as Record<string, unknown>)
: undefined;
const nested = providers?.google;
const providers = asOptionalRecord(rawConfig.providers);
const raw =
typeof nested === "object" && nested !== null && !Array.isArray(nested)
? (nested as Record<string, unknown>)
: typeof rawConfig.google === "object" &&
rawConfig.google !== null &&
!Array.isArray(rawConfig.google)
? (rawConfig.google as Record<string, unknown>)
: rawConfig;
asOptionalRecord(providers?.google) ?? asOptionalRecord(rawConfig.google) ?? rawConfig;
return {
...raw,
...(raw.apiKey === undefined
+3 -12
View File
@@ -50,6 +50,7 @@ import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-i
import {
asBoolean,
asFiniteNumber,
asOptionalRecord,
asSafeIntegerInRange,
isRecord,
normalizeOptionalString,
@@ -226,18 +227,8 @@ function asGoogleRealtimeThinkingBudget(value: unknown): number | undefined {
function resolveGoogleRealtimeProviderConfigRecord(
config: Record<string, unknown>,
): Record<string, unknown> | undefined {
const providers =
typeof config.providers === "object" &&
config.providers !== null &&
!Array.isArray(config.providers)
? (config.providers as Record<string, unknown>)
: undefined;
const nested = providers?.google;
return typeof nested === "object" && nested !== null && !Array.isArray(nested)
? (nested as Record<string, unknown>)
: typeof config.google === "object" && config.google !== null && !Array.isArray(config.google)
? (config.google as Record<string, unknown>)
: config;
const providers = asOptionalRecord(config.providers);
return asOptionalRecord(providers?.google) ?? asOptionalRecord(config.google) ?? config;
}
function normalizeProviderConfig(
+2 -10
View File
@@ -3,6 +3,7 @@ import { stat } from "node:fs/promises";
import path from "node:path";
import { Readable, Writable } from "node:stream";
import { toErrorObject as toLintErrorObject } from "openclaw/plugin-sdk/error-runtime";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
export type LobsterEnvelope =
| {
@@ -80,11 +81,6 @@ type EmbeddedToolRuntime = {
const workflowExts = new Set([".lobster", ".yaml", ".yml", ".json"]);
function normalizeForCwdSandbox(p: string): string {
const normalized = path.normalize(p);
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
}
export function resolveLobsterCwd(cwdRaw: unknown): string {
if (typeof cwdRaw !== "string" || !cwdRaw.trim()) {
return process.cwd();
@@ -96,11 +92,7 @@ export function resolveLobsterCwd(cwdRaw: unknown): string {
const base = process.cwd();
const resolved = path.resolve(base, cwd);
const rel = path.relative(normalizeForCwdSandbox(base), normalizeForCwdSandbox(resolved));
if (rel === "" || rel === ".") {
return resolved;
}
if (rel.startsWith("..") || path.isAbsolute(rel)) {
if (!isPathInside(base, resolved)) {
throw new Error("cwd must stay within the gateway working directory");
}
return resolved;
@@ -1,30 +1,14 @@
// Mattermost plugin module implements slash commands behavior.
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf8Prefix } from "openclaw/plugin-sdk/text-utility-runtime";
import type { MattermostClient } from "./client.js";
// ─── Types ───────────────────────────────────────────────────────────────────
export const MATTERMOST_SLASH_POST_METHOD = "P";
const MATTERMOST_COMMAND_DESCRIPTION_MAX_BYTES = 128;
// Mattermost rejects command descriptions above 128 UTF-8 bytes. Keep portable
// descriptions intact until this API boundary so other channels retain their text.
function truncateMattermostCommandDescription(description: string): string {
if (Buffer.byteLength(description, "utf8") <= MATTERMOST_COMMAND_DESCRIPTION_MAX_BYTES) {
return description;
}
let bytes = 0;
let end = 0;
for (const char of description) {
const charBytes = Buffer.byteLength(char, "utf8");
if (bytes + charBytes > MATTERMOST_COMMAND_DESCRIPTION_MAX_BYTES) {
break;
}
bytes += charBytes;
end += char.length;
}
return description.slice(0, end);
}
export const MATTERMOST_SLASH_POST_METHOD = "P";
const MATTERMOST_COMMAND_DESCRIPTION_MAX_BYTES = 128;
export type MattermostSlashCommandConfig = {
/** Enable native slash commands. "auto" resolves to false for now (opt-in). */
@@ -311,7 +295,10 @@ export async function registerSlashCommands(params: {
const registered: MattermostRegisteredCommand[] = [];
for (const spec of commands) {
const description = truncateMattermostCommandDescription(spec.description);
const description = truncateUtf8Prefix(
spec.description,
MATTERMOST_COMMAND_DESCRIPTION_MAX_BYTES,
);
const existingForTrigger = existingByTrigger.get(spec.trigger) ?? [];
const ownedCommands = existingForTrigger.filter(
(cmd) => cmd.creator_id?.trim() === normalizedCreatorUserId,
@@ -32,6 +32,7 @@ import {
import { MAX_TIMER_TIMEOUT_MS, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
import { runSqliteImmediateTransactionSync } from "openclaw/plugin-sdk/sqlite-runtime";
import { chunkItems } from "openclaw/plugin-sdk/text-chunking";
import { readSessionResetRecallCutoffMetadata } from "../session-reset-recall-metadata.js";
import type { EmbeddingProvider } from "./embeddings.js";
import {
@@ -216,12 +217,7 @@ function formatBatchSourceCounts(counts: Record<string, number>): string {
}
function splitSourceWideEmbeddingChunks<T>(chunks: T[], maxRequests: number): T[][] {
const limit = Math.max(1, Math.floor(maxRequests));
const batches: T[][] = [];
for (let start = 0; start < chunks.length; start += limit) {
batches.push(chunks.slice(start, start + limit));
}
return batches;
return chunkItems(chunks, Math.max(1, Math.floor(maxRequests)));
}
function resolveEmbeddingTimeoutMs(params: {
@@ -2,6 +2,7 @@
import fsSync from "node:fs";
import path from "node:path";
import chokidar, { type FSWatcher } from "chokidar";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import { classifyMemoryMultimodalPath } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import {
createSubsystemLogger,
@@ -102,11 +103,6 @@ function shouldIgnoreMemoryWatchPath(
return classifyMemoryMultimodalPath(normalized, multimodalSettings) === null;
}
function isWithinMemoryWatchRoot(root: string, candidate: string): boolean {
const relative = path.relative(root, candidate);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
function runDetachedMemorySync(sync: () => Promise<void>, reason: "interval" | "watch") {
void sync().catch((err: unknown) => {
log.warn(`memory sync failed (${reason}): ${String(err)}`);
@@ -161,11 +157,9 @@ export abstract class MemoryManagerWatchOps extends MemoryManagerSyncBase {
const markDirty = (watchPath?: string, stats?: MemoryWatchEventStats) => {
if (watchPath && stats && !stats.isDirectory?.()) {
const normalizedWatchPath = path.resolve(watchPath);
const matchingEntries = isWithinMemoryWatchRoot(memoryDir, normalizedWatchPath)
const matchingEntries = isPathInside(memoryDir, normalizedWatchPath)
? []
: additionalPaths.filter((entry) =>
isWithinMemoryWatchRoot(entry.path, normalizedWatchPath),
);
: additionalPaths.filter((entry) => isPathInside(entry.path, normalizedWatchPath));
if (
matchingEntries.length > 0 &&
!matchingEntries.some((entry) => matchesExtraMemoryPathEntry(entry, normalizedWatchPath))
@@ -1,6 +1,7 @@
// Memory Core plugin module classifies indexed workspace paths by provenance owner.
import fs from "node:fs/promises";
import path from "node:path";
import { isPathStrictlyInside } from "openclaw/plugin-sdk/file-access-runtime";
import type {
MemoryEntryProvenance,
MemorySource,
@@ -33,15 +34,10 @@ export async function resolveMemoryPathClassification(params: {
} catch {
return { curatedRoot: false, originClass: "untrusted" };
}
const relativePath = path.relative(workspacePath, filePath);
if (
!relativePath ||
path.isAbsolute(relativePath) ||
relativePath === ".." ||
relativePath.startsWith(`..${path.sep}`)
) {
if (!isPathStrictlyInside(workspacePath, filePath)) {
return { curatedRoot: false, originClass: "untrusted" };
}
const relativePath = path.relative(workspacePath, filePath);
const segments = relativePath.split(path.sep);
const curatedRoot =
segments.length === 1 &&
+2 -9
View File
@@ -2,6 +2,7 @@
import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import {
getMemoryCapabilityRegistration,
listActiveMemoryPublicArtifacts,
@@ -114,7 +115,7 @@ async function collectBridgeArtifacts(
continue;
}
const syncKey = await resolveArtifactKey(artifact.absolutePath);
if (isPathInsideOrEqual(vaultRootKey, syncKey)) {
if (isPathInside(vaultRootKey, syncKey)) {
continue;
}
collected.push({
@@ -132,14 +133,6 @@ async function collectBridgeArtifacts(
return [...deduped.values()];
}
function isPathInsideOrEqual(parentPath: string, candidatePath: string): boolean {
const relative = path.relative(parentPath, candidatePath);
return (
relative === "" ||
(relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
);
}
function resolveBridgeTitle(artifact: BridgeArtifact, agentIds: string[]): string {
if (artifact.artifactType === "memory-events") {
if (agentIds.length === 0) {
+2 -2
View File
@@ -2,6 +2,7 @@
import { createHash, randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import {
replaceManagedMarkdownBlock,
withTrailingNewline,
@@ -904,8 +905,7 @@ function resolveContainedImportPath(root: string, relativePath: string, label: s
}
const resolvedRoot = path.resolve(root);
const resolvedPath = path.resolve(resolvedRoot, relativePath);
const relative = path.relative(resolvedRoot, resolvedPath);
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
if (!isPathInside(resolvedRoot, resolvedPath)) {
throw new Error(`${label} must stay inside ${resolvedRoot}: ${relativePath}`);
}
return resolvedPath;
+5 -19
View File
@@ -9,6 +9,7 @@ import {
normalizeOptionalString,
normalizeSingleOrTrimmedStringList,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { truncateUtf8Prefix } from "openclaw/plugin-sdk/text-utility-runtime";
import YAML from "yaml";
const WIKI_PAGE_KINDS = ["entity", "concept", "source", "synthesis", "report"] as const;
@@ -141,29 +142,15 @@ const WIKI_RESERVED_PAGE_STEMS = new Set(["index"]);
const HUMAN_START_MARKER = "<!-- openclaw:human:start -->";
const HUMAN_END_MARKER = "<!-- openclaw:human:end -->";
function truncateUtf8CodePointSafe(value: string, maxBytes: number): string {
let result = "";
let bytes = 0;
for (const char of value) {
const nextBytes = Buffer.byteLength(char);
if (bytes + nextBytes > maxBytes) {
break;
}
result += char;
bytes += nextBytes;
}
return result;
}
function capWikiValueWithHash(raw: string, maxBytes: number, fallback: string): string {
if (Buffer.byteLength(raw) <= maxBytes) {
return raw;
}
const suffix = createHash("sha1").update(raw).digest("hex").slice(0, WIKI_SEGMENT_HASH_BYTES);
const truncated = truncateUtf8CodePointSafe(
raw,
maxBytes - Buffer.byteLength(`-${suffix}`),
).replace(/-+$/g, "");
const truncated = truncateUtf8Prefix(raw, maxBytes - Buffer.byteLength(`-${suffix}`)).replace(
/-+$/g,
"",
);
return `${truncated || fallback}-${suffix}`;
}
@@ -761,4 +748,3 @@ export function toWikiPageSummary(params: {
const result = scanWikiPageSummary(params);
return result.status === "valid" ? result.page : null;
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
+2 -9
View File
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { walkMemoryWikiDirectory } from "./bounded-walk.js";
import type { BridgeMemoryWikiResult } from "./bridge.js";
@@ -112,14 +113,6 @@ async function collectUnsafeLocalArtifacts(
return { artifacts: [...deduped.values()], unavailableConfiguredPaths };
}
function isSourceWithinConfiguredPath(sourcePath: string, configuredPath: string): boolean {
const relative = path.relative(configuredPath, sourcePath);
return (
relative === "" ||
(relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
);
}
function resolveUnsafeLocalPagePath(params: { configuredPath: string; absolutePath: string }): {
pageId: string;
pagePath: string;
@@ -240,7 +233,7 @@ export async function syncMemoryWikiUnsafeLocalSources(
if (
entry.group === "unsafe-local" &&
unavailableConfiguredPaths.some((configuredPath) =>
isSourceWithinConfiguredPath(entry.sourcePath, configuredPath),
isPathInside(configuredPath, entry.sourcePath),
)
) {
// A configured source scope remains authoritative until it is readable again or removed
+2 -6
View File
@@ -1,5 +1,6 @@
// Migrate Hermes plugin module implements source behavior.
import path from "node:path";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import { exists, isDirectory, readText, resolveHomePath } from "./helpers.js";
export type HermesSource = {
@@ -67,11 +68,6 @@ const HERMES_STATE_MARKERS = [
...HERMES_ARCHIVE_FILES,
] as const;
function isSameOrInside(parent: string, candidate: string): boolean {
const relative = path.relative(path.resolve(parent), path.resolve(candidate));
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
function resolveOpenCodeXdgAuthPath(env: NodeJS.ProcessEnv = process.env): string | undefined {
const xdgDataHome = env.XDG_DATA_HOME?.trim();
return xdgDataHome ? path.join(resolveHomePath(xdgDataHome), "opencode", "auth.json") : undefined;
@@ -88,7 +84,7 @@ async function discoverOpenCodeAuthPath(params: {
const candidates = Array.from(
new Set(
[
...(xdgAuthPath && (params.includeGlobalFallback || isSameOrInside(rootParent, xdgAuthPath))
...(xdgAuthPath && (params.includeGlobalFallback || isPathInside(rootParent, xdgAuthPath))
? [xdgAuthPath]
: []),
path.join(rootParent, OPENCODE_AUTH_RELATIVE_PATH),
+2 -2
View File
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
import { mkdtempSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
import path from "node:path";
import type { ContainerConfig } from "@microsoft/mxc-sdk";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import { runCommandBuffered } from "openclaw/plugin-sdk/process-runtime";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/sandbox";
import type {
@@ -89,8 +90,7 @@ function createSandboxTempDir(hostEnv: BaselineHostEnv): string {
function assertWorkdirInsideWorkspace(workspaceDir: string, workdir: string): string {
const workspace = realpathForExistingPath(workspaceDir, "sandbox workspace");
const candidate = realpathForPotentialPath(workdir);
const relative = path.relative(workspace, candidate);
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
if (isPathInside(workspace, candidate)) {
return candidate;
}
throw new Error(
+3 -3
View File
@@ -1,5 +1,6 @@
// Qa Lab plugin module implements cli paths behavior.
import path from "node:path";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import { assertNoSymlinkParents, pathScope } from "openclaw/plugin-sdk/security-runtime";
export function toRepoPath(filePath: string): string {
@@ -29,11 +30,10 @@ export function resolveRepoRelativeOutputDir(repoRoot: string, outputDir?: strin
}
function assertRepoRelativePath(repoRoot: string, targetPath: string, label: string) {
const relative = path.relative(repoRoot, targetPath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
if (!isPathInside(repoRoot, targetPath)) {
throw new Error(`${label} must stay within the repo root.`);
}
return relative;
return path.relative(repoRoot, targetPath);
}
async function assertNoSymlinkSegments(repoRoot: string, targetPath: string, label: string) {
+12 -15
View File
@@ -6,6 +6,7 @@ import { StringDecoder } from "node:string_decoder";
import { pathToFileURL } from "node:url";
import { runTasksWithConcurrency } from "openclaw/plugin-sdk/concurrency-runtime";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import {
asNullableRecord as readRecord,
readStringValue,
@@ -57,11 +58,6 @@ function evidenceError(message: string, statusCode: number): QaEvidenceGalleryEr
return new QaEvidenceGalleryError(message, statusCode);
}
function isInside(root: string, candidate: string) {
const relative = path.relative(root, candidate);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
function sanitizeGalleryText(
value: string,
params: {
@@ -94,7 +90,7 @@ function displayGalleryPath(
const absolute = path.resolve(value);
for (const root of [params.repoRoot, ...(params.extraRoots ?? [])]) {
const resolvedRoot = path.resolve(root);
if (isInside(resolvedRoot, absolute)) {
if (isPathInside(resolvedRoot, absolute)) {
return sanitizeGalleryText(toRepoPath(path.relative(resolvedRoot, absolute)), params);
}
}
@@ -138,7 +134,7 @@ async function resolveContainedFileIfExists(
if (!realFile) {
return null;
}
if (!allowedRoots.some((root) => isInside(root, realFile))) {
if (!allowedRoots.some((root) => isPathInside(root, realFile))) {
return null;
}
const stats = await fs.stat(realFile).catch(() => null);
@@ -159,7 +155,7 @@ async function resolveQaEvidenceFile(params: {
if (!realCandidate) {
throw evidenceError("Evidence path not found.", 404);
}
if (!isInside(repoRoot, realCandidate)) {
if (!isPathInside(repoRoot, realCandidate)) {
throw evidenceError("Evidence path must stay inside the repo root.", 403);
}
const stats = await fs.stat(realCandidate);
@@ -170,7 +166,7 @@ async function resolveQaEvidenceFile(params: {
if (!realEvidencePath) {
throw evidenceError("qa-evidence.json not found.", 404);
}
if (!isInside(repoRoot, realEvidencePath)) {
if (!isPathInside(repoRoot, realEvidencePath)) {
throw evidenceError("qa-evidence.json must stay inside the repo root.", 403);
}
return realEvidencePath;
@@ -313,7 +309,10 @@ async function resolveArtifactFileWithinRoots(params: {
if (!realCandidate) {
continue;
}
if (!isInside(params.repoRoot, realCandidate) && !isInside(params.evidenceDir, realCandidate)) {
if (
!isPathInside(params.repoRoot, realCandidate) &&
!isPathInside(params.evidenceDir, realCandidate)
) {
continue;
}
const stats = await fs.stat(realCandidate).catch(() => null);
@@ -440,9 +439,7 @@ async function readJsonIfExists(
}
try {
const value = JSON.parse(await fs.readFile(realFile, "utf8")) as unknown;
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
return readRecord(value);
} catch {
return null;
}
@@ -518,7 +515,7 @@ async function buildArtifactView(params: {
repoRoot: params.repoRoot,
}).catch(() => null);
const realFileRepoPath =
realFile && isInside(params.repoRoot, realFile)
realFile && isPathInside(params.repoRoot, realFile)
? toRepoRelativePath(params.repoRoot, realFile)
: null;
const displayPath =
@@ -733,7 +730,7 @@ async function candidateProducerRoots(params: {
continue;
}
let current = path.dirname(artifactPath);
while (isInside(repoRoot, current)) {
while (isPathInside(repoRoot, current)) {
roots.add(current);
const parent = path.dirname(current);
if (parent === current) {
@@ -5,6 +5,7 @@ import fs from "node:fs/promises";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import { isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import { replaceFileAtomic } from "openclaw/plugin-sdk/security-runtime";
const PROCESS_BOUNDARY_VERSION = 1;
@@ -236,8 +237,7 @@ function parseQaGatewayProcessRuntimeProof(value: unknown): QaGatewayProcessRunt
async function assertContainedPath(root: string, target: string, label: string) {
const rootPath = await fs.realpath(root);
const targetPath = await fs.realpath(target);
const relative = path.relative(rootPath, targetPath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
if (!isPathInside(rootPath, targetPath)) {
throw new Error(`${label} escaped its trusted root`);
}
return targetPath;
+2 -6
View File
@@ -5,6 +5,7 @@ import { access, mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import type { OpenClawCrablineChannelDriverSelection } from "@openclaw/crabline";
import { coerceErrorMessage, toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
import { isPathInside } from "openclaw/plugin-sdk/file-access-runtime";
import { runExec } from "openclaw/plugin-sdk/process-runtime";
import { sleep } from "openclaw/plugin-sdk/runtime-env";
import { appendRegularFile } from "openclaw/plugin-sdk/security-runtime";
@@ -160,11 +161,6 @@ function resolveExistingPath(value: string) {
return currentPath;
}
function isPathInside(parentPath: string, childPath: string) {
const relativePath = path.relative(parentPath, childPath);
return !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
}
function validatePnpmVersion(version: string) {
if (!/^[0-9A-Za-z.+_-]+$/u.test(version)) {
throw new Error(`unsupported pnpm version in packageManager: ${version}`);
@@ -183,7 +179,7 @@ function resolveMountedOutputPath(repoRoot: string, hostPath: string) {
const realRepoRoot = resolveRealPath(repoRoot);
const existingHostPath = resolveExistingPath(hostPath);
const realExistingHostPath = resolveRealPath(existingHostPath);
if (!isPathInside(realRepoRoot, realExistingHostPath) && realExistingHostPath !== realRepoRoot) {
if (!isPathInside(realRepoRoot, realExistingHostPath)) {
throw new Error(
`qa suite --runner multipass requires --output-dir to stay under the repo root (${repoRoot}), got ${hostPath}.`,
);
@@ -2,6 +2,7 @@
import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import { setTimeout as sleep } from "node:timers/promises";
import { asNullableRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
import { readRequestBodyWithLimit } from "openclaw/plugin-sdk/webhook-ingress";
import { writeJson } from "../shared/http-json.js";
@@ -408,9 +409,7 @@ export function readBody(req: IncomingMessage): Promise<string> {
export function parseJsonObjectBody(raw: string): Record<string, unknown> | null {
try {
const parsed = raw ? (JSON.parse(raw) as unknown) : {};
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: null;
return asNullableRecord(parsed);
} catch {
return null;
}
+12 -20
View File
@@ -404,22 +404,6 @@ function protectSlackAssistantTranscriptRoleHeaders(text: string): string {
return `${SLACK_ASSISTANT_TRANSCRIPT_PREFIX}${text}`;
}
function hardSliceSlackToken(token: string, limit: number): string[] {
const chunks: string[] = [];
let chunk = "";
for (const character of token) {
if (chunk && chunk.length + character.length > limit) {
chunks.push(chunk);
chunk = "";
}
chunk += character;
}
if (chunk) {
chunks.push(chunk);
}
return chunks;
}
function buildSlackRenderOptions() {
return {
annotationMarkers: {
@@ -505,12 +489,20 @@ export function chunkSlackMrkdwnText(text: string, limit: number): string[] {
if (activeMarker && isAllowedSlackAngleToken(token)) {
if (marker) {
chunks.push(
...hardSliceSlackToken(token, contentLimit).map(
(fragment) => `${marker}${fragment}${marker}`,
),
...chunkTextForOutbound(token, Math.max(1, Math.floor(contentLimit)), {
preserveWhitespace: true,
}).map((fragment) => `${marker}${fragment}${marker}`),
);
} else {
chunks.push(...hardSliceSlackToken(escapeSlackMrkdwnSegment(token), limit));
chunks.push(
...chunkTextForOutbound(
escapeSlackMrkdwnSegment(token),
Math.max(1, Math.floor(limit)),
{
preserveWhitespace: true,
},
),
);
}
continue;
}
+7 -21
View File
@@ -1,5 +1,6 @@
import type { Block, KnownBlock } from "@slack/web-api";
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { renderSlackBlockFallbackText } from "./blocks-fallback.js";
import { SLACK_MAX_BLOCKS } from "./blocks-input.js";
import { SLACK_MESSAGE_TEXT_HARD_LIMIT, SLACK_MESSAGE_TEXT_RECOMMENDED_LIMIT } from "./limits.js";
@@ -33,37 +34,22 @@ type OrderedFallbackBlock = {
continuesText?: boolean;
};
function sliceSlackTextChunk(text: string, start: number, limit: number): string {
return (
sliceUtf16Safe(text, start, Math.min(text.length, start + limit)) ||
Array.from(text.slice(start))[0] ||
""
);
}
export function chunkSlackTextAtHardLimit(
text: string,
limit = SLACK_MESSAGE_TEXT_HARD_LIMIT,
): string[] {
const effectiveLimit = Math.max(1, Math.floor(limit));
const chunks: string[] = [];
let offset = 0;
while (offset < text.length) {
const chunk = sliceSlackTextChunk(text, offset, effectiveLimit);
if (!chunk) {
throw new Error("Slack plain-text fallback chunking made no progress.");
}
chunks.push(chunk);
offset += chunk.length;
if (!text) {
return [];
}
return chunks;
const effectiveLimit = Math.max(1, Math.floor(limit));
return chunkTextForOutbound(text, effectiveLimit, { preserveWhitespace: true });
}
function fitsSlackTextLimit(text: string, limit: number): boolean {
if (text.length <= limit) {
return true;
}
return sliceSlackTextChunk(text, 0, limit).length === text.length;
return truncateUtf16Safe(text, limit).length === text.length;
}
function buildPlainTextBlocks(text: string, textLimit: number): OrderedFallbackBlock[] {
+1 -12
View File
@@ -1,4 +1,5 @@
// Telegram plugin module implements native quote behavior.
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import type { TelegramTextEntity } from "./body-helpers.js";
const TELEGRAM_NATIVE_QUOTE_MAX_LENGTH = 1024;
@@ -11,18 +12,6 @@ type TelegramNativeQuoteCandidate = {
export type TelegramNativeQuoteCandidateByMessageId = Record<string, TelegramNativeQuoteCandidate>;
function truncateUtf16Safe(value: string, maxLength: number): string {
if (value.length <= maxLength) {
return value;
}
let end = Math.max(0, Math.trunc(maxLength));
const lastCodeUnit = value.charCodeAt(end - 1);
if (lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff) {
end -= 1;
}
return value.slice(0, end);
}
function sliceTelegramEntitiesForQuote(
entities: readonly TelegramTextEntity[] | undefined,
quoteLength: number,
@@ -1,6 +1,7 @@
// withTelegramPlainFallback owns formatted-to-plain recovery for durable sends,
// final replies, and draft previews. A second orchestrator reintroduces silent drift.
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
import type { TelegramRichBlocksDegradationReason } from "./rich-block-model.js";
// Any RICH_MESSAGE_*_INVALID rejection (entities, media, depth) degrades to
@@ -76,14 +77,7 @@ export function splitTelegramPlainTextChunks(text: string, limit: number): strin
return [];
}
const normalizedLimit = Math.max(1, Math.floor(limit));
const chunks: string[] = [];
let start = 0;
while (start < text.length) {
const end = surrogateSafeChunkEnd(text, start + normalizedLimit, start);
chunks.push(text.slice(start, end));
start = end;
}
return chunks;
return chunkTextForOutbound(text, normalizedLimit, { preserveWhitespace: true });
}
function splitTelegramPlainTextFallback(text: string, chunkCount: number, limit: number): string[] {
@@ -1,5 +1,9 @@
// Provider-specific media stream frame parsing and serialization.
import {
asNullableRecord,
asOptionalObjectRecord,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import { canonicalizeVoiceCallMediaBase64 } from "../media-base64.js";
/** Normalized inbound media stream frame. */
@@ -41,9 +45,7 @@ function parseTimestampMs(value: unknown): number | undefined {
function tryParseJson(rawMessage: string): Record<string, unknown> | null {
try {
const parsed = JSON.parse(rawMessage) as unknown;
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;
}
return asNullableRecord(parsed);
} catch {
/* fall through */
}
@@ -55,10 +57,7 @@ function readRecordField(
record: Record<string, unknown>,
field: string,
): Record<string, unknown> | undefined {
const value = record[field];
return typeof value === "object" && value !== null
? (value as Record<string, unknown>)
: undefined;
return asOptionalObjectRecord(record[field]);
}
/** Parse a common provider media frame. */
+3 -7
View File
@@ -2,6 +2,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
import { isPathStrictlyInside } from "openclaw/plugin-sdk/file-access-runtime";
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
import {
info,
@@ -284,11 +285,6 @@ async function shouldClearOnLogout(authDir: string, isLegacyAuthDir: boolean): P
}
}
function isPathInsideDirectory(baseDir: string, targetPath: string): boolean {
const relativePath = path.relative(baseDir, targetPath);
return relativePath !== "" && !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
}
async function pathHasSymlinkComponent(baseDir: string, targetPath: string): Promise<boolean> {
const relativePath = path.relative(baseDir, targetPath);
let currentPath = baseDir;
@@ -320,7 +316,7 @@ async function isLegacyWebAuthDir(authDir: string): Promise<boolean> {
async function classifyWebAuthDirOwnership(authDir: string): Promise<WebAuthDirOwnership> {
const whatsappAuthBase = path.resolve(resolveOAuthDir(), "whatsapp");
const resolvedAuthDir = path.resolve(authDir);
if (!isPathInsideDirectory(whatsappAuthBase, resolvedAuthDir)) {
if (!isPathStrictlyInside(whatsappAuthBase, resolvedAuthDir)) {
return { kind: "external" };
}
@@ -331,7 +327,7 @@ async function classifyWebAuthDirOwnership(authDir: string): Promise<WebAuthDirO
if (!baseRealPath || !authDirRealPath) {
return { kind: "unsafe-owned" };
}
if (!isPathInsideDirectory(baseRealPath, authDirRealPath)) {
if (!isPathStrictlyInside(baseRealPath, authDirRealPath)) {
return { kind: "unsafe-owned" };
}
if (await pathHasSymlinkComponent(whatsappAuthBase, resolvedAuthDir)) {
+1
View File
@@ -14,4 +14,5 @@ export {
} from "../infra/directory-durability.js";
export { removePathWithinRoot } from "../infra/fs-safe-remove.js";
export { basenameFromMediaSource, safeFileURLToPath } from "../infra/local-file-access.js";
export { isPathInside, isPathStrictlyInside } from "../infra/path-guards.js";
export { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
+1
View File
@@ -63,4 +63,5 @@ export {
truncateUtf16Safe,
} from "../utils.js";
export { fetchWithTimeout } from "../utils/fetch-timeout.js";
export { truncateUtf8Prefix } from "../utils/utf8-truncate.js";
export { withTimeout };