parse-stack-next 5.6.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +409 -0
  3. data/README.md +378 -6
  4. data/docs/caching.md +748 -0
  5. data/examples/basic_client.rb +3 -3
  6. data/examples/basic_server.rb +3 -3
  7. data/examples/live_query_listener.rb +12 -12
  8. data/examples/rag_chatbot.rb +14 -14
  9. data/examples/transaction_example.rb +44 -45
  10. data/examples/webhook_server.rb +3 -3
  11. data/lib/parse/access.rb +493 -0
  12. data/lib/parse/acl_scope.rb +125 -52
  13. data/lib/parse/agent/approval_gate.rb +0 -0
  14. data/lib/parse/agent/cancellation_token.rb +3 -3
  15. data/lib/parse/agent/constraint_translator.rb +4 -4
  16. data/lib/parse/agent/describe.rb +34 -34
  17. data/lib/parse/agent/errors.rb +9 -9
  18. data/lib/parse/agent/mcp_client.rb +61 -59
  19. data/lib/parse/agent/mcp_dispatcher.rb +89 -101
  20. data/lib/parse/agent/mcp_rack_app.rb +92 -93
  21. data/lib/parse/agent/mcp_server.rb +1 -1
  22. data/lib/parse/agent/mcp_subscriptions.rb +22 -22
  23. data/lib/parse/agent/metadata_audit.rb +1 -2
  24. data/lib/parse/agent/metadata_dsl.rb +7 -7
  25. data/lib/parse/agent/metadata_registry.rb +12 -12
  26. data/lib/parse/agent/prompt_hardening.rb +4 -4
  27. data/lib/parse/agent/prompts.rb +10 -10
  28. data/lib/parse/agent/result_formatter.rb +4 -4
  29. data/lib/parse/agent/tools.rb +493 -400
  30. data/lib/parse/agent.rb +157 -116
  31. data/lib/parse/api/hooks.rb +1 -1
  32. data/lib/parse/api/server.rb +2 -2
  33. data/lib/parse/api/users.rb +2 -2
  34. data/lib/parse/atlas_search/index_manager.rb +1 -1
  35. data/lib/parse/atlas_search/session.rb +40 -218
  36. data/lib/parse/atlas_search.rb +181 -60
  37. data/lib/parse/authorization.rb +466 -0
  38. data/lib/parse/cache/invalidation.rb +167 -0
  39. data/lib/parse/cache/keyspace.rb +306 -0
  40. data/lib/parse/cache/moneta_surface.rb +126 -0
  41. data/lib/parse/cache/pool.rb +48 -5
  42. data/lib/parse/cache/redis.rb +425 -10
  43. data/lib/parse/cache/scoped_view.rb +443 -0
  44. data/lib/parse/cache/sub_cache.rb +264 -0
  45. data/lib/parse/cache/upstream_roles.rb +230 -0
  46. data/lib/parse/client/authentication.rb +1 -1
  47. data/lib/parse/client/body_builder.rb +1 -2
  48. data/lib/parse/client/caching.rb +84 -5
  49. data/lib/parse/client.rb +327 -189
  50. data/lib/parse/clp_scope.rb +225 -28
  51. data/lib/parse/console.rb +3 -3
  52. data/lib/parse/embeddings/batch_embedder.rb +3 -3
  53. data/lib/parse/embeddings/cache.rb +17 -17
  54. data/lib/parse/embeddings/cohere.rb +21 -22
  55. data/lib/parse/embeddings/image_fetch.rb +22 -21
  56. data/lib/parse/embeddings/jina.rb +21 -21
  57. data/lib/parse/embeddings/local_http.rb +6 -7
  58. data/lib/parse/embeddings/media_file.rb +5 -4
  59. data/lib/parse/embeddings/openai.rb +5 -5
  60. data/lib/parse/embeddings/provider.rb +1 -1
  61. data/lib/parse/embeddings/qwen.rb +10 -10
  62. data/lib/parse/embeddings/spend_cap.rb +2 -3
  63. data/lib/parse/embeddings/streaming_body.rb +11 -11
  64. data/lib/parse/embeddings/video_source.rb +7 -6
  65. data/lib/parse/embeddings/voyage.rb +59 -61
  66. data/lib/parse/embeddings.rb +28 -27
  67. data/lib/parse/graphql/type_generator.rb +1 -1
  68. data/lib/parse/graphql.rb +5 -5
  69. data/lib/parse/live_query/client.rb +1 -1
  70. data/lib/parse/live_query.rb +1 -1
  71. data/lib/parse/lock.rb +15 -18
  72. data/lib/parse/lock_backend.rb +1 -1
  73. data/lib/parse/lookup_rewriter.rb +6 -6
  74. data/lib/parse/model/classes/role.rb +227 -28
  75. data/lib/parse/model/classes/user.rb +60 -1
  76. data/lib/parse/model/clp.rb +4 -4
  77. data/lib/parse/model/core/actions.rb +97 -15
  78. data/lib/parse/model/core/create_lock.rb +0 -2
  79. data/lib/parse/model/core/describe.rb +57 -57
  80. data/lib/parse/model/core/embed_managed.rb +21 -23
  81. data/lib/parse/model/core/fetching.rb +2 -0
  82. data/lib/parse/model/core/indexing.rb +14 -14
  83. data/lib/parse/model/core/parse_reference.rb +1 -1
  84. data/lib/parse/model/core/properties.rb +2 -2
  85. data/lib/parse/model/core/querying.rb +1 -1
  86. data/lib/parse/model/core/schema.rb +2 -2
  87. data/lib/parse/model/core/search_indexing.rb +2 -2
  88. data/lib/parse/model/core/vector_searchable.rb +4 -5
  89. data/lib/parse/model/file.rb +21 -15
  90. data/lib/parse/model/geojson.rb +2 -2
  91. data/lib/parse/model/geopoint.rb +1 -0
  92. data/lib/parse/model/object.rb +132 -66
  93. data/lib/parse/model/pointer.rb +2 -0
  94. data/lib/parse/model/polygon.rb +3 -6
  95. data/lib/parse/model/push.rb +2 -2
  96. data/lib/parse/model/vector.rb +3 -1
  97. data/lib/parse/mongodb.rb +512 -282
  98. data/lib/parse/pipeline_security.rb +3 -0
  99. data/lib/parse/query/constraints.rb +46 -46
  100. data/lib/parse/query.rb +146 -59
  101. data/lib/parse/retrieval/agent_tool.rb +34 -20
  102. data/lib/parse/retrieval/chunk.rb +1 -0
  103. data/lib/parse/retrieval/reranker/cohere.rb +11 -11
  104. data/lib/parse/retrieval/reranker.rb +3 -4
  105. data/lib/parse/retrieval/retriever.rb +4 -6
  106. data/lib/parse/schema/index_migrator.rb +26 -26
  107. data/lib/parse/schema/search_index_migrator.rb +19 -19
  108. data/lib/parse/stack/tasks.rb +6 -6
  109. data/lib/parse/stack/version.rb +1 -1
  110. data/lib/parse/stack.rb +10 -10
  111. data/lib/parse/vector_search/hybrid.rb +66 -41
  112. data/lib/parse/vector_search.rb +14 -11
  113. data/lib/parse/webhooks/payload.rb +1 -0
  114. data/lib/parse/webhooks/registration.rb +4 -4
  115. data/lib/parse/webhooks/trigger_audit.rb +50 -49
  116. data/lib/parse/webhooks.rb +22 -4
  117. data/parse-stack-next.gemspec +6 -6
  118. metadata +10 -1
