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.



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

Returns:

  • (Boolean) —

    true when bound to fingerprint; false on a principal mismatch or blank input.



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

Returns:

  • (Boolean)


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

Returns:

  • (Integer) —

    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