Class: Parse::Agent::MCPRackApp::SessionOwnerRegistry Private
- Inherits:
-
Object
- Object
- Parse::Agent::MCPRackApp::SessionOwnerRegistry
- Defined in:
- lib/parse/agent/mcp_rack_app.rb
Overview
This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.
Per-app store of in-flight cancellable requests. Lookups for
cancellation are keyed by [correlation_id, request_id], but
every #register returns an opaque entry-id token that
uniquely identifies the registration. #deregister requires
that entry-id and removes the matching token only when it
still owns the slot — so a second registration under the same
(correlation_id, request_id) key cannot cause the first
registration's on_close to evict the wrong token.
SSEBody registers an entry before spawning its dispatcher_thread
and deregisters via the MCPRackApp-supplied on_close hook. A
notifications/cancelled POST calls #cancel to trip the
matching CancellationToken.
Identity binding: cancellation requires the cancelling request's
Mcp-Session-Id (sanitized into agent.correlation_id) to
match the original request's. This prevents an attacker who
guesses sequential JSON-RPC request ids from cancelling other
clients' in-flight requests. A registration with a nil
correlation_id is dropped silently (cancellation is disabled for
the request).
Scope: per MCPRackApp instance. Cancellation does NOT span multiple mount points within a process, nor multiple processes in a clustered deployment.
Binds an MCP session id to the principal that established it, so a listening stream (the server→client notification channel) can only be attached by the same principal — closing the cross-session hijack where any authenticated caller who knows/guesses another session's id could subscribe to its notifications or evict its listener via overwrite.
Trust model and limitations (mirrored in the docs):
- Initialize-bound vs TOFU. A session established through an
initializePOST is bound to that caller's principal authoritatively. A session id that was never seen byinitialize(the decouplednotifications:bus, where app code pushes to arbitrary ids) is claimed trust-on-first-use by whoever attaches a listener first; subsequent attaches by a different principal are refused. TOFU is strictly better than the prior bearer model (eviction-after-claim is closed) but a first-mover attacker can still claim an unused id — so notification-bus ids should be high-entropy. - Per-instance / single-process, exactly like CancellationRegistry: it does not span Puma workers or survive restart. In a cluster the GET stream and the initialize POST may land on different workers, so the initialize-binding degrades to TOFU there.
- Principal fidelity depends on the factory. The fingerprint is
derived from the agent the factory builds (session_token → acl_user →
acl_role), or an operator-supplied
principal_resolver. A master-key-everywhere factory yields one shared "mk" principal, so owner-binding is a no-op unless aprincipal_resolver(or per-user impersonation) supplies a real identity.
LRU-bounded so an initialize-without-DELETE stream of sessions can't grow it without limit; evicting an active owner just downgrades it to TOFU on the next attach.
Constant Summary collapse
- DEFAULT_MAX_ENTRIES =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
10_000
Instance Method Summary collapse
-
#authorize_attach(session_id, fingerprint) ⇒ Object
private
Authorize a listening-stream attach.
-
#bind(session_id, fingerprint) ⇒ Object
private
Authoritatively bind a session to a principal (initialize).
-
#forget(session_id) ⇒ Object
private
Drop a session's owner binding (explicit DELETE termination).
-
#initialize(max_entries: DEFAULT_MAX_ENTRIES) ⇒ SessionOwnerRegistry
constructor
private
A new instance of SessionOwnerRegistry.
-
#size ⇒ Integer
private
Current number of bound sessions (tests/metrics).
Constructor Details
#initialize(max_entries: DEFAULT_MAX_ENTRIES) ⇒ SessionOwnerRegistry
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns a new instance of SessionOwnerRegistry.
1617 1618 1619 1620 1621 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1617 def initialize(max_entries: DEFAULT_MAX_ENTRIES) @owners = {} # session_id => principal fingerprint (insertion-ordered for LRU) @max = max_entries @mutex = Mutex.new end |
Instance Method Details
#authorize_attach(session_id, fingerprint) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Authorize a listening-stream attach. Returns true when the session is unclaimed (claims it TOFU for this principal) or already owned by this principal (refreshing its LRU position); false on a principal mismatch. Blank inputs fail closed.
1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1638 def (session_id, fingerprint) return false if blank?(session_id) || blank?(fingerprint) @mutex.synchronize do owner = @owners[session_id] if owner.nil? @owners[session_id] = fingerprint evict_lru! true elsif owner == fingerprint @owners.delete(session_id) @owners[session_id] = owner true else false end end end |
#bind(session_id, fingerprint) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Authoritatively bind a session to a principal (initialize). A re-initialize by the same caller refreshes the binding.
1625 1626 1627 1628 1629 1630 1631 1632 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1625 def bind(session_id, fingerprint) return if blank?(session_id) || blank?(fingerprint) @mutex.synchronize do @owners.delete(session_id) @owners[session_id] = fingerprint evict_lru! end end |
#forget(session_id) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Drop a session's owner binding (explicit DELETE termination). Not called on mere stream close, so a reconnecting owner keeps its claim and an attacker can't grab the id during a brief disconnect.
1659 1660 1661 1662 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1659 def forget(session_id) return if blank?(session_id) @mutex.synchronize { @owners.delete(session_id) } end |
#size ⇒ Integer
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns current number of bound sessions (tests/metrics).
1665 1666 1667 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1665 def size @mutex.synchronize { @owners.size } end |