@@ -137,9 +137,9 @@ module Parse
137
137
  # SSE response headers. X-Accel-Buffering disables Nginx proxy buffering.
138
138
  # Frozen template — call {#sse_headers} to obtain a per-response copy.
139
139
  SSE_HEADERS = {
140
- "Content-Type" => "text/event-stream",
141
- "Cache-Control" => "no-cache",
142
- "Connection" => "keep-alive",
140
+ "Content-Type" => "text/event-stream",
141
+ "Cache-Control" => "no-cache",
142
+ "Connection" => "keep-alive",
143
143
  "X-Accel-Buffering" => "no",
144
144
  }.freeze
145
145
 
@@ -356,28 +356,28 @@ module Parse
356
356
  "notification stream; do not also pass streaming:/notifications: " \
357
357
  "(resource_subscriptions: may still be combined to upgrade the bus to LiveQuery)"
358
358
  end
359
- streaming = true
359
+ streaming = true
360
360
  notifications = true
361
361
  end
362
362
  # Collapse the nil sentinel to the historical default for the
363
363
  # remainder of the constructor (and @streaming below).
364
- streaming = false if streaming.nil?
364
+ streaming = false if streaming.nil?
365
365
  notifications = false if notifications.nil?
366
366
 
367
- @agent_factory = agent_factory || block
368
- @max_body_size = max_body_size
369
- @logger = logger
370
- @streaming = streaming
371
- @heartbeat_interval = heartbeat_interval
367
+ @agent_factory = agent_factory || block
368
+ @max_body_size = max_body_size
369
+ @logger = logger
370
+ @streaming = streaming
371
+ @heartbeat_interval = heartbeat_interval
372
372
  # The dispatcher cap defaults to the finite DEFAULT_MAX_CONCURRENT_DISPATCHERS
373
373
  # (set in the signature). An explicit positive Integer overrides it; an
374
374
  # explicit nil knowingly opts into the unbounded surface; anything else
375
375
  # is a config error and raises.
376
376
  validate_max_concurrent_dispatchers!(max_concurrent_dispatchers)
377
377
  @max_concurrent_dispatchers = max_concurrent_dispatchers
