agentadmit 1.13.0 → 1.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d3b45175d51202f94a44cfda45acd2eedbd6e96453686633e2c23fe1e9e4b07
4
- data.tar.gz: 8e6e17f6848db25e36996afcb1e16ed45ec0ab591647cdc9bbd56f51b1b1957d
3
+ metadata.gz: 4614ce271d54fa958f580f2180349bbdad92764566efe17d3a459c0fca62815c
4
+ data.tar.gz: 5c46517cd626d99eda4482cf47fdb14ae98b13f9a451216bcb117f6fd236f9c8
5
5
  SHA512:
6
- metadata.gz: e77aed424389ce8a3c15445505cf32cadad2f04a004d4eb003b06c07891556c1968dda9c27506045ea7762c054f31b26c5c7271b79c96fc696423edee7655a6f
7
- data.tar.gz: 4526e4389bc7dad6a0799d0633082f9c885afff37c4aaa80be55065856d82a507bf12d20270e27cb636a057342995e880aeaa5734edfbfe8bd67e2110cb61223
6
+ metadata.gz: 716a550dd977efc852984419c4f79e8fba5e28fd282a3c5b8630251fc48dba14548ac6e1950d2c548057acf84862d9c023525cdfe1ba5e8c2ef60e5ca0d021a7
7
+ data.tar.gz: c568d905a1930f6376109751c2fe3c464a181049f359b50f280872f9b18042d82d75a73e628119ed0d8f51270bfa843b39ca4585f7867182c4ed3242d6d8000f
data/README.md CHANGED
@@ -398,6 +398,29 @@ result.user_intent # => "Book my flights to the Austin offsite in October" or ni
398
398
 
399
399
  `user_intent` is nullable -- connections issued without one (or by servers that predate the field) read as `nil`. Months later, a review screen can answer "is this still appropriate?" with the user's own stated boundary, not just the app's. Like purpose, it is a review-time record and never an enforcement input; authorization decisions ride scopes, connection status, and consent.
400
400
 
401
+ ## Agent Designation
402
+
403
+ On AgentAdmit's consent page the user says which agent app a connection is for and, optionally, which AI model family it mainly uses. That is the user's statement, recorded with the grant; it is not a verified identity of the agent. Separately, an agent may report its own platform and model when it redeems the connection token; that is unverified too.
404
+
405
+ The verify response reports both on every introspection, and the SDK surfaces them on the result as `designated_agent` and `agent_reported` (`nil` when the connection carries none):
406
+
407
+ ```ruby
408
+ result = AgentAdmit::IntrospectionClient.new.verify(token)
409
+
410
+ # {"agent_id" => ..., "name" => ..., "source" => "user_declared",
411
+ # "model" => {"model_id" => ..., "name" => ...} or nil}
412
+ named = result.designated_agent
413
+ # {"platform" => ..., "model" => {"id" => ..., "version" => ...} or nil}
414
+ reported = result.agent_reported
415
+
416
+ puts "Named by the user: #{named ? named['name'] : '(not named)'}"
417
+ puts "Reported by the agent: #{reported&.dig('model', 'id') || '(nothing)'}"
418
+ ```
419
+
420
+ Use `designated_agent["name"]` when you tell a user which of their connections did something: it is the name they chose. `agent_id` and `model["model_id"]` are the ids of your list entries, or `nil` when the user typed a name that is not on your list.
421
+
422
+ Do not branch authorization on either field. They are records for people reviewing a connection, not a gate. Scope checks, connection status, and consent verdicts remain the only decision inputs. To act on a designation, use the hosted revoke API (`designated_agent_id`, `designated_model_id`) or your list's block setting in the dashboard.
423
+
401
424
  ## App-Attested Presence
402
425
 
403
426
  If your app gates token minting behind its own embedded passkey/WebAuthn ceremony, AgentAdmit never witnesses that ceremony (it is origin-bound), so by default the hosted service reports `presence.verified: false` for those connections. Attest the ceremony fact at issuance to close that gap -- AFTER verifying and consuming your own fresh, purpose-bound attestation:
@@ -34,7 +34,8 @@ module AgentAdmit
34
34
  :sub, :role, :app_id, :jti, :exp, :consent,
35
35
  :presence, :purpose, :user_intent,
36
36
  :action_confirmation, :audit_row_id,
37
- :consumed_receipt, keyword_init: true) do
37
+ :consumed_receipt, :designated_agent,
38
+ :agent_reported, keyword_init: true) do
38
39
  def has_scope?(scope)
39
40
  scopes.include?(scope)
40
41
  end
@@ -94,6 +95,24 @@ module AgentAdmit
94
95
  # `consumed_receipt` (Hash or nil) is a replay diagnostic for an
95
96
  # already-consumed confirm-each-time attestation. It is not a fresh
96
97
  # authorization and must not be treated as permission to run.
