agentadmit 1.12.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 +4 -4
- data/README.md +94 -0
- data/lib/agentadmit/introspection_client.rb +183 -2
- data/lib/agentadmit/middleware.rb +34 -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: 4614ce271d54fa958f580f2180349bbdad92764566efe17d3a459c0fca62815c
|
|
4
|
+
data.tar.gz: 5c46517cd626d99eda4482cf47fdb14ae98b13f9a451216bcb117f6fd236f9c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
@@ -449,6 +472,77 @@ result = AgentAdmit::IntrospectionClient.new.verify(
|
|
|
449
472
|
|
|
450
473
|
The local `ScopeEnforcement` checks (`require_scope!`, `require_scope_if_agent!`) are unchanged -- defense in depth on top of the hosted decision.
|
|
451
474
|
|
|
475
|
+
### Outcome reporting
|
|
476
|
+
|
|
477
|
+
Successful verify responses may include `result.audit_row_id`, the hosted
|
|
478
|
+
audit row id for that call. After your handler runs, you can append what your
|
|
479
|
+
app observed:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
client = AgentAdmit::IntrospectionClient.new
|
|
483
|
+
result = client.verify(token, scope_used: "write:payments",
|
|
484
|
+
endpoint: request.path, method: request.request_method)
|
|
485
|
+
|
|
486
|
+
begin
|
|
487
|
+
# run the app action
|
|
488
|
+
client.report_outcome(result.audit_row_id,
|
|
489
|
+
outcome: "executed",
|
|
490
|
+
status_class: "2xx") if result.audit_row_id
|
|
491
|
+
rescue
|
|
492
|
+
client.report_outcome(result.audit_row_id,
|
|
493
|
+
outcome: "failed",
|
|
494
|
+
status_class: "5xx") if result.audit_row_id
|
|
495
|
+
raise
|
|
496
|
+
end
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
`outcome` must be `"executed"`, `"failed"`, or `"unknown"`. `unknown` is
|
|
500
|
+
only for an explicit "we cannot truthfully classify this" case; it is never
|
|
501
|
+
chosen automatically. `status_class` may be `"1xx"` through `"5xx"` or `nil`.
|
|
502
|
+
Outcome rows report what the app observed after verification; they are not
|
|
503
|
+
independent proof of execution.
|
|
504
|
+
|
|
505
|
+
Rack middleware can do the common status mapping for you:
|
|
506
|
+
|
|
507
|
+
```ruby
|
|
508
|
+
use AgentAdmit::Middleware,
|
|
509
|
+
scope_for: "write:payments",
|
|
510
|
+
action_summary: ->(env) { "Pay Alex $50" },
|
|
511
|
+
report_outcome: true
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
With `report_outcome: true`, the middleware reports only after the downstream
|
|
515
|
+
Rack app returns a response triple and the status has an observable class.
|
|
516
|
+
Statuses below 400 report `"executed"`; statuses 400 and above report
|
|
517
|
+
`"failed"`. If the app raises, aborts before returning a triple, returns no
|
|
518
|
+
observable status, or verify did not include `audit_row_id`, the middleware
|
|
519
|
+
does not guess. Reporting errors are logged and never replace the app's
|
|
520
|
+
response.
|
|
521
|
+
|
|
522
|
+
For downstream code, the Rack env includes `env["agentadmit.audit_row_id"]`
|
|
523
|
+
when the hosted service returned one.
|
|
524
|
+
|
|
525
|
+
### Replay receipts
|
|
526
|
+
|
|
527
|
+
When an agent retries with an already-consumed confirm-each-time attestation,
|
|
528
|
+
verify may return `result.consumed_receipt`. It is a diagnostic receipt for
|
|
529
|
+
the earlier consumption event:
|
|
530
|
+
|
|
531
|
+
```ruby
|
|
532
|
+
result.consumed_receipt
|
|
533
|
+
# => {
|
|
534
|
+
# "consumed_at" => "2026-09-30T02:54:07.000Z",
|
|
535
|
+
# "connection_id" => "conn_123",
|
|
536
|
+
# "chain_seq" => nil,
|
|
537
|
+
# "row_hash" => nil
|
|
538
|
+
# }
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
This is not a fresh authorization and must not be treated as permission to
|
|
542
|
+
run the action. It exists so agents and apps can explain an
|
|
543
|
+
`already_consumed` replay without staging another confirmation. Rack exposes
|
|
544
|
+
the typed block at `env["agentadmit.consumed_receipt"]` when present.
|
|
545
|
+
|
|
452
546
|
### Active-error responses are denials
|
|
453
547
|
|
|
454
548
|
An introspection response with `active: true` AND a string `error` field means the token itself is valid but the authorization service refused this call. The SDK treats every such response as a denial, never a pass-through -- the downstream app does not run:
|
|
@@ -27,11 +27,15 @@ module AgentAdmit
|
|
|
27
27
|
ATTESTATION_MAX = 120
|
|
28
28
|
DIGEST_MAX = 128
|
|
29
29
|
SUMMARY_MAX = 200
|
|
30
|
+
OUTCOMES = %w[executed failed unknown].freeze
|
|
31
|
+
STATUS_CLASSES = %w[1xx 2xx 3xx 4xx 5xx].freeze
|
|
30
32
|
|
|
31
33
|
IntrospectionResult = Struct.new(:user_id, :connection_id, :scopes, :agent_label,
|
|
32
34
|
:sub, :role, :app_id, :jti, :exp, :consent,
|
|
33
35
|
:presence, :purpose, :user_intent,
|
|
34
|
-
:action_confirmation,
|
|
36
|
+
:action_confirmation, :audit_row_id,
|
|
37
|
+
:consumed_receipt, :designated_agent,
|
|
38
|
+
:agent_reported, keyword_init: true) do
|
|
35
39
|
def has_scope?(scope)
|
|
36
40
|
scopes.include?(scope)
|
|
37
41
|
end
|
|
@@ -83,9 +87,48 @@ module AgentAdmit
|
|
|
83
87
|
def action_confirmed?
|
|
84
88
|
action_confirmation.is_a?(Hash) && action_confirmation["consumed"] == true
|
|
85
89
|
end
|
|
90
|
+
|
|
91
|
+
# `audit_row_id` (String or nil) identifies the hosted audit row for
|
|
92
|
+
# this successful verify call. Use it with #report_outcome after the
|
|
93
|
+
# application observes whether its handler executed or failed.
|
|
94
|
+
|
|
95
|
+
# `consumed_receipt` (Hash or nil) is a replay diagnostic for an
|
|
96
|
+
# already-consumed confirm-each-time attestation. It is not a fresh
|
|
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.
|
|
86
116
|
end
|
|
87
117
|
|
|
88
118
|
class << self
|
|
119
|
+
def status_class_for(status)
|
|
120
|
+
code = Integer(status) rescue nil
|
|
121
|
+
return nil unless code && (100..599).cover?(code)
|
|
122
|
+
|
|
123
|
+
"#{code / 100}xx"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def outcome_for_status(status)
|
|
127
|
+
status_class_for(status) ? (Integer(status) < 400 ? "executed" : "failed") : nil
|
|
128
|
+
rescue ArgumentError, TypeError
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
89
132
|
##
|
|
90
133
|
# `sha256:<hex>` over the RAW request body bytes, so a confirmation
|
|
91
134
|
# covers the exact payload and not merely the route. nil for an empty
|
|
@@ -151,6 +194,75 @@ module AgentAdmit
|
|
|
151
194
|
"summary" => nullable.call(raw["summary"]) }
|
|
152
195
|
end
|
|
153
196
|
|
|
197
|
+
##
|
|
198
|
+
# A strictly-typed replay diagnostic for an already-consumed action
|
|
199
|
+
# attestation, or nil when the block is absent/malformed. This is a
|
|
200
|
+
# receipt for an earlier consumption event, not authorization for this
|
|
201
|
+
# call.
|
|
202
|
+
#
|
|
203
|
+
def parse_consumed_receipt(raw)
|
|
204
|
+
return nil unless raw.is_a?(Hash)
|
|
205
|
+
return nil unless raw["consumed_at"].is_a?(String)
|
|
206
|
+
return nil unless raw["connection_id"].is_a?(String)
|
|
207
|
+
|
|
208
|
+
{ "consumed_at" => raw["consumed_at"],
|
|
209
|
+
"connection_id" => raw["connection_id"],
|
|
210
|
+
"chain_seq" => raw["chain_seq"].is_a?(Integer) ? raw["chain_seq"] : nil,
|
|
211
|
+
"row_hash" => raw["row_hash"].is_a?(String) ? raw["row_hash"] : nil }
|
|
212
|
+
end
|
|
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
|
+
|
|
154
266
|
##
|
|
155
267
|
# The agent's X-AgentAdmit-Action-Attestation header from a Rack env:
|
|
156
268
|
# first value only, trimmed, capped at 120 characters. nil when absent
|
|
@@ -363,6 +475,17 @@ module AgentAdmit
|
|
|
363
475
|
"consumed" => true }
|
|
364
476
|
end
|
|
365
477
|
|
|
478
|
+
audit_row_id = data["audit_row_id"]
|
|
479
|
+
audit_row_id = nil unless audit_row_id.is_a?(String)
|
|
480
|
+
consumed_receipt = self.class.parse_consumed_receipt(data["consumed_receipt"])
|
|
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
|
+
|
|
366
489
|
return IntrospectionResult.new(
|
|
367
490
|
user_id: data["user_id"],
|
|
368
491
|
connection_id: data["connection_id"],
|
|
@@ -377,7 +500,11 @@ module AgentAdmit
|
|
|
377
500
|
presence: presence,
|
|
378
501
|
purpose: purpose,
|
|
379
502
|
user_intent: user_intent,
|
|
380
|
-
action_confirmation: action_confirmation
|
|
503
|
+
action_confirmation: action_confirmation,
|
|
504
|
+
audit_row_id: audit_row_id,
|
|
505
|
+
consumed_receipt: consumed_receipt,
|
|
506
|
+
designated_agent: designated_agent,
|
|
507
|
+
agent_reported: agent_reported
|
|
381
508
|
)
|
|
382
509
|
end
|
|
383
510
|
|
|
@@ -385,6 +512,60 @@ module AgentAdmit
|
|
|
385
512
|
raise IntrospectionError, "Unexpected exit from retry loop"
|
|
386
513
|
end
|
|
387
514
|
|
|
515
|
+
##
|
|
516
|
+
# Report what the application observed after a successful verify call.
|
|
517
|
+
#
|
|
518
|
+
# This appends an outcome row to the hosted tamper-evident audit chain.
|
|
519
|
+
# It reports the app's observed result; it is not independent proof of
|
|
520
|
+
# execution. Use outcome: "unknown" only when you explicitly cannot
|
|
521
|
+
# classify the result.
|
|
522
|
+
#
|
|
523
|
+
# @param audit_row_id [String] result.audit_row_id from #verify
|
|
524
|
+
# @param outcome [String] "executed" | "failed" | "unknown"
|
|
525
|
+
# @param status_class [String, nil] "1xx".."5xx" or nil
|
|
526
|
+
# @return [Hash] parsed hosted outcome response
|
|
527
|
+
# @raise [ArgumentError] invalid arguments
|
|
528
|
+
# @raise [IntrospectionError] hosted service unreachable or rejected the report
|
|
529
|
+
#
|
|
530
|
+
def report_outcome(audit_row_id, outcome:, status_class: nil)
|
|
531
|
+
unless audit_row_id.is_a?(String) && !audit_row_id.empty?
|
|
532
|
+
raise ArgumentError, "audit_row_id is required"
|
|
533
|
+
end
|
|
534
|
+
unless OUTCOMES.include?(outcome)
|
|
535
|
+
raise ArgumentError, "outcome must be executed, failed, or unknown"
|
|
536
|
+
end
|
|
537
|
+
unless status_class.nil? || STATUS_CLASSES.include?(status_class)
|
|
538
|
+
raise ArgumentError, "status_class must be 1xx, 2xx, 3xx, 4xx, 5xx, or nil"
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
origin = @config.api_url.sub(%r{/\z}, "")
|
|
542
|
+
uri = URI.parse("#{origin}/api/v1/audit/#{audit_row_id}/outcome")
|
|
543
|
+
http = build_http(uri)
|
|
544
|
+
|
|
545
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
546
|
+
request["Authorization"] = "Bearer #{@config.api_key}"
|
|
547
|
+
request["Content-Type"] = "application/json"
|
|
548
|
+
request.body = JSON.generate(outcome: outcome, status_class: status_class)
|
|
549
|
+
|
|
550
|
+
response = begin
|
|
551
|
+
http.request(request)
|
|
552
|
+
rescue StandardError => e
|
|
553
|
+
raise IntrospectionError, "Outcome report failed: #{e.message}"
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
unless (200..299).cover?(response.code.to_i)
|
|
557
|
+
data = JSON.parse(response.body) rescue {}
|
|
558
|
+
raise IntrospectionError,
|
|
559
|
+
data["error_description"] || data["error"] || "Outcome report returned #{response.code}"
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
begin
|
|
563
|
+
JSON.parse(response.body)
|
|
564
|
+
rescue JSON::ParserError
|
|
565
|
+
raise IntrospectionError, "Outcome report response is not valid JSON"
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
388
569
|
CALLER_CLASSES = %w[human_session in_app_ai external_agent].freeze
|
|
389
570
|
|
|
390
571
|
##
|
|
@@ -12,6 +12,10 @@ module AgentAdmit
|
|
|
12
12
|
# env['agentadmit.connection_id'] -- connection identifier
|
|
13
13
|
# env['agentadmit.agent_label'] -- agent display name
|
|
14
14
|
# env['agentadmit.presence'] -- human-presence block (Hash) or nil
|
|
15
|
+
# env['agentadmit.audit_row_id'] -- hosted audit row id for this verify
|
|
16
|
+
# env['agentadmit.consumed_receipt']
|
|
17
|
+
# -- replay diagnostic for an already-
|
|
18
|
+
# consumed confirmation, or nil
|
|
15
19
|
# env['agentadmit.action_confirmation']
|
|
16
20
|
# -- {"action_session_id" =>, "consumed" => true}
|
|
17
21
|
# when a confirm-each-time confirmation
|
|
@@ -66,16 +70,25 @@ module AgentAdmit
|
|
|
66
70
|
# request-body digest for this middleware. AgentAdmit does not verify
|
|
67
71
|
# the summary against the request; it proves what the human was shown.
|
|
68
72
|
#
|
|
69
|
-
|
|
73
|
+
# @param report_outcome [Boolean] when true, reports the downstream
|
|
74
|
+
# Rack response status class to AgentAdmit after the app returns a
|
|
75
|
+
# response triple. Reporting is skipped when the app raises, returns no
|
|
76
|
+
# observable Rack triple, or has no audit_row_id. Reporting failures are
|
|
77
|
+
# logged and never replace the app's response.
|
|
78
|
+
#
|
|
79
|
+
def initialize(app, scope_for: nil, action_summary: nil, report_outcome: false,
|
|
80
|
+
&action_summary_block)
|
|
70
81
|
@app = app
|
|
71
82
|
@client = IntrospectionClient.new
|
|
72
83
|
@config = AgentAdmit.configuration || Config.new
|
|
73
84
|
@scope_for = scope_for
|
|
74
85
|
@action_summary = action_summary || action_summary_block
|
|
86
|
+
@report_outcome = report_outcome
|
|
75
87
|
end
|
|
76
88
|
|
|
77
89
|
def call(env)
|
|
78
90
|
auth = env["HTTP_AUTHORIZATION"] || ""
|
|
91
|
+
result = nil
|
|
79
92
|
|
|
80
93
|
if BEARER_AGENT_RE.match?(auth)
|
|
81
94
|
# Strip the scheme prefix (case-insensitively) to get the bare token.
|
|
@@ -96,6 +109,8 @@ module AgentAdmit
|
|
|
96
109
|
env["agentadmit.connection_id"] = result.connection_id
|
|
97
110
|
env["agentadmit.agent_label"] = result.agent_label
|
|
98
111
|
env["agentadmit.presence"] = result.presence
|
|
112
|
+
env["agentadmit.audit_row_id"] = result.audit_row_id
|
|
113
|
+
env["agentadmit.consumed_receipt"] = result.consumed_receipt if result.consumed_receipt
|
|
99
114
|
# Only set when the hosted service actually SPENT a confirmation on
|
|
100
115
|
# this call; the key stays absent otherwise, so `env.key?` is a
|
|
101
116
|
# truthful test.
|
|
@@ -116,7 +131,9 @@ module AgentAdmit
|
|
|
116
131
|
end
|
|
117
132
|
end
|
|
118
133
|
|
|
119
|
-
@app.call(env)
|
|
134
|
+
response = @app.call(env)
|
|
135
|
+
report_outcome_after_response(result, response) if @report_outcome && result
|
|
136
|
+
response
|
|
120
137
|
end
|
|
121
138
|
|
|
122
139
|
private
|
|
@@ -168,5 +185,20 @@ module AgentAdmit
|
|
|
168
185
|
rescue StandardError
|
|
169
186
|
nil
|
|
170
187
|
end
|
|
188
|
+
|
|
189
|
+
def report_outcome_after_response(result, response)
|
|
190
|
+
return unless result.audit_row_id
|
|
191
|
+
return unless response.is_a?(Array) && response.length >= 3
|
|
192
|
+
|
|
193
|
+
status_class = IntrospectionClient.status_class_for(response[0])
|
|
194
|
+
outcome = IntrospectionClient.outcome_for_status(response[0])
|
|
195
|
+
return unless status_class && outcome
|
|
196
|
+
|
|
197
|
+
@client.report_outcome(result.audit_row_id,
|
|
198
|
+
outcome: outcome,
|
|
199
|
+
status_class: status_class)
|
|
200
|
+
rescue StandardError => e
|
|
201
|
+
warn "[AgentAdmit] Outcome report failed: #{e.message}"
|
|
202
|
+
end
|
|
171
203
|
end
|
|
172
204
|
end
|
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.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-
|
|
11
|
+
date: 2026-11-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|