Allow for this session now sticks

2 days ago

On an untrusted MCP server, choosing Allow for this session, or Always, now actually stops the next prompt for that tool. Prompts that can only cover one request now offer just Allow Once and Deny. It's a salvage of an earlier fix, rebuilt on current main, and it closes two issues.

Ask about this presentation

Chapters

  1. 0:00Allow for this session now sticks
  2. 0:19The next call asked again
  3. 0:39Why every yes covered one call
  4. 0:59An oversight, not a design choice
  5. 1:19One key per server and tool
  6. 1:39Session and Always are stored
  7. 1:59One-shot prompts offer Once and Deny
  8. 2:17Over the limit, every call asks
  9. 2:30Revoking Always
  10. 2:50Live repro on a real gateway
  11. 3:12Tests fail on main, pass here
  12. 3:34What it leaves alone
Show transcript
  1. On an untrusted MCP server, choosing Allow for this session, or Always, now actually stops the next prompt for that tool. Prompts that can only cover one request now offer just Allow Once and Deny. It's a salvage of an earlier fix, rebuilt on current main, and it closes two issues.

    Slide text
    Hermes
    Agent
    MCP trust approvals

    On a trust: untrusted MCP server

    Session and Always stop the next prompt for that tool. One-shot prompts offer only Once and Deny.

    Cover art reading Allow for this session, with Before, Now, One-shot prompts and Every surface panels
  2. Every write-capable call to an untrusted MCP server asks first, with Once, Session, Always and Deny. You press Session on the first call, and the tool runs. Then the next call to the same tool asks again. Session and Always behaved exactly like Once, on the gateway, the CLI and the TUI.

    Slide text
    Hermes
    Agent
    What was wrong

    Pressing Session allowed one call, and the next call asked again

    Turn 1

    notes · write_note

    Allow Once Session Always Deny

    Runs

    →

    Turn 2, same tool

    notes · write_note

    Allow Once Session Always Deny

    Asks again

  3. The trust gate asks through request elicitation consent, in the approval prompt module. That helper turned every yes into a one-call accept, and recorded nothing. Its gateway request also left out both scope flags, so the gateway runner and the TUI server defaulted them to on, and showed buttons the helper never honoured.

    Slide text
    Hermes
    Agent
    Root cause

    The consent helper turned every yes into a one-call accept

    Recorded nothing

    tools/approval_prompt.py

    request_elicitation_consent mapped Once, Session and Always to the same single accept.

    return _consent(choice, "decline")

    Offered everything

    gateway/run_turn_runner.py · tui_gateway/server.py

    The gateway request left out allow_session and allow_permanent, so both scopes defaulted to on.

  4. The trust gate reused a helper written for server-initiated elicitations, before the gate existed. Its comment says per-call confirmation, no pattern to remember. A trust-gate call does have something to remember: one server and tool pair. So the broken Session and Always buttons were an oversight.

    Slide text
    Hermes
    Agent
    Intent check

    The helper's comment, written for server-initiated elicitations

    # allow_permanent=False: elicitation is a per-call confirmation — no pattern to remember.

    A trust-gate call does have something to remember: one (server, tool) pair.

  5. In the MCP tool handlers, each answer is kept under a key made of the server name and the tool name, so allowing one write tool never covers the server's others. Before it asks, the gate checks whether that key is already approved in this session. A granted tool skips the prompt, and the spend limit check still runs.

    Slide text
    Hermes
    Agent
    tools/mcp_tool_handlers.py

    The gate keys each grant on one server and one tool

    Key for the notes server's write tool

    mcp:notes/write_note
    def _trust_approval_key(server_name, tool_name):
        return f"mcp:{server_name}/{tool_name}"
    
    def _trust_granted(server_name, tool_name):
        return is_approved(
            get_current_session_key(),
            _trust_approval_key(server_name, tool_name))
    
    gated = (
        trust == _core._TRUST_UNTRUSTED
        and not _tool_is_read_only(server_name, tool_name)
        and not _trust_granted(server_name, tool_name))
  6. request elicitation consent gains one keyword argument, remember key. With a key, the answer goes through the existing persist choice, the same path command approvals use. Session covers that tool for the rest of this chat. Always writes the key into the active profile's command allowlist, so other chats skip the prompt too.

    Slide text
    Hermes
    Agent
    tools/approval_prompt.py

    A key turns Session and Always into stored answers

    def _remembered_consent(session_key: str, choice,
                            remember_key: str | None,
                            unresolved: str) -> str:
        if remember_key is not None:
            from tools import approval as _a
            _a._persist_choice(session_key, choice,
                               [remember_key])
        return _consent(choice, unresolved)
    Session
    That tool, for the rest of this chat
    Always
    Written to the active profile's command_allowlist
  7. Without a key, the helper sends both scope flags as false. Every surface then shows only Allow Once and Deny. That covers MCP elicitation, sampling, payment card fill, and spend limit prompts. A stale or typed broader answer still covers just that one call.

    Slide text
    Hermes
    Agent
    Consent without a key

    Prompts with nothing to remember offer only Once and Deny

    Allow Once Session Always Deny
    allow_session: false allow_permanent: false
    MCP elicitation
    Sampling
    Payment-card fill
    Spend-limit prompts
  8. A call that would take the chat past its spend limit carries no key. So it offers only Once and Deny, and the description says so. An earlier Session or Always answer for that tool doesn't silence it.

    Slide text
    Hermes
    Agent
    _ask_for_call

    An over-limit call asks every time, even after Always

    # Over the limit the answer covers this call only:
    # a remembered trust grant must not pre-approve
    # the next over-limit call.
    remember_key = (
        None if over
        else _trust_approval_key(server_name, tool_name))
    scope = (
        "once" if over
        else "once, for this session, or always")
  9. The automated reviewer raised a real concern about revocation. Persist choice also keeps an Always answer in the granting chat's session, so deleting the allowlist entry leaves that chat approved. The author agreed, since command approvals work the same way, and fixed the docs to say the granting chat keeps it until it ends.

    Slide text
    Hermes
    Agent
    Review thread

    Review found a gap in revoking Always, and the docs now cover it

    “Deleting the command_allowlist entry does not revoke an Always answer in the chat that granted it.”

    greptile-apps · mcp-config-reference.md

    “Deleting the entry revokes it for every other chat, and the chat that answered Always keeps it until it ends.”

    teknium1 · fixed in f3414ff1dd6

  10. The live repro ran a real gateway process, a Telegram stand-in pressing real inline buttons, and an untrusted notes server. On the base commit, Session or Always on turn one still prompted on turn two. On this branch, turn two wrote the second note without a prompt, and Always left the key in the allowlist. Once, the control, asks both times.

    Slide text
    Hermes
    Agent
    Live repro · Telegram stand-in

    On a real gateway, Session and Always stopped the second prompt

    A hermes gateway run child, real inline-button presses, a stdio server notes with trust: untrusted

    Pressed on turn 1Base · turn 2Head · turn 2command_allowlist on head
    SessionNew promptNo prompt, note two writtennone
    AlwaysNew promptNo prompt, note two written['mcp:notes/write_note']
    OncePromptPromptnone
  11. The new tests drive the real gateway approval queue. On main, all three cases fail, with three prompts where two were expected, and a missing allow session flag. All three pass here. More than 140 existing MCP and approval tests still pass, and the classic CLI and the TUI backend showed the same fix with real processes.

    Slide text
    Hermes
    Agent
    tests/tools/test_mcp_trust_approval_scope.py

    Three new test cases fail on main and pass here

    On origin/main

    assert 3 == 2 # prompts KeyError: 'allow_session'

    Driven through the real gateway approval queue

    143

    existing MCP and approval tests passed

    Same fix seen live on

    Classic CLI TUI / Desktop backend
  12. Elicitation answers still cover one request each. Remembering them per server request is a separate feature. The adapters stay as they were, because the messaging buttons, the TUI, Desktop, the API server and ACP already honour these flags. To take back an Always answer, delete its key from the command allowlist, then end the chat that granted it. Allow for this session now sticks, one tool at a time.

    Slide text
    Hermes
    Agent
    Scope and next steps

    Still one-shot

    Remembering elicitation answers per server request is a separate feature, in #142803.

    No adapter changes

    Telegram, Discord, Slack, Matrix, Teams and Feishu buttons, /approve, TUI and Desktop, the API server and ACP already honour the flags.

    Revoke an Always answer

    command_allowlist: - mcp:<server>/<tool> # delete

    then end the chat that answered Always.

    Allow for this session now sticks.

What happened in AI today, in your inbox

One short email each evening. Read it and you’re caught up.

Built in New York with ❤️