mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
docs: absorb documentation PR sweep
This commit is contained in:
File diff suppressed because one or more lines are too long
+24
-10
@@ -7,6 +7,7 @@ import {
|
||||
CircularIncludeError,
|
||||
ConfigIncludeError,
|
||||
MAX_INCLUDE_FILE_BYTES,
|
||||
MAX_INCLUDE_PATH_LENGTH,
|
||||
deepMerge,
|
||||
type IncludeResolver,
|
||||
resolveConfigIncludes,
|
||||
@@ -576,17 +577,30 @@ describe("security: path traversal protection (CWE-22)", () => {
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it.each([
|
||||
{ includePath: "./file\x00.json", expectedError: undefined },
|
||||
{ includePath: "//etc/passwd", expectedError: ConfigIncludeError },
|
||||
] as const)("rejects malformed include path $includePath", ({ includePath, expectedError }) => {
|
||||
const obj = { $include: includePath };
|
||||
if (expectedError) {
|
||||
expectResolveIncludeError(() => resolve(obj, {}));
|
||||
return;
|
||||
it("rejects malformed include paths", () => {
|
||||
const cases = [
|
||||
{ includePath: "./file\x00.json", pattern: /null bytes?/i },
|
||||
{ includePath: "./a\x00b.json", pattern: /null bytes?/i },
|
||||
{ includePath: "//etc/passwd", pattern: /escapes config directory/ },
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
const obj = { $include: testCase.includePath };
|
||||
expectResolveIncludeError(() => resolve(obj, {}), testCase.pattern);
|
||||
}
|
||||
// Path with null byte should be rejected or handled safely.
|
||||
expectResolveIncludeError(() => resolve(obj, {}));
|
||||
});
|
||||
|
||||
it("rejects include path at or over maximum length (>= MAX_INCLUDE_PATH_LENGTH)", () => {
|
||||
const overLimit = "a".repeat(MAX_INCLUDE_PATH_LENGTH + 1);
|
||||
expectResolveIncludeError(() => resolve({ $include: overLimit }, {}), /maximum length/);
|
||||
// Boundary: length exactly 4096 must be rejected (Linux PATH_MAX includes NUL)
|
||||
const atLimit = "b".repeat(MAX_INCLUDE_PATH_LENGTH);
|
||||
expectResolveIncludeError(() => resolve({ $include: atLimit }, {}), /maximum length/);
|
||||
});
|
||||
|
||||
it("accepts include path at or under maximum length when file exists", () => {
|
||||
const shortPath = configPath("base.json");
|
||||
const files = { [shortPath]: { ok: true } };
|
||||
expect(resolve({ $include: shortPath }, files)).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("allows child include when config is at filesystem root", () => {
|
||||
|
||||
@@ -22,6 +22,9 @@ export const INCLUDE_KEY = "$include";
|
||||
export const MAX_INCLUDE_DEPTH = 10;
|
||||
export const MAX_INCLUDE_FILE_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
/** Maximum length for $include path and resolved path (CWE-22 hardening). */
|
||||
export const MAX_INCLUDE_PATH_LENGTH = 4096;
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
@@ -212,12 +215,29 @@ class IncludeProcessor {
|
||||
}
|
||||
|
||||
private resolvePath(includePath: string): { resolvedPath: string; root: IncludeRoot } {
|
||||
if (includePath.includes("\0")) {
|
||||
throw new ConfigIncludeError("Include path must not contain null bytes", includePath);
|
||||
}
|
||||
if (includePath.length >= MAX_INCLUDE_PATH_LENGTH) {
|
||||
throw new ConfigIncludeError(
|
||||
`Include path exceeds maximum length (${MAX_INCLUDE_PATH_LENGTH} characters)`,
|
||||
includePath,
|
||||
);
|
||||
}
|
||||
|
||||
const configDir = path.dirname(this.basePath);
|
||||
const resolved = path.isAbsolute(includePath)
|
||||
? includePath
|
||||
: path.resolve(configDir, includePath);
|
||||
const normalized = path.normalize(resolved);
|
||||
|
||||
if (normalized.length >= MAX_INCLUDE_PATH_LENGTH) {
|
||||
throw new ConfigIncludeError(
|
||||
`Resolved include path exceeds maximum length (${MAX_INCLUDE_PATH_LENGTH} characters)`,
|
||||
includePath,
|
||||
);
|
||||
}
|
||||
|
||||
// SECURITY: Reject paths outside the config directory and any caller-allowed
|
||||
// roots (CWE-22: Path Traversal). Allowed roots come from
|
||||
// OPENCLAW_INCLUDE_ROOTS and let operators opt into shared include trees
|
||||
|
||||
@@ -18,6 +18,8 @@ export type SignalAccountConfig = CommonChannelMessagingConfig & {
|
||||
account?: string;
|
||||
/** Optional account UUID for signal-cli (used for loop protection). */
|
||||
accountUuid?: string;
|
||||
/** Optional signal-cli config directory path (passed as --config). */
|
||||
configPath?: string;
|
||||
/** Optional full base URL for signal-cli HTTP daemon. */
|
||||
httpUrl?: string;
|
||||
/** HTTP host for signal-cli daemon (default 127.0.0.1). */
|
||||
|
||||
@@ -276,7 +276,7 @@ export type TelegramGroupConfig = {
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
/** If specified, only load these skills for this group (when no topic). Omit = all skills; empty = no skills. */
|
||||
skills?: string[];
|
||||
/** Per-topic configuration (key is message_thread_id as string) */
|
||||
/** Per-topic configuration (key is message_thread_id as string, or "*" for topic defaults). */
|
||||
topics?: Record<string, TelegramTopicConfig>;
|
||||
/** If false, disable the bot for this group (and its topics). */
|
||||
enabled?: boolean;
|
||||
@@ -311,7 +311,7 @@ export type TelegramDirectConfig = {
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
/** If specified, only load these skills for this DM (when no topic). Omit = all skills; empty = no skills. */
|
||||
skills?: string[];
|
||||
/** Per-topic configuration for DM topics (key is message_thread_id as string) */
|
||||
/** Per-topic configuration for DM topics (key is message_thread_id as string, or "*" for topic defaults). */
|
||||
topics?: Record<string, TelegramTopicConfig>;
|
||||
/** If false, disable the bot for this DM (and its topics). */
|
||||
enabled?: boolean;
|
||||
|
||||
@@ -1204,6 +1204,7 @@ export const SignalAccountSchemaBase = z
|
||||
configWrites: z.boolean().optional(),
|
||||
account: z.string().optional(),
|
||||
accountUuid: z.string().optional(),
|
||||
configPath: z.string().optional(),
|
||||
httpUrl: z.string().optional(),
|
||||
httpHost: z.string().optional(),
|
||||
httpPort: z.number().int().positive().optional(),
|
||||
|
||||
Reference in New Issue
Block a user