378
- @pre_auth_rate_limiter = pre_auth_rate_limiter
379
- @allowed_origins = normalize_allowed_origins(allowed_origins)
380
- @required_custom_header = normalize_required_custom_header(require_custom_header)
378
+ @pre_auth_rate_limiter = pre_auth_rate_limiter
379
+ @allowed_origins = normalize_allowed_origins(allowed_origins)
380
+ @required_custom_header = normalize_required_custom_header(require_custom_header)
381
381
  # NEW-9: when no explicit allowed_origins / require_custom_header CSRF
382
382
  # gate is configured but the server was started on an unauthenticated
383
383
  # loopback bind, default to a loopback-only Origin policy. A browser
@@ -387,15 +387,15 @@ module Parse
387
387
  # SDK-to-SDK) send NO Origin and stay allowed, and a legitimate local
388
388
  # browser UI sends a loopback Origin and is allowed. Ignored when an
389
389
  # explicit allowlist is configured (operator owns the policy then).
390
- @loopback_csrf_default = loopback_csrf_default && @allowed_origins.nil?
391
- @health_path = health_path.is_a?(String) && !health_path.empty? ? health_path : nil
390
+ @loopback_csrf_default = loopback_csrf_default && @allowed_origins.nil?
391
+ @health_path = health_path.is_a?(String) && !health_path.empty? ? health_path : nil
392
392
  # Per-app registry of in-flight cancellable requests. Keyed by
393
393
  # [correlation_id, request_id]. A `notifications/cancelled` POST
394
394
  # whose `params.requestId` matches an entry trips the registered
395
395
  # CancellationToken. Scoped per-instance, not per-process: this
396
396
  # registry does not span multiple MCPRackApp mount points within
397
397
  # a process, nor multiple processes in a clustered deployment.
398
- @cancellation_registry = CancellationRegistry.new
398
+ @cancellation_registry = CancellationRegistry.new
399
399
 
400
400
  # Elicitation (human-in-the-loop approval) state, shared across
401
401
  # this app's requests and its GET listening streams. The
@@ -404,18 +404,18 @@ module Parse
404
404
  # holds server→client requests awaiting a reply. Both are cheap
405
405
  # and always present; they only do work when
406
406
  # Parse::Agent.require_approval_for opts a tier in.
407
- @elicitation_capabilities = Parse::Agent::ClientCapabilityRegistry.new
408
- @pending_elicitations = Parse::Agent::PendingElicitationRegistry.new
409
- @approval_timeout = approval_timeout
407
+ @elicitation_capabilities = Parse::Agent::ClientCapabilityRegistry.new
408
+ @pending_elicitations = Parse::Agent::PendingElicitationRegistry.new
409
+ @approval_timeout = approval_timeout
410
410
 
411
411
  # Binds each MCP session id to the principal that established it so a
412
412
  # listening stream can't be hijacked by another authenticated caller.
413
413
  # Same per-instance / single-process scope as @cancellation_registry.
414
- @session_owners = SessionOwnerRegistry.new
414
+ @session_owners = SessionOwnerRegistry.new
415
415
  if principal_resolver && !principal_resolver.respond_to?(:call)
416
416
  raise ArgumentError, "principal_resolver must respond to #call"
417
417
  end
418
- @principal_resolver = principal_resolver
418
+ @principal_resolver = principal_resolver
419
419
 
420
420
  # Listening-stream coordinator (the server→client broadcast bus
421
421
  # backing resource subscriptions, MCP elicitation, and
@@ -431,8 +431,7 @@ module Parse
431
431
  # decoupling lever — a server can push arbitrary notifications
432
432
  # without enabling LiveQuery resource subscriptions.
433
433
  # nil disables the GET listening stream entirely.
434
- @subscription_manager =
435
- if subscription_manager
434
+ @subscription_manager = if subscription_manager
436
435
  subscription_manager
437
436
  elsif resource_subscriptions
438
437
  Parse::Agent::MCPSubscriptions::Manager.new(logger: @logger)
@@ -922,12 +921,12 @@ module Parse
922
921
  return nil unless agent.respond_to?(:correlation_id)
923
922
  mgr = @subscription_manager
924
923
  Parse::Agent::MCPElicitationGate.new(
925
- correlation_id: agent.correlation_id,
926
- pending: @pending_elicitations,
927
- publish: ->(cid, req) { mgr ? !!mgr.publish(cid, req) : false },
924
+ correlation_id: agent.correlation_id,
925
+ pending: @pending_elicitations,
926
+ publish: ->(cid, req) { mgr ? !!mgr.publish(cid, req) : false },
928
927
  capability_check: ->(cid) { @elicitation_capabilities.get(cid) },
929
- listener_check: ->(cid) { mgr ? mgr.listener?(cid) : false },
930
- timeout: @approval_timeout,
928
+ listener_check: ->(cid) { mgr ? mgr.listener?(cid) : false },
929
+ timeout: @approval_timeout,
931
930
  )
932
931
  end
