fix(cron): bind projected exec authority to Codex

This commit is contained in:
Josh Lehman
2026-08-21 10:26:35 -07:00
parent e51ad6845e
commit d3e30d1ab9
14 changed files with 333 additions and 59 deletions
@@ -33,10 +33,34 @@ import {
resolveScheduledCodexAppCreatorCaptureDecision,
} from "./scheduled-app-authority.js";
type CodexCronCreatorTool = Parameters<typeof captureFinalCodexCronCreatorToolAllowlist>[0][number];
type CodexCronRuntimeAuthority = NonNullable<EmbeddedRunAttemptParams["scheduledRuntimeAuthority"]>;
function isAuthorityResolutionOperationAbort(error: unknown, signal: AbortSignal | undefined) {
return signal?.aborted === true && error === signal.reason;
}
function bindCodexCronScheduledTools(
authority: CodexCronRuntimeAuthority | undefined,
tools: readonly CodexCronCreatorTool[],
): CodexCronRuntimeAuthority | undefined {
const toolBindings = tools.flatMap((tool) =>
typeof tool === "string" || !tool.scheduledToolBinding ? [] : [tool.scheduledToolBinding],
);
if (toolBindings.length === 0) {
return authority;
}
return {
...(authority ?? {
version: 1,
runtimeId: "codex",
namespace: "scheduled-tools",
payload: { version: 1 },
}),
toolBindings,
};
}
export async function prepareCodexAttemptTools(runtime: CodexAttemptRuntime) {
const {
connection,
@@ -140,7 +164,7 @@ export async function prepareCodexAttemptTools(runtime: CodexAttemptRuntime) {
frameToolCallId?: string;
frameImageIdentity?: string;
} = { value: 0 };
const cronCreatorToolAllowlist: Array<string | { name: string; pluginId?: string }> = [];
const cronCreatorToolAllowlist: CodexCronCreatorTool[] = [];
const cronCreatorToolAllowlistCaptureRef: {
value?: { version: 1; source: "final-executable-surface" };
} = {};
@@ -174,20 +198,22 @@ export async function prepareCodexAttemptTools(runtime: CodexAttemptRuntime) {
const canResolveScheduledCodexAppAuthority = appCreatorCapture.supported;
const requiresScheduledCodexAppAuthority = appCreatorCapture.required;
const canResolveAnyScheduledCreatorAuthority =
canResolveScheduledConfiguredMcpCreatorAuthority || requiresScheduledCodexAppAuthority;
canResolveScheduledConfiguredMcpCreatorAuthority ||
requiresScheduledCodexAppAuthority ||
(nativeToolSurfaceEnabled === true && sandbox?.enabled !== true);
let toolBridge: ReturnType<typeof createCodexDynamicToolBridge> | undefined;
let creatorAuthorityPromise:
| Promise<{
tools: readonly (string | { name: string; pluginId?: string })[];
tools: readonly CodexCronCreatorTool[];
provenance: { version: 1; source: "final-executable-surface" };
runtimeAuthority?: NonNullable<EmbeddedRunAttemptParams["scheduledRuntimeAuthority"]>;
runtimeAuthority?: CodexCronRuntimeAuthority;
}>
| undefined;
let resolveCreatorAuthorityImpl:
| ((options?: { signal?: AbortSignal }) => Promise<{
tools: readonly (string | { name: string; pluginId?: string })[];
tools: readonly CodexCronCreatorTool[];
provenance: { version: 1; source: "final-executable-surface" };
runtimeAuthority?: NonNullable<EmbeddedRunAttemptParams["scheduledRuntimeAuthority"]>;
runtimeAuthority?: CodexCronRuntimeAuthority;
}>)
| undefined;
const runtimeYieldCompletionClaim: { current?: () => boolean } = {};
@@ -224,7 +250,7 @@ export async function prepareCodexAttemptTools(runtime: CodexAttemptRuntime) {
? {
resolveCronCreatorToolAuthority: (options?: { signal?: AbortSignal }) => {
if (!resolveCreatorAuthorityImpl) {
throw new Error("configured MCP authority resolver was invoked before tool setup");
throw new Error("cron creator authority resolver was invoked before tool setup");
}
options?.signal?.throwIfAborted();
if (creatorAuthorityPromise) {
@@ -449,7 +475,7 @@ export async function prepareCodexAttemptTools(runtime: CodexAttemptRuntime) {
if (!toolBridge) {
throw new Error("cron creator authority resolver lost the active tool bridge");
}
const authorityTools: Array<string | { name: string; pluginId?: string }> = [];
const authorityTools: CodexCronCreatorTool[] = [];
const captureRef: {
value?: { version: 1; source: "final-executable-surface" };
} = {};
@@ -462,7 +488,7 @@ export async function prepareCodexAttemptTools(runtime: CodexAttemptRuntime) {
throw new Error("cron creator authority snapshot did not produce provenance");
}
const appSource = scheduledAppAuthoritySourceRef.current;
const runtimeAuthority =
const appRuntimeAuthority =
canResolveScheduledCodexAppAuthority && preparedChatgptAuth
? appSource
? await captureScheduledCodexAppAuthority({
@@ -476,6 +502,7 @@ export async function prepareCodexAttemptTools(runtime: CodexAttemptRuntime) {
);
})()
: undefined;
const runtimeAuthority = bindCodexCronScheduledTools(appRuntimeAuthority, authorityTools);
if (!canResolveScheduledConfiguredMcpCreatorAuthority) {
options?.signal?.throwIfAborted();
return Object.freeze({
@@ -5,14 +5,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
const mcpMocks = vi.hoisted(() => ({
authorityResolvers: [] as Array<
(options?: { signal?: AbortSignal }) => Promise<{
tools: readonly (string | { name: string; pluginId?: string })[];
tools: readonly (
| string
| { name: string; pluginId?: string; scheduledToolBinding?: unknown }
)[];
provenance: { version: 1; source: "final-executable-surface" };
runtimeAuthority?: unknown;
}>
>,
captureCalls: [] as Array<{
sourceNames: string[];
storedNames: string[];
provenance?: unknown;
storedBindings: unknown[];
}>,
captureRefs: [] as Array<{
value?: { version: 1; source: "final-executable-surface" };
@@ -127,21 +132,18 @@ vi.mock("openclaw/plugin-sdk/codex-mcp-projection", async (importOriginal) => {
const [target, captureRef, tools] = args;
mcpMocks.captureRefs.push(captureRef);
mcpMocks.captureFacade(target, captureRef, tools);
target.length = 0;
for (const tool of tools) {
if (
!target.some((entry) => (typeof entry === "string" ? entry : entry.name) === tool.name)
) {
target.push({ name: tool.name });
}
}
captureRef.value = { version: 1, source: "final-executable-surface" };
await actual.captureFinalCodexCronCreatorToolAllowlist(target, captureRef, tools);
mcpMocks.captureCalls.push({
sourceNames: tools.map((tool) => tool.name).toSorted(),
storedNames: target
.map((entry) => (typeof entry === "string" ? entry : entry.name))
.toSorted(),
provenance: captureRef.value,
storedBindings: target.flatMap((entry) =>
typeof entry === "string" || !entry.scheduledToolBinding
? []
: [entry.scheduledToolBinding],
),
});
},
};
@@ -232,6 +234,7 @@ describe("runCodexAppServerAttempt configured MCP ownership", () => {
setCodexTestModelSupportsTools(params, true);
params.disableTools = false;
params.runtimePlan = createCodexRuntimePlanFixture();
admitLocalOperatorCronAuthority(params);
const harness = createStartedThreadHarness();
const run = runCodexAppServerAttempt(params);
@@ -244,6 +247,22 @@ describe("runCodexAppServerAttempt configured MCP ownership", () => {
expect(mcpMocks.captureCalls[0]?.storedNames).toContain("gateway_exec");
expect(mcpMocks.captureCalls[0]?.storedNames).toContain("gateway_process");
expect(mcpMocks.captureCalls[0]?.storedNames).not.toContain("exec");
expect(mcpMocks.captureCalls[0]?.storedBindings).toEqual([
{
sourceTool: "gateway_exec",
targetTool: "exec",
execTarget: { host: "gateway" },
},
{ sourceTool: "gateway_process", targetTool: "process" },
]);
const authority = await mcpMocks.authorityResolvers[0]!();
expect(authority.runtimeAuthority).toMatchObject({
version: 1,
runtimeId: "codex",
namespace: "scheduled-tools",
toolBindings: mcpMocks.captureCalls[0]?.storedBindings,
});
});
it("does not replace bundle discovery with partial prepared plugin metadata", async () => {
@@ -1,4 +1,4 @@
import { pinExecToolTarget } from "openclaw/plugin-sdk/codex-mcp-projection";
import { bindCronScheduledTool, pinExecToolTarget } from "openclaw/plugin-sdk/codex-mcp-projection";
import type { CodexPluginConfig } from "./config.js";
import { normalizeCodexDynamicToolName } from "./dynamic-tool-profile.js";
@@ -50,34 +50,48 @@ export function createExecAliasDynamicTool(
? { host: "node", ...(pinnedNode ? { node: pinnedNode } : {}) }
: { host: "gateway", ...(params.ask ? { ask: params.ask } : {}) },
);
return {
const execute: OpenClawDynamicTool["execute"] = async (toolCallId, args, signal, onUpdate) => {
const result = await pinnedTool.execute(toolCallId, args, signal, onUpdate);
return {
...result,
content: result.content.map((item) =>
item.type === "text"
? Object.assign({}, item, {
text: item.text.replace(PROCESS_FOLLOWUP_TEXT, followupText),
})
: item,
),
};
};
const alias = {
...pinnedTool,
name,
description,
execute: async (toolCallId, args, signal, onUpdate) => {
const result = await pinnedTool.execute(toolCallId, args, signal, onUpdate);
return {
...result,
content: result.content.map((item) =>
item.type === "text"
? Object.assign({}, item, {
text: item.text.replace(PROCESS_FOLLOWUP_TEXT, followupText),
})
: item,
),
};
},
execute,
};
return nodeAlias
? alias
: bindCronScheduledTool(alias, {
sourceTool: CODEX_GATEWAY_EXEC_DYNAMIC_TOOL_NAME,
targetTool: "exec",
execTarget: { host: "gateway" },
});
}
export function createGatewayProcessAliasDynamicTool(
processTool: OpenClawDynamicTool,
): OpenClawDynamicTool {
return {
...processTool,
name: CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME,
description:
"Manage background shell sessions in the existing per-session OpenClaw process scope: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use for gateway_exec follow-up; use native Codex shell session handling for ordinary local work.",
};
return bindCronScheduledTool(
{
...processTool,
name: CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME,
description:
"Manage background shell sessions in the existing per-session OpenClaw process scope: list, poll, log, write, send-keys, submit, paste, kill, clear, or remove. Use for gateway_exec follow-up; use native Codex shell session handling for ordinary local work.",
},
{
sourceTool: CODEX_GATEWAY_PROCESS_DYNAMIC_TOOL_NAME,
targetTool: "process",
},
);
}
+17
View File
@@ -1,6 +1,9 @@
import { asNonArrayRecord } from "@openclaw/normalization-core/record-coerce";
import type { CronScheduledToolBinding } from "../cron/runtime-authority.js";
import type { AnyAgentTool } from "./tools/common.js";
const scheduledToolBindings = new WeakMap<AnyAgentTool, CronScheduledToolBinding>();
const EXEC_POLICY_PARAMETER_NAMES = new Set(["host", "security", "ask"]);
const NODE_EXEC_PARAMETER_NAMES = new Set(["command", "workdir", "env", "timeoutSeconds", "node"]);
@@ -8,6 +11,20 @@ type PinnedExecToolTarget =
| { host: "gateway"; ask?: "always" }
| { host: "node"; node?: string };
export function bindCronScheduledTool(
tool: AnyAgentTool,
binding: CronScheduledToolBinding,
): AnyAgentTool {
scheduledToolBindings.set(tool, binding);
return tool;
}
export function getCronScheduledToolBinding(
tool: AnyAgentTool,
): CronScheduledToolBinding | undefined {
return scheduledToolBindings.get(tool);
}
/** Restricts an exec tool to one host target even when callers submit broader arguments. */
export function pinExecToolTarget(tool: AnyAgentTool, target: PinnedExecToolTarget): AnyAgentTool {
const pinnedNode = target.host === "node" ? target.node?.trim() : undefined;
+13 -3
View File
@@ -1,3 +1,4 @@
import type { CronScheduledToolBinding } from "../../cron/runtime-authority.js";
import { isRecord } from "../../utils.js";
import { isToolAllowedByPolicyName } from "../tool-policy-match.js";
import {
@@ -43,7 +44,9 @@ export function assertInheritedCronToolCaptureReady(
export function replaceWithEffectiveCronCreatorToolAllowlist<T extends { name: string }>(
target: CronCreatorToolAllowlistEntry[],
tools: readonly T[],
toolMeta?: (tool: T) => { pluginId?: string } | undefined,
toolMeta?: (
tool: T,
) => { pluginId?: string; scheduledToolBinding?: CronScheduledToolBinding } | undefined,
): void {
target.length = 0;
const seen = new Set<string>();
@@ -56,7 +59,12 @@ export function replaceWithEffectiveCronCreatorToolAllowlist<T extends { name: s
const meta = toolMeta?.(tool);
const pluginId =
typeof meta?.pluginId === "string" ? normalizeToolPolicyName(meta.pluginId) : undefined;
target.push(pluginId ? { name, pluginId } : { name });
const scheduledToolBinding = meta?.scheduledToolBinding;
target.push({
name,
...(pluginId ? { pluginId } : {}),
...(scheduledToolBinding ? { scheduledToolBinding } : {}),
});
}
}
@@ -65,7 +73,9 @@ export function captureFinalEffectiveCronCreatorToolAllowlist<T extends { name:
target: CronCreatorToolAllowlistEntry[],
captureRef: CronToolsAllowCaptureRef,
tools: readonly T[],
toolMeta?: (tool: T) => { pluginId?: string } | undefined,
toolMeta?: (
tool: T,
) => { pluginId?: string; scheduledToolBinding?: CronScheduledToolBinding } | undefined,
): void {
replaceWithEffectiveCronCreatorToolAllowlist(target, tools, toolMeta);
captureRef.value = { version: 1, source: "final-executable-surface" };
+5 -1
View File
@@ -1,6 +1,9 @@
// Cron tool type declarations shared with the cron tool implementation.
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { CronRuntimeAuthority } from "../../cron/runtime-authority.js";
import type {
CronRuntimeAuthority,
CronScheduledToolBinding,
} from "../../cron/runtime-authority.js";
import type { CronCreatorAuthorityGrant } from "../../gateway/cron-creator-authority-grant.js";
import type { DeliveryContext } from "../../utils/delivery-context.shared.js";
import type { callGatewayTool } from "./gateway.js";
@@ -10,6 +13,7 @@ export type CronCreatorToolAllowlistEntry =
| {
name: string;
pluginId?: string;
scheduledToolBinding?: CronScheduledToolBinding;
};
type CronToolsAllowCaptureProvenance = {
+38
View File
@@ -26,7 +26,45 @@ describe("normalizeCronRuntimeAuthority", () => {
expect(Object.isFrozen((normalized.payload.apps as unknown[])[0])).toBe(true);
});
it("normalizes producer-bound scheduled tool bindings", () => {
const input = {
...authority({ version: 1 }),
toolBindings: [
{
sourceTool: "gateway_exec",
targetTool: "exec",
execTarget: { host: "gateway" },
},
{ sourceTool: "gateway_process", targetTool: "process" },
],
};
const normalized = normalizeCronRuntimeAuthority(input);
expect(normalized).toEqual(input);
expect(Object.isFrozen(normalized?.toolBindings)).toBe(true);
expect(Object.isFrozen(normalized?.toolBindings?.[0])).toBe(true);
});
it.each([
{
...authority({}),
toolBindings: [
{
sourceTool: "gateway_exec",
targetTool: "exec",
execTarget: { host: "node" },
},
],
},
{
...authority({}),
toolBindings: [
{ sourceTool: "gateway_exec", targetTool: "process" },
{ sourceTool: "gateway_exec", targetTool: "process" },
],
},
{ ...authority({}), toolBindings: [{ sourceTool: "gateway_exec", targetTool: "write" }] },
authority({ value: Number.NaN }),
authority({ value: Number.POSITIVE_INFINITY }),
authority({ value: undefined }),
+73 -2
View File
@@ -5,11 +5,29 @@ const CRON_RUNTIME_AUTHORITY_MAX_BYTES = 64 * 1024;
const CRON_RUNTIME_AUTHORITY_MAX_ID_LENGTH = 128;
const CRON_RUNTIME_AUTHORITY_MAX_DEPTH = 16;
const CRON_RUNTIME_AUTHORITY_ID_PATTERN = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/u;
const CRON_RUNTIME_AUTHORITY_KEYS = new Set(["version", "runtimeId", "namespace", "payload"]);
const CRON_RUNTIME_AUTHORITY_KEYS = new Set([
"version",
"runtimeId",
"namespace",
"payload",
"toolBindings",
]);
const CRON_RUNTIME_AUTHORITY_MAX_TOOL_BINDINGS = 16;
type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
export type CronScheduledToolBinding =
| Readonly<{
sourceTool: string;
targetTool: "exec";
execTarget: Readonly<{ host: "gateway" }>;
}>
| Readonly<{
sourceTool: string;
targetTool: "process";
}>;
export type CronRuntimeAuthority = Readonly<{
version: 1;
/** Concrete harness runtime that alone may consume this opaque authority. */
@@ -17,6 +35,8 @@ export type CronRuntimeAuthority = Readonly<{
/** Runtime-owned payload discriminator; core never interprets its value. */
namespace: string;
payload: Readonly<Record<string, unknown>>;
/** Host-validated projections from runtime-specific creator tools to scheduled tools. */
toolBindings?: readonly CronScheduledToolBinding[];
}>;
function normalizeAuthorityId(value: unknown): string | undefined {
@@ -99,6 +119,55 @@ function deepFreezeJson(value: JsonValue): JsonValue {
return value;
}
function normalizeToolBindings(value: unknown): readonly CronScheduledToolBinding[] | undefined {
if (value === undefined) {
return undefined;
}
if (!Array.isArray(value) || value.length > CRON_RUNTIME_AUTHORITY_MAX_TOOL_BINDINGS) {
return undefined;
}
const bindings: CronScheduledToolBinding[] = [];
const seenSources = new Set<string>();
for (const candidate of value) {
if (!isRecord(candidate)) {
return undefined;
}
const sourceTool = normalizeAuthorityId(candidate.sourceTool);
if (!sourceTool || seenSources.has(sourceTool)) {
return undefined;
}
if (
candidate.targetTool === "exec" &&
Object.keys(candidate).every((key) =>
["sourceTool", "targetTool", "execTarget"].includes(key),
) &&
isRecord(candidate.execTarget) &&
Object.keys(candidate.execTarget).length === 1 &&
candidate.execTarget.host === "gateway"
) {
seenSources.add(sourceTool);
bindings.push(
Object.freeze({
sourceTool,
targetTool: "exec",
execTarget: Object.freeze({ host: "gateway" }),
}),
);
continue;
}
if (
candidate.targetTool === "process" &&
Object.keys(candidate).every((key) => key === "sourceTool" || key === "targetTool")
) {
seenSources.add(sourceTool);
bindings.push(Object.freeze({ sourceTool, targetTool: "process" }));
continue;
}
return undefined;
}
return Object.freeze(bindings);
}
/** Validates the private persisted transport without learning runtime-owned payload semantics. */
export function normalizeCronRuntimeAuthority(value: unknown): CronRuntimeAuthority | undefined {
if (
@@ -114,7 +183,8 @@ export function normalizeCronRuntimeAuthority(value: unknown): CronRuntimeAuthor
const runtimeId = normalizeAuthorityId(value.runtimeId);
const namespace = normalizeAuthorityId(value.namespace);
const payload = cloneJsonObject(value.payload);
if (!runtimeId || !namespace || !payload) {
const toolBindings = normalizeToolBindings(value.toolBindings);
if (!runtimeId || !namespace || !payload || (value.toolBindings !== undefined && !toolBindings)) {
return undefined;
}
const normalized = {
@@ -122,6 +192,7 @@ export function normalizeCronRuntimeAuthority(value: unknown): CronRuntimeAuthor
runtimeId,
namespace,
payload: deepFreezeJson(payload) as Readonly<Record<string, unknown>>,
...(toolBindings ? { toolBindings } : {}),
} as const;
if (Buffer.byteLength(JSON.stringify(normalized), "utf8") > CRON_RUNTIME_AUTHORITY_MAX_BYTES) {
return undefined;
+2 -2
View File
@@ -101,7 +101,7 @@ export type CronServiceDeps = {
/** List enabled, configured channel ids without exposing channel machinery to cron core. */
listConfiguredChannels?: () => readonly string[] | Promise<readonly string[]>;
evaluateCronTrigger?: (params: {
job: CronJob;
job: CronStoredJob;
script: string;
state: unknown;
streamBatch?: string;
@@ -219,7 +219,7 @@ export type CronServiceDeps = {
} & CronRunOutcome
>;
runScriptJob?: (params: {
job: CronJob;
job: CronStoredJob;
streamBatch?: string;
abortSignal?: AbortSignal;
}) => Promise<
+7
View File
@@ -99,6 +99,13 @@ function makeAuthorityStore(jobId: string): CronStoreFile {
runtimeId: "codex",
namespace: "codex.apps",
payload: { apps: [{ id: "calendar" }] },
toolBindings: [
{
sourceTool: "gateway_exec",
targetTool: "exec",
execTarget: { host: "gateway" },
},
],
};
return store;
}
+35
View File
@@ -92,11 +92,46 @@ describe("cron trigger script evaluator", () => {
script: 'await exec({ command: "printf openclaw-cron-alias-ok" }); return { fire: false };',
state: null,
toolsAllow: ["gateway_exec", "gateway_process"],
runtimeAuthority: {
version: 1,
runtimeId: "codex",
namespace: "scheduled-tools",
payload: { version: 1 },
toolBindings: [
{
sourceTool: "gateway_exec",
targetTool: "exec",
execTarget: { host: "gateway" },
},
{ sourceTool: "gateway_process", targetTool: "process" },
],
},
scheduledToolPolicy: { version: 1, mode: "trusted" },
}),
).resolves.toEqual({ kind: "evaluated", fire: false });
});
it("does not project a colliding raw alias without producer-bound authority", async () => {
const workspaceDir = tempDirs.make("openclaw-cron-codex-alias-collision-");
const evaluate = createCronScriptRuntime({
config: {
agents: { defaults: { workspace: workspaceDir } },
tools: { exec: { host: "gateway", security: "full", ask: "off" } },
} as OpenClawConfig,
}).evaluateTrigger;
const result = await evaluate({
jobId: "job-colliding-gateway-exec",
script: 'await exec({ command: "printf must-not-run" }); return { fire: false };',
state: null,
toolsAllow: ["gateway_exec"],
scheduledToolPolicy: { version: 1, mode: "trusted" },
});
expect(result).toMatchObject({ kind: "error", code: "internal_error" });
expect(result.kind === "error" ? result.error : "").toContain("exec is not defined");
});
it.each(["node_exec", "sandbox_exec"])(
"does not widen %s into generic exec authority",
async (creatorAlias) => {
+28 -9
View File
@@ -53,6 +53,7 @@ import {
resolveCronAgentConfig,
} from "./isolated-agent/run-config.js";
import { resolveCronAgentSessionKey } from "./isolated-agent/session-key.js";
import type { CronRuntimeAuthority } from "./runtime-authority.js";
import {
DEFAULT_CRON_SCRIPT_TIMEOUT_SECONDS,
DEFAULT_CRON_SCRIPT_TOOL_BUDGET,
@@ -66,8 +67,6 @@ const MAX_TRIGGER_STATE_BYTES = 16 * 1024;
const MAX_CACHED_TRIGGER_RUNTIMES = 128;
const HEADLESS_TRIGGER_WALL_CLOCK_MS = 30_000;
const HEADLESS_TRIGGER_TOOL_BUDGET = 5;
const GATEWAY_EXEC_CREATOR_ALIAS = "gateway_exec";
const GATEWAY_PROCESS_CREATOR_ALIAS = "gateway_process";
let activeTriggerEvaluations = 0;
@@ -93,6 +92,7 @@ type PrepareTriggerRuntime = (params: {
jobId: string;
agentId?: string;
toolsAllow?: string[];
runtimeAuthority?: CronRuntimeAuthority;
scheduledToolPolicy?: ScheduledToolPolicyContext;
signal?: AbortSignal;
}) => Promise<PreparedTriggerRuntime>;
@@ -114,22 +114,30 @@ function resolveTriggerAgentId(config: OpenClawConfig, agentId?: string): string
return agentId?.trim() ? normalizeAgentId(agentId) : resolveDefaultAgentId(config);
}
function projectTriggerToolAuthority(toolsAllow: string[] | undefined): {
function projectTriggerToolAuthority(
toolsAllow: string[] | undefined,
runtimeAuthority: CronRuntimeAuthority | undefined,
): {
toolsAllow: string[] | undefined;
pinGatewayExec: boolean;
} {
if (!toolsAllow?.includes(GATEWAY_EXEC_CREATOR_ALIAS)) {
if (!toolsAllow || !runtimeAuthority?.toolBindings?.length) {
return { toolsAllow, pinGatewayExec: false };
}
const bindingsBySource = new Map(
runtimeAuthority.toolBindings.map((binding) => [binding.sourceTool, binding]),
);
let pinGatewayExec = false;
const projected = new Set(
toolsAllow.map((name) => {
if (name === GATEWAY_EXEC_CREATOR_ALIAS) {
return "exec";
const binding = bindingsBySource.get(name);
if (binding?.targetTool === "exec") {
pinGatewayExec = binding.execTarget.host === "gateway";
}
return name === GATEWAY_PROCESS_CREATOR_ALIAS ? "process" : name;
return binding?.targetTool ?? name;
}),
);
return { toolsAllow: [...projected], pinGatewayExec: true };
return { toolsAllow: [...projected], pinGatewayExec };
}
async function prepareTriggerRuntime(params: {
@@ -137,6 +145,7 @@ async function prepareTriggerRuntime(params: {
jobId: string;
agentId?: string;
toolsAllow?: string[];
runtimeAuthority?: CronRuntimeAuthority;
scheduledToolPolicy?: ScheduledToolPolicyContext;
signal?: AbortSignal;
}): Promise<PreparedTriggerRuntime> {
@@ -179,7 +188,10 @@ async function prepareTriggerRuntime(params: {
params.signal?.throwIfAborted();
const effectiveWorkspace =
sandbox?.enabled && sandbox.workspaceAccess !== "rw" ? sandbox.workspaceDir : workspaceDir;
const projectedAuthority = projectTriggerToolAuthority(params.toolsAllow);
const projectedAuthority = projectTriggerToolAuthority(
params.toolsAllow,
params.runtimeAuthority,
);
const toolPlan = resolveEmbeddedAttemptToolConstructionPlan({
toolsEnabled: true,
toolsAllow: projectedAuthority.toolsAllow,
@@ -381,6 +393,7 @@ function createCronCodeModeRunner(deps: CronTriggerEvaluatorDeps) {
requestedAgentId?: string;
agentId: string;
toolsAllow?: string[];
runtimeAuthority?: CronRuntimeAuthority;
scheduledToolPolicy?: ScheduledToolPolicyContext;
toolsAllowKey: string;
signal: AbortSignal;
@@ -416,6 +429,7 @@ function createCronCodeModeRunner(deps: CronTriggerEvaluatorDeps) {
jobId: request.jobId,
agentId: request.requestedAgentId,
toolsAllow: request.toolsAllow,
runtimeAuthority: request.runtimeAuthority,
scheduledToolPolicy: request.scheduledToolPolicy,
signal: request.signal,
});
@@ -442,6 +456,7 @@ function createCronCodeModeRunner(deps: CronTriggerEvaluatorDeps) {
agentId?: string;
script: string;
toolsAllow?: string[];
runtimeAuthority?: CronRuntimeAuthority;
scheduledToolPolicy?: ScheduledToolPolicyContext;
abortSignal?: AbortSignal;
wallClockMs: number;
@@ -462,6 +477,7 @@ function createCronCodeModeRunner(deps: CronTriggerEvaluatorDeps) {
const agentId = resolveTriggerAgentId(runtimeConfig, params.agentId);
const toolsAllowKey = JSON.stringify([
params.toolsAllow ?? null,
params.runtimeAuthority?.toolBindings ?? null,
params.scheduledToolPolicy ?? null,
]);
const runtime = await resolveCachedRuntime({
@@ -470,6 +486,7 @@ function createCronCodeModeRunner(deps: CronTriggerEvaluatorDeps) {
requestedAgentId: params.agentId,
agentId,
toolsAllow: params.toolsAllow,
runtimeAuthority: params.runtimeAuthority,
scheduledToolPolicy: params.scheduledToolPolicy,
toolsAllowKey,
signal: evaluationScope.signal,
@@ -643,6 +660,7 @@ export function createCronScriptRuntime(deps: CronTriggerEvaluatorDeps) {
state: unknown;
streamBatch?: string;
toolsAllow?: string[];
runtimeAuthority?: CronRuntimeAuthority;
scheduledToolPolicy?: ScheduledToolPolicyContext;
abortSignal?: AbortSignal;
}): Promise<CronTriggerEvaluationResult> => {
@@ -670,6 +688,7 @@ export function createCronScriptRuntime(deps: CronTriggerEvaluatorDeps) {
state: unknown;
streamBatch?: string;
toolsAllow?: string[];
runtimeAuthority?: CronRuntimeAuthority;
scheduledToolPolicy?: ScheduledToolPolicyContext;
timeoutSeconds?: number;
toolBudget?: number;
+2
View File
@@ -726,6 +726,7 @@ export function buildGatewayCronService(params: {
state,
streamBatch,
toolsAllow: job.payload.toolsAllow,
runtimeAuthority: job.runtimeAuthority,
scheduledToolPolicy: resolveCronScheduledToolPolicy({
toolsAllow: job.payload.toolsAllow,
scheduledToolPolicy: job.scheduledToolPolicy,
@@ -927,6 +928,7 @@ export function buildGatewayCronService(params: {
state: job.state.triggerState,
streamBatch,
toolsAllow: job.payload.toolsAllow,
runtimeAuthority: job.runtimeAuthority,
scheduledToolPolicy: resolveCronScheduledToolPolicy({
toolsAllow: job.payload.toolsAllow,
scheduledToolPolicy: job.scheduledToolPolicy,
+13 -2
View File
@@ -2,13 +2,18 @@
// runtime's user-mcp-server projection so the bundled Codex app-server harness
// can attach the same user `mcp.servers` entries to its thread config without
// deep-importing core helpers.
import {
bindCronScheduledTool,
getCronScheduledToolBinding,
pinExecToolTarget,
} from "../agents/exec-tool-target-pinning.js";
import type { AnyAgentTool } from "../agents/tools/common.js";
import type {
CronCreatorToolAllowlistEntry,
CronToolsAllowCaptureRef,
} from "../agents/tools/cron-tool.types.js";
export { pinExecToolTarget } from "../agents/exec-tool-target-pinning.js";
export { bindCronScheduledTool, getCronScheduledToolBinding, pinExecToolTarget };
export {
buildCodexUserMcpServersThreadConfigPatch,
buildCodexUserMcpServersThreadConfigPatchForRuntime,
@@ -38,5 +43,11 @@ export async function captureFinalCodexCronCreatorToolAllowlist(
) {
const [{ captureFinalEffectiveCronCreatorToolAllowlist: capture }, { getPluginToolMeta }] =
await Promise.all([import("../agents/tools/cron-tool.js"), import("../plugins/tools.js")]);
return capture(target, captureRef, tools, (tool) => getPluginToolMeta(tool));
return capture(target, captureRef, tools, (tool) => {
const scheduledToolBinding = getCronScheduledToolBinding(tool);
return {
...getPluginToolMeta(tool),
...(scheduledToolBinding ? { scheduledToolBinding } : {}),
};
});
}