fix(coven): bound runtime metadata

This commit is contained in:
Val Alexander
2026-04-27 11:15:29 -05:00
parent 3c4f40324a
commit c589394450
5 changed files with 61 additions and 51 deletions

View File

@@ -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);

View File

@@ -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) {

View 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;
}
}

View File

@@ -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 {

View File

@@ -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 } : {}),
};