Class: Parse::Agent::MCPRackApp::SessionOwnerRegistry Private

Inherits:
Object
  • Object
show all
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 initialize POST is bound to that caller's principal authoritatively. A session id that was never seen by initialize (the decoupled notifications: 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 a principal_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

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 authorize_attach(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

#sizeInteger

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).

Returns:

  • (Integer)

    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