sveda-ruby-sdk 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +2 -1
- data/lib/sveda/client.rb +4 -2
- data/lib/sveda/embed.rb +5 -1
- data/lib/sveda/host/mcp_handler.rb +10 -9
- data/lib/sveda/host/server.rb +60 -11
- data/lib/sveda/host/token_store.rb +12 -6
- data/lib/sveda/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '00651658d2c76433a154d2242b34dee5ab7bce230eb736e40f4db82bee57e361'
|
|
4
|
+
data.tar.gz: 55290570fc7c2d55a1c7c5fbe2ec6a89121197806052c72074d48f17737efb09
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7981b49a07f64c70bfeb78ff82f4b3e6deb6b37f581c55c737f4ed70756a0bab2e47a0cb5eb19eb213aed848535cf3ba8b7ebc622775ea2ca84b6dac7443ea6
|
|
7
|
+
data.tar.gz: 19da124e14de0579233719d35922362b38f437de533606eb1f00980c5dfbaa1a73ffb18d4013a94d4e1a342040370dbb4e38e36b68d7bc8bcb6167a1b2882cd8
|
data/README.md
CHANGED
|
@@ -45,7 +45,8 @@ server = Sveda::Host::Server.new(
|
|
|
45
45
|
server_name: "My App",
|
|
46
46
|
instructions: "Tools for the current user."
|
|
47
47
|
)
|
|
48
|
-
server.resolve_tools_using { [SearchPostsTool.new] }
|
|
48
|
+
server.resolve_tools_using { |user| [SearchPostsTool.new] }
|
|
49
|
+
server.policy_using { |_user| "agent" }
|
|
49
50
|
|
|
50
51
|
# Rails routes.rb
|
|
51
52
|
post "/sveda/session", to: "sveda_sessions#create"
|
data/lib/sveda/client.rb
CHANGED
|
@@ -23,7 +23,7 @@ module Sveda
|
|
|
23
23
|
Histories.new(@transport)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def self.start_host_session(base_url:, host_api_key:, visitor_id:, host_mcp_url: nil, host_mcp_token: nil, **client_kwargs)
|
|
26
|
+
def self.start_host_session(base_url:, host_api_key:, visitor_id:, host_mcp_url: nil, host_mcp_token: nil, policy: nil, grants: nil, **client_kwargs)
|
|
27
27
|
origin = base_url.to_s.strip.sub(%r{/\z}, "")
|
|
28
28
|
key = host_api_key.to_s.strip
|
|
29
29
|
if origin.empty? || key.empty?
|
|
@@ -34,7 +34,9 @@ module Sveda
|
|
|
34
34
|
token = client.embed.create_token(
|
|
35
35
|
visitor_id: visitor_id,
|
|
36
36
|
host_mcp_url: host_mcp_url,
|
|
37
|
-
host_mcp_token: host_mcp_token
|
|
37
|
+
host_mcp_token: host_mcp_token,
|
|
38
|
+
policy: policy,
|
|
39
|
+
grants: grants
|
|
38
40
|
)
|
|
39
41
|
if token.token.empty?
|
|
40
42
|
raise Error, "Sidecar returned an empty embed token."
|
data/lib/sveda/embed.rb
CHANGED
|
@@ -6,13 +6,17 @@ module Sveda
|
|
|
6
6
|
@transport = transport
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
def create_token(visitor_id: nil, host_mcp_url: nil, host_mcp_token: nil)
|
|
9
|
+
def create_token(visitor_id: nil, host_mcp_url: nil, host_mcp_token: nil, policy: nil, grants: nil)
|
|
10
10
|
payload = {}
|
|
11
11
|
payload["visitor_id"] = visitor_id if visitor_id.to_s != ""
|
|
12
12
|
if host_mcp_url.to_s != "" && host_mcp_token.to_s != ""
|
|
13
13
|
payload["host_mcp_url"] = host_mcp_url
|
|
14
14
|
payload["host_mcp_token"] = host_mcp_token
|
|
15
15
|
end
|
|
16
|
+
if !policy.nil? && policy.to_s.strip != ""
|
|
17
|
+
payload["policy"] = policy.to_s.strip
|
|
18
|
+
end
|
|
19
|
+
payload["grants"] = grants if grants.is_a?(Hash)
|
|
16
20
|
|
|
17
21
|
EmbedToken.from_payload(@transport.request_json("POST", "/sveda/embed/token", payload))
|
|
18
22
|
end
|
|
@@ -15,9 +15,10 @@ module Sveda
|
|
|
15
15
|
return method_not_allowed unless request.post?
|
|
16
16
|
|
|
17
17
|
token = bearer_token(request)
|
|
18
|
-
|
|
19
|
-
return
|
|
20
|
-
@server.
|
|
18
|
+
user = @server.authenticate_token(token)
|
|
19
|
+
return unauthorized if user.nil?
|
|
20
|
+
return forbidden unless @server.authorized?(user)
|
|
21
|
+
@server.run_after_authenticate(user)
|
|
21
22
|
|
|
22
23
|
payload = JSON.parse(request.body.read)
|
|
23
24
|
return bad_request unless payload["jsonrpc"] == "2.0"
|
|
@@ -32,9 +33,9 @@ module Sveda
|
|
|
32
33
|
when "initialize"
|
|
33
34
|
json_rpc(id, initialize_result)
|
|
34
35
|
when "tools/list"
|
|
35
|
-
json_rpc(id, { tools: list_tools })
|
|
36
|
+
json_rpc(id, { tools: list_tools(user) })
|
|
36
37
|
when "tools/call"
|
|
37
|
-
json_rpc(id, call_tool(params))
|
|
38
|
+
json_rpc(id, call_tool(params, user))
|
|
38
39
|
else
|
|
39
40
|
[202, {}, []]
|
|
40
41
|
end
|
|
@@ -58,8 +59,8 @@ module Sveda
|
|
|
58
59
|
result
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
def list_tools
|
|
62
|
-
@server.tools.map do |tool|
|
|
62
|
+
def list_tools(user)
|
|
63
|
+
@server.tools(user).map do |tool|
|
|
63
64
|
{
|
|
64
65
|
name: tool.name,
|
|
65
66
|
title: tool.name,
|
|
@@ -73,10 +74,10 @@ module Sveda
|
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
def call_tool(params)
|
|
77
|
+
def call_tool(params, user)
|
|
77
78
|
name = params["name"].to_s
|
|
78
79
|
arguments = params["arguments"].is_a?(Hash) ? params["arguments"] : {}
|
|
79
|
-
tool = @server.tools.find { |candidate| candidate.name == name }
|
|
80
|
+
tool = @server.tools(user).find { |candidate| candidate.name == name }
|
|
80
81
|
return tool_error("unknown tool: #{name}") unless tool
|
|
81
82
|
|
|
82
83
|
result = tool.handle(arguments)
|
data/lib/sveda/host/server.rb
CHANGED
|
@@ -23,17 +23,23 @@ module Sveda
|
|
|
23
23
|
}
|
|
24
24
|
@token_store = MemoryTokenStore.new(ttl_seconds: token_ttl_seconds)
|
|
25
25
|
@resolve_tools = nil
|
|
26
|
+
@policy_using = nil
|
|
26
27
|
@tools = []
|
|
27
|
-
@mint_token = -> { @token_store.mint }
|
|
28
|
-
@authenticate = ->(token) { @token_store.
|
|
28
|
+
@mint_token = ->(user = nil) { @token_store.mint(user_id: user_id_for(user)) }
|
|
29
|
+
@authenticate = ->(token) { @token_store.lookup(token) }
|
|
29
30
|
@authorize = nil
|
|
30
31
|
@after_authenticate = nil
|
|
32
|
+
@current_user = nil
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def resolve_tools_using(&block)
|
|
34
36
|
@resolve_tools = block
|
|
35
37
|
end
|
|
36
38
|
|
|
39
|
+
def policy_using(&block)
|
|
40
|
+
@policy_using = block
|
|
41
|
+
end
|
|
42
|
+
|
|
37
43
|
def register_tool(tool)
|
|
38
44
|
@tools << tool
|
|
39
45
|
end
|
|
@@ -64,35 +70,60 @@ module Sveda
|
|
|
64
70
|
@config[:mcp_path]
|
|
65
71
|
end
|
|
66
72
|
|
|
67
|
-
def tools
|
|
68
|
-
|
|
73
|
+
def tools(user = nil)
|
|
74
|
+
if @resolve_tools
|
|
75
|
+
return Array(invoke_resolve_tools(user))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@tools
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def policy_for(user)
|
|
82
|
+
return nil unless @policy_using
|
|
83
|
+
|
|
84
|
+
value = @policy_using.call(user)
|
|
85
|
+
return nil if value.nil?
|
|
86
|
+
|
|
87
|
+
policy = value.to_s.strip
|
|
88
|
+
policy.empty? ? nil : policy
|
|
69
89
|
end
|
|
70
90
|
|
|
71
91
|
def authenticate_token(token)
|
|
72
|
-
@authenticate.call(token)
|
|
92
|
+
result = @authenticate.call(token)
|
|
93
|
+
return nil if result.nil? || result == false
|
|
94
|
+
|
|
95
|
+
if result == true
|
|
96
|
+
{ "id" => "anonymous" }
|
|
97
|
+
elsif result.is_a?(Hash)
|
|
98
|
+
result
|
|
99
|
+
else
|
|
100
|
+
{ "id" => result.to_s }
|
|
101
|
+
end
|
|
73
102
|
end
|
|
74
103
|
|
|
75
|
-
def authorized?
|
|
104
|
+
def authorized?(user = nil)
|
|
76
105
|
return true unless @authorize
|
|
77
106
|
|
|
78
|
-
@authorize.call != false
|
|
107
|
+
@authorize.call(user) != false
|
|
79
108
|
end
|
|
80
109
|
|
|
81
|
-
def run_after_authenticate
|
|
82
|
-
@after_authenticate&.call
|
|
110
|
+
def run_after_authenticate(user = nil)
|
|
111
|
+
@after_authenticate&.call(user)
|
|
83
112
|
end
|
|
84
113
|
|
|
85
|
-
def start_session(visitor_id:, mcp_url: nil)
|
|
114
|
+
def start_session(visitor_id:, mcp_url: nil, user: nil)
|
|
86
115
|
raise ConfigurationError, "Задайте SVEDA_CLIENT_BASE_URL и SVEDA_CLIENT_HOST_API_KEY" unless configured?
|
|
87
116
|
|
|
117
|
+
session_user = user.nil? ? visitor_id : user
|
|
88
118
|
mcp = (mcp_url || self.mcp_url).to_s.strip.sub(%r{/\z}, "")
|
|
89
|
-
mcp_token = @mint_token.call
|
|
119
|
+
mcp_token = @mint_token.arity.zero? ? @mint_token.call : @mint_token.call(session_user)
|
|
90
120
|
Sveda::Client.start_host_session(
|
|
91
121
|
base_url: @config[:base_url],
|
|
92
122
|
host_api_key: @config[:host_api_key],
|
|
93
123
|
visitor_id: visitor_id,
|
|
94
124
|
host_mcp_url: mcp.empty? ? nil : mcp,
|
|
95
125
|
host_mcp_token: mcp_token,
|
|
126
|
+
policy: policy_for(session_user),
|
|
96
127
|
**@client_kwargs
|
|
97
128
|
)
|
|
98
129
|
end
|
|
@@ -103,6 +134,24 @@ module Sveda
|
|
|
103
134
|
|
|
104
135
|
private
|
|
105
136
|
|
|
137
|
+
def invoke_resolve_tools(user)
|
|
138
|
+
if @resolve_tools.arity.zero?
|
|
139
|
+
@resolve_tools.call
|
|
140
|
+
else
|
|
141
|
+
@resolve_tools.call(user)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def user_id_for(user)
|
|
146
|
+
return "anonymous" if user.nil?
|
|
147
|
+
|
|
148
|
+
if user.is_a?(Hash)
|
|
149
|
+
(user[:id] || user["id"] || "anonymous").to_s
|
|
150
|
+
else
|
|
151
|
+
user.to_s
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
106
155
|
def normalize_path(path)
|
|
107
156
|
value = path.to_s.strip
|
|
108
157
|
value = "/mcp/sveda" if value.empty?
|
|
@@ -11,23 +11,29 @@ module Sveda
|
|
|
11
11
|
@mutex = Mutex.new
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def mint
|
|
14
|
+
def mint(user_id: "anonymous")
|
|
15
15
|
token = SecureRandom.hex(32)
|
|
16
16
|
expires_at = Time.now + @ttl_seconds
|
|
17
17
|
@mutex.synchronize do
|
|
18
18
|
prune_locked
|
|
19
|
-
@tokens[token] = expires_at
|
|
19
|
+
@tokens[token] = { expires_at: expires_at, user_id: user_id.to_s }
|
|
20
20
|
end
|
|
21
21
|
token
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def valid?(token)
|
|
25
|
-
|
|
25
|
+
!lookup(token).nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def lookup(token)
|
|
29
|
+
return nil if token.to_s.empty?
|
|
26
30
|
|
|
27
31
|
@mutex.synchronize do
|
|
28
32
|
prune_locked
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
record = @tokens[token.to_s]
|
|
34
|
+
return nil unless record && Time.now < record[:expires_at]
|
|
35
|
+
|
|
36
|
+
{ "id" => record[:user_id] }
|
|
31
37
|
end
|
|
32
38
|
end
|
|
33
39
|
|
|
@@ -35,7 +41,7 @@ module Sveda
|
|
|
35
41
|
|
|
36
42
|
def prune_locked
|
|
37
43
|
now = Time.now
|
|
38
|
-
@tokens.delete_if { |_token,
|
|
44
|
+
@tokens.delete_if { |_token, record| record[:expires_at] <= now }
|
|
39
45
|
end
|
|
40
46
|
end
|
|
41
47
|
end
|
data/lib/sveda/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sveda-ruby-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Neresson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|