Skip to content

🔓 feat: Remember Tool Approvals Per Conversation - #16557

Merged
berry-13 merged 8 commits into
devfrom
feat/tool-allow
Oct 3, 2026
Merged

berry-13 merged 8 commits into
devfrom
feat/tool-allow

Conversation

@berry-13

@berry-13 berry-13 commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Every paused tool call asks for approval again, even when the user already approved the same tool several times in the same conversation. Admins can write allow rules in librechat.yaml, but users have no way to say "stop asking for this tool here".

With the new endpoints.agents.toolApproval.allowAlways option enabled, the approval card offers "Always allow" for eligible calls. Choosing it approves the call and stores that exact tool name on the conversation (MCP tool names include their server), and later runs in the same conversation add it to the policy as an exact-name allow entry. The server enforces each step: the resume route rejects an "always" scope the pause did not offer, eligibility is rechecked against the current policy before storing, and the list is written only through an owner-scoped, bounded method that generic saves and imports cannot touch. Admin deny and ask rules and programmatic hooks still win, stored names are ignored under dontAsk or when the flag is off, and native code tools stay under the code approval picker. Offering and resume eligibility, scheduled pause admission and the run itself all read one effective policy (admin rules healed against MCP aliases, plus remembered tools), the choice is stored only after every resume fence passes, and allowAlwaysMaxTools / allowAlwaysMaxToolNameLength (defaults 64 and 256) bound the list; at the cap the choice is no longer offered for new tools. The choice is not offered when a programmatic or plugin approval hook could still ask for the tool, and remembered names cover every MCP alias spelling of the same tool. The flag defaults to false, so existing deployments keep prompting.

Type of change

  • Feature

Testing

Exercised the real SDK policy hook with stored names against admin deny, ask and allow rules, MCP names from two servers, wildcard names, dontAsk and the flag turned off; exercised the resume route with offered and unoffered scopes and a storage failure.

Tested environments/configuration:

  • endpoints.agents.toolApproval.allowAlways: true and unset

Automated tests:

  • packages/api: npx jest src/agents/hitl: 376 of 377 passed, including an invariant spec that runs offer, record and the real hook wiring for 12 cases; the one failure, hookLoader.spec.ts "continues past a bad entry to load the good ones", fails identically on an unmodified canary checkout
  • api: resume.spec.js: 167 passed, including fence rejections that store nothing
  • client: ToolApproval.test.tsx: 10 passed
  • npx tsc --noEmit clean in packages/data-provider, packages/data-schemas, packages/api and client
  • Added allow.spec.ts, resume route cases, approval card cases, and conversation.spec.ts cases for the storage method; the latter could not run locally because MongoMemoryServer fails to start in this environment (including on untouched specs), so CI is their first run

Screenshots / recordings

Captured from the running app with a seeded paused MCP call carrying allow_always; before is canary bddb8f8ab, after is this branch.

Before After
Before: Approve, Reject, Edit and Respond only After: Always allow selected, with the rest-of-conversation hint

Dark mode, after:

After in dark mode: Always allow selected

Risk / compatibility

Adds optional allowAlways, allowAlwaysMaxTools and allowAlwaysMaxToolNameLength fields to the tool approval config and an optional field on conversations; no migration. Security-sensitive: the server never trusts the client's scope without the pause having offered it, deny and ask rules always take precedence, and a rejected resume stores nothing.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • User-facing or complex behavior is documented where necessary
  • Required dependency changes have been merged/published
  • Required documentation PR: librechat.ai docs for toolApproval.allowAlways still to be written

@berry-13
berry-13 marked this pull request as ready for review September 30, 2026 09:56
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T22:45:03.402089Z 6e89574 New commits
🔒 Security Review ✅ Completed 2026-09-30T10:19:12.827656Z 7becdfd Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7becdfdad0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/agents/hitl/allow.ts Outdated
Comment on lines +7 to +10
/** Upper bound on remembered tools per conversation; later choices are not stored. */
export const MAX_CONVERSATION_TOOL_ALLOWS = 64;
/** Longest tool name accepted as a remembered allow. */
const MAX_ALLOWED_TOOL_NAME_LENGTH = 256;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make remembered-approval limits configurable

