parse-stack-next 5.6.0 → 5.7.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +489 -0
- data/README.md +378 -6
- data/docs/caching.md +748 -0
- data/examples/basic_client.rb +3 -3
- data/examples/basic_server.rb +3 -3
- data/examples/live_query_listener.rb +12 -12
- data/examples/rag_chatbot.rb +14 -14
- data/examples/transaction_example.rb +44 -45
- data/examples/webhook_server.rb +3 -3
- data/lib/parse/access.rb +493 -0
- data/lib/parse/acl_scope.rb +125 -52
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/cancellation_token.rb +3 -3
- data/lib/parse/agent/constraint_translator.rb +4 -4
- data/lib/parse/agent/describe.rb +34 -34
- data/lib/parse/agent/errors.rb +9 -9
- data/lib/parse/agent/mcp_client.rb +61 -59
- data/lib/parse/agent/mcp_dispatcher.rb +89 -101
- data/lib/parse/agent/mcp_rack_app.rb +92 -93
- data/lib/parse/agent/mcp_server.rb +1 -1
- data/lib/parse/agent/mcp_subscriptions.rb +22 -22
- data/lib/parse/agent/metadata_audit.rb +1 -2
- data/lib/parse/agent/metadata_dsl.rb +7 -7
- data/lib/parse/agent/metadata_registry.rb +12 -12
- data/lib/parse/agent/prompt_hardening.rb +4 -4
- data/lib/parse/agent/prompts.rb +10 -10
- data/lib/parse/agent/result_formatter.rb +4 -4
- data/lib/parse/agent/tools.rb +493 -400
- data/lib/parse/agent.rb +157 -116
- data/lib/parse/api/hooks.rb +1 -1
- data/lib/parse/api/server.rb +2 -2
- data/lib/parse/api/users.rb +2 -2
- data/lib/parse/atlas_search/index_manager.rb +1 -1
- data/lib/parse/atlas_search/session.rb +40 -218
- data/lib/parse/atlas_search.rb +181 -60
- data/lib/parse/authorization.rb +466 -0
- data/lib/parse/cache/invalidation.rb +219 -0
- data/lib/parse/cache/keyspace.rb +306 -0
- data/lib/parse/cache/moneta_surface.rb +126 -0
- data/lib/parse/cache/pool.rb +48 -5
- data/lib/parse/cache/redis.rb +425 -10
- data/lib/parse/cache/scoped_view.rb +443 -0
- data/lib/parse/cache/sub_cache.rb +264 -0
- data/lib/parse/cache/upstream_roles.rb +230 -0
- data/lib/parse/client/authentication.rb +1 -1
- data/lib/parse/client/body_builder.rb +1 -2
- data/lib/parse/client/caching.rb +84 -5
- data/lib/parse/client.rb +327 -189
- data/lib/parse/clp_scope.rb +225 -28
- data/lib/parse/console.rb +3 -3
- data/lib/parse/embeddings/batch_embedder.rb +3 -3
- data/lib/parse/embeddings/cache.rb +17 -17
- data/lib/parse/embeddings/cohere.rb +21 -22
- data/lib/parse/embeddings/image_fetch.rb +22 -21
- data/lib/parse/embeddings/jina.rb +21 -21
- data/lib/parse/embeddings/local_http.rb +6 -7
- data/lib/parse/embeddings/media_file.rb +5 -4
- data/lib/parse/embeddings/openai.rb +5 -5
- data/lib/parse/embeddings/provider.rb +1 -1
- data/lib/parse/embeddings/qwen.rb +10 -10
- data/lib/parse/embeddings/spend_cap.rb +2 -3
- data/lib/parse/embeddings/streaming_body.rb +11 -11
- data/lib/parse/embeddings/video_source.rb +7 -6
- data/lib/parse/embeddings/voyage.rb +59 -61
- data/lib/parse/embeddings.rb +28 -27
- data/lib/parse/graphql/type_generator.rb +1 -1
- data/lib/parse/graphql.rb +5 -5
- data/lib/parse/live_query/client.rb +1 -1
- data/lib/parse/live_query.rb +1 -1
- data/lib/parse/lock.rb +15 -18
- data/lib/parse/lock_backend.rb +1 -1
- data/lib/parse/lookup_rewriter.rb +6 -6
- data/lib/parse/model/associations/belongs_to.rb +4 -0
- data/lib/parse/model/associations/collection_proxy.rb +3 -0
- data/lib/parse/model/associations/has_many.rb +4 -0
- data/lib/parse/model/classes/role.rb +227 -28
- data/lib/parse/model/classes/user.rb +60 -1
- data/lib/parse/model/clp.rb +4 -4
- data/lib/parse/model/core/actions.rb +366 -113
- data/lib/parse/model/core/create_lock.rb +0 -2
- data/lib/parse/model/core/describe.rb +57 -57
- data/lib/parse/model/core/embed_managed.rb +21 -23
- data/lib/parse/model/core/fetching.rb +6 -0
- data/lib/parse/model/core/indexing.rb +14 -14
- data/lib/parse/model/core/parse_reference.rb +1 -1
- data/lib/parse/model/core/properties.rb +6 -2
- data/lib/parse/model/core/querying.rb +1 -1
- data/lib/parse/model/core/schema.rb +2 -2
- data/lib/parse/model/core/search_indexing.rb +2 -2
- data/lib/parse/model/core/vector_searchable.rb +4 -5
- data/lib/parse/model/file.rb +21 -15
- data/lib/parse/model/geojson.rb +2 -2
- data/lib/parse/model/geopoint.rb +1 -0
- data/lib/parse/model/object.rb +136 -66
- data/lib/parse/model/pointer.rb +2 -0
- data/lib/parse/model/polygon.rb +3 -6
- data/lib/parse/model/push.rb +2 -2
- data/lib/parse/model/vector.rb +3 -1
- data/lib/parse/mongodb.rb +512 -282
- data/lib/parse/pipeline_security.rb +3 -0
- data/lib/parse/query/constraints.rb +46 -46
- data/lib/parse/query.rb +146 -59
- data/lib/parse/retrieval/agent_tool.rb +34 -20
- data/lib/parse/retrieval/chunk.rb +1 -0
- data/lib/parse/retrieval/reranker/cohere.rb +11 -11
- data/lib/parse/retrieval/reranker.rb +3 -4
- data/lib/parse/retrieval/retriever.rb +4 -6
- data/lib/parse/schema/index_migrator.rb +26 -26
- data/lib/parse/schema/search_index_migrator.rb +19 -19
- data/lib/parse/stack/tasks.rb +6 -6
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +10 -10
- data/lib/parse/vector_search/hybrid.rb +66 -41
- data/lib/parse/vector_search.rb +14 -11
- data/lib/parse/webhooks/payload.rb +1 -0
- data/lib/parse/webhooks/registration.rb +4 -4
- data/lib/parse/webhooks/trigger_audit.rb +50 -49
- data/lib/parse/webhooks.rb +120 -5
- data/parse-stack-next.gemspec +6 -6
- 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"
|
|
141
|
-
"Cache-Control"
|
|
142
|
-
"Connection"
|
|
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
|
|
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
|
|
364
|
+
streaming = false if streaming.nil?
|
|
365
365
|
notifications = false if notifications.nil?
|
|
366
366
|
|
|
367
|
-
@agent_factory
|
|
368
|
-
@max_body_size
|
|
369
|
-
@logger
|
|
370
|
-
@streaming
|
|
371
|
-
@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
|
|
379
|
-
@allowed_origins
|
|
380
|
-
@required_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
|
|
391
|
-
@health_path
|
|
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
|
|
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
|
|
408
|
-
@pending_elicitations
|
|
409
|
-
@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
|
|
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
|
|
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:
|
|
926
|
-
pending:
|
|
927
|
-
publish:
|
|
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:
|
|
930
|
-
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
|
|
987
|
-
interval
|
|
988
|
-
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
|
|
1003
|
-
registry_entry_id
|
|
1004
|
-
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:
|
|
1017
|
-
agent:
|
|
1018
|
-
logger:
|
|
1019
|
-
progress_callback:
|
|
1020
|
-
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:
|
|
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
|
|
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
|
|
1247
|
-
@req_id
|
|
1248
|
-
@interval
|
|
1249
|
-
@logger
|
|
1250
|
-
@dispatcher_blk
|
|
1251
|
-
@cancellation_token
|
|
1252
|
-
@on_close
|
|
1253
|
-
@heartbeat_waiter
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
@queue
|
|
1257
|
-
@worker
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1280
|
-
@unsubscribe_prompts
|
|
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
|
|
1286
|
-
@closed
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
1425
|
-
dispatcher_alive:
|
|
1426
|
-
request_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
|
|
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"
|
|
1569
|
-
"params"
|
|
1567
|
+
"method" => "notifications/progress",
|
|
1568
|
+
"params" => {
|
|
1570
1569
|
"progressToken" => @heartbeat_token,
|
|
1571
|
-
"progress"
|
|
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"
|
|
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"
|
|
1605
|
+
"progress" => progress,
|
|
1607
1606
|
}
|
|
1608
|
-
params["total"]
|
|
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"
|
|
1613
|
-
"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"
|
|
1665
|
-
"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
|
|
1705
|
-
@session_id
|
|
1703
|
+
@manager = manager
|
|
1704
|
+
@session_id = session_id
|
|
1706
1705
|
@heartbeat_interval = heartbeat_interval
|
|
1707
|
-
@logger
|
|
1708
|
-
@queue
|
|
1709
|
-
@heartbeat
|
|
1710
|
-
@closed
|
|
1711
|
-
@counted
|
|
1712
|
-
@close_mutex
|
|
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
|
|
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
|
|
1852
|
-
@mutex
|
|
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
|
|
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
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
238
|
-
@emit
|
|
239
|
-
@timer
|
|
240
|
-
@armed
|
|
241
|
-
@mutex
|
|
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
|
|
328
|
-
@debounce_interval
|
|
329
|
-
@notifier
|
|
330
|
-
@both_client
|
|
331
|
-
@admin_client
|
|
332
|
-
@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
|
|
335
|
-
@max_per_session
|
|
336
|
-
@max_sessions
|
|
337
|
-
@client_mutex
|
|
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
|
|
340
|
-
@mutex
|
|
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"
|
|
587
|
-
"params"
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
|
824
|
-
|
|
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
|
|
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:
|
|
923
|
-
type:
|
|
924
|
-
permission:
|
|
925
|
-
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:
|
|
928
|
-
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
|
|
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]
|
|
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]
|
|
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
|
|
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
|