933
932
 
@@ -983,9 +982,9 @@ module Parse
983
982
  end
984
983
 
985
984
  progress_token = body.dig("params", "_meta", "progressToken") || SecureRandom.uuid
986
- req_id = body["id"]
987
- interval = @heartbeat_interval
988
- logger = @logger
985
+ req_id = body["id"]
986
+ interval = @heartbeat_interval
987
+ logger = @logger
989
988
 
990
989
  # Register a cancellation token in the per-app registry so a
991
990
  # subsequent notifications/cancelled with a matching
@@ -999,9 +998,9 @@ module Parse
999
998
  # (correlation_id, request_id) key cannot have its token evicted
1000
999
  # when this request closes.
1001
1000
  cancellation_token = Parse::Agent::CancellationToken.new
1002
- correlation_id = agent.respond_to?(:correlation_id) ? agent.correlation_id : nil
1003
- registry_entry_id = @cancellation_registry.register(correlation_id, req_id, cancellation_token)
1004
- registry = @cancellation_registry
1001
+ correlation_id = agent.respond_to?(:correlation_id) ? agent.correlation_id : nil
1002
+ registry_entry_id = @cancellation_registry.register(correlation_id, req_id, cancellation_token)
1003
+ registry = @cancellation_registry
1005
1004
 
1006
1005
  # The block receives the SSEBody's progress_callback so tools can
1007
1006
  # emit `notifications/progress` events through it. The callback is
@@ -1013,13 +1012,13 @@ module Parse
1013
1012
  on_close: -> { registry.deregister(correlation_id, req_id, registry_entry_id) if registry_entry_id },
1014
1013
  ) do |progress_callback|
1015
1014
  Parse::Agent::MCPDispatcher.call(
1016
- body: body,
1017
- agent: agent,
1018
- logger: logger,
1019
- progress_callback: progress_callback,
1020
- cancellation_token: cancellation_token,
1015
+ body: body,
1016
+ agent: agent,
1017
+ logger: logger,
1018
+ progress_callback: progress_callback,
1019
+ cancellation_token: cancellation_token,
1021
1020
  subscription_manager: @subscription_manager,
1022
- approval_gate: build_approval_gate(agent),
1021
+ approval_gate: build_approval_gate(agent),
1023
1022
  )
1024
1023
  end
1025
1024
 
@@ -1236,54 +1235,54 @@ module Parse
1236
1235
  def initialize(progress_token, req_id, interval, logger,
1237
1236
  cancellation_token: nil, on_close: nil,
1238
1237
  heartbeat_waiter: nil, &dispatcher_blk)
1239
- @progress_token = progress_token
1238
+ @progress_token = progress_token
1240
1239
  # Heartbeats use a dedicated server-generated progressToken so
1241
1240
  # the elapsed-seconds scale of heartbeats never appears on the
1242
1241
  # same MCP progressToken as work-unit values reported by tools.
1243
1242
  # The MCP spec requires `progress` to increase monotonically
1244
1243
  # per progressToken; mixing the two scales would violate it
1245
1244
  # at the boundary where a tool first reports.
1246
- @heartbeat_token = "parse-stack:heartbeat:#{SecureRandom.uuid}"
1247
- @req_id = req_id
1248
- @interval = interval
1249
- @logger = logger
1250
- @dispatcher_blk = dispatcher_blk
1251
- @cancellation_token = cancellation_token
1252
- @on_close = on_close
1253
- @heartbeat_waiter = heartbeat_waiter ||
1254
- Thread.current[:parse_mcp_sse_heartbeat_waiter] ||
1255
- ->(t, i) { t.join(i) }
1256
- @queue = Queue.new
1257
- @worker = nil
1245
+ @heartbeat_token = "parse-stack:heartbeat:#{SecureRandom.uuid}"
1246
+ @req_id = req_id
1247
+ @interval = interval
1248
+ @logger = logger
1249
+ @dispatcher_blk = dispatcher_blk
1250
+ @cancellation_token = cancellation_token
1251
+ @on_close = on_close
1252
+ @heartbeat_waiter = heartbeat_waiter ||
1253
+ Thread.current[:parse_mcp_sse_heartbeat_waiter] ||
1254
+ ->(t, i) { t.join(i) }
1255
+ @queue = Queue.new
1256
+ @worker = nil
1258
1257
  # The dispatcher thread spawned inside @worker. Published under
1259
1258
  # @close_mutex once started so {#close} can snapshot its liveness for
1260
1259
  # the abandonment signal. Never force-killed (see #close).
1261
- @dispatcher_thread = nil
1260
+ @dispatcher_thread = nil
1262
1261
  # Flipped to true by #each when the DONE sentinel is consumed.
1263
1262
  # #close uses this to decide whether to trip the cancellation
1264
1263
  # token (false = client disconnect) or skip the trip (true =
1265
1264
  # the request finished on its own). Reads and writes happen
