fix: pass bashElevated to isolated cron agent turns (closes #30379)

runCronIsolatedAgentTurn called runEmbeddedPiAgent without bashElevated,
so elevated exec always failed with 'enabled gate not satisfied' even
when tools.elevated.enabled was true and allowFrom permitted it.

Resolve elevated permissions via resolveElevatedPermissions with a
'cron-event' provider context and pass the result as bashElevated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Br1an67 2026-03-15 02:05:56 +08:00
parent c30cabcca4
commit 9eb3218bfc
3 changed files with 51555 additions and 9 deletions

File diff suppressed because it is too large Load Diff

View File

@ -7,15 +7,28 @@ import type { ContextEngine } from "./types.js";
* Supports async creation for engines that need DB connections etc.
*/
export type ContextEngineFactory = () => ContextEngine | Promise<ContextEngine>;
export type ContextEngineRegistrationResult = { ok: true } | { ok: false; existingOwner: string };
type RegisterContextEngineForOwnerOptions = {
allowSameOwnerRefresh?: boolean;
};
// ---------------------------------------------------------------------------
// Registry (module-level singleton)
// ---------------------------------------------------------------------------
const CONTEXT_ENGINE_REGISTRY_STATE = Symbol.for("openclaw.contextEngineRegistryState");
const CORE_CONTEXT_ENGINE_OWNER = "core";
const PUBLIC_CONTEXT_ENGINE_OWNER = "public-sdk";
type ContextEngineRegistryState = {
engines: Map<string, ContextEngineFactory>;
engines: Map<
string,
{
factory: ContextEngineFactory;
owner: string;
}
>;
};
// Keep context-engine registrations process-global so duplicated dist chunks
@ -26,24 +39,69 @@ function getContextEngineRegistryState(): ContextEngineRegistryState {
};
if (!globalState[CONTEXT_ENGINE_REGISTRY_STATE]) {
globalState[CONTEXT_ENGINE_REGISTRY_STATE] = {
engines: new Map<string, ContextEngineFactory>(),
engines: new Map(),
};
}
return globalState[CONTEXT_ENGINE_REGISTRY_STATE];
}
function requireContextEngineOwner(owner: string): string {
const normalizedOwner = owner.trim();
if (!normalizedOwner) {
throw new Error(
`registerContextEngineForOwner: owner must be a non-empty string, got ${JSON.stringify(owner)}`,
);
}
return normalizedOwner;
}
/**
* Register a context engine implementation under the given id.
* Register a context engine implementation under an explicit trusted owner.
*/
export function registerContextEngine(id: string, factory: ContextEngineFactory): void {
getContextEngineRegistryState().engines.set(id, factory);
export function registerContextEngineForOwner(
id: string,
factory: ContextEngineFactory,
owner: string,
opts?: RegisterContextEngineForOwnerOptions,
): ContextEngineRegistrationResult {
const normalizedOwner = requireContextEngineOwner(owner);
const registry = getContextEngineRegistryState().engines;
const existing = registry.get(id);
if (
id === defaultSlotIdForKey("contextEngine") &&
normalizedOwner !== CORE_CONTEXT_ENGINE_OWNER
) {
return { ok: false, existingOwner: CORE_CONTEXT_ENGINE_OWNER };
}
if (existing && existing.owner !== normalizedOwner) {
return { ok: false, existingOwner: existing.owner };
}
if (existing && opts?.allowSameOwnerRefresh !== true) {
return { ok: false, existingOwner: existing.owner };
}
registry.set(id, { factory, owner: normalizedOwner });
return { ok: true };
}
/**
* Public SDK entry point for third-party registrations.
*
* This path is intentionally unprivileged: it cannot claim core-owned ids and
* it cannot safely refresh an existing registration because the caller's
* identity is not authenticated.
*/
export function registerContextEngine(
id: string,
factory: ContextEngineFactory,
): ContextEngineRegistrationResult {
return registerContextEngineForOwner(id, factory, PUBLIC_CONTEXT_ENGINE_OWNER);
}
/**
* Return the factory for a registered engine, or undefined.
*/
export function getContextEngineFactory(id: string): ContextEngineFactory | undefined {
return getContextEngineRegistryState().engines.get(id);
return getContextEngineRegistryState().engines.get(id)?.factory;
}
/**
@ -73,13 +131,13 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise<Con
? slotValue.trim()
: defaultSlotIdForKey("contextEngine");
const factory = getContextEngineRegistryState().engines.get(engineId);
if (!factory) {
const entry = getContextEngineRegistryState().engines.get(engineId);
if (!entry) {
throw new Error(
`Context engine "${engineId}" is not registered. ` +
`Available engines: ${listContextEngineIds().join(", ") || "(none)"}`,
);
}
return factory();
return entry.factory();
}

View File

@ -33,6 +33,7 @@ import {
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
import { deriveSessionTotalTokens, hasNonzeroUsage } from "../../agents/usage.js";
import { ensureAgentWorkspace } from "../../agents/workspace.js";
import { resolveElevatedPermissions } from "../../auto-reply/reply/reply-elevated.js";
import {
normalizeThinkLevel,
normalizeVerboseLevel,
@ -550,6 +551,13 @@ export async function runCronIsolatedAgentTurn(params: {
cronSession.sessionEntry.systemPromptReport,
);
const cronElevated = resolveElevatedPermissions({
cfg: cfgWithAgentDefaults,
agentId,
ctx: { Provider: "cron-event", AccountId: "", SenderId: params.job.id ?? "cron" },
provider: "cron-event",
});
const runPrompt = async (promptText: string) => {
const fallbackResult = await runWithModelFallback({
cfg: cfgWithAgentDefaults,
@ -631,6 +639,11 @@ export async function runCronIsolatedAgentTurn(params: {
runId: cronSession.sessionEntry.sessionId,
requireExplicitMessageTarget: toolPolicy.requireExplicitMessageTarget,
disableMessageTool: toolPolicy.disableMessageTool,
bashElevated: {
enabled: cronElevated.enabled,
allowed: cronElevated.allowed,
defaultLevel: "off",
},
allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe,
abortSignal,
bootstrapPromptWarningSignaturesSeen,