Files
openclaw/extensions/workspaces/src/binding-contract.ts
Peter Steinberger 9b565e8d50 refactor(plugins): trim long-tail dead exports (#106230)
* refactor(extensions): trim long-tail plugin exports

* refactor(whatsapp): remove dead media relay
2026-07-13 02:34:06 -07:00

72 lines
1.8 KiB
TypeScript

// Leaf contract shared by write-time schema validation and resolve-time data reads.
// Kept dependency-free so schema.ts and data-read.ts never import each other.
import path from "node:path";
export const DATA_READ_RPC_ALLOWLIST = [
"health",
"system-presence",
"usage.status",
"usage.cost",
"agents.list",
"sessions.list",
"sessions.resolve",
"sessions.get",
"sessions.usage",
"sessions.usage.timeseries",
"sessions.usage.logs",
"node.list",
"node.describe",
"cron.get",
"cron.list",
"cron.status",
"cron.runs",
] as const;
type WorkspaceBindingErrorCode =
| "binding_denied"
| "binding_not_found"
| "binding_too_large"
| "binding_invalid"
| "binding_client_resolved";
export class WorkspaceBindingResolutionError extends Error {
constructor(
readonly code: WorkspaceBindingErrorCode,
message: string,
) {
super(message);
this.name = "WorkspaceBindingResolutionError";
}
}
function hasControlCharacter(value: string): boolean {
for (const char of value) {
const code = char.charCodeAt(0);
if (code < 0x20 || code === 0x7f) {
return true;
}
}
return false;
}
export function normalizeWorkspaceDataLogicalPath(value: string): string {
if (
value.startsWith("/") ||
path.isAbsolute(value) ||
path.win32.isAbsolute(value) ||
hasControlCharacter(value)
) {
throw new WorkspaceBindingResolutionError("binding_invalid", "file binding path is invalid");
}
const normalized = value.replaceAll("\\", "/");
const parts = normalized.split("/").filter(Boolean);
if (
parts.length === 0 ||
parts[0] === "~" ||
parts.some((part) => part === "." || part === ".." || part.includes(":"))
) {
throw new WorkspaceBindingResolutionError("binding_invalid", "file binding path is invalid");
}
return parts.join("/");
}