1266
1265
  # under @close_mutex below.
1267
- @completed_normally = false
1266
+ @completed_normally = false
1268
1267
  # Volatile flag flipped by the progress_callback the first time a
1269
1268
  # tool reports. Heartbeats now use a separate progressToken so
1270
1269
  # the flag is no longer a spec-correctness gate, but we keep
1271
1270
  # it as a small bandwidth optimization — once a tool is
1272
1271
  # actively reporting, time-based heartbeats are noise.
1273
1272
  @tool_progress_reported = false
1274
- @progress_callback = build_progress_callback
1273
+ @progress_callback = build_progress_callback
1275
1274
  # Deregistration callbacks for the Tools/Prompts subscribe
1276
1275
  # bindings. Set when the worker starts (so a request that is
1277
1276
  # never driven via #each does not register a stale entry) and
1278
1277
  # cleared in #close.
1279
- @unsubscribe_tools = nil
1280
- @unsubscribe_prompts = nil
1278
+ @unsubscribe_tools = nil
1279
+ @unsubscribe_prompts = nil
1281
1280
  # Guards concurrent invocations of #close. Rack servers
1282
1281
  # sometimes call close from both the I/O fiber's ensure and a
1283
1282
  # separate disconnect-handler thread; without a mutex the
1284
1283
  # subscriber-deregister and on_close paths can run twice.
1285
- @close_mutex = Mutex.new
1286
- @closed = false
1284
+ @close_mutex = Mutex.new
1285
+ @closed = false
1287
1286
  end
1288
1287
 
1289
1288
  # Rack body interface — called once by the Rack server.
@@ -1353,12 +1352,12 @@ module Parse
1353
1352
  # a disconnect-handler thread short-circuit after the first
1354
1353
  # caller wins the mutex.
1355
1354
  completed_normally = nil
1356
- dispatcher_alive = false
1355
+ dispatcher_alive = false
1357
1356
  @close_mutex.synchronize do
1358
1357
  return if @closed
1359
1358
  @closed = true
1360
1359
  completed_normally = @completed_normally
1361
- dispatcher_alive = @dispatcher_thread&.alive? || false
1360
+ dispatcher_alive = @dispatcher_thread&.alive? || false
1362
1361
  end
1363
1362
  unless completed_normally
1364
1363
  @cancellation_token&.cancel!(reason: :client_disconnect)
@@ -1380,7 +1379,7 @@ module Parse
1380
1379
  warn line
1381
1380
  end
1382
1381
  ensure
1383
- @unsubscribe_tools = nil
1382
+ @unsubscribe_tools = nil
1384
1383
  @unsubscribe_prompts = nil
1385
1384
  end
1386
1385
  if @on_close
@@ -1421,9 +1420,9 @@ module Parse
1421
1420
  return unless defined?(ActiveSupport::Notifications)
1422
1421
  ActiveSupport::Notifications.instrument(
1423
1422
  "parse.agent.mcp_dispatcher_abandoned",
1424
- reason: :client_disconnect,
1425
- dispatcher_alive: dispatcher_alive,
1426
- request_id: @req_id,
1423
+ reason: :client_disconnect,
1424
+ dispatcher_alive: dispatcher_alive,
1425
+ request_id: @req_id,
1427
1426
  )
1428
1427
  rescue StandardError => e
1429
1428
  line = "[Parse::Agent::MCPRackApp::SSEBody] abandonment-record error: #{e.class}: #{e.message}"
@@ -1446,7 +1445,7 @@ module Parse
1446
1445
  @worker = Thread.new do
1447
1446
  Thread.current[:parse_mcp_sse_worker] = true
1448
1447
  started_at = Time.now
1449
- result = nil
1448
+ result = nil
1450
1449
 
1451
1450
  begin
1452
1451
  # Run the dispatcher in the background. Meanwhile emit heartbeats
@@ -1565,10 +1564,10 @@ module Parse
1565
1564
  def build_progress_event(elapsed)
