agentadmit 1.13.0 → 1.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d3b45175d51202f94a44cfda45acd2eedbd6e96453686633e2c23fe1e9e4b07
4
- data.tar.gz: 8e6e17f6848db25e36996afcb1e16ed45ec0ab591647cdc9bbd56f51b1b1957d
3
+ metadata.gz: 1af95260c413c1ce419cfdd29dbb64abffa228c2c252d729f62d8d8514713d43
4
+ data.tar.gz: c9a638031a64b6d5a634687f0d8bd150b928ff10e76af2b6203215c908aae7c6
5
5
  SHA512:
6
- metadata.gz: e77aed424389ce8a3c15445505cf32cadad2f04a004d4eb003b06c07891556c1968dda9c27506045ea7762c054f31b26c5c7271b79c96fc696423edee7655a6f
7
- data.tar.gz: 4526e4389bc7dad6a0799d0633082f9c885afff37c4aaa80be55065856d82a507bf12d20270e27cb636a057342995e880aeaa5734edfbfe8bd67e2110cb61223
6
+ metadata.gz: e12e4ede6e53c77634ef65d249a5e8363ba9a2a7bba650fe9a3b5696410dbe30a90c170bdd44ac1a44dbe8b7c0fa5145b8fa4d1ae5c8d47084d5f2e2e6d4f0e4
7
+ data.tar.gz: d6c0bfb352a0af3044b8153846969b0e9500e13d7a82489d06ad80383bc0b171156bbe5a95e84913c43b21f11127523342359f6f9939a0ba5e3b1ea8f004e6f1
data/README.md CHANGED
@@ -398,6 +398,31 @@ 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
+ If your code redeems connection tokens with `TokensClient#exchange`, pass `agent_platform:` and `agent_model:` to send along what the agent says it runs on (since 1.14.1).
423
+
424
+ 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.
425
+
401
426
  ## App-Attested Presence
402
427
 
403
428
  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
 
@@ -111,13 +111,22 @@ module AgentAdmit
111
111
  # @param connection_token [String] the ag_ct_… connection token
112
112
  # @param agent_label [String, nil] human-readable agent name
113
113
  # @param agent_id [String, nil] agent identifier
114
+ # @param agent_platform [String, nil] the agent app or runtime the agent
115
+ # says it runs in (reported by the agent, unverified)
116
+ # @param agent_model [Hash, nil] the AI model the agent says it runs on:
117
+ # {"id" => String, "version" => String (optional)} (reported by the
118
+ # agent, unverified). The hosted service records both and never lets
119
+ # them decide the outcome of the exchange.
114
120
  # @return [Hash] the exchange response — "access_token" is the ag_at_… token
115
121
  # @raise [IntrospectionError] if the exchange fails
116
122
  #
117
- def exchange(connection_token, agent_label: nil, agent_id: nil)
123
+ def exchange(connection_token, agent_label: nil, agent_id: nil,
124
+ agent_platform: nil, agent_model: nil)
118
125
  body = { "token" => connection_token }
119
126
  body["agent_label"] = agent_label if agent_label
120
127
  body["agent_id"] = agent_id if agent_id
128
+ body["agent_platform"] = agent_platform if agent_platform
129
+ body["agent_model"] = agent_model if agent_model
121
130
 
122
131
  post("/api/v1/exchange", body, authenticated: false, op: "exchange")
123
132
  end
@@ -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.1"
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.1
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