mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
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:
committed by
GitHub
parent
629f37e841
commit
3378e07d50
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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,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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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. */
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user