mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-12 06:46:05 +00:00
* fix(codex): honor bound agent exec host policy * fix(codex): carry bound agent into app-server runtime * fix(codex): preserve main session exec overrides * fix(codex): narrow runtime session reads * fix(codex): honor default agent session aliases * fix(codex): isolate bound session exec overrides * fix(codex): honor unscoped default session policy
247 lines
7.6 KiB
TypeScript
247 lines
7.6 KiB
TypeScript
/**
|
|
* Resolves whether Codex app-server native execution can own shell/file work,
|
|
* or whether OpenClaw must keep exec/process on a configured node host.
|
|
*/
|
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
|
import { resolveSandboxRuntimeStatus } from "openclaw/plugin-sdk/sandbox";
|
|
import { getSessionEntry, type SessionEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
|
|
|
type ExecHost = "sandbox" | "gateway" | "node";
|
|
type ExecTarget = "auto" | ExecHost;
|
|
|
|
type ExecHostOverride = {
|
|
host?: string;
|
|
node?: string;
|
|
};
|
|
|
|
type AgentEntry = NonNullable<NonNullable<OpenClawConfig["agents"]>["list"]>[number];
|
|
|
|
const DEFAULT_AGENT_ID = "main";
|
|
const VALID_AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
const INVALID_AGENT_ID_CHARS_PATTERN = /[^a-z0-9_-]+/g;
|
|
const LEADING_DASH_PATTERN = /^-+/;
|
|
const TRAILING_DASH_PATTERN = /-+$/;
|
|
|
|
/** Effective execution-host policy for the Codex app-server native tool surface. */
|
|
export type CodexNativeExecutionPolicy = {
|
|
nativeToolSurfaceAllowed: boolean;
|
|
requestedExecHost: ExecTarget;
|
|
effectiveExecHost: ExecHost;
|
|
node?: string;
|
|
blockReason?: string;
|
|
};
|
|
|
|
/** Resolves node/gateway/sandbox execution ownership from overrides, session, agent, and config. */
|
|
export function resolveCodexNativeExecutionPolicy(params: {
|
|
config?: OpenClawConfig;
|
|
sessionEntry?: SessionEntry;
|
|
sessionKey?: string;
|
|
sessionId?: string;
|
|
agentId?: string;
|
|
execOverrides?: ExecHostOverride;
|
|
sandboxAvailable?: boolean;
|
|
readRuntimeSessionEntry?: boolean;
|
|
}): CodexNativeExecutionPolicy {
|
|
const config = params.config ?? {};
|
|
const sessionKey = params.sessionKey?.trim() || params.sessionId?.trim() || undefined;
|
|
const agentId = resolvePolicyAgentId({ config, sessionKey, agentId: params.agentId });
|
|
const canReadSessionEntry =
|
|
params.readRuntimeSessionEntry &&
|
|
shouldReadRuntimeSessionEntry({ config, sessionKey, agentId: params.agentId });
|
|
const sessionEntry =
|
|
params.sessionEntry ??
|
|
(canReadSessionEntry && sessionKey
|
|
? readRuntimeSessionEntryBestEffort({ sessionKey, agentId })
|
|
: undefined);
|
|
const sandboxAvailable =
|
|
params.sandboxAvailable ??
|
|
(sessionKey
|
|
? resolveSandboxRuntimeStatus({
|
|
cfg: config,
|
|
sessionKey,
|
|
}).sandboxed
|
|
: false);
|
|
const agentExec = resolvePolicyAgentExec({ config, agentId });
|
|
const globalExec = config.tools?.exec;
|
|
const requestedExecHost =
|
|
normalizeExecTarget(params.execOverrides?.host) ??
|
|
normalizeExecTarget(sessionEntry?.execHost) ??
|
|
normalizeExecTarget(agentExec?.host) ??
|
|
normalizeExecTarget(globalExec?.host) ??
|
|
"auto";
|
|
const effectiveExecHost = resolveEffectiveExecHost({
|
|
requestedExecHost,
|
|
sandboxAvailable,
|
|
});
|
|
const node =
|
|
params.execOverrides?.node ?? sessionEntry?.execNode ?? agentExec?.node ?? globalExec?.node;
|
|
if (effectiveExecHost !== "node") {
|
|
return {
|
|
nativeToolSurfaceAllowed: true,
|
|
requestedExecHost,
|
|
effectiveExecHost,
|
|
node,
|
|
};
|
|
}
|
|
return {
|
|
nativeToolSurfaceAllowed: false,
|
|
requestedExecHost,
|
|
effectiveExecHost,
|
|
node,
|
|
blockReason:
|
|
"OpenClaw exec host=node is active for this session. Codex app-server native execution cannot route shell, filesystem, MCP, or app-backed work through the selected OpenClaw node.",
|
|
};
|
|
}
|
|
|
|
/** Formats the user-facing explanation shown when native tools are blocked by exec host=node. */
|
|
export function formatCodexNativeNodeExecBlock(params: {
|
|
surface: string;
|
|
reason?: string;
|
|
}): string {
|
|
return [
|
|
`Codex-native ${params.surface} is unavailable because OpenClaw exec host=node is active for this session.`,
|
|
params.reason ??
|
|
"Codex app-server native execution cannot route execution through the selected OpenClaw node.",
|
|
"Use a normal Codex harness turn so OpenClaw exec/process tools run on the node, or switch exec host to gateway for native Codex app-server execution.",
|
|
].join(" ");
|
|
}
|
|
|
|
function resolvePolicyAgentId(params: {
|
|
config: OpenClawConfig;
|
|
sessionKey?: string;
|
|
agentId?: string;
|
|
}): string {
|
|
const explicitAgentId = normalizeAgentIdOrDefault(params.agentId);
|
|
if (explicitAgentId) {
|
|
return explicitAgentId;
|
|
}
|
|
const sessionAgentId = parseAgentIdFromSessionKey(params.sessionKey);
|
|
if (sessionAgentId) {
|
|
return sessionAgentId;
|
|
}
|
|
const agents = listAgentEntries(params.config);
|
|
return resolveDefaultPolicyAgentId(agents);
|
|
}
|
|
|
|
function resolvePolicyAgentExec(params: {
|
|
config: OpenClawConfig;
|
|
agentId: string;
|
|
}): ExecHostOverride | undefined {
|
|
return listAgentEntries(params.config).find(
|
|
(entry) => normalizeAgentId(entry?.id) === params.agentId,
|
|
)?.tools?.exec;
|
|
}
|
|
|
|
function listAgentEntries(config: OpenClawConfig): AgentEntry[] {
|
|
return (config.agents?.list ?? []).filter(
|
|
(entry): entry is AgentEntry => entry !== null && typeof entry === "object",
|
|
);
|
|
}
|
|
|
|
function parseAgentIdFromSessionKey(sessionKey?: string): string | undefined {
|
|
const raw = sessionKey?.trim();
|
|
if (!raw) {
|
|
return undefined;
|
|
}
|
|
const parts = raw.toLowerCase().split(":").filter(Boolean);
|
|
if (parts.length < 3 || parts[0] !== "agent" || !parts[2]) {
|
|
return undefined;
|
|
}
|
|
return normalizeAgentIdOrDefault(parts[1]);
|
|
}
|
|
|
|
function shouldReadRuntimeSessionEntry(params: {
|
|
config: OpenClawConfig;
|
|
sessionKey?: string;
|
|
agentId?: string;
|
|
}): boolean {
|
|
if (!params.sessionKey) {
|
|
return false;
|
|
}
|
|
const explicitAgentId = normalizeAgentIdOrDefault(params.agentId);
|
|
if (!explicitAgentId) {
|
|
return true;
|
|
}
|
|
const sessionAgentId = parseAgentIdFromSessionKey(params.sessionKey);
|
|
if (!sessionAgentId) {
|
|
return isDefaultAgentSessionKeyForAgent({ config: params.config, agentId: explicitAgentId });
|
|
}
|
|
return sessionAgentId === explicitAgentId;
|
|
}
|
|
|
|
function isDefaultAgentSessionKeyForAgent(params: {
|
|
config: OpenClawConfig;
|
|
agentId: string;
|
|
}): boolean {
|
|
return (
|
|
normalizeAgentId(params.agentId) ===
|
|
resolveDefaultPolicyAgentId(listAgentEntries(params.config))
|
|
);
|
|
}
|
|
|
|
function resolveDefaultPolicyAgentId(agents: AgentEntry[]): string {
|
|
const defaultEntry = agents.find((entry) => entry?.default) ?? agents[0];
|
|
return normalizeAgentId(defaultEntry?.id);
|
|
}
|
|
|
|
function normalizeAgentIdOrDefault(value?: string | null): string | undefined {
|
|
const normalized = normalizeAgentId(value);
|
|
return normalized === DEFAULT_AGENT_ID && !(value ?? "").trim() ? undefined : normalized;
|
|
}
|
|
|
|
function normalizeAgentId(value?: string | null): string {
|
|
const trimmed = (value ?? "").trim();
|
|
if (!trimmed) {
|
|
return DEFAULT_AGENT_ID;
|
|
}
|
|
const normalized = trimmed.toLowerCase();
|
|
if (VALID_AGENT_ID_PATTERN.test(trimmed)) {
|
|
return normalized;
|
|
}
|
|
return (
|
|
normalized
|
|
.replace(INVALID_AGENT_ID_CHARS_PATTERN, "-")
|
|
.replace(LEADING_DASH_PATTERN, "")
|
|
.replace(TRAILING_DASH_PATTERN, "")
|
|
.slice(0, 64) || DEFAULT_AGENT_ID
|
|
);
|
|
}
|
|
|
|
function normalizeExecTarget(value?: string | null): ExecTarget | undefined {
|
|
const normalized = value?.trim().toLowerCase();
|
|
if (
|
|
normalized === "auto" ||
|
|
normalized === "sandbox" ||
|
|
normalized === "gateway" ||
|
|
normalized === "node"
|
|
) {
|
|
return normalized;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function resolveEffectiveExecHost(params: {
|
|
requestedExecHost: ExecTarget;
|
|
sandboxAvailable: boolean;
|
|
}): ExecHost {
|
|
if (params.requestedExecHost === "auto") {
|
|
return params.sandboxAvailable ? "sandbox" : "gateway";
|
|
}
|
|
return params.requestedExecHost;
|
|
}
|
|
|
|
function readRuntimeSessionEntryBestEffort(params: {
|
|
sessionKey: string;
|
|
agentId: string;
|
|
}): SessionEntry | undefined {
|
|
try {
|
|
return getSessionEntry({
|
|
sessionKey: params.sessionKey,
|
|
agentId: params.agentId,
|
|
hydrateSkillPromptRefs: false,
|
|
});
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|