fix(codex): preserve allowed tools under channel policy (#122282)

Preserve Codex native tools when conversation policies deny only audited OpenClaw capabilities. Fail closed for computer-control denies and retain structured plan progress.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Kelaw - Keshav's Agent <keshavbotagent@gmail.com>
This commit is contained in:
keshavbotagent
2026-08-12 10:57:08 +05:30
committed by GitHub
parent f166d4ee98
commit 3807eb9c58
27 changed files with 206 additions and 30 deletions
@@ -1 +1 @@
{"contentHash":"48dad42d04caeb62f04f9dbe32b4562ecef695e9f0c97fb891040408400f37bd","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
{"contentHash":"3d680ab26bba9b4ccd86bd7734dbf9672920df842ae3446142319a283e711941","entrypoint":"agent-harness-runtime","importSpecifier":"openclaw/plugin-sdk/agent-harness-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"3d1c84bcc585b57a93054889e16fb3be02ff4d599d7d2a0566df6f463f2254a8","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
{"contentHash":"08051f15195453d4f35616cdb4f0ff47e974f6c2260f0ee1078e1754f5284899","entrypoint":"agent-harness","importSpecifier":"openclaw/plugin-sdk/agent-harness"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"4245891bae7517cf001f637e80d1d813c11385d695eebbd69a61258e83399c4e","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
{"contentHash":"0c78a76e24e4afa2f37813ea3202262605159ddc1bba101778fd68036ee0ae70","entrypoint":"channel-core","importSpecifier":"openclaw/plugin-sdk/channel-core"}
@@ -1 +1 @@
{"contentHash":"f719599ed89a658109698bc1ef1309e1709e5ae3385b7ce3f1c583343ea23b33","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
{"contentHash":"3b5041b5034fac21a85f1e6fbe993d79e2168b99abc98279d7e83013de000496","entrypoint":"channel-entry-contract","importSpecifier":"openclaw/plugin-sdk/channel-entry-contract"}
@@ -1 +1 @@
{"contentHash":"d531b32d0544c42f375f628bd7a6b3d3582a91078db37bc048af7c3f23a9e59f","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
{"contentHash":"a152541a6f351027b97e0c99a02f27038ea5475e96f4513df1b950465c390242","entrypoint":"channel-message","importSpecifier":"openclaw/plugin-sdk/channel-message"}
@@ -1 +1 @@
{"contentHash":"40b25681a1ee102cf1d2d5b23a29f96e2501ea521404deed59303e59c35089ec","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
{"contentHash":"d75ce81f95cf3a2382057e90d70f5f8b925cd3d6236a5acce844e3512f5539b6","entrypoint":"channel-outbound","importSpecifier":"openclaw/plugin-sdk/channel-outbound"}
@@ -1 +1 @@
{"contentHash":"67884fcf5fc91b8b40a3c83e83080e69f54f7750cdd780368af63363f84ddc40","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
{"contentHash":"ac15e49c895e670c58a6574e31839a8a04a124f84801eb2e63adc15d1fb59064","entrypoint":"channel-plugin-common","importSpecifier":"openclaw/plugin-sdk/channel-plugin-common"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"ab54be164a2b0affece4d6c196ee64ae9259cac072d4ac6c32dd27daf4941565","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
{"contentHash":"a6436ac71e8ba6dd99d2fb9dee159bf90675a4ed8beec9805109731630a29957","entrypoint":"core","importSpecifier":"openclaw/plugin-sdk/core"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"21823a6741645b81797b4be6bf16a2d067e7e2a007177fbc5c7a6cab98b08027","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
{"contentHash":"eb7884340d27b0e901a2431eff2a64fae5a3d018ea6e910725ac7df2d35520b8","entrypoint":"discord","importSpecifier":"openclaw/plugin-sdk/discord"}
@@ -1 +1 @@
{"contentHash":"80686774b5023519466038e4a20604d9efce462b0cd2915ab5597fb1087ef8af","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
{"contentHash":"9d305ddc636bf8fb05767cb6be2bf3320b7801d99b0f24f9b012eae2c28959df","entrypoint":"inbound-reply-dispatch","importSpecifier":"openclaw/plugin-sdk/inbound-reply-dispatch"}
@@ -1 +1 @@
{"contentHash":"9eb227dd27a3d08b868b1144bd64e1ff22da707af353699b9eb0f6438d4d98c1","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
{"contentHash":"ebad10b0c7b1838e6cdf68153383fdc57096dd252ccbc24de5691c1f1821f3cd","entrypoint":"meeting-runtime","importSpecifier":"openclaw/plugin-sdk/meeting-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"696f33ed5d8f059b82ff4159183b80f7e4af086abb2383bae5c02d909427fe43","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
{"contentHash":"2d9af7dec89f421a59872de6623d51ed84d8ee23204f147d49b5218ae64ddf2c","entrypoint":"plugin-entry","importSpecifier":"openclaw/plugin-sdk/plugin-entry"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"411769ac67fd1a5b0ea4ada41b45678688e86194d03adc8d8efcad65c5f0c38c","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
{"contentHash":"793f98ff0d772f586920d8a75df073deba94321dcba74daf1caea9ba59cca05e","entrypoint":"plugin-runtime","importSpecifier":"openclaw/plugin-sdk/plugin-runtime"}
@@ -1 +1 @@
{"contentHash":"6539e277a49a27344f2844bcce06f609c5e463bb5a9be9eb1532994f9fec6479","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
{"contentHash":"4841c241310369c97349cc63c956d466a078a1864443e24955ed79031613c806","entrypoint":"provider-catalog-runtime","importSpecifier":"openclaw/plugin-sdk/provider-catalog-runtime"}
+1 -1
View File
@@ -1 +1 @@
{"contentHash":"9d1c23c498e989db4592749e735d0a09fc41ffeb86ab66f70b6894a2161e1212","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
{"contentHash":"77a1e0b93fa1a501677e5a571ed5ce44358812407554e5e494488d83bcad34d6","entrypoint":"tool-plugin","importSpecifier":"openclaw/plugin-sdk/tool-plugin"}
@@ -1 +1 @@
{"contentHash":"d0b42cb31d5b2e84acb3a63ca79fdd2bc988bde3505de54dc07dd85bb76fd60c","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
{"contentHash":"03433e97b67ba2d0a95fbb7876710f2f4f4425c66d724f55bb4476797b9f2846","entrypoint":"webhook-ingress","importSpecifier":"openclaw/plugin-sdk/webhook-ingress"}
+9
View File
@@ -53,6 +53,15 @@ threads. Core passes `params.pluginHarnessToolPolicyRestricted` as the prepared
decision that the native surface must be isolated. Default tool-profile narrowing
does not set this flag.
Harnesses with an independently managed native surface can also declare
`conversationToolPolicySafeDenyTools` using canonical OpenClaw tool names. Core
preserves the native surface only when every expanded deny is a known core tool
in that audited safe list. Finite allowlists, undeclared or unknown tool names,
wildcards, and groups containing any undeclared name remain native-surface
restrictions. Omit the list to retain the conservative behavior where every
explicit restriction isolates the native surface. Because omissions fail
closed, new tools cannot silently relax the policy boundary.
Omit the declaration when any native capability can bypass those layers.
OpenClaw then visibly rejects explicitly restricted turns before invoking the
harness. The operator can switch the session to the embedded runtime or upgrade
+6
View File
@@ -31,6 +31,12 @@ describe("Codex agent harness supports()", () => {
expect(harness.autoSelection?.providerIds).toEqual(["codex", "openai"]);
});
it("keeps computer-control denies out of the native-surface exemption", () => {
expect(harness.conversationToolPolicySafeDenyTools).not.toEqual(
expect.arrayContaining(["browser", "computer", "mobile_ui", "nodes", "screen"]),
);
});
const harness = createCodexAppServerAgentHarness({
bindingStore: testCodexAppServerBindingStore,
});
+21
View File
@@ -17,6 +17,26 @@ import type { CodexSessionCatalogControl } from "./src/session-catalog-types.js"
// New runtime identity uses the `openai` provider.
const DEFAULT_CODEX_HARNESS_PROVIDER_IDS = new Set(["codex", "openai"]);
const SHARED_CODEX_APP_SERVER_CLIENT_DISPOSER = Symbol.for("openclaw.codexAppServerClientDisposer");
// Audited against @openai/codex 0.147.0 (rust-v0.147.0). These exact denies
// target OpenClaw-owned capabilities with no Codex-native equivalent. Keep the
// list positive and conservative: an omitted tool isolates the native surface.
const CODEX_TOOL_POLICY_SAFE_DENY_NAMES = [
"web_fetch",
"x_search",
"memory_search",
"memory_get",
"dashboard",
"canvas",
"show_widget",
"message",
"heartbeat_respond",
"automations",
"gateway",
"skill_workshop",
"music_generate",
"video_generate",
"tts",
] as const;
const CODEX_APP_SERVER_CONTEXT_ENGINE_HOST_CAPABILITIES = [
"bootstrap",
"assemble-before-prompt",
@@ -103,6 +123,7 @@ export function createCodexAppServerAgentHarness(options: {
delegatedExecutionPluginIds: ["voice-call"],
contextEngineHostCapabilities: CODEX_APP_SERVER_CONTEXT_ENGINE_HOST_CAPABILITIES,
conversationToolPolicySupport: "exact",
conversationToolPolicySafeDenyTools: CODEX_TOOL_POLICY_SAFE_DENY_NAMES,
deliveryDefaults: {
visibleReplies: "message_tool",
},
@@ -18,6 +18,7 @@ type ReasoningTextGroup = {
};
type AgentEvent = Parameters<NonNullable<EmbeddedRunAttemptParams["onAgentEvent"]>>[0];
type PlanUpdateSource = "codex-app-server" | "openclaw";
export class CodexReasoningProjection {
private readonly reasoningTextByGroup = new Map<string, ReasoningTextGroup>();
@@ -75,7 +76,7 @@ export class CodexReasoningProjection {
});
}
handleTurnPlanUpdated(params: JsonObject): void {
handleTurnPlanUpdated(params: JsonObject, source: PlanUpdateSource = "codex-app-server"): void {
const explanation = readNullableString(params, "explanation");
const plan = Array.isArray(params.plan)
? params.plan.flatMap((entry) => {
@@ -101,10 +102,13 @@ export class CodexReasoningProjection {
// non-empty update so the terminal transcript proves planning occurred.
this.turnPlanText = planText;
}
this.emitPlanUpdate({
explanation,
steps: plan,
});
this.emitPlanUpdate(
{
explanation,
steps: plan,
},
source,
);
}
recordItem(item: CodexThreadItem | undefined): void {
@@ -138,7 +142,10 @@ export class CodexReasoningProjection {
);
}
private emitPlanUpdate(params: { explanation?: string | null; steps?: AgentPlanStep[] }): void {
private emitPlanUpdate(
params: { explanation?: string | null; steps?: AgentPlanStep[] },
source: PlanUpdateSource = "codex-app-server",
): void {
if (!params.explanation && (!params.steps || params.steps.length === 0)) {
return;
}
@@ -147,7 +154,7 @@ export class CodexReasoningProjection {
data: {
phase: "update",
title: "Plan updated",
source: "codex-app-server",
source,
...(params.explanation ? { explanation: params.explanation } : {}),
...(params.steps && params.steps.length > 0 ? { steps: params.steps } : {}),
},
@@ -345,6 +345,13 @@ export class CodexAppServerEventProjector {
this.toolTranscriptProjection.recordDynamicToolCall(params);
}
/** Projects a successful OpenClaw update_plan call through the native plan stream. */
recordDynamicPlanUpdate(params: unknown): void {
if (isJsonObject(params)) {
this.reasoningProjection.handleTurnPlanUpdated(params, "openclaw");
}
}
recordDynamicToolResult(params: {
callId: string;
tool: string;
@@ -269,6 +269,9 @@ export function createCodexAttemptServerRequestController(
contentItems: protocolResponse.contentItems,
});
recordCodexDynamicToolResult(projector, call, response, protocolResponse);
if (protocolResponse.success && call.tool === "update_plan") {
projector?.recordDynamicPlanUpdate(response.executedArguments ?? call.arguments);
}
if (shouldEmitDynamicToolProgress) {
const progressResponse = toCodexDynamicToolProgressResponse(response, protocolResponse);
void emitCodexAppServerEvent(params, {
@@ -1,6 +1,7 @@
import type { EmbeddedRunAttemptParamsV2 as EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { isSystemAgentOnlyCodexDynamicToolAllowlist } from "./dynamic-tool-profile.js";
import type { CodexDynamicToolRuntimeResponse } from "./dynamic-tool-response-state.js";
import type { CodexDynamicToolCallParams, CodexDynamicToolCallResponse } from "./protocol.js";
import { sanitizeCodexToolResponse } from "./tool-progress-normalization.js";
@@ -49,7 +50,7 @@ type CodexDynamicToolExecutionIdentity = Pick<
>;
export function createCodexDynamicToolExecutionRegistry() {
const executions = new Map<string, Promise<CodexDynamicToolCallResponse>>();
const executions = new Map<string, Promise<CodexDynamicToolRuntimeResponse>>();
const keyFor = (call: CodexDynamicToolExecutionIdentity) =>
JSON.stringify([call.threadId, call.turnId, call.callId]);
@@ -59,7 +60,7 @@ export function createCodexDynamicToolExecutionRegistry() {
},
claim(
call: CodexDynamicToolExecutionIdentity,
start: () => Promise<CodexDynamicToolCallResponse>,
start: () => Promise<CodexDynamicToolRuntimeResponse>,
) {
const existing = executions.get(keyFor(call));
if (existing) {
@@ -87,5 +88,11 @@ export function resolveCodexDynamicToolDirectNames(
if (params.sourceReplyDeliveryMode === "message_tool_only") {
names.push("message");
}
// Restricted plugin runs replace Codex's native tool surface with an exact
// OpenClaw policy-filtered catalog. Keep the replacement planner visible in
// the initial context so Codex can maintain the same user-facing plan stream.
if (params.pluginHarnessToolPolicyRestricted === true) {
names.push("update_plan");
}
return names;
}
@@ -167,6 +167,9 @@ const testing = {
if (params.sourceReplyDeliveryMode === "message_tool_only") {
names.push("message");
}
if (params.pluginHarnessToolPolicyRestricted === true) {
names.push("update_plan");
}
return names;
},
setOpenClawCodingToolsFactoryForTests(
@@ -2314,7 +2317,9 @@ describe("runCodexAppServerAttempt", () => {
it("replaces the native surface with an exact conversation-policy-filtered catalog", async () => {
testing.setOpenClawCodingToolsFactoryForTests((options) =>
createOpenClawCodingTools(options).filter((tool) =>
["read", "write", "edit", "apply_patch", "exec", "process"].includes(tool.name),
["read", "write", "edit", "apply_patch", "exec", "process", "update_plan"].includes(
tool.name,
),
),
);
const params = createRunParams();
@@ -2325,6 +2330,8 @@ describe("runCodexAppServerAttempt", () => {
deny: ["exec", "process", "write", "edit"],
};
params.pluginHarnessToolPolicyRestricted = true;
const onAgentEvent = vi.fn();
params.onAgentEvent = onAgentEvent;
const harness = createStartedThreadHarness(async (method) => {
if (method === "config/read") {
return { config: {}, layers: [] };
@@ -2353,7 +2360,12 @@ describe("runCodexAppServerAttempt", () => {
);
expect(startParams?.environments).toEqual([]);
expect(dynamicToolNames.toSorted()).toEqual(["apply_patch", "read"]);
expect(dynamicToolNames.toSorted()).toEqual(["apply_patch", "read", "update_plan"]);
const updatePlanSpec = flattenSpecsWithNamespace(startParams?.dynamicTools ?? []).find(
(tool) => tool.name === "update_plan",
);
expect(updatePlanSpec).not.toHaveProperty("namespace");
expect(updatePlanSpec).not.toHaveProperty("deferLoading");
expect(startParams?.config).toMatchObject({
"features.hooks": false,
"hooks.PreToolUse": [],
@@ -2363,6 +2375,34 @@ describe("runCodexAppServerAttempt", () => {
});
expect(harness.requests.map((request) => request.method)).toContain("mcpServerStatus/list");
const plan = [
{ step: "Inspect regression", status: "completed" },
{ step: "Restore progress", status: "in_progress" },
];
const response = await harness.handleServerRequest({
id: "request-plan-1",
method: "item/tool/call",
params: {
threadId: "thread-1",
turnId: "turn-1",
callId: "call-plan-1",
namespace: null,
tool: "update_plan",
arguments: { explanation: "Plan restored", plan },
},
});
expect(response).toMatchObject({ success: true });
expect(onAgentEvent).toHaveBeenCalledWith({
stream: "plan",
data: {
phase: "update",
title: "Plan updated",
source: "openclaw",
explanation: "Plan restored",
steps: plan,
},
});
await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" });
await run;
});
+42
View File
@@ -1298,6 +1298,48 @@ describe("runAgentHarnessAttempt", () => {
]);
});
it("isolates native tools unless every exact deny is explicitly safe", async () => {
const received: boolean[] = [];
const runAttempt = vi.fn<AgentHarness["runAttempt"]>(async (attempt) => {
received.push(attempt.pluginHarnessToolPolicyRestricted === true);
return createAttemptResult("codex");
});
const harness: AgentHarness = {
id: "codex",
label: "Codex",
conversationToolPolicySupport: "exact",
conversationToolPolicySafeDenyTools: [
"tts",
"music_generate",
"browser",
"unknown_native_tool",
],
supports: (ctx) =>
ctx.provider === "codex" ? { supported: true, priority: 100 } : { supported: false },
runAttempt,
};
registerAgentHarness(harness, { ownerPluginId: "codex" });
const policies = [
{ deny: ["tts", "music_generate"] },
{ deny: ["browser"] },
{ deny: ["exec"] },
{ deny: ["video_generate"] },
{ deny: ["unknown_native_tool"] },
{ deny: ["group:runtime"] },
{ deny: ["*"] },
{ allow: ["tts"] },
];
for (const conversationToolPolicy of policies) {
await runAgentHarnessAttempt({
...createAttemptParams(),
conversationToolPolicy,
});
}
expect(received).toEqual([false, false, true, true, true, true, true, true]);
});
it("marks only explicit restrictive policy layers for plugin harness isolation", async () => {
const received: boolean[] = [];
const runAttempt = vi.fn<AgentHarness["runAttempt"]>(async (attempt) => {
+32 -3
View File
@@ -30,6 +30,7 @@ import {
unwrapSecretSentinelsForProviderEgress,
} from "../provider-secret-egress.js";
import { resolveSandboxRuntimeStatus } from "../sandbox/runtime-status.js";
import { isKnownCoreToolId } from "../tool-catalog.js";
import {
expandToolGroups,
mergeAlsoAllowPolicy,
@@ -575,7 +576,7 @@ async function runSelectedAgentHarnessAttempt(
isSystemAgentOnlyAllowlist(pluginAttempt.params.toolsAllow);
const preparedParams = selection.builtIn
? pluginAttempt.params
: preparePluginHarnessParams(pluginAttempt.params);
: preparePluginHarnessParams(pluginAttempt.params, harness);
const effectiveAttemptParams =
hostOpenClawAuthority && preparedParams.pluginHarnessToolPolicyRestricted
? { ...preparedParams, pluginHarnessToolPolicyRestricted: false }
@@ -773,6 +774,7 @@ function withoutPluginHarnessPrivateState(
function preparePluginHarnessParams(
params: import("./types.js").AgentHarnessAttemptParamsV2,
harness: AgentHarness,
): import("./types.js").AgentHarnessAttemptParamsV2 {
const boundary = "plugin harness handoff";
const resolvedApiKey = params.resolvedApiKey
@@ -783,7 +785,12 @@ function preparePluginHarnessParams(
model === params.model && resolvedApiKey === params.resolvedApiKey
? params
: { ...params, model, resolvedApiKey };
const policies = resolvePluginHarnessToolPolicies(preparedParams);
const policies = resolvePluginHarnessToolPolicies(
preparedParams,
harness.conversationToolPolicySupport === "exact"
? harness.conversationToolPolicySafeDenyTools
: undefined,
);
return applyPluginHarnessDenyAllToolPolicy(
{
...preparedParams,
@@ -861,6 +868,7 @@ function resolvePluginHarnessDenyAllToolPolicyPrompt(
function resolvePluginHarnessToolPolicies(
params: PluginHarnessToolPolicyContext,
safeDenyToolNames?: readonly string[],
): ResolvedPluginHarnessToolPolicies {
const messageProvider = params.messageProvider ?? params.messageChannel;
const sandboxSessionKey = params.sandboxSessionKey ?? params.sessionKey;
@@ -924,6 +932,9 @@ function resolvePluginHarnessToolPolicies(
policy.inheritedToolPolicy,
policy.runtimeToolPolicyForInheritance,
];
const safeDenyToolNameSet = safeDenyToolNames
? new Set(safeDenyToolNames.map(normalizeToolPolicyName))
: undefined;
return {
senderPolicy: policy.senderPolicy,
senderScopedGroupPolicy: resolveSenderScopedGroupToolPolicy(
@@ -943,10 +954,28 @@ function resolvePluginHarnessToolPolicies(
policy.subagentPolicy,
policy.inheritedToolPolicy,
],
toolPolicyRestricted: explicitPolicies.some(toolPolicyRestrictsTools),
toolPolicyRestricted: explicitPolicies.some((explicitPolicy) =>
toolPolicyRestrictsHarnessNativeTools(explicitPolicy, safeDenyToolNameSet),
),
};
}
function toolPolicyRestrictsHarnessNativeTools(
policy: PluginHarnessToolPolicy | undefined,
safeDenyToolNames: ReadonlySet<string> | undefined,
): boolean {
if (!safeDenyToolNames) {
return toolPolicyRestrictsTools(policy);
}
if (!policy || toolPolicyRestrictsTools({ allow: policy.allow })) {
return toolPolicyRestrictsTools(policy);
}
return expandToolGroups(policy.deny ?? []).some((deniedName) => {
const normalized = normalizeToolPolicyName(deniedName);
return !isKnownCoreToolId(normalized) || !safeDenyToolNames.has(normalized);
});
}
function resolveSenderScopedGroupToolPolicy(
params: PluginHarnessToolPolicyContext,
groupPolicyParams: Parameters<typeof resolveGroupToolPolicy>[0],
+5
View File
@@ -347,6 +347,11 @@ type AgentHarnessRunCapability<
deliveryDefaults?: AgentHarnessDeliveryDefaults;
/** Certifies exact runAttempt enforcement; direct-policy-restricted channel side questions fail in core. */
conversationToolPolicySupport?: "exact";
/**
* Canonical OpenClaw tool names whose exact denies are fully enforced outside
* this harness's native surface. Every other deny remains fail-closed.
*/
conversationToolPolicySafeDenyTools?: readonly string[];
supports(ctx: AgentHarnessSupportContext): AgentHarnessSupport;
/** Lets this harness resolve forwarded profiles or its own native credentials. */
authBootstrap?: "harness";