mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-25 03:21:13 +00:00
Catalog session rows (sidebar context menu + click), the built-in viewer header, and a new "Open Codex/Claude sessions in" preference can launch the native CLI (codex resume / claude --resume) in the operator terminal on the machine that owns the session. - Gateway-local sessions spawn through the existing terminal launch policy (sandbox/enabled gates preserved) with the resume command in the session cwd. - Paired-node sessions run through a new seq-ordered node PTY relay: a duplex node-host command streams PTY output via node.invoke.progress and receives keystrokes/resize via a new node.invoke.input event, behind the unchanged terminal.* client protocol (TerminalSessionManager gains a backend abstraction; node relay reuses the streaming-invoke controller). - Owner boundary: each plugin owns its resume command and builds argv from a validated thread id; the gateway routes node opens through the node command allowlist and plugin invoke policy (no advertisement-only trust), and nodes re-verify session eligibility before spawning. - UI setting catalogOpenTarget + canOpenTerminal capability gate every entry point; capability requires the owning host to actually have the CLI. Node PATH is normalized before command-availability probes, Windows .cmd/.bat shims spawn via ComSpec, and catalog terminal opens reattach persisted tabs before opening the new tab.
71 lines
2.1 KiB
TypeScript
71 lines
2.1 KiB
TypeScript
import type { NodeInvokeRequestPayload } from "./invoke-types.js";
|
|
|
|
const MAX_INVOKE_INPUT_BYTES = 16 * 1024;
|
|
|
|
export function coerceNodeInvokePayload(payload: unknown): NodeInvokeRequestPayload | null {
|
|
if (!payload || typeof payload !== "object") {
|
|
return null;
|
|
}
|
|
const obj = payload as Record<string, unknown>;
|
|
const id = typeof obj.id === "string" ? obj.id.trim() : "";
|
|
const nodeId = typeof obj.nodeId === "string" ? obj.nodeId.trim() : "";
|
|
const command = typeof obj.command === "string" ? obj.command.trim() : "";
|
|
if (!id || !nodeId || !command) {
|
|
return null;
|
|
}
|
|
const paramsJSON =
|
|
typeof obj.paramsJSON === "string"
|
|
? obj.paramsJSON
|
|
: obj.params !== undefined
|
|
? JSON.stringify(obj.params)
|
|
: null;
|
|
const timeoutMs = typeof obj.timeoutMs === "number" ? obj.timeoutMs : null;
|
|
const idempotencyKey = typeof obj.idempotencyKey === "string" ? obj.idempotencyKey : null;
|
|
return {
|
|
id,
|
|
nodeId,
|
|
command,
|
|
paramsJSON,
|
|
timeoutMs,
|
|
idempotencyKey,
|
|
};
|
|
}
|
|
|
|
export function coerceNodeInvokeCancelPayload(
|
|
payload: unknown,
|
|
): { invokeId: string; nodeId: string } | null {
|
|
const value =
|
|
payload && typeof payload === "object" && !Array.isArray(payload)
|
|
? (payload as Record<string, unknown>)
|
|
: null;
|
|
return value && typeof value.invokeId === "string" && typeof value.nodeId === "string"
|
|
? { invokeId: value.invokeId, nodeId: value.nodeId }
|
|
: null;
|
|
}
|
|
|
|
export function coerceNodeInvokeInputPayload(
|
|
payload: unknown,
|
|
): { invokeId: string; nodeId: string; seq: number; payloadJSON: string } | null {
|
|
const value =
|
|
payload && typeof payload === "object" && !Array.isArray(payload)
|
|
? (payload as Record<string, unknown>)
|
|
: null;
|
|
if (
|
|
!value ||
|
|
typeof value.id !== "string" ||
|
|
typeof value.nodeId !== "string" ||
|
|
!Number.isInteger(value.seq) ||
|
|
(value.seq as number) < 0 ||
|
|
typeof value.payloadJSON !== "string" ||
|
|
Buffer.byteLength(value.payloadJSON, "utf8") > MAX_INVOKE_INPUT_BYTES
|
|
) {
|
|
return null;
|
|
}
|
|
return {
|
|
invokeId: value.id,
|
|
nodeId: value.nodeId,
|
|
seq: value.seq as number,
|
|
payloadJSON: value.payloadJSON,
|
|
};
|
|
}
|