mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:30:44 +00:00
fix(coven): bound runtime metadata
This commit is contained in:
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import http from "node:http";
|
||||
import net from "node:net";
|
||||
import path from "node:path";
|
||||
import { lstatIfExists, pathIsInside } from "./path-utils.js";
|
||||
|
||||
export type CovenSessionRecord = {
|
||||
id: string;
|
||||
@@ -86,19 +87,6 @@ const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
||||
const MAX_REQUEST_BYTES = 1_000_000;
|
||||
const MAX_RESPONSE_BYTES = 1_000_000;
|
||||
|
||||
function pathIsInside(parent: string, child: string): boolean {
|
||||
const relative = path.relative(parent, child);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function lstatIfExists(filePath: string): fs.Stats | null {
|
||||
try {
|
||||
return fs.lstatSync(filePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function statExistingPath(filePath: string, label: string): fs.Stats {
|
||||
try {
|
||||
return fs.statSync(filePath);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { buildPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
||||
import { z } from "openclaw/plugin-sdk/zod";
|
||||
import { lstatIfExists, pathIsInside, realpathIfExists } from "./path-utils.js";
|
||||
|
||||
export type CovenPluginConfig = {
|
||||
covenHome?: string;
|
||||
@@ -62,27 +62,6 @@ function resolveConfiguredPath(raw: string, label: "covenHome" | "socketPath"):
|
||||
return path.resolve(expanded);
|
||||
}
|
||||
|
||||
function pathIsInside(parent: string, child: string): boolean {
|
||||
const relative = path.relative(parent, child);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function realpathIfExists(filePath: string): string | null {
|
||||
try {
|
||||
return fs.realpathSync.native(filePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function lstatIfExists(filePath: string): fs.Stats | null {
|
||||
try {
|
||||
return fs.lstatSync(filePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCovenHome(raw: string | undefined): string {
|
||||
const fromConfig = raw?.trim();
|
||||
if (fromConfig) {
|
||||
|
||||
23
extensions/coven/src/path-utils.ts
Normal file
23
extensions/coven/src/path-utils.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export function pathIsInside(parent: string, child: string): boolean {
|
||||
const relative = path.relative(parent, child);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
export function realpathIfExists(filePath: string): string | null {
|
||||
try {
|
||||
return fs.realpathSync.native(filePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function lstatIfExists(filePath: string): fs.Stats | null {
|
||||
try {
|
||||
return fs.lstatSync(filePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -531,6 +531,22 @@ describe("CovenAcpRuntime", () => {
|
||||
expect(__testing.decodeRuntimeSessionName(`coven:${"a".repeat(2_049)}`)).toBeNull();
|
||||
});
|
||||
|
||||
it("bounds encoded Coven runtime session metadata before persistence", () => {
|
||||
const encoded = __testing.encodeRuntimeSessionName({
|
||||
agent: "A".repeat(5_000),
|
||||
mode: "prompt".repeat(1_000),
|
||||
sessionMode: "persistent".repeat(1_000),
|
||||
cwd: "/workspace/".repeat(1_000),
|
||||
});
|
||||
|
||||
expect(Buffer.byteLength(encoded, "utf8")).toBeLessThanOrEqual("coven:".length + 2_048);
|
||||
expect(__testing.decodeRuntimeSessionName(encoded)).toEqual({
|
||||
agent: "a".repeat(128),
|
||||
mode: "promptpromptpromptpromptpromptpr",
|
||||
sessionMode: "persistentpersistentpersistentpe",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects missing Coven cwd paths before launching", async () => {
|
||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-coven-workspace-"));
|
||||
try {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
AcpRuntimeError,
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
type CovenSessionRecord,
|
||||
} from "./client.js";
|
||||
import type { ResolvedCovenPluginConfig } from "./config.js";
|
||||
import { pathIsInside, realpathIfExists } from "./path-utils.js";
|
||||
|
||||
export const COVEN_BACKEND_ID = "coven";
|
||||
|
||||
@@ -40,6 +40,8 @@ const MAX_EVENTS_PER_POLL = 500;
|
||||
const MAX_EVENT_PAYLOAD_BYTES = 64_000;
|
||||
const MAX_TRACKED_EVENT_IDS = 10_000;
|
||||
const MAX_RUNTIME_SESSION_NAME_BYTES = 2_048;
|
||||
const MAX_RUNTIME_AGENT_CHARS = 128;
|
||||
const MAX_RUNTIME_MODE_CHARS = 32;
|
||||
const MAX_STATUS_FIELD_CHARS = 256;
|
||||
|
||||
type CovenRuntimeSessionState = {
|
||||
@@ -61,7 +63,23 @@ function normalizeAgentId(value: string | undefined): string {
|
||||
}
|
||||
|
||||
function encodeRuntimeSessionName(state: CovenRuntimeSessionState): string {
|
||||
return `coven:${Buffer.from(JSON.stringify(state), "utf8").toString("base64url")}`;
|
||||
const prefix = "coven:";
|
||||
const safeState: CovenRuntimeSessionState = {
|
||||
agent: normalizeAgentId(state.agent).slice(0, MAX_RUNTIME_AGENT_CHARS) || "codex",
|
||||
mode: (state.mode.trim() || "prompt").slice(0, MAX_RUNTIME_MODE_CHARS),
|
||||
...(state.sessionMode
|
||||
? { sessionMode: state.sessionMode.trim().slice(0, MAX_RUNTIME_MODE_CHARS) }
|
||||
: {}),
|
||||
};
|
||||
const encoded = Buffer.from(JSON.stringify(safeState), "utf8").toString("base64url");
|
||||
const value = `${prefix}${encoded}`;
|
||||
if (Buffer.byteLength(value, "utf8") > prefix.length + MAX_RUNTIME_SESSION_NAME_BYTES) {
|
||||
throw new AcpRuntimeError(
|
||||
"ACP_SESSION_INIT_FAILED",
|
||||
"Coven runtime session metadata is too large.",
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function decodeRuntimeSessionName(value: string): CovenRuntimeSessionState | null {
|
||||
@@ -236,19 +254,6 @@ function terminalStatusEvent(session: CovenSessionRecord): AcpRuntimeEvent {
|
||||
};
|
||||
}
|
||||
|
||||
function pathIsInside(parent: string, child: string): boolean {
|
||||
const relative = path.relative(parent, child);
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function realpathIfExists(filePath: string): string | null {
|
||||
try {
|
||||
return fs.realpathSync.native(filePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class CovenAcpRuntime implements AcpRuntime {
|
||||
private readonly config: ResolvedCovenPluginConfig;
|
||||
private readonly client: CovenClient;
|
||||
@@ -282,7 +287,6 @@ export class CovenAcpRuntime implements AcpRuntime {
|
||||
agent,
|
||||
mode: "prompt",
|
||||
sessionMode: input.mode,
|
||||
...(input.cwd ? { cwd: input.cwd } : {}),
|
||||
}),
|
||||
...(input.cwd ? { cwd: input.cwd } : {}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user