mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 13:44:47 +00:00
Summary: - derive apply_patch target paths for before_tool_call and trusted policy events - route native Codex PreToolUse cwd/sandbox path facts through the host parser - document the additive derivedPaths hook field and refresh the SDK API baseline Verification: - pnpm test src/agents/apply-patch-paths.test.ts src/plugins/host-tool-param-parsers.test.ts src/agents/pi-tools.before-tool-call.e2e.test.ts src/agents/harness/native-hook-relay.test.ts src/plugins/contracts/host-hooks.contract.test.ts - pnpm check:test-types - pnpm lint:core - pnpm plugin-sdk:api:gen - pnpm plugin-sdk:api:check - pnpm run check:no-conflict-markers - pnpm exec oxfmt --check --threads=1 CHANGELOG.md docs/plugins/hooks.md docs/.generated/plugin-sdk-api-baseline.sha256 src/agents/apply-patch-paths.test.ts src/agents/apply-patch-paths.ts src/agents/harness/native-hook-relay.test.ts src/agents/harness/native-hook-relay.ts src/agents/pi-tools.before-tool-call.e2e.test.ts src/agents/pi-tools.before-tool-call.ts src/agents/pi-tools.ts src/auto-reply/reply/dispatch-from-config.test.ts src/plugins/contracts/host-hooks.contract.test.ts src/plugins/hook-types.ts src/plugins/host-tool-param-parsers.test.ts src/plugins/host-tool-param-parsers.ts src/plugins/trusted-tool-policy.ts - git diff --check origin/main...HEAD && git diff --check - pnpm build Co-authored-by: Eva <eva@100yen.org> Co-authored-by: Josh Lehman <josh@martian.engineering>
128 lines
4.9 KiB
TypeScript
128 lines
4.9 KiB
TypeScript
import { getRuntimeConfig } from "../config/config.js";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { isPlainObject } from "../utils.js";
|
|
import type {
|
|
PluginHookBeforeToolCallEvent,
|
|
PluginHookBeforeToolCallResult,
|
|
PluginHookToolContext,
|
|
} from "./hook-types.js";
|
|
import { getPluginSessionExtensionStateSync } from "./host-hook-state.js";
|
|
import type { PluginJsonValue } from "./host-hooks.js";
|
|
import { getActivePluginRegistry } from "./runtime.js";
|
|
|
|
export function hasTrustedToolPolicies(): boolean {
|
|
return (getActivePluginRegistry()?.trustedToolPolicies?.length ?? 0) > 0;
|
|
}
|
|
|
|
function normalizeDerivedEventFields(
|
|
value: Pick<PluginHookBeforeToolCallEvent, "derivedPaths"> | undefined,
|
|
): Pick<PluginHookBeforeToolCallEvent, "derivedPaths"> {
|
|
return Array.isArray(value?.derivedPaths)
|
|
? { derivedPaths: Object.freeze([...value.derivedPaths]) }
|
|
: {};
|
|
}
|
|
|
|
export async function runTrustedToolPolicies(
|
|
event: PluginHookBeforeToolCallEvent,
|
|
ctx: PluginHookToolContext,
|
|
options?: {
|
|
config?: OpenClawConfig;
|
|
deriveEvent?: (
|
|
params: Record<string, unknown>,
|
|
) => Pick<PluginHookBeforeToolCallEvent, "derivedPaths">;
|
|
},
|
|
): Promise<PluginHookBeforeToolCallResult | undefined> {
|
|
const policies = getActivePluginRegistry()?.trustedToolPolicies ?? [];
|
|
let adjustedParams = event.params;
|
|
let hasAdjustedParams = false;
|
|
let approval: PluginHookBeforeToolCallResult["requireApproval"];
|
|
const sessionExtensionStateCache = new Map<string, Record<string, PluginJsonValue> | undefined>();
|
|
let resolvedSessionConfig: OpenClawConfig | undefined = options?.config;
|
|
let didResolveSessionConfig = Boolean(options?.config);
|
|
const resolveSessionConfig = (): OpenClawConfig | undefined => {
|
|
if (!didResolveSessionConfig) {
|
|
didResolveSessionConfig = true;
|
|
try {
|
|
resolvedSessionConfig = getRuntimeConfig();
|
|
} catch {
|
|
resolvedSessionConfig = undefined;
|
|
}
|
|
}
|
|
return resolvedSessionConfig;
|
|
};
|
|
const { derivedPaths, ...eventWithoutDerivedPaths } = event;
|
|
let currentDerivedEvent = normalizeDerivedEventFields({ derivedPaths });
|
|
const buildEvent = (): PluginHookBeforeToolCallEvent => {
|
|
return {
|
|
...eventWithoutDerivedPaths,
|
|
params: adjustedParams,
|
|
...currentDerivedEvent,
|
|
};
|
|
};
|
|
for (const registration of policies) {
|
|
const policyCtx: PluginHookToolContext = {
|
|
...ctx,
|
|
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Plugin callers type JSON reads by namespace.
|
|
getSessionExtension: <T extends PluginJsonValue = PluginJsonValue>(namespace: string) => {
|
|
const normalizedNamespace = namespace.trim();
|
|
const cacheKey = registration.pluginId;
|
|
if (!sessionExtensionStateCache.has(cacheKey)) {
|
|
const config = ctx.sessionKey ? resolveSessionConfig() : undefined;
|
|
sessionExtensionStateCache.set(
|
|
cacheKey,
|
|
config
|
|
? getPluginSessionExtensionStateSync({
|
|
cfg: config,
|
|
pluginId: registration.pluginId,
|
|
sessionKey: ctx.sessionKey,
|
|
})
|
|
: undefined,
|
|
);
|
|
}
|
|
const pluginState = sessionExtensionStateCache.get(cacheKey);
|
|
if (!normalizedNamespace || !pluginState) {
|
|
return undefined;
|
|
}
|
|
return pluginState[normalizedNamespace] as T | undefined;
|
|
},
|
|
};
|
|
const decision = await registration.policy.evaluate(buildEvent(), policyCtx);
|
|
if (!decision) {
|
|
continue;
|
|
}
|
|
if ("allow" in decision && decision.allow === false) {
|
|
return {
|
|
block: true,
|
|
blockReason: decision.reason ?? `blocked by ${registration.policy.id}`,
|
|
};
|
|
}
|
|
// `block: true` is terminal; normalize a missing blockReason to a deterministic
|
|
// reason so downstream diagnostics match the `{ allow: false }` path above.
|
|
if ("block" in decision && decision.block === true) {
|
|
return {
|
|
...decision,
|
|
blockReason: decision.blockReason ?? `blocked by ${registration.policy.id}`,
|
|
};
|
|
}
|
|
// `block: false` is a no-op (matches the regular `before_tool_call` hook
|
|
// pipeline) — it does NOT short-circuit the policy chain. Params and
|
|
// approvals are remembered so later trusted policies can still inspect or
|
|
// block the final call.
|
|
if ("params" in decision && isPlainObject(decision.params)) {
|
|
adjustedParams = decision.params;
|
|
hasAdjustedParams = true;
|
|
currentDerivedEvent = normalizeDerivedEventFields(options?.deriveEvent?.(adjustedParams));
|
|
}
|
|
if ("requireApproval" in decision && decision.requireApproval && !approval) {
|
|
approval = decision.requireApproval;
|
|
}
|
|
}
|
|
if (!hasAdjustedParams && !approval) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
...(hasAdjustedParams ? { params: adjustedParams } : {}),
|
|
...(approval ? { requireApproval: approval } : {}),
|
|
};
|
|
}
|