Move these newly introduced bounds into toolApproval configuration with backward-compatible defaults. Once a conversation reaches 64 remembered tools—or a provider exposes a tool name longer than 256 characters—“Always allow” silently degrades to a one-time approval, and deployments with broader tool catalogs have no way to adjust that behavior.

AGENTS.md reference: AGENTS.md:L77-L79

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cc187a9. Added toolApproval.allowAlwaysMaxTools and allowAlwaysMaxToolNameLength (defaults 64 and 256, bounded 1 to 1024). Once a conversation is at the cap the server stops offering Always allow for new tools instead of silently downgrading; an already remembered tool is still offered. Tests in allow.spec.ts.

Comment thread api/server/controllers/agents/client.js Outdated
Comment on lines +4326 to +4328
? markToolApprovalAllowAlways(
markNativeCodeToolApprovalRequests(interrupt.payload, reachableAgents),
appConfig?.endpoints?.[EModelEndpoint.agents]?.toolApproval,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check eligibility against the effective approval policy

When an admin ask pattern matches only an MCP tool's alternate spelling, createRun heals that pattern before evaluating the tool, so the call pauses, but this code checks the raw endpoint policy and incorrectly offers “Always allow.” Subsequent runs heal the same ask rule again, and because ask outranks the remembered allow, the user is prompted indefinitely despite selecting the option; marking and resume-time eligibility should use the same healed effective policy as the run.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cc187a9. buildEffectiveToolApprovalPolicy in allow.ts heals the policy against the reachable agents' MCP aliases (reusing healToolApprovalPolicy) and folds in remembered allows; marking and resume time eligibility both use it. Test: a call held by an ask rule written against the alternate spelling is not offered the choice and nothing is stored.

Comment on lines +4945 to +4948
toolApprovalAllows: resolveRunToolApprovalAllows(
agentsEConfig?.toolApproval,
this.options.req?.resolvedConversation,
this.conversationId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include remembered allows in scheduled pause admission

In the AgentClient scheduled-fire path, canAgentGraphPause runs earlier against effectiveToolApprovalPolicy without these remembered allows, while only createRun receives them here. If the static policy would ask for all eagerly known tools but this conversation has remembered each of them and has no other pause source, the actual run policy would auto-approve them, yet a deployment without Redis or a durable checkpointer still rejects the schedule with SCHEDULED_HITL_REQUIRES_SHARED_STORE or SCHEDULED_HITL_REQUIRES_DURABLE_CHECKPOINT; fold the stored allows into the policy used for admission as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cc187a9. canAgentGraphPause now receives the remembered tools and evaluates the same effective policy as createRun, so a scheduled run whose known tools are all remembered is not rejected; an alias matched ask rule still makes it pausable. Covered in admission specs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed covered at cc187a9 and still at 881e833: client.js passes the remembered allows into canAgentGraphPause, which evaluates the same buildEffectiveToolApprovalPolicy as createRun. Tested by 'lets scheduled admission skip a run whose every known tool is remembered', and the new invariant spec asserts admission for every row where the offer is made.

Comment thread api/server/controllers/agents/resume.js Outdated
Comment on lines +1707 to +1715
await recordToolApprovalAllows({
userId,
conversationId,
policy: req.config?.endpoints?.[EModelEndpoint.agents]?.toolApproval,
pendingAction,
resolutions: req.body.decisions,
request: req,
addConvoToolApprovalAllows,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Delay persistence until all resume fences pass

Persist the remembered approval only after the schedule and project-context fences below have accepted the continuation. A schedule edit can make finalizeScheduleResumeClaim reject, and a project move can make rejectChangedResumeProjectContext return a 409, but this write has already committed in both cases; the submitted call never runs while a later turn silently auto-approves that tool despite the failed response.

AGENTS.md reference: AGENTS.md:L42-L44

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cc187a9. The store now runs after finalizeScheduleResumeClaim, rejectChangedResumeProjectContext, initializeClient and beginProviderExecution. resume.spec.js asserts nothing is stored on a schedule rejection or a 409 project context rejection, and that a storage failure still resumes once.

@danny-avila danny-avila added the 🗺️ Agent Runtime codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) label Sep 30, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc187a9e13

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/agents/hitl/allow.ts Outdated
Comment on lines +125 to +127
if (
!config.allowed_decisions.includes('approve') ||
!isToolAllowAlwaysEligible(effective, name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Suppress Always allow when an approval hook can still ask

When a programmatic or plugin PreToolUse hook is what returned ask—for example, a per-argument payment check—while the static policy does not ask or deny this tool, this predicate still marks the card allow_always. Selecting it only adds the name to the static allow list; createRun continues to register the same hook with ask taking precedence, so the next matching call pauses again despite the UI promising it will run without asking. The current alias-healing change is fresh evidence for this separate path: the marker now sees healed static lists, but it is still called without the resolved or plugin hook information. Suppress this offer for hook-governed pauses or carry the actual pause provenance into the payload.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 881e833. The interrupt payload does not record which hook asked (the request only carries source for native code tools), so the offer is now suppressed whenever a registered programmatic or plugin PreToolUse hook could apply to any spelling of the tool (toolApprovalHookCanApply in allow.ts, built with the same request context as the run). client.js only passes hookContext and pluginHookSource. Covered by the new invariant spec's hook cases.

Comment thread packages/api/src/agents/hitl/allow.ts Outdated
aliases: readonly MCPToolAlias[],
allowedTools?: readonly string[],
): TToolApprovalPolicy | undefined {
return applyConversationToolAllows(healToolApprovalPolicy(policy, aliases), allowedTools);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Heal remembered allows across MCP aliases

When an MCP tool switches between its legacy prefixed key and current stripped key—for example after an agent is resaved against the current catalog—the conversation retains the spelling that was approved. This function heals the endpoint policy before adding those stored entries, so the stored spelling is never expanded through aliases; the equivalent runtime name then falls through to ask and the supposedly remembered tool prompts again. Normalize remembered names through the alias map, or add them before healing, while retaining deny/ask precedence.

AGENTS.md reference: AGENTS.md:L42-L44

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 881e833. Remembered names now expand through the exact MCP alias pairs (getEquivalentToolNames) into allow entries for every spelling, and a tool is eligible only if every spelling is, so deny and ask on either spelling still win and similarly named tools are never included. Covered by legacy vs stripped cases in the invariant spec 'Always allow is offered only when the next identical call is auto-approved' (12 cases, each running offer, record and the real hook wiring through the SDK executeHooks).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 881e833fb4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/agents/hitl/allow.ts Outdated
Comment thread packages/api/src/agents/hitl/allow.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eed374ac38

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4328 to +4330
{
policy: appConfig?.endpoints?.[EModelEndpoint.agents]?.toolApproval,
agents: reachableAgents,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Carry lazy-discovered aliases into approval marking

When a lazy subagent resolves an MCP tool whose alternate spelling matches an admin ask rule or programmatic-hook matcher, createRun adds that alias only to its run-local mcpToolAliases, while reachableAgents here still contains the unresolved descriptor. The live run therefore pauses using the healed rule, but markToolApprovalAllowAlways does not see the alias and offers “Always allow”; the resume check has the same blind spot, stores the name, and the rebuilt run rediscovers the alias and prompts again. Unlike the earlier fixed eager-alias path, the fresh evidence is the separate registerResolvedMCPToolAliases path in createRun; propagate those runtime aliases or pause provenance into marking and resume validation.

AGENTS.md reference: AGENTS.md:L42-L44

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dae5445. createRun now keeps its live alias list (including aliases reported by lazily resolved subagents through registerResolvedMCPToolAliases) per run, and the interrupt passes getRunMCPToolAliases(this.run) into markToolApprovalAllowAlways, so the offer is judged against the same healed rules the paused run used. Resume only stores a name the pause offered, so it inherits the narrower offer. allow.spec.ts: 'heals against aliases the run discovered after its agents were collected'.

Comment thread packages/api/src/agents/hitl/allow.ts Outdated
Comment on lines +193 to +194
const stored = new Set(storedTools);
let room = getToolAllowAlwaysMaxTools(policy) - stored.size;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the active cap before recognizing stored tools

When an operator lowers allowAlwaysMaxTools below the number already stored, applyConversationToolAllows honors only the first max entries, but this code puts the entire stored array in stored. A tool beyond that active prefix therefore pauses yet is treated as already remembered and is still offered “Always allow” even with negative room; persistence then rejects the over-cap array and the next identical call prompts again. Limit the stored set to the same active prefix used by the run policy before deciding whether an offer will hold.

AGENTS.md reference: AGENTS.md:L42-L44

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dae5445. markToolApprovalAllowAlways now counts only storedTools.slice(0, allowAlwaysMaxTools), the same prefix applyConversationToolAllows honors, so a tool past a lowered cap is neither treated as remembered nor offered. allow.spec.ts: 'counts only the stored prefix the run honors after the cap is lowered'.

@berry-13
berry-13 requested a review from danny-avila as a code owner October 1, 2026 17:09
@berry-13
berry-13 changed the base branch from canary to dev October 1, 2026 17:10

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b8feed196

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/api/src/agents/hitl/allow.ts Outdated
Comment on lines +373 to +378
const aliases = collectAgentAliases(agents);
const eligible = collectToolApprovalAllows(
payload,
resolutions as Agents.ToolApprovalResolution[],
buildEffectiveToolApprovalPolicy(policy, aliases),
aliases,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck lazy MCP aliases before persisting approvals

When a paused lazy-subagent tool was offered “Always allow” and an administrator adds an ask/deny rule for its alternate MCP spelling before resume, this recheck still sees only aliases on the rebuilt, unresolved reachableAgents; the original run’s aliases from registerResolvedMCPToolAliases are not persisted in the pending action. The approval is consequently stored, but the resumed run rediscovers the alias, heals the new rule, and prompts again. The fresh evidence beyond the earlier lazy-alias finding is that marking now receives getRunMCPToolAliases(this.run), while this resume path still derives aliases solely via collectAgentAliases(agents); carry the pause-time aliases into resume validation or resolve them before storing.

AGENTS.md reference: AGENTS.md:L44-L47

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bad8c44. The interrupt now stores the paused run's alias pairs for the tools it offered (collectAllowAlwaysAliases over getRunMCPToolAliases(run)) as a server-only pendingAction.toolApprovalAliases, stripped by toClientPendingAction, and recordToolApprovalAllows unions them with the rebuilt agents' aliases before rechecking, so an ask/deny rule added mid-pause on the lazy alternate spelling makes the approval one-time. allow.spec.ts 'rechecks against the alias pairs the paused run kept for its offers'; policy.spec.ts asserts the field never reaches the client.

Copilot AI balanced review requested due to automatic review settings October 1, 2026 17:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Persistent auto-approval crosses security-sensitive policy, storage, resume, and execution paths and warrants final human validation.

Review effort: Balanced
Findings: None

What changed in this PR

Adds conversation-scoped tool approval persistence across the data, agent runtime, resume, and client layers.

Changes:

  • Stores bounded, owner-scoped remembered tool approvals.
  • Enforces policy, alias, hook, and resume eligibility server-side.
  • Adds the “Always allow” UI and comprehensive unit/E2E coverage.
File Description
packages/​data-schemas/​src/​types/​convo.ts Adds the persisted approval list type.
packages/​data-schemas/​src/​schema/​convo.ts Defines conversation storage.
packages/​data-schemas/​src/​methods/​conversation.ts Adds bounded storage and write protection.
packages/​data-schemas/​src/​methods/​conversation.spec.ts Tests storage invariants.
packages/​data-provider/​src/​types/​agents.ts Extends approval wire types.
packages/​data-provider/​src/​config.ts Adds configuration options.
packages/​api/​src/​agents/​run.ts Applies remembered approvals to runs.
packages/​api/​src/​agents/​hitl/​resume.ts Validates remembered scopes.
packages/​api/​src/​agents/​hitl/​policy.ts Persists and hides alias metadata.
packages/​api/​src/​agents/​hitl/​policy.spec.ts Tests client-safe projection.
packages/​api/​src/​agents/​hitl/​index.ts Exports approval helpers.
packages/​api/​src/​agents/​hitl/​allow.ts Implements eligibility and persistence policy.
packages/​api/​src/​agents/​hitl/​allow.spec.ts Tests policy invariants.
packages/​api/​src/​agents/​hitl/​admission.ts Includes remembered tools in pause admission.
librechat.example.yaml Documents the feature toggle.
e2e/​specs/​mock/​scenarios/​tool-allow-always.spec.ts Covers refusal scenarios end-to-end.
e2e/​config/​librechat.e2e.yaml Enables the feature for E2E.
client/​src/​utils/​approval.ts Projects offer eligibility onto tool calls.
client/​src/​locales/​en/​translation.json Adds localized UI copy.
client/​src/​components/​Chat/​Messages/​Content/​ToolApproval.tsx Adds the selectable approval scope.
client/​src/​components/​Chat/​Messages/​Content/​ApprovalContext.tsx Stores scope in decision drafts.
client/​src/​components/​Chat/​Messages/​Content/​__tests__/​ToolApproval.test.tsx Tests UI behavior and submission.
client/​src/​components/​Chat/​approval/​Review.tsx Passes eligibility into composer cards.
api/​server/​controllers/​agents/​resume.js Records approvals after resume fences.
api/​server/​controllers/​agents/​client.js Wires approvals through run lifecycle.
api/​server/​controllers/​agents/​__tests__/​resume.spec.js Tests route ordering and failures.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Adds an opt-in "Always allow" choice to the tool approval card. When
endpoints.agents.toolApproval.allowAlways is true, the server marks eligible
paused calls with allow_always; approving with scope "session" stores that
exact tool name on the conversation, and later runs in the same conversation
fold the stored names into the policy as exact-name allow entries.

The server enforces every step: the resume route rejects a session scope the
pause did not offer, eligibility is rechecked against the live policy before
storing, the list is written only through an owner-scoped bounded method, and
generic saves and imports cannot set or clear it. Admin deny and ask rules and
programmatic hooks still win, stored names are ignored under dontAsk or when
the flag is off, and native code tools stay under the code approval picker.
The flag defaults to false, so existing deployments keep prompting.
…ume Fences

Offering, resume re-check, scheduled admission and createRun now read one
policy from buildEffectiveToolApprovalPolicy: endpoint rules healed against
MCP alias spellings, then the conversation's remembered tools. A call held by
a healed ask rule is no longer offered "Always allow", and a scheduled fire
whose known tools are all remembered is no longer rejected as pause capable.

The remembered-tools cap and name length cap move to
toolApproval.allowAlwaysMaxTools and allowAlwaysMaxToolNameLength (defaults
64 and 256). The card stops offering the choice for new tools once the cap is
reached instead of silently approving once.

The resume route stores remembered tools only after the schedule and project
context fences pass; a storage failure still approves the call once.
Offer allow_always only when storing the name would auto-approve the next
identical call on the run's own path: every MCP spelling of the tool passes
the healed static policy and no programmatic or plugin approval hook can
apply to it (the pause payload does not record which hook asked).

Remembered names now expand to every alias spelling of the same tool, and a
deny or ask on any spelling keeps the tool ineligible. A table-driven spec
walks the invariant through the real hook wiring.
The resume-time eligibility recheck now applies the same programmatic and
plugin PreToolUse hook check as marking, with a hook context built from the
resume request and the live plugin hook source. A hook registered after the
pause turns the choice into a one-time approval: nothing is stored and the
resume continues.

Document that toolApproval.allowAlways should be enabled only once every
replica supports it, since older replicas ignore the choice.
@berry-13
berry-13 merged commit fb1dd57 into dev Oct 3, 2026
45 checks passed
@berry-13
berry-13 deleted the feat/tool-allow branch October 3, 2026 09:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🗺️ Agent Runtime codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants