agentadmit 1.3.0 → 1.4.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: 74299884ea21ad2cb70261b0d2e7025bf4bc07e330f76bfc44009af8bba3b71b
4
- data.tar.gz: 846ec2f9b8ed3afe7e9113fa7e1c1bf4b2fd7b074ce0f61e96550347db6c185b
3
+ metadata.gz: bc3085f5469fd5380afb22df98cf1517e1f74df3a23cf7ab07d7f6c490cd9feb
4
+ data.tar.gz: fb0142a79357486644fff3e68903194b998cf5b41c1f140a8bda8f7a2f0c2909
5
5
  SHA512:
6
- metadata.gz: 25964578da91a2be9763398021fad033e9538cf6270c2fe99d66185c4da6d884514bcf9c9cfd9a2f60d017e0aeecdeb62d9d6ce1ef19ebfa543eb274cf2cc688
7
- data.tar.gz: fdba91838161e85923cca0e4f1c84d72b724ad7bcf8e1be1c248f6af5ab1c1ec2506345e27016e7c8b3a7a989c5c65c3474fa263c23466cc7b100ec06b4263fc
6
+ metadata.gz: e54b1b2f7c357b546388d09a9accf94c29c98964ade0d5888e87962d3d30219486cf30b932234d503e46d6ea80e4a6348a165a6172a09b4d182390709c605713
7
+ data.tar.gz: 3d237b464785de4d0a8a081d992dd7313c77c84a3bdac92f0a3a9dc41a2909137cfec0415d7eb67dd4408c1eb772d24ff514e996184ad0d580b9f047fcd0a59d
data/README.md CHANGED
@@ -96,6 +96,27 @@ head :forbidden unless verdict["granted"]
96
96
 
97
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
98
 
99
+ ### Presence verification
100
+
101
+ AgentAdmit can attest that the human who authorized a connection completed a WebAuthn presence ceremony on the consent page. The verify result carries the fact:
102
+
103
+ ```ruby
104
+ result = AgentAdmit::IntrospectionClient.new.verify(token)
105
+ result.presence_verified? # true only when the ceremony completed
106
+ ```
107
+
108
+ For sensitive actions, enforce it in your controllers the same way you enforce scopes:
109
+
110
+ ```ruby
111
+ class TransfersController < ApplicationController
112
+ include AgentAdmit::ScopeEnforcement
113
+
114
+ before_action -> { require_presence! }, only: [:create]
115
+ end
116
+ ```
117
+
118
+ `require_presence!` fails closed: agents whose connection was minted without a completed ceremony get a 403 `presence_required`, and so do connections from servers that predate the feature. `presence_verified?` returns true only on an explicit boolean `verified: true`; absent or malformed presence data reads as not verified.
119
+
99
120
  ## Rate Limiting
100
121
 
101
122
  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,7 +16,8 @@ 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, :consent, keyword_init: true) do
19
+ :sub, :role, :app_id, :jti, :exp, :consent,
20
+ :presence, keyword_init: true) do
20
21
  def has_scope?(scope)
21
22
  scopes.include?(scope)
22
23
  end
@@ -34,6 +35,16 @@ module AgentAdmit
34
35
  return true if consent.nil?
35
36
  consent["granted"] == true
36
37
  end
38
+
39
+ # Human-presence fact from the WebAuthn step-up (additive; may be nil).
40
+ # True ONLY when the connection was authorized by a human who completed
41
+ # a presence ceremony on the consent page: verified must be the boolean
42
+ # true. Unlike consent, absence fails closed -- older servers never send
43
+ # the block, and connections minted without a ceremony carry
44
+ # verified: false, so nil/false/malformed all read as not verified.
45
+ def presence_verified?
46
+ presence.is_a?(Hash) && presence["verified"] == true
47
+ end
37
48
  end
38
49
 
39
50
  def initialize(config = nil)
@@ -157,6 +168,13 @@ module AgentAdmit
157
168
  consent = data["consent"]
158
169
  consent = nil unless consent.is_a?(Hash)
159
170
 
171
+ # Presence rides along when the platform returns it. Same strictness
172
+ # as active: verified must be the boolean true or false, never coerced.
173
+ # Unlike consent, a malformed block is dropped -- presence_verified?
174
+ # fails closed on nil, so dropping cannot fail open.
175
+ presence = data["presence"]
176
+ presence = nil unless presence.is_a?(Hash) && [true, false].include?(presence["verified"])
177
+
160
178
  return IntrospectionResult.new(
161
179
  user_id: data["user_id"],
162
180
  connection_id: data["connection_id"],
@@ -167,7 +185,8 @@ module AgentAdmit
167
185
  app_id: data["app_id"],
168
186
  jti: data["jti"],
169
187
  exp: data["exp"],
170
- consent: consent
188
+ consent: consent,
189
+ presence: presence
171
190
  )
172
191
  end
173
192
 
@@ -11,6 +11,7 @@ module AgentAdmit
11
11
  # env['agentadmit.scopes'] -- granted scopes array
12
12
  # env['agentadmit.connection_id'] -- connection identifier
13
13
  # env['agentadmit.agent_label'] -- agent display name
14
+ # env['agentadmit.presence'] -- human-presence block (Hash) or nil
14
15
  #
15
16
  class Middleware
16
17
  # RFC 7235: the auth-scheme token is case-insensitive.
@@ -37,6 +38,7 @@ module AgentAdmit
37
38
  env["agentadmit.scopes"] = result.scopes
38
39
  env["agentadmit.connection_id"] = result.connection_id
39
40
  env["agentadmit.agent_label"] = result.agent_label
41
+ env["agentadmit.presence"] = result.presence
40
42
  rescue InvalidTokenError => e
41
43
  return [401, { "Content-Type" => "application/json" },
42
44
  [{ error: "invalid_token", error_description: e.message }.to_json]]
@@ -10,6 +10,7 @@ module AgentAdmit
10
10
  #
11
11
  # before_action -> { require_scope!("read:orders") }, only: [:index, :show]
12
12
  # before_action -> { require_scope_if_agent!("create:orders") }, only: [:create]
13
+ # before_action -> { require_presence! }, only: [:destroy]
13
14
  # end
14
15
  #
15
16
  module ScopeEnforcement
@@ -54,6 +55,27 @@ module AgentAdmit
54
55
  end
55
56
  end
56
57
 
58
+ ##
59
+ # Enforce human-presence verification -- agent's connection MUST have been
60
+ # authorized with a completed WebAuthn presence ceremony or gets 403.
61
+ # Fail closed: connections from servers that predate the presence feature
62
+ # (no presence block) are treated as not verified.
63
+ #
64
+ def require_presence!
65
+ unless request.env["agentadmit.auth_type"] == "agent"
66
+ render json: { error: "invalid_token", error_description: "AgentAdmit token required" }, status: :unauthorized
67
+ return
68
+ end
69
+
70
+ presence = request.env["agentadmit.presence"]
71
+ unless presence.is_a?(Hash) && presence["verified"] == true
72
+ render json: {
73
+ error: "presence_required",
74
+ error_description: "This action requires a connection authorized with human presence verification."
75
+ }, status: :forbidden
76
+ end
77
+ end
78
+
57
79
  ##
58
80
  # Get the current agent/user context.
59
81
  #
@@ -64,6 +86,7 @@ module AgentAdmit
64
86
  scopes: request.env["agentadmit.scopes"],
65
87
  connection_id: request.env["agentadmit.connection_id"],
66
88
  agent_label: request.env["agentadmit.agent_label"],
89
+ presence: request.env["agentadmit.presence"],
67
90
  }
68
91
  end
69
92
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentAdmit
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.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.3.0
4
+ version: 1.4.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-04 00:00:00.000000000 Z
11
+ date: 2026-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json