98
+
99
+ # `designated_agent` (Hash or nil) is what the USER said on AgentAdmit's
100
+ # consent page: which agent app the connection is for and, optionally,
101
+ # which AI model family it mainly uses:
102
+ # {"agent_id" => String|nil, "name" => String,
103
+ # "source" => "user_declared",
104
+ # "model" => {"model_id" => String|nil, "name" => String} | nil}.
105
+ # It is the user's statement, recorded with the grant; it is not a
106
+ # verified identity of the agent. `agent_id` / `model_id` are nil when
107
+ # the user typed a name that is not on the app's list.
108
+
109
+ # `agent_reported` (Hash or nil) is what the AGENT reported about itself
110
+ # when it redeemed the connection token:
111
+ # {"platform" => String|nil,
112
+ # "model" => {"id" => String, "version" => String|nil} | nil}.
113
+ # Unverified. Both fields are review-time records only, never an
114
+ # enforcement input; authorization decisions ride scopes, connection
115
+ # status, and consent.
97
116
  end
98
117
 
99
118
  class << self
@@ -192,6 +211,58 @@ module AgentAdmit
192
211
  "row_hash" => raw["row_hash"].is_a?(String) ? raw["row_hash"] : nil }
193
212
  end
194
213
 
214
+ ##
215
+ # The user's agent designation from a verify response, or nil when the
216
+ # block is absent, null, or not a user-declared designation. Strict: a
217
+ # non-empty String name and source "user_declared", or the whole block
218
+ # is dropped. A malformed model is dropped and the app is kept. This is
219
+ # what the user said on the consent page, not a verified identity.
220
+ #
221
+ # @param raw [Object] the `designated_agent` value from the verify body
222
+ # @return [Hash, nil]
223
+ #
224
+ def parse_designated_agent(raw)
225
+ return nil unless raw.is_a?(Hash)
226
+ name = raw["name"]
227
+ return nil unless name.is_a?(String) && !name.empty?
228
+ return nil unless raw["source"] == "user_declared"
229
+
230
+ m = raw["model"]
231
+ model =
232
+ if m.is_a?(Hash) && m["name"].is_a?(String) && !m["name"].empty?
233
+ { "model_id" => m["model_id"].is_a?(String) ? m["model_id"] : nil,
234
+ "name" => m["name"] }
235
+ end
236
+
237
+ { "agent_id" => raw["agent_id"].is_a?(String) ? raw["agent_id"] : nil,
238
+ "name" => name,
239
+ "source" => "user_declared",
240
+ "model" => model }
241
+ end
242
+
243
+ ##
244
+ # What the agent reported about itself at token exchange, or nil when
245
+ # the block carries neither a platform nor a model id. Unverified.
246
+ #
247
+ # @param raw [Object] the `agent_reported` value from the verify body
248
+ # @return [Hash, nil]
249
+ #
250
+ def parse_agent_reported(raw)
251
+ return nil unless raw.is_a?(Hash)
252
+
253
+ platform = raw["platform"]
254
+ platform = nil unless platform.is_a?(String) && !platform.empty?
255
+ m = raw["model"]
256
+ model =
257
+ if m.is_a?(Hash) && m["id"].is_a?(String) && !m["id"].empty?
258
+ { "id" => m["id"],
259
+ "version" => m["version"].is_a?(String) ? m["version"] : nil }
260
+ end
261
+ return nil if platform.nil? && model.nil?
262
+
263
+ { "platform" => platform, "model" => model }
264
+ end
265
+
195
266
  ##
196
267
  # The agent's X-AgentAdmit-Action-Attestation header from a Rack env:
197
268
  # first value only, trimmed, capped at 120 characters. nil when absent
@@ -408,6 +479,13 @@ module AgentAdmit
408
479
  audit_row_id = nil unless audit_row_id.is_a?(String)
409
480
  consumed_receipt = self.class.parse_consumed_receipt(data["consumed_receipt"])
410
481
 
482
+ # Agent designation (1.14.0): what the user said the connection is
483
+ # for, and what the agent reported about itself. Review-time records,
484
+ # never an enforcement input, so a malformed block is dropped rather
485
+ # than failing the verify.
486
+ designated_agent = self.class.parse_designated_agent(data["designated_agent"])
487
+ agent_reported = self.class.parse_agent_reported(data["agent_reported"])
488
+
411
489
  return IntrospectionResult.new(
412
490
  user_id: data["user_id"],
413
491
  connection_id: data["connection_id"],
@@ -424,7 +502,9 @@ module AgentAdmit
424
502
  user_intent: user_intent,
425
503
  action_confirmation: action_confirmation,
426
504
  audit_row_id: audit_row_id,
427
- consumed_receipt: consumed_receipt
505
+ consumed_receipt: consumed_receipt,
506
+ designated_agent: designated_agent,
507
+ agent_reported: agent_reported
428
508
  )
429
509
  end
430
510
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentAdmit
4
- VERSION = "1.13.0"
4
+ VERSION = "1.14.0"
5
5
  end
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.13.0
4
+ version: 1.14.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-09-30 00:00:00.000000000 Z
11
+ date: 2026-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json