diff --git a/packages/api/src/agents/hitl/policy.spec.ts b/packages/api/src/agents/hitl/policy.spec.ts index 436e44a0cd..f234b68876 100644 --- a/packages/api/src/agents/hitl/policy.spec.ts +++ b/packages/api/src/agents/hitl/policy.spec.ts @@ -90,10 +90,22 @@ describe('resolveToolApprovalPolicy', () => { }); }); - test('keeps the BYOM bypass baseline when the endpoint normally uses default mode', () => { + test.each(['default', 'dontAsk', 'bypass'] as const)( + 'preserves an enabled endpoint %s policy when BYOM is active', + (mode) => { + const endpoint: TToolApprovalPolicy = { + enabled: true, + mode, + deny: ['dangerous_tool'], + }; + expect(resolveToolApprovalPolicy({ endpoint, attachedCodeEnvironment: true })).toBe(endpoint); + }, + ); + + test('uses the BYOM bypass baseline for a configured but inactive endpoint policy', () => { expect( resolveToolApprovalPolicy({ - endpoint: { enabled: true, mode: 'default', deny: ['dangerous_tool'] }, + endpoint: { mode: 'dontAsk', deny: ['dangerous_tool'] }, attachedCodeEnvironment: true, }), ).toEqual({ diff --git a/packages/api/src/agents/hitl/policy.ts b/packages/api/src/agents/hitl/policy.ts index ef400e559e..95ee5f40ff 100644 --- a/packages/api/src/agents/hitl/policy.ts +++ b/packages/api/src/agents/hitl/policy.ts @@ -56,15 +56,20 @@ export interface ToolApprovalPolicyLayers { * - `agent` overrides `mode`/`allow`/`deny`/`ask`/`reason`; * - `skills` may only tighten (add `ask`/`deny`), never loosen. * - * The BYOM activation adds only `enabled: true, mode: 'bypass'`; an agent-scoped - * hook supplies the risky coding decisions. This avoids prompting managed sibling - * agents in the same graph. An explicit endpoint `enabled: false` remains the - * administrator emergency override. `agent`/`skills` are accepted but not yet merged. + * When no endpoint policy is active, BYOM adds `enabled: true, mode: 'bypass'` and + * an agent-scoped hook supplies its coding decisions. An already-enabled endpoint + * policy remains the run-wide administrative baseline, including its unmatched-tool + * mode. An explicit endpoint `enabled: false` remains the administrator emergency + * override. `agent`/`skills` are accepted but not yet merged. */ export function resolveToolApprovalPolicy( layers: ToolApprovalPolicyLayers, ): TToolApprovalPolicy | undefined { - if (layers.attachedCodeEnvironment === true && layers.endpoint?.enabled !== false) { + if ( + layers.attachedCodeEnvironment === true && + layers.endpoint?.enabled !== true && + layers.endpoint?.enabled !== false + ) { return { ...layers.endpoint, enabled: true, diff --git a/packages/api/src/agents/hitl/runtime.spec.ts b/packages/api/src/agents/hitl/runtime.spec.ts index 65ae715075..6b0923a8d9 100644 --- a/packages/api/src/agents/hitl/runtime.spec.ts +++ b/packages/api/src/agents/hitl/runtime.spec.ts @@ -1,5 +1,7 @@ -import { HookRegistry } from '@librechat/agents'; +import { HookRegistry, executeHooks } from '@librechat/agents'; import { registerToolApprovalHook, clearToolApprovalHooks } from './hooks'; +import { createAttachedCodeEnvironmentPolicyHook } from './byom'; +import { resolveToolApprovalPolicy } from './policy'; import { buildHITLRunWiring } from './runtime'; describe('buildHITLRunWiring', () => { @@ -53,6 +55,40 @@ describe('buildHITLRunWiring', () => { }); expect(wiring?.hooks.getMatchers('PreToolUse')).toHaveLength(1); }); + + test.each([ + ['default', 'ask'], + ['dontAsk', 'deny'], + ] as const)( + 'keeps the enabled endpoint %s fallback for unrelated tools in BYOM runs', + async (mode, expectedDecision) => { + const policy = resolveToolApprovalPolicy({ + endpoint: { enabled: true, mode }, + attachedCodeEnvironment: true, + }); + const wiring = buildHITLRunWiring( + policy, + {}, + [], + [{ hook: createAttachedCodeEnvironmentPolicyHook(new Set(['attached-agent'])) }], + ); + + const result = await executeHooks({ + registry: wiring?.hooks as HookRegistry, + matchQuery: 'mcp:github:create_issue', + input: { + hook_event_name: 'PreToolUse', + runId: 'run-byom-policy', + toolName: 'mcp:github:create_issue', + toolInput: {}, + toolUseId: 'tool-unrelated', + executingAgentId: 'attached-agent', + }, + }); + + expect(result.decision).toBe(expectedDecision); + }, + ); }); describe('buildHITLRunWiring host-hook composition', () => {