agentadmit 1.5.1 → 1.7.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: 810b958cd0fd3558a19bfc79910e194e45e02d95331b98747ff4edf29e337d8a
4
- data.tar.gz: 4c18ad2b306eea918107543eeee24c3f2eb7f30010bfb3c1bb6502bcfa94718b
3
+ metadata.gz: 53205cacbbf7942a6cb2600c9cea4c0c739d991be95bd7c627df405287e5f52c
4
+ data.tar.gz: b9849673334811698875c9733abc1b69c2938111cf4b928cde741cb0e6ba6d0f
5
5
  SHA512:
6
- metadata.gz: 4a0a65b2ce47a30683c14de7c397bea637adbe18ed76db7b8b579c5835d5dcdb90881ef93e20c687710cc37184ec3fc796a9fd153e1f6bf19edcb8f1c2875463
7
- data.tar.gz: b81a5431a0a2b37728e675963ab79e188e5d0e288f4bb2a4e8497fe5df81ba7a962fc15d99b36fd21d72fa6472de266bb2dacef92aab79f46c04b615bcd31e58
6
+ metadata.gz: ed8c4e40cc18a7749257674d2ba0fba6d8fb79946aeb868cf84fa5f5fa67075eff3cc80456e67e34886157f63cbaaceb1156abc3ce92f87a3400096920354f69
7
+ data.tar.gz: e184a592c70ac59b82f906f00a7cae3c83ecd3b9681e5a35421bad55c3979a11bbfbfe45f85f8135fe4a909ed1f1c394108b03d86b199f91bf999ac8cf40e1bc
data/README.md CHANGED
@@ -302,3 +302,26 @@ granted = tokens.exchange(connection_token, agent_label: "MyAssistant")
302
302
  # Revoke when the user disconnects the agent.
303
303
  tokens.revoke(granted["connection_id"], reason: "user_requested")
304
304
  ```
305
+
306
+ ### Declared purpose
307
+
308
+ Declared purpose: the user-facing reason recorded on the grant at the consent moment. Review-time record only, never an enforcement input; authorization decisions ride scopes, connection status, and consent.
309
+
310
+ Pass it when issuing a connection token (optional, max 300 characters; omitted from the request when `nil`):
311
+
312
+ ```ruby
313
+ issued = tokens.issue_token(
314
+ user_id: "user_42",
315
+ scopes: ["read:orders"],
316
+ purpose: "Book quarterly travel for the sales team"
317
+ )
318
+ ```
319
+
320
+ The verify result carries it back for display in dashboards, review screens, and audit views:
321
+
322
+ ```ruby
323
+ result = AgentAdmit::IntrospectionClient.new.verify(token)
324
+ result.purpose # => "Book quarterly travel for the sales team" or nil
325
+ ```
326
+
327
+ `purpose` is nullable -- connections issued without one (or by servers that predate the field) read as `nil`. Do not branch authorization on it; keep enforcement on scopes, connection status, and consent.
@@ -17,7 +17,7 @@ module AgentAdmit
17
17
 
18
18
  IntrospectionResult = Struct.new(:user_id, :connection_id, :scopes, :agent_label,
19
19
  :sub, :role, :app_id, :jti, :exp, :consent,
20
- :presence, keyword_init: true) do
20
+ :presence, :purpose, keyword_init: true) do
21
21
  def has_scope?(scope)
22
22
  scopes.include?(scope)
23
23
  end
@@ -47,6 +47,11 @@ module AgentAdmit
47
47
  def presence_verified?
48
48
  presence.is_a?(Hash) && presence["verified"] == true
49
49
  end
50
+
51
+ # `purpose` (String or nil) is the declared purpose: the user-facing
52
+ # reason recorded on the grant at the consent moment. Review-time record
53
+ # only, never an enforcement input; authorization decisions ride scopes,
54
+ # connection status, and consent.
50
55
  end
51
56
 
52
57
  def initialize(config = nil)
@@ -177,6 +182,12 @@ module AgentAdmit
177
182
  presence = data["presence"]
178
183
  presence = nil unless presence.is_a?(Hash) && [true, false].include?(presence["verified"])
179
184
 
185
+ # Declared purpose passes through as-is when it is a String (the
186
+ # hosted /verify returns it nullable). It is a review-time record,
187
+ # never an enforcement input, so a malformed value is simply dropped.
188
+ purpose = data["purpose"]
189
+ purpose = nil unless purpose.is_a?(String)
190
+
180
191
  return IntrospectionResult.new(
181
192
  user_id: data["user_id"],
182
193
  connection_id: data["connection_id"],
@@ -188,7 +199,8 @@ module AgentAdmit
188
199
  jti: data["jti"],
189
200
  exp: data["exp"],
190
201
  consent: consent,
191
- presence: presence
202
+ presence: presence,
203
+ purpose: purpose
192
204
  )
193
205
  end
194
206
 
@@ -15,6 +15,9 @@ module AgentAdmit
15
15
  # instead for an until-revoked connection (explicit JSON null).
16
16
  UNSET = Object.new.freeze
17
17
 
18
+ # Maximum length of a declared purpose (matches the hosted API contract).
19
+ PURPOSE_MAX_LENGTH = 300
20
+
18
21
  def initialize(config = nil)
19
22
  @config = config || AgentAdmit.configuration || Config.new
20
23
  @config.validate_api_key!
@@ -34,13 +37,24 @@ module AgentAdmit
34
37
  # @param scopes [Array<String>] scopes the connection grants
35
38
  # @param role [String, nil] the user's role on the connection
36
39
  # @param duration_seconds [Integer, nil, UNSET] see above
40
+ # @param purpose [String, nil] declared purpose: the user-facing reason
41
+ # recorded on the grant at the consent moment. Review-time record only,
42
+ # never an enforcement input; authorization decisions ride scopes,
43
+ # connection status, and consent. Max 300 characters; omitted from the
44
+ # request when nil.
37
45
  # @return [Hash] the issue response — "token" is the self-describing
38
46
  # ag_ct_… connection token to hand to the user's agent
47
+ # @raise [ArgumentError] if purpose exceeds 300 characters
39
48
  # @raise [IntrospectionError] if issuance fails
40
49
  #
41
- def issue_token(user_id:, scopes:, role: nil, duration_seconds: UNSET)
50
+ def issue_token(user_id:, scopes:, role: nil, duration_seconds: UNSET, purpose: nil)
51
+ if purpose && purpose.length > PURPOSE_MAX_LENGTH
52
+ raise ArgumentError, "purpose must be at most #{PURPOSE_MAX_LENGTH} characters"
53
+ end
54
+
42
55
  body = { "user_id" => user_id, "scopes" => scopes }
43
56
  body["role"] = role if role
57
+ body["purpose"] = purpose if purpose
44
58
  # Tri-state: the UNSET sentinel omits the key entirely; nil survives
45
59
  # JSON.generate as explicit JSON null (no compact, no nil-guard).
46
60
  body["duration_seconds"] = duration_seconds unless duration_seconds.equal?(UNSET)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentAdmit
4
- VERSION = "1.5.1"
4
+ VERSION = "1.7.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.5.1
4
+ version: 1.7.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-17 00:00:00.000000000 Z
11
+ date: 2026-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json