1566
1565
  data = JSON.generate({
1567
1566
  "jsonrpc" => "2.0",
1568
- "method" => "notifications/progress",
1569
- "params" => {
1567
+ "method" => "notifications/progress",
1568
+ "params" => {
1570
1569
  "progressToken" => @heartbeat_token,
1571
- "progress" => elapsed,
1570
+ "progress" => elapsed,
1572
1571
  },
1573
1572
  })
1574
1573
  "event: message\ndata: #{data}\n\n"
@@ -1585,7 +1584,7 @@ module Parse
1585
1584
  def build_list_changed_event(method)
1586
1585
  data = JSON.generate({
1587
1586
  "jsonrpc" => "2.0",
1588
- "method" => method,
1587
+ "method" => method,
1589
1588
  })
1590
1589
  "event: message\ndata: #{data}\n\n"
1591
1590
  end
@@ -1603,14 +1602,14 @@ module Parse
1603
1602
  def build_tool_progress_event(progress, total, message)
1604
1603
  params = {
1605
1604
  "progressToken" => @progress_token,
1606
- "progress" => progress,
1605
+ "progress" => progress,
1607
1606
  }
1608
- params["total"] = total unless total.nil?
1607
+ params["total"] = total unless total.nil?
1609
1608
  params["message"] = message if message
1610
1609
  data = JSON.generate({
1611
1610
  "jsonrpc" => "2.0",
1612
- "method" => "notifications/progress",
1613
- "params" => params,
1611
+ "method" => "notifications/progress",
1612
+ "params" => params,
1614
1613
  })
1615
1614
  "event: message\ndata: #{data}\n\n"
1616
1615
  end
@@ -1661,8 +1660,8 @@ module Parse
1661
1660
  def build_error_envelope(error)
1662
1661
  {
1663
1662
  "jsonrpc" => "2.0",
1664
- "id" => @req_id,
1665
- "error" => { "code" => -32_603, "message" => "Internal error" },
1663
+ "id" => @req_id,
1664
+ "error" => { "code" => -32_603, "message" => "Internal error" },
1666
1665
  }
1667
1666
  end
1668
1667
  end
@@ -1701,15 +1700,15 @@ module Parse
1701
1700
  # seconds; `<= 0` disables heartbeats.
1702
1701
  # @param logger [#warn, nil]
1703
1702
  def initialize(manager, session_id, heartbeat_interval, logger)
1704
- @manager = manager
1705
- @session_id = session_id
1703
+ @manager = manager
1704
+ @session_id = session_id
1706
1705
  @heartbeat_interval = heartbeat_interval
1707
- @logger = logger
1708
- @queue = Queue.new
1709
- @heartbeat = nil
1710
- @closed = false
1711
- @counted = false
1712
- @close_mutex = Mutex.new
1706
+ @logger = logger
1707
+ @queue = Queue.new
1708
+ @heartbeat = nil
1709
+ @closed = false
1710
+ @counted = false
1711
+ @close_mutex = Mutex.new
1713
1712
  end
1714
1713
 
1715
1714
  # Rack body interface — called once by the Rack server.
@@ -1763,7 +1762,7 @@ module Parse
1763
1762
 
1764
1763
  def start_heartbeat
1765
1764
  return unless @heartbeat_interval && @heartbeat_interval > 0
1766
- queue = @queue
1765
+ queue = @queue
1767
1766
  interval = @heartbeat_interval
1768
1767
  @heartbeat = Thread.new do
1769
1768
  loop do
@@ -1848,8 +1847,8 @@ module Parse
1848
1847
 
1849
1848
  def initialize(max_entries: DEFAULT_MAX_ENTRIES)
1850
1849
  @owners = {} # session_id => principal fingerprint (insertion-ordered for LRU)
1851
- @max = max_entries
1852
- @mutex = Mutex.new
1850
+ @max = max_entries
1851
+ @mutex = Mutex.new
1853
1852
  end
1854
1853
 
1855
1854
  # Authoritatively bind a session to a principal (initialize). A
@@ -1913,7 +1912,7 @@ module Parse
1913
1912
  class CancellationRegistry
1914
1913
  def initialize
1915
1914
  @entries = {}
1916
- @mutex = Mutex.new
1915
+ @mutex = Mutex.new
1917
1916
  end
1918
1917
 
1919
1918
  # Register a cancellation token for the given session and
@@ -2167,10 +2166,10 @@ module Parse
2167
2166
  # is refused, while a real local UI on http://localhost:<port> passes.
2168
2167
  def origin_is_loopback?(origin)
2169
2168
  host = begin
2170
- URI.parse(origin).host
2171
- rescue URI::InvalidURIError, StandardError
2172
- nil
2173
- end
2169
+ URI.parse(origin).host
2170
+ rescue URI::InvalidURIError, StandardError
2171
+ nil
2172
+ end
2174
2173
  return false if host.nil?
2175
2174
  host = host.downcase.delete_prefix("[").delete_suffix("]") # unwrap IPv6 brackets
2176
2175
  host == "localhost" || host == "127.0.0.1" || host == "::1"
@@ -132,7 +132,7 @@ module Parse
132
132
  raise ArgumentError,
133
133
  "MCPServer refuses to bind non-loopback host #{host.inspect} without an api_key. " \
134
134
  "Set MCP_API_KEY in the environment, pass api_key: explicitly, or use a loopback " \
135
- "host (one of: #{LOOPBACK_HOSTS.join(', ')})."
135
+ "host (one of: #{LOOPBACK_HOSTS.join(", ")})."
136
136
  end
137
137
 
138
138
  @port = port
@@ -101,10 +101,10 @@ module Parse
101
101
  "Invalid resource URI: #{uri}. Expected parse://<Class>/{count|samples}."
102
102
  end
103
103
  class_name = match[1]
104
- kind = match[2]
104
+ kind = match[2]
105
105
  unless SUBSCRIBABLE_KINDS.include?(kind)
106
106
  raise Parse::Agent::ValidationError,
107
- "Resource kind '#{kind}' is not subscribable — only #{SUBSCRIBABLE_KINDS.join(' and ')} " \
107
+ "Resource kind '#{kind}' is not subscribable — only #{SUBSCRIBABLE_KINDS.join(" and ")} " \
108
108
  "are backed by LiveQuery. Schema changes are not LiveQuery events."
109
109
  end
110
110
  [class_name, kind]
@@ -184,7 +184,7 @@ module Parse
184
184
  class LocalNotifier
185
185
  def initialize
186
186
  @listeners = {}
187
- @mutex = Mutex.new
187
+ @mutex = Mutex.new
188
188
  end
189
189
 
190
190
  # @yieldparam notification_hash [Hash] the JSON-RPC notification.
@@ -234,11 +234,11 @@ module Parse
234
234
  # one-shot emit. Default spawns a thread.
235
235
  # @yield the emit action invoked once per coalesced burst.
236
236
  def initialize(interval:, timer: nil, &emit)
237
- @interval = interval
238
- @emit = emit
239
- @timer = timer || method(:default_timer)
240
- @armed = false
241
- @mutex = Mutex.new
237
+ @interval = interval
238
+ @emit = emit
239
+ @timer = timer || method(:default_timer)
240
+ @armed = false
241
+ @mutex = Mutex.new
242
242
  end
243
243
 
244
244
  # Record an event; arm the timer if not already armed.
@@ -324,20 +324,20 @@ module Parse
324
324
  live_query_admin_client: nil, live_query_scoped_client: nil,
325
325
  max_subscriptions_per_session: DEFAULT_MAX_SUBSCRIPTIONS_PER_SESSION,
326
326
  max_sessions: DEFAULT_MAX_SESSIONS)
