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) ⇒ Boolean
private
Bind a session to a principal at 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.
-
#owned_by?(session_id, fingerprint) ⇒ Boolean
private
True when
session_idis bound to exactly this principal. -
#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.
1934 1935 1936 1937 1938 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1934 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.
1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1965 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) ⇒ Boolean
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.
Bind a session to a principal at initialize. An unclaimed session is claimed; a re-initialize by the owning principal refreshes the binding. A session already owned by a different principal is NOT rebound, so knowing another caller's session id is not enough to take it over (and with it, its log level, elicitation capability, and listening stream).
1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1949 def bind(session_id, fingerprint) return false if blank?(session_id) || blank?(fingerprint) @mutex.synchronize do owner = @owners[session_id] return false if owner && owner != fingerprint @owners.delete(session_id) @owners[session_id] = fingerprint evict_lru! true 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.
1993 1994 1995 1996 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1993 def forget(session_id) return if blank?(session_id) @mutex.synchronize { @owners.delete(session_id) } end |
#owned_by?(session_id, fingerprint) ⇒ Boolean
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.
True when session_id is bound to exactly this principal. Never
claims an unbound session (unlike #authorize_attach).
1985 1986 1987 1988 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1985 def owned_by?(session_id, fingerprint) return false if blank?(session_id) || blank?(fingerprint) @mutex.synchronize { @owners[session_id] == fingerprint } 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).
1999 2000 2001 |
# File 'lib/parse/agent/mcp_rack_app.rb', line 1999 def size @mutex.synchronize { @owners.size } end |