From a850cb7eaddf02a2c743956f9773d55b9dfd127c Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 11 Jul 2026 23:31:39 -0700 Subject: [PATCH] fix: document non-pi customTools fail-open when gate context omitted Matches pi wrapToolsWithActionGate semantics: callers that omit actionGateContext (chat/triage) intentionally leave tools ungated. Add a content-free warn when a non-pi runtime receives customTools without gate context so the path is visible without inventing deny-all defaults. --- packages/engine/src/agent-session-helpers.ts | 27 +++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/engine/src/agent-session-helpers.ts b/packages/engine/src/agent-session-helpers.ts index ff1014c145..6423916962 100644 --- a/packages/engine/src/agent-session-helpers.ts +++ b/packages/engine/src/agent-session-helpers.ts @@ -38,6 +38,10 @@ import { import type { RunAuditor } from "./run-audit.js"; import { MockAgentRuntime } from "./providers/mock-provider.js"; +/** Logger for agent session helpers */ +const sessionLog = createLogger("agent-session"); +const mockRuntimeSingleton = new MockAgentRuntime(); + /* FNXC:GrokAcp 2026-07-12-06:30: Non-pi plugin runtimes (Grok ACP, Hermes, OpenClaw, …) receive `customTools` as @@ -58,14 +62,32 @@ const RUNTIMES_WITH_INTERNAL_TOOL_GATING = new Set(["pi"]); /** * Apply Fusion tool policy wrappers for plugin runtimes that do not wrap tools * themselves. Mirrors the customTools portion of the pi createFnAgent chain. + * + * FNXC:GrokAcp 2026-07-12-06:35: + * Missing `actionGateContext` / `permanentAgentGating` is intentionally fail-open + * and matches `wrapToolsWithActionGate` / pi `createFnAgent`: when the caller + * omits gate context (dashboard chat, room responders, triage), tools stay + * ungated coordination primitives. Executor/heartbeat permanent-agent lanes + * pass gate context and get full policy. Do not invent a deny-all default here — + * that would break chat workflow tools on Grok while pi still allows them. + * Emit a content-free warn (tool count + which layers were applied) so missing + * context on a non-pi path is visible without logging tool names/args. */ export function wrapCustomToolsForPluginRuntime( tools: ToolDefinition[] | undefined, options: Pick, + logContext?: { runtimeId: string; sessionPurpose: string }, ): ToolDefinition[] | undefined { if (!tools || tools.length === 0) { return tools; } + const hasActionGate = Boolean(options.actionGateContext) && options.actionGateContext?.isEphemeral !== true; + const hasPermanentGate = Boolean(options.permanentAgentGating); + if (!hasActionGate && !hasPermanentGate && logContext) { + sessionLog.warn( + `[${logContext.sessionPurpose}] non-pi runtime "${logContext.runtimeId}" received ${tools.length} customTool(s) without actionGateContext/permanentAgentGating; wrappers apply RTK rewrite only (matches pi fail-open when gate context is omitted)`, + ); + } const withRtk = wrapToolsWithRtkRewrite(tools); const withPermanent = wrapToolsWithPermanentAgentGating(withRtk, options.permanentAgentGating); return wrapToolsWithActionGate(withPermanent, options.actionGateContext); @@ -75,10 +97,6 @@ function shouldWrapCustomToolsForRuntime(runtimeId: string): boolean { return !RUNTIMES_WITH_INTERNAL_TOOL_GATING.has(runtimeId); } -/** Logger for agent session helpers */ -const sessionLog = createLogger("agent-session"); -const mockRuntimeSingleton = new MockAgentRuntime(); - function extractSkillNamesFromSelection(skillSelection: SkillSelectionContext | undefined): string[] { if (!skillSelection || !Array.isArray(skillSelection.requestedSkillNames)) { return []; @@ -638,6 +656,7 @@ export async function createResolvedAgentSession( customTools: wrapCustomToolsForPluginRuntime( effectiveRuntimeOptionsWithModel.customTools, effectiveRuntimeOptionsWithModel, + { runtimeId: resolved.runtimeId, sessionPurpose }, ), } : effectiveRuntimeOptionsWithModel;