327
- @logger = logger
328
- @debounce_interval = debounce_interval
329
- @notifier = notifier || LocalNotifier.new
330
- @both_client = live_query_client
331
- @admin_client = live_query_admin_client
332
- @scoped_client = live_query_scoped_client
327
+ @logger = logger
328
+ @debounce_interval = debounce_interval
329
+ @notifier = notifier || LocalNotifier.new
330
+ @both_client = live_query_client
331
+ @admin_client = live_query_admin_client
332
+ @scoped_client = live_query_scoped_client
333
333
  @supported_override = supported
334
- @timer = timer
335
- @max_per_session = max_subscriptions_per_session
336
- @max_sessions = max_sessions
337
- @client_mutex = Mutex.new
334
+ @timer = timer
335
+ @max_per_session = max_subscriptions_per_session
336
+ @max_sessions = max_sessions
337
+ @client_mutex = Mutex.new
338
338
  # session_id => { uri => { sub:, debouncer: } }
339
- @sessions = Hash.new { |h, k| h[k] = {} }
340
- @mutex = Mutex.new
339
+ @sessions = Hash.new { |h, k| h[k] = {} }
340
+ @mutex = Mutex.new
341
341
  end
342
342
 
343
343
  attr_reader :notifier
@@ -583,8 +583,8 @@ module Parse
583
583
  def publish_update(session_id, uri)
584
584
  notification = {
585
585
  "jsonrpc" => "2.0",
586
- "method" => "notifications/resources/updated",
587
- "params" => { "uri" => uri },
586
+ "method" => "notifications/resources/updated",
587
+ "params" => { "uri" => uri },
588
588
  }
589
589
  @notifier.publish(session_id, notification)
590
590
  rescue StandardError => e
@@ -221,8 +221,7 @@ module Parse
221
221
  described = klass.property_descriptions.keys.map(&:to_sym).to_set
222
222
  declared_properties = klass.field_map.keys.map(&:to_sym)
223
223
 
224
- candidates =
225
- if klass.respond_to?(:agent_field_allowlist) && klass.agent_field_allowlist.any?
224
+ candidates = if klass.respond_to?(:agent_field_allowlist) && klass.agent_field_allowlist.any?
226
225
  klass.agent_field_allowlist.map(&:to_sym)
227
226
  else
228
227
  declared_properties
@@ -92,12 +92,12 @@ module Parse
92
92
  def agent_hidden(except: nil)
93
93
  @agent_hidden = true
94
94
  @agent_hidden_except = case except
95
- when nil then nil
96
- when :master_key, "master_key" then :master_key
97
- else
98
- raise ArgumentError,
99
- "agent_hidden(except:) accepts only :master_key (got #{except.inspect})"
100
- end
95
+ when nil then nil
96
+ when :master_key, "master_key" then :master_key
97
+ else
98
+ raise ArgumentError,
99
+ "agent_hidden(except:) accepts only :master_key (got #{except.inspect})"
100
+ end
101
101
  Parse::Agent::MetadataRegistry.register_hidden_class(self, except: @agent_hidden_except)
102
102
  true
103
103
  end
@@ -462,7 +462,7 @@ module Parse
462
462
  # stricter MCP clients can validate before dispatch.
463
463
  # @return [Hash] the method metadata
464
464
  def agent_method(method_name, description = nil, permission: :readonly,
465
- supports_dry_run: false, permitted_keys: nil, parameters: nil)
465
+ supports_dry_run: false, permitted_keys: nil, parameters: nil)
466
466
  method_sym = method_name.to_sym
467
467
 
468
468
  unless AGENT_METHOD_PERMISSIONS.include?(permission)
@@ -769,9 +769,9 @@ module Parse
769
769
  end
770
770
  return unless emit
771
771
  warn "[Parse::Agent:SECURITY] class '#{name}' is agent-visible but declares no " \
772
- "agent_tenant_scope while other classes do — queries against it are NOT " \
773
- "tenant-scoped and may return cross-tenant rows. Add agent_tenant_scope to " \
774
- "'#{name}', or confirm it is intentionally global."
772
+ "agent_tenant_scope while other classes do — queries against it are NOT " \
773
+ "tenant-scoped and may return cross-tenant rows. Add agent_tenant_scope to " \
774
+ "'#{name}', or confirm it is intentionally global."
775
775
  end
776
776
 
777
777
  # @!visibility private
@@ -820,10 +820,10 @@ module Parse
820
820
  # @return [Hash] enriched fields
821
821
  def enrich_fields(fields, klass)
822
822
  descriptions = klass.property_descriptions
823
- enums = klass.respond_to?(:property_enum_descriptions) ?
824
- klass.property_enum_descriptions : {}
823
+ enums = klass.respond_to?(:property_enum_descriptions) ?
824
+ klass.property_enum_descriptions : {}
825
825
  large_fields = klass.respond_to?(:agent_large_field_list) ? klass.agent_large_field_list : []
826
- large_set = large_fields.map(&:to_sym).to_set
826
+ large_set = large_fields.map(&:to_sym).to_set
827
827
 
828
828
  # Reverse field_map (wire symbol -> Ruby symbol) so descriptions
829
829
  # and enums declared on properties with an explicit `field:`
@@ -919,13 +919,13 @@ module Parse
919
919
  # environments where the LLM needs the full method contract.
920
920
  keys = Parse::Agent.agent_debug? ? info[:permitted_keys]&.map(&:to_s) : nil
921
921
  {
922
- name: name.to_s,
923
- type: info[:type]&.to_s || "unknown",
924
- permission: info[:permission]&.to_s || "readonly",
925
- description: info[:description],
922
+ name: name.to_s,
923
+ type: info[:type]&.to_s || "unknown",
924
+ permission: info[:permission]&.to_s || "readonly",
925
+ description: info[:description],
926
926
  supports_dry_run: info[:supports_dry_run] ? true : nil,
927
- permitted_keys: keys,
928
- parameters: info[:parameters],
927
+ permitted_keys: keys,
928
+ parameters: info[:parameters],
929
929
  }.compact
930
930
  end
931
931
  end
@@ -27,7 +27,7 @@ module Parse
27
27
  # Max characters retained from any LLM-surfaced description.
28
28
  DESCRIPTION_CAP = 200
29
29
 
30
- SCHEMA_DESC_OPEN = "<schema_description>"
30
+ SCHEMA_DESC_OPEN = "<schema_description>"
31
31
  SCHEMA_DESC_CLOSE = "</schema_description>"
32
32
 
33
33
  # C0 (0x00-0x1F except \t\n) + DEL + C1 (0x7F-0x9F) + zero-width
@@ -73,7 +73,7 @@ module Parse
73
73
  allowed.each do |v|
74
74
  next unless v.is_a?(Hash)
75
75
  v["description"] = sanitize_description(v["description"]) if v["description"].is_a?(String)
76
- v[:description] = sanitize_description(v[:description]) if v[:description].is_a?(String)
76
+ v[:description] = sanitize_description(v[:description]) if v[:description].is_a?(String)
77
77
  end
78
78
  end
79
79
  end
@@ -89,7 +89,7 @@ module Parse
89
89
  methods.each do |m|
90
90
  next unless m.is_a?(Hash)
91
91
  m["description"] = sanitize_description(m["description"]) if m["description"].is_a?(String)
92
- m[:description] = sanitize_description(m[:description]) if m[:description].is_a?(String)
92
+ m[:description] = sanitize_description(m[:description]) if m[:description].is_a?(String)
93
93
  sanitize_nested_descriptions!(m["parameters"] || m[:parameters])
94
94
  end
95
95
  end
@@ -202,7 +202,7 @@ module Parse
202
202
 
203
203
  def deep_dup(obj)
204
204
  case obj
205
- when Hash then obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
205
+ when Hash then obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
206
206
  when Array then obj.map { |e| deep_dup(e) }
207
207
  when String then obj.dup
208
208
  else obj