Skip to content

refactor(gateway): one Caller value for role and session; the three binding scopes side by side - #166

Merged
earakely-scale merged 1 commit into
mainfrom
refactor/gateway-caller-and-lifespans
Oct 12, 2026
Merged

earakely-scale merged 1 commit into
mainfrom
refactor/gateway-caller-and-lifespans

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Behaviour-neutral restructuring, rebased onto main after #165 merged (its chaos-round fix included).

One caller value. The role lived in one ContextVar bound per request, the session key in another bound per MCP session, and the _meta stamp was assembled by hand in three places (the MCP proxy tool, /step, the trigger engine). caller.py adds a frozen Caller(role, session): Caller.current() reads it, Caller.meta() is the one place that produces the stamp, and _call_child_tool takes a Caller (None for an unattributed trigger action). The default sentinel now has its home there, with why it is load-bearing: under the protocol a _meta role entry of any value marks a call as proxied, so an unforwarded caller is stamped default on purpose.

The three scopes side by side. The per-MCP-session lifespan leaves __init__ for a _session_lifespan method next to _asgi_app (request scope) and _process_lifespan (process scope), with one comment explaining how the session's server task inherits the initialize request's role and passes the key to every tools/call. __init__ only wires.

Wire names. The issue asked to import the keys from agentenv_protocol; the gateway container ships without that package (constants.py is a hand-kept mirror by design), so they stay literals and a unit test pins them to the protocol's exports instead.

No wire change: same _meta keys and values, same headers, same JSON-RPC rewriting. tst/unit/env/gateway: 377 passing (372 after #165, plus the five caller tests); the two tests that reached into the old ContextVars bind a Caller instead.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no actionable issues were found.

Summary

The PR replaces separate role and session values with one frozen Caller and moves the MCP session lifespan out of __init__.

  • Gateway calls now carry role and session together in one Caller.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  HTTP[HTTP request role] --> Caller[Caller]
  MCP[MCP session key] --> Caller
  Caller --> Filter[Tool role filter]
  Caller --> Meta[Caller.meta]
  Step[REST step role] --> Meta
  Trigger[Attributed trigger role] --> Meta
  Meta --> Child[Child tools/call]
  Unattributed[Unattributed trigger] -->|No metadata| Child
Loading

Reviews (2) · Last reviewed commit: "refactor(gateway): one Caller value for ..." · Reviewed by Greptile

@earakely-scale
earakely-scale requested a review from a team as a code owner October 11, 2026 23:23
Base automatically changed from fix/gateway-child-outage-fails-fast to main October 12, 2026 02:04
…inding scopes side by side

Behaviour-neutral. The role lived in one ContextVar bound per request, the
session key in another bound per MCP session, and the _meta stamp was
assembled by hand in the MCP proxy tool, in /step and in the trigger
engine. A frozen Caller(role, session) in caller.py now carries both,
Caller.current() reads it and Caller.meta() is the one place that produces
the stamp, with the `default` sentinel documented where it is stamped: a
_meta role entry of any value is what marks a call as proxied under the
protocol, so an unforwarded caller is stamped `default` on purpose.
_call_child_tool takes a Caller (None for an unattributed trigger action).

The per-MCP-session lifespan leaves __init__ for a _session_lifespan method
next to _asgi_app (request scope) and _process_lifespan (process scope), so
the three scopes read in one place and __init__ only wires.

The wire names stay literals in constants.py because the gateway container
ships without agentenv_protocol; a unit test now pins them to the
protocol's exports instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@earakely-scale
earakely-scale force-pushed the refactor/gateway-caller-and-lifespans branch from 5f76657 to 7ace4a1 Compare October 12, 2026 02:05
@earakely-scale

Copy link
Copy Markdown
Collaborator Author

Light chaos round on this head (7ace4a1), loopback: a real Gateway behind its own MCP front door, a child that echoes the _meta it receives, 12 concurrent MCP sessions (8 distinct roles, 4 with no header). The script stays in scratch.

  • 1,440 calls at 96-way concurrency: every answer carried exactly the caller's role, one session key per MCP session, 12 distinct keys; the header-less sessions read default and still got distinct keys.
  • Same storm with the child killed and restarted three times: 1,425 answers, 13 Connection closed, zero wrong identities, no call over 2.1 s, every session answered correctly afterwards.
  • /step with AgentEnv-Role: alice reaches the child as the role alone (no session key); without a header, default.
  • Trigger-originated calls: no as role stamps no _meta at all; as: carol stamps the role alone.
  • Gateway started with the child down comes up within the startup discovery timeout, and the first MCP session after the child appears heals the tool list in its session lifespan.

The same round on #167's head (6a9a432, which includes this commit) is identical: 14/14.

@earakely-scale
earakely-scale merged commit 5453427 into main Oct 12, 2026
14 checks passed
@earakely-scale
earakely-scale deleted the refactor/gateway-caller-and-lifespans branch October 12, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant