agentadmit 1.2.0 → 1.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 +24 -0
- data/lib/agentadmit/introspection_client.rb +73 -2
- data/lib/agentadmit/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: 74299884ea21ad2cb70261b0d2e7025bf4bc07e330f76bfc44009af8bba3b71b
|
|
4
|
+
data.tar.gz: 846ec2f9b8ed3afe7e9113fa7e1c1bf4b2fd7b074ce0f61e96550347db6c185b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25964578da91a2be9763398021fad033e9538cf6270c2fe99d66185c4da6d884514bcf9c9cfd9a2f60d017e0aeecdeb62d9d6ce1ef19ebfa543eb274cf2cc688
|
|
7
|
+
data.tar.gz: fdba91838161e85923cca0e4f1c84d72b724ad7bcf8e1be1c248f6af5ab1c1ec2506345e27016e7c8b3a7a989c5c65c3474fa263c23466cc7b100ec06b4263fc
|
data/README.md
CHANGED
|
@@ -72,6 +72,30 @@ The token goes to the human, not the agent. No automated delivery = no prompt in
|
|
|
72
72
|
|
|
73
73
|
**In-app AI scopes.** If your app has built-in AI features (analysis, plan generation, photo recognition), do not expose those as agent scopes. The user's AI agent can read the raw data and do the analysis itself. Exposing in-app AI endpoints to agents creates double cost.
|
|
74
74
|
|
|
75
|
+
### Consent Ledger (Caller-Identity Consent)
|
|
76
|
+
|
|
77
|
+
AgentAdmit can host per-user consent switches for three independent caller classes: `human_session`, `in_app_ai`, and `external_agent`. No class's setting implies another's.
|
|
78
|
+
|
|
79
|
+
**External agents:** the verify result already carries the verdict:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
result = AgentAdmit::IntrospectionClient.new.verify(token)
|
|
83
|
+
unless result.consent_granted?
|
|
84
|
+
# the data owner has switched external agents off: return your own 403
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Human sessions and in-app AI** never hold AgentAdmit tokens, so ask directly:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
verdict = AgentAdmit::IntrospectionClient.new.check_consent(
|
|
92
|
+
app_user_id: "user_8842", caller_class: "in_app_ai"
|
|
93
|
+
)
|
|
94
|
+
head :forbidden unless verdict["granted"]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Consent is orthogonal to revocation: a denied verdict means your app returns its own 403; the connection and token stay valid so the user can flip consent back on without re-connecting. Write switches through `PUT /api/v1/consent/settings` from your backend; export the audit trail with `GET /api/v1/consent/export` (every plan).
|
|
98
|
+
|
|
75
99
|
## Rate Limiting
|
|
76
100
|
|
|
77
101
|
The AgentAdmit introspection endpoint enforces rate limits. The Ruby SDK handles HTTP 429 responses **automatically** with exponential backoff and jitter -- no changes needed in your middleware code.
|
|
@@ -16,10 +16,24 @@ module AgentAdmit
|
|
|
16
16
|
MAX_RETRY_BUDGET_MS = 120_000
|
|
17
17
|
|
|
18
18
|
IntrospectionResult = Struct.new(:user_id, :connection_id, :scopes, :agent_label,
|
|
19
|
-
:sub, :role, :app_id, :jti, :exp, keyword_init: true) do
|
|
19
|
+
:sub, :role, :app_id, :jti, :exp, :consent, keyword_init: true) do
|
|
20
20
|
def has_scope?(scope)
|
|
21
21
|
scopes.include?(scope)
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
# Consent Ledger verdict for the external-agent path (additive; may be
|
|
25
|
+
# nil). A denied verdict means the app returns its own 403 -- the token
|
|
26
|
+
# itself stays valid (consent is orthogonal to revocation).
|
|
27
|
+
#
|
|
28
|
+
# Contract (matches the PHP and Java SDKs): an ABSENT consent block
|
|
29
|
+
# means a legacy server that never sends one -- treated as allowed,
|
|
30
|
+
# which is also the platform default for the external-agent class. A
|
|
31
|
+
# PRESENT block grants only on an explicit boolean true; a missing or
|
|
32
|
+
# non-boolean granted value is treated as denied (malformed = deny).
|
|
33
|
+
def consent_granted?
|
|
34
|
+
return true if consent.nil?
|
|
35
|
+
consent["granted"] == true
|
|
36
|
+
end
|
|
23
37
|
end
|
|
24
38
|
|
|
25
39
|
def initialize(config = nil)
|
|
@@ -136,6 +150,13 @@ module AgentAdmit
|
|
|
136
150
|
# Validate that consumed fields have the expected types when present.
|
|
137
151
|
validate_introspection_types!(data)
|
|
138
152
|
|
|
153
|
+
# Keep any PRESENT consent hash, even with a malformed granted value:
|
|
154
|
+
# coercing it to nil would read as "legacy server, allowed" in
|
|
155
|
+
# consent_granted?, silently failing open. A malformed verdict must
|
|
156
|
+
# stay visible so consent_granted? can deny it.
|
|
157
|
+
consent = data["consent"]
|
|
158
|
+
consent = nil unless consent.is_a?(Hash)
|
|
159
|
+
|
|
139
160
|
return IntrospectionResult.new(
|
|
140
161
|
user_id: data["user_id"],
|
|
141
162
|
connection_id: data["connection_id"],
|
|
@@ -145,7 +166,8 @@ module AgentAdmit
|
|
|
145
166
|
role: data["role"],
|
|
146
167
|
app_id: data["app_id"],
|
|
147
168
|
jti: data["jti"],
|
|
148
|
-
exp: data["exp"]
|
|
169
|
+
exp: data["exp"],
|
|
170
|
+
consent: consent
|
|
149
171
|
)
|
|
150
172
|
end
|
|
151
173
|
|
|
@@ -153,6 +175,55 @@ module AgentAdmit
|
|
|
153
175
|
raise IntrospectionError, "Unexpected exit from retry loop"
|
|
154
176
|
end
|
|
155
177
|
|
|
178
|
+
CALLER_CLASSES = %w[human_session in_app_ai external_agent].freeze
|
|
179
|
+
|
|
180
|
+
##
|
|
181
|
+
# Ask the Consent Ledger whether a caller class may act on a user's data.
|
|
182
|
+
# Decision point for the token-less caller classes (human_session,
|
|
183
|
+
# in_app_ai); external agents get the same verdict on the verify result.
|
|
184
|
+
#
|
|
185
|
+
# @param app_user_id [String] your app's identifier for the data owner
|
|
186
|
+
# @param caller_class [String] "human_session" | "in_app_ai" | "external_agent"
|
|
187
|
+
# @param scope_group [String, nil] optional finer-than-class group
|
|
188
|
+
# @return [Hash] verdict: granted, caller_class, scope_group, source, evaluated_at
|
|
189
|
+
# @raise [ArgumentError] unknown caller_class
|
|
190
|
+
# @raise [IntrospectionError] hosted service unreachable or rejected the call
|
|
191
|
+
#
|
|
192
|
+
def check_consent(app_user_id:, caller_class:, scope_group: nil)
|
|
193
|
+
unless CALLER_CLASSES.include?(caller_class)
|
|
194
|
+
raise ArgumentError, "caller_class must be one of #{CALLER_CLASSES.join(', ')}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
uri = URI.parse("#{@config.api_url.sub(%r{/\z}, '')}/api/v1/consent/check")
|
|
198
|
+
http = build_http(uri)
|
|
199
|
+
|
|
200
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
201
|
+
request["Authorization"] = "Bearer #{@config.api_key}"
|
|
202
|
+
request["Content-Type"] = "application/json"
|
|
203
|
+
body = { app_user_id: app_user_id, caller_class: caller_class }
|
|
204
|
+
body[:scope_group] = scope_group if scope_group
|
|
205
|
+
request.body = JSON.generate(body)
|
|
206
|
+
|
|
207
|
+
response = begin
|
|
208
|
+
http.request(request)
|
|
209
|
+
rescue StandardError => e
|
|
210
|
+
raise IntrospectionError, "Consent check failed: #{e.message}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
unless (200..299).cover?(response.code.to_i)
|
|
214
|
+
data = JSON.parse(response.body) rescue {}
|
|
215
|
+
raise IntrospectionError,
|
|
216
|
+
data["error_description"] || data["error"] || "Consent check returned #{response.code}"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# 2xx -- parse strictly; a garbage body must not read as an empty verdict.
|
|
220
|
+
begin
|
|
221
|
+
JSON.parse(response.body)
|
|
222
|
+
rescue JSON::ParserError
|
|
223
|
+
raise IntrospectionError, "Consent check response is not valid JSON"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
156
227
|
private
|
|
157
228
|
|
|
158
229
|
##
|
data/lib/agentadmit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agentadmit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Christopher Emerson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|