Skip to content

Dynamic tools: approvalKey is dropped by toHarnessToolDefinition, silently downgrading scoped approvals to bare tool names #2319

Description

@ycarmel

Version: eve 0.39.1 (also 0.39.0)

What happens

ToolDefinition.approvalKey is documented as letting the runtime record a compound, input-derived key into the session's approved-tools set instead of the bare tool name (runtime/types.d.ts), and the harness honors it: resolveApprovalKeyFromTools in harness/tool-loop.js reads approvalKey off the resolved tool map, and approval recording resolves through it.

But for tools produced by a dynamic resolver (e.g. step.started), the conversion in context/dynamic-tool-lifecycle.js strips the field:

function toHarnessToolDefinition(e, t) {
  return {
    description: t.description,
    execute: createToolExecuteWithAuth({...}),
    inputSchema: toInputSchema(t.inputSchema),
    name: e,
    approval: t.approval,
    // approvalKey is not copied
    outputSchema: toOutputSchema(t.outputSchema),
    ...(t.toModelOutput === void 0 ? {} : { toModelOutput: t.toModelOutput }),
  };
}

So a dynamic tool that declares approvalKey gets its approvals recorded under the bare tool name. The failure is silent: an approval policy that checks ctx.approvedTools for the scoped key never matches, and the UX degrades to re-prompting on every call — or worse, a policy written against bare names grants more than the author scoped.

Repro

  1. Return a tool with approvalKey: (input) => \mytool@${input.scope}`from astep.started` dynamic resolver.
  2. Trigger it, approve the request.
  3. Inspect session state / ctx.approvedTools on the next call: it contains "mytool", not "mytool@...".

Expected

approvalKey survives the dynamic-tool conversion, matching the behavior documented on ToolDefinition and already implemented in the harness read path.

Suggested fix

One field-passthrough in toHarnessToolDefinition — we run this as a patch in production and it restores the documented behavior:

approval: t.approval,
...(t.approvalKey === void 0 ? {} : { approvalKey: t.approvalKey }),

Possibly related: replayDynamicSessionTools in the same module also rebuilds tools without approval/approvalKey — unreachable for step-resolved tools, but worth aligning while in there.

Context: we use input-scoped approvals to let one human approval cover a chain of GitHub-authoring writes bound to a (repo, branch) target; without the passthrough the scoping silently never engages.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp1tools

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions