datagrout-conduit 0.4.0 → 0.5.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 -2
- data/lib/datagrout_conduit/client.rb +37 -4
- data/lib/datagrout_conduit/identity.rb +6 -7
- data/lib/datagrout_conduit/onramp.rb +155 -0
- data/lib/datagrout_conduit/version.rb +1 -1
- data/lib/datagrout_conduit.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3a08c2cb30deefe712f0083b473838a1ec6b578c1b7decbcc53ac14aca4cffb
|
|
4
|
+
data.tar.gz: ea88b13e4a5998164c4dfaa1b71d448e5d498b694b8de312449a8c82d4daf555
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70c40acb181dcc382395c454e769e26fede6798a32dc2dab7797ffaf66b1936dd0e89cf767f2f9f16a5e668ad1ceff9dfd3d2a22d03db34bcb146ab1eef78ba6
|
|
7
|
+
data.tar.gz: 3c2cf29896ae08b63876e60e8807ccb7ea85118fdb5d422aaf9b94e4ef3aa76345786b42bb0406f1c8df1b938d3755cf78a84fd3bf9728f5ecc4fd54a6c7b737
|
data/README.md
CHANGED
|
@@ -9,13 +9,13 @@ Connect to remote MCP and JSONRPC servers, invoke tools, discover capabilities w
|
|
|
9
9
|
Add to your Gemfile:
|
|
10
10
|
|
|
11
11
|
```ruby
|
|
12
|
-
gem "datagrout-conduit", "~> 0.
|
|
12
|
+
gem "datagrout-conduit", "~> 0.5.0"
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Or install directly:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
gem install datagrout-conduit -v 0.
|
|
18
|
+
gem install datagrout-conduit -v 0.5.0
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Quick Start
|
|
@@ -31,13 +31,15 @@ module DatagroutConduit
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def initialize(url:, auth: {}, transport: :mcp, identity: nil, identity_dir: nil,
|
|
34
|
-
use_intelligent_interface: nil, max_retries: 3, logger: nil,
|
|
34
|
+
use_intelligent_interface: nil, max_retries: 3, logger: nil,
|
|
35
|
+
identity_auto: false, disable_mtls: false)
|
|
35
36
|
@url = url
|
|
36
37
|
@auth = auth
|
|
37
38
|
@transport_mode = transport
|
|
38
39
|
@identity = identity
|
|
39
40
|
@identity_dir = identity_dir
|
|
40
|
-
@
|
|
41
|
+
@identity_auto = identity_auto
|
|
42
|
+
@disable_mtls = disable_mtls # deprecated, kept for backward compat
|
|
41
43
|
@max_retries = max_retries
|
|
42
44
|
@initialized = false
|
|
43
45
|
@server_info = nil
|
|
@@ -104,6 +106,38 @@ module DatagroutConduit
|
|
|
104
106
|
bootstrap_identity(url: url, auth_token: token, name: name, identity_dir: identity_dir)
|
|
105
107
|
end
|
|
106
108
|
|
|
109
|
+
# Bootstrap by performing the autonomous DG onramp flow.
|
|
110
|
+
#
|
|
111
|
+
# The all-in-one flow: onramp (no prior credentials required) →
|
|
112
|
+
# OAuth token exchange → mTLS identity registration and persistence.
|
|
113
|
+
#
|
|
114
|
+
# On subsequent runs the saved mTLS identity is auto-discovered and
|
|
115
|
+
# no credentials are needed.
|
|
116
|
+
#
|
|
117
|
+
# @param opts [DatagroutConduit::Onramp::OnrampOptions]
|
|
118
|
+
# @param url [String, nil] MCP server URL; required when +opts.mcp_url+ is absent
|
|
119
|
+
# @param name [String] human-readable identity label
|
|
120
|
+
# @param identity_dir [String, nil] custom identity storage directory
|
|
121
|
+
# @return [Client] unconnected client; call +#connect+ before use
|
|
122
|
+
def self.bootstrap_onramp(opts:, url: nil, name: "conduit-client", identity_dir: nil)
|
|
123
|
+
dir = identity_dir || Registration.default_identity_dir || File.join(Dir.home, ".conduit")
|
|
124
|
+
|
|
125
|
+
# Fast path: existing valid identity.
|
|
126
|
+
identity = Identity.try_discover(override_dir: dir)
|
|
127
|
+
if identity && !identity.needs_rotation?
|
|
128
|
+
raise ArgumentError, "'url' must be provided when an existing identity is reused" if url.nil?
|
|
129
|
+
return new(url: url, identity: identity)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Slow path: full onramp flow.
|
|
133
|
+
creds, token = Onramp.register_and_exchange(opts)
|
|
134
|
+
|
|
135
|
+
mcp_url = creds.mcp_url || url
|
|
136
|
+
raise ArgumentError, "'url' must be provided when mcp_url is absent from onramp response" if mcp_url.nil?
|
|
137
|
+
|
|
138
|
+
bootstrap_identity(url: mcp_url, auth_token: token, name: name, identity_dir: identity_dir)
|
|
139
|
+
end
|
|
140
|
+
|
|
107
141
|
def connect
|
|
108
142
|
@transport.connect
|
|
109
143
|
|
|
@@ -418,8 +452,7 @@ module DatagroutConduit
|
|
|
418
452
|
|
|
419
453
|
def resolve_identity!
|
|
420
454
|
return if @identity
|
|
421
|
-
return
|
|
422
|
-
return unless @is_dg
|
|
455
|
+
return unless @identity_auto
|
|
423
456
|
|
|
424
457
|
@identity = Identity.try_discover(override_dir: @identity_dir)
|
|
425
458
|
end
|
|
@@ -67,31 +67,30 @@ module DatagroutConduit
|
|
|
67
67
|
# Walk the auto-discovery chain and return the first identity found,
|
|
68
68
|
# or nil if nothing is available.
|
|
69
69
|
def self.try_discover(override_dir: nil)
|
|
70
|
-
#
|
|
70
|
+
# When an explicit directory is given, scope search to that dir only.
|
|
71
71
|
if override_dir
|
|
72
|
-
|
|
73
|
-
return id if id
|
|
72
|
+
return try_load_from_dir(override_dir)
|
|
74
73
|
end
|
|
75
74
|
|
|
76
|
-
#
|
|
75
|
+
# 1. Environment variables (individual cert/key PEMs)
|
|
77
76
|
id = from_env
|
|
78
77
|
return id if id
|
|
79
78
|
|
|
80
|
-
#
|
|
79
|
+
# 2. CONDUIT_IDENTITY_DIR env var
|
|
81
80
|
identity_dir = ENV["CONDUIT_IDENTITY_DIR"]
|
|
82
81
|
if identity_dir && !identity_dir.empty?
|
|
83
82
|
id = try_load_from_dir(identity_dir)
|
|
84
83
|
return id if id
|
|
85
84
|
end
|
|
86
85
|
|
|
87
|
-
#
|
|
86
|
+
# 3. ~/.conduit/
|
|
88
87
|
home = ENV["HOME"] || ENV["USERPROFILE"]
|
|
89
88
|
if home
|
|
90
89
|
id = try_load_from_dir(File.join(home, ".conduit"))
|
|
91
90
|
return id if id
|
|
92
91
|
end
|
|
93
92
|
|
|
94
|
-
#
|
|
93
|
+
# 4. .conduit/ relative to cwd
|
|
95
94
|
id = try_load_from_dir(File.join(Dir.pwd, ".conduit"))
|
|
96
95
|
return id if id
|
|
97
96
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module DatagroutConduit
|
|
7
|
+
# Autonomous agent self-registration (onramp) for DataGrout.
|
|
8
|
+
#
|
|
9
|
+
# The onramp flow lets a machine intelligence register itself with DG
|
|
10
|
+
# without a human in the loop, using only plain HTTP JSON — no MCP client
|
|
11
|
+
# required.
|
|
12
|
+
#
|
|
13
|
+
# == Flow
|
|
14
|
+
#
|
|
15
|
+
# 1. POST to +/onramp+ with agent identity metadata (no auth).
|
|
16
|
+
# 2. DG returns a short-lived +session_token+ (5 minutes).
|
|
17
|
+
# 3. POST to +/onramp/complete+ with +Authorization: Bearer <session_token>+.
|
|
18
|
+
# 4. DG issues provisional +client_id+ + +client_secret+ (restricted scopes).
|
|
19
|
+
#
|
|
20
|
+
# == Example
|
|
21
|
+
#
|
|
22
|
+
# opts = DatagroutConduit::Onramp::OnrampOptions.new(
|
|
23
|
+
# gateway: "https://app.datagrout.ai",
|
|
24
|
+
# agent_name: "my-research-agent",
|
|
25
|
+
# agent_type: "claude-sonnet-4-6"
|
|
26
|
+
# )
|
|
27
|
+
# client = DatagroutConduit::Client.bootstrap_onramp(opts: opts, url: nil)
|
|
28
|
+
module Onramp
|
|
29
|
+
# Options for the autonomous agent onramp flow.
|
|
30
|
+
OnrampOptions = Struct.new(
|
|
31
|
+
:gateway,
|
|
32
|
+
:agent_name,
|
|
33
|
+
:agent_type,
|
|
34
|
+
:intended_use,
|
|
35
|
+
:access_code,
|
|
36
|
+
keyword_init: true
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Provisional credentials returned by the DG onramp complete endpoint.
|
|
40
|
+
#
|
|
41
|
+
# Store +client_id+ and +client_secret+ securely — the secret is shown
|
|
42
|
+
# exactly once and cannot be recovered after this point.
|
|
43
|
+
OnrampCredentials = Struct.new(
|
|
44
|
+
:client_id,
|
|
45
|
+
:client_secret,
|
|
46
|
+
:token_url,
|
|
47
|
+
:scopes,
|
|
48
|
+
:expires_in,
|
|
49
|
+
:rpc_url,
|
|
50
|
+
:mcp_url,
|
|
51
|
+
keyword_init: true
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
class OnrampError < StandardError; end
|
|
55
|
+
|
|
56
|
+
# Perform the onramp handshake and return provisional OAuth credentials.
|
|
57
|
+
#
|
|
58
|
+
# Low-level entry point. Most callers should use
|
|
59
|
+
# {DatagroutConduit::Client.bootstrap_onramp} instead.
|
|
60
|
+
#
|
|
61
|
+
# @param opts [OnrampOptions]
|
|
62
|
+
# @return [OnrampCredentials]
|
|
63
|
+
def self.register_only(opts)
|
|
64
|
+
register(opts)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Perform the full onramp handshake and OAuth token exchange.
|
|
68
|
+
#
|
|
69
|
+
# @param opts [OnrampOptions]
|
|
70
|
+
# @return [Array(OnrampCredentials, String)] credentials and access token
|
|
71
|
+
def self.register_and_exchange(opts)
|
|
72
|
+
creds = register(opts)
|
|
73
|
+
token = exchange_token(creds)
|
|
74
|
+
[creds, token]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @api private
|
|
78
|
+
def self.register(opts)
|
|
79
|
+
base = opts.gateway.chomp("/")
|
|
80
|
+
|
|
81
|
+
body = { agent_name: opts.agent_name }
|
|
82
|
+
body[:agent_type] = opts.agent_type if opts.agent_type
|
|
83
|
+
body[:intended_use] = opts.intended_use if opts.intended_use
|
|
84
|
+
body[:access_code] = opts.access_code if opts.access_code
|
|
85
|
+
|
|
86
|
+
conn = build_conn
|
|
87
|
+
|
|
88
|
+
init_resp = conn.post("#{base}/onramp") do |req|
|
|
89
|
+
req.headers["Content-Type"] = "application/json"
|
|
90
|
+
req.body = JSON.generate(body)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
raise OnrampError, "onramp init rejected (HTTP #{init_resp.status}): #{init_resp.body}" \
|
|
94
|
+
unless init_resp.success?
|
|
95
|
+
|
|
96
|
+
init_data = parse_body(init_resp.body)
|
|
97
|
+
session_token = init_data["session_token"]
|
|
98
|
+
|
|
99
|
+
complete_resp = conn.post("#{base}/onramp/complete") do |req|
|
|
100
|
+
req.headers["Authorization"] = "Bearer #{session_token}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
raise OnrampError, "onramp complete rejected (HTTP #{complete_resp.status}): #{complete_resp.body}" \
|
|
104
|
+
unless complete_resp.success?
|
|
105
|
+
|
|
106
|
+
data = parse_body(complete_resp.body)
|
|
107
|
+
|
|
108
|
+
OnrampCredentials.new(
|
|
109
|
+
client_id: data["client_id"],
|
|
110
|
+
client_secret: data["client_secret"],
|
|
111
|
+
token_url: data["token_url"],
|
|
112
|
+
scopes: data["scopes"] || [],
|
|
113
|
+
expires_in: data["expires_in"] || 0,
|
|
114
|
+
rpc_url: data["rpc_url"],
|
|
115
|
+
mcp_url: data["mcp_url"]
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
private_class_method :register
|
|
119
|
+
|
|
120
|
+
# @api private
|
|
121
|
+
def self.exchange_token(creds)
|
|
122
|
+
conn = Faraday.new do |f|
|
|
123
|
+
f.request :url_encoded
|
|
124
|
+
f.response :json, content_type: /\bjson$/
|
|
125
|
+
f.adapter Faraday.default_adapter
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
resp = conn.post(creds.token_url, {
|
|
129
|
+
grant_type: "client_credentials",
|
|
130
|
+
client_id: creds.client_id,
|
|
131
|
+
client_secret: creds.client_secret
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
raise OnrampError, "token exchange failed (HTTP #{resp.status}): #{resp.body}" \
|
|
135
|
+
unless resp.success?
|
|
136
|
+
|
|
137
|
+
data = parse_body(resp.body)
|
|
138
|
+
data["access_token"]
|
|
139
|
+
end
|
|
140
|
+
private_class_method :exchange_token
|
|
141
|
+
|
|
142
|
+
def self.build_conn
|
|
143
|
+
Faraday.new do |f|
|
|
144
|
+
f.response :json, content_type: /\bjson$/
|
|
145
|
+
f.adapter Faraday.default_adapter
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
private_class_method :build_conn
|
|
149
|
+
|
|
150
|
+
def self.parse_body(body)
|
|
151
|
+
body.is_a?(String) ? JSON.parse(body) : body
|
|
152
|
+
end
|
|
153
|
+
private_class_method :parse_body
|
|
154
|
+
end
|
|
155
|
+
end
|
data/lib/datagrout_conduit.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "datagrout_conduit/types"
|
|
|
8
8
|
require_relative "datagrout_conduit/identity"
|
|
9
9
|
require_relative "datagrout_conduit/oauth"
|
|
10
10
|
require_relative "datagrout_conduit/registration"
|
|
11
|
+
require_relative "datagrout_conduit/onramp"
|
|
11
12
|
require_relative "datagrout_conduit/transport/base"
|
|
12
13
|
require_relative "datagrout_conduit/transport/mcp"
|
|
13
14
|
require_relative "datagrout_conduit/transport/jsonrpc"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: datagrout-conduit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- DataGrout
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -129,6 +129,7 @@ files:
|
|
|
129
129
|
- lib/datagrout_conduit/namespaces/prism.rb
|
|
130
130
|
- lib/datagrout_conduit/namespaces/warden.rb
|
|
131
131
|
- lib/datagrout_conduit/oauth.rb
|
|
132
|
+
- lib/datagrout_conduit/onramp.rb
|
|
132
133
|
- lib/datagrout_conduit/registration.rb
|
|
133
134
|
- lib/datagrout_conduit/transport/base.rb
|
|
134
135
|
- lib/datagrout_conduit/transport/jsonrpc.rb
|