agentadmit 1.11.0 → 1.12.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 +14 -0
- data/lib/agentadmit/introspection_client.rb +52 -0
- data/lib/agentadmit/version.rb +1 -1
- data/lib/agentadmit.rb +52 -0
- 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: 35cfc75d6a091ef7f5940c488c472efad1c6d48a68c670e6c5e1522dbe9abb06
|
|
4
|
+
data.tar.gz: 0a9c2ecbc1fcdab6255e709519625cc1dbc9944d36d2b9725d5069a44544b222
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 39bb513f8747187ffeb5d7bb73f4557c47a9da0a95f1bd8b56130f95b38406081befb47570f425afbdfbc1ca322069459c5ebf3e4fcb1f4c164320242c306882
|
|
7
|
+
data.tar.gz: 35577380cb5d5e783fe05e68c67c291238de414dd9b8fb99cb4d9ea8f75a5befed74c501ccd50e1a2eade6ffe3dc3d009d712ef5f44f1b1ba41e7928d8c20a99
|
data/README.md
CHANGED
|
@@ -177,6 +177,19 @@ fail-closed `ActiveDenialError` instances with no link. On an accepted retry,
|
|
|
177
177
|
expose the consumed ceremony so your own transaction step-up can avoid asking
|
|
178
178
|
the human twice.
|
|
179
179
|
|
|
180
|
+
**The user can decline.** If the user taps Decline on the hosted page, the
|
|
181
|
+
hosted service answers the agent's retry with `confirmation_declined` and
|
|
182
|
+
holds that answer until `declined["hold_until"]`; no new ceremony is staged
|
|
183
|
+
and the user is not notified again. Middlewares return 403 with the strictly
|
|
184
|
+
typed `declined` block (`action_session_id`, `declined_at`, `hold_until`,
|
|
185
|
+
`scope`, plus nullable `method`, `endpoint`, `request_digest`, `summary`) and
|
|
186
|
+
`renewal`; custom gates receive `AgentAdmit::ConfirmationDeclinedError` (an
|
|
187
|
+
`ActiveDenialError`) with `.declined` and `.attestation_status`. Agents should
|
|
188
|
+
relay the decline to the user and not retry unless the user asks; only the
|
|
189
|
+
user can lift a decline, and after the hold ends a retry stages a fresh
|
|
190
|
+
confirmation. A malformed `declined` block degrades to a generic 403 with no
|
|
191
|
+
block.
|
|
192
|
+
|
|
180
193
|
## Rate Limiting
|
|
181
194
|
|
|
182
195
|
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.
|
|
@@ -443,6 +456,7 @@ An introspection response with `active: true` AND a string `error` field means t
|
|
|
443
456
|
- `insufficient_scope` -> `AgentAdmit::InsufficientScopeError`; middlewares return 403 with the step-up shape (`error`, `required_scope`, `granted_scopes`)
|
|
444
457
|
- `bound_exceeded` -> `AgentAdmit::BoundExceededError`; middlewares return 403 passing the hosted fields (`error_description`, `bound`, `renewal`) through verbatim
|
|
445
458
|
- `confirmation_required` -> `AgentAdmit::ConfirmationRequiredError`; middlewares return 403 with the strictly typed `confirmation` block (`action_session_id`, `action_session_url`, `expires_at`, `scope`, ...) plus `attestation_status`/`attestation_description`/`renewal` when the hosted service sent them, so the agent can relay the confirmation link to the human (see [Confirm Each Time](#confirm-each-time-exercise-time-human-confirmation)); a malformed `confirmation` block degrades to the generic `ActiveDenialError` shape with no link
|
|
459
|
+
- `confirmation_declined` -> `AgentAdmit::ConfirmationDeclinedError`; middlewares return 403 with the strictly typed `declined` block (`action_session_id`, `declined_at`, `hold_until`, `scope`, ...) plus `attestation_status`/`attestation_description`/`renewal` when sent, so the agent can relay the user's decline instead of nagging with a link; a malformed `declined` block degrades to the generic shape with no block
|
|
446
460
|
- any other error string -> `AgentAdmit::ActiveDenialError`; middlewares return 403 with `{error: <code>, error_description: "Call refused by the authorization service."}` -- unknown codes fail closed
|
|
447
461
|
|
|
448
462
|
All four inherit from `AgentAdmit::ActiveDenialError` and expose `#code`, `#data` (the parsed hosted response), and `#denial_body` (the ready-made 403 JSON body) for apps that call `verify` directly.
|
|
@@ -125,6 +125,32 @@ module AgentAdmit
|
|
|
125
125
|
"summary" => nullable.call(raw["summary"]) }
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
+
##
|
|
129
|
+
# A strictly-typed copy of the wire `declined` block (SDK 1.12.0), or
|
|
130
|
+
# nil when it is malformed. action_session_id, declined_at, hold_until
|
|
131
|
+
# and scope must be Strings; method, endpoint, request_digest and
|
|
132
|
+
# summary are nullable Strings (anything else reads as nil). Nothing
|
|
133
|
+
# outside the contract is copied through.
|
|
134
|
+
#
|
|
135
|
+
# @param raw [Object] the `declined` value from the hosted response
|
|
136
|
+
# @return [Hash, nil]
|
|
137
|
+
#
|
|
138
|
+
def parse_action_decline(raw)
|
|
139
|
+
return nil unless raw.is_a?(Hash)
|
|
140
|
+
return nil unless %w[action_session_id declined_at hold_until scope]
|
|
141
|
+
.all? { |key| raw[key].is_a?(String) }
|
|
142
|
+
|
|
143
|
+
nullable = ->(value) { value.is_a?(String) ? value : nil }
|
|
144
|
+
{ "action_session_id" => raw["action_session_id"],
|
|
145
|
+
"declined_at" => raw["declined_at"],
|
|
146
|
+
"hold_until" => raw["hold_until"],
|
|
147
|
+
"scope" => raw["scope"],
|
|
148
|
+
"method" => nullable.call(raw["method"]),
|
|
149
|
+
"endpoint" => nullable.call(raw["endpoint"]),
|
|
150
|
+
"request_digest" => nullable.call(raw["request_digest"]),
|
|
151
|
+
"summary" => nullable.call(raw["summary"]) }
|
|
152
|
+
end
|
|
153
|
+
|
|
128
154
|
##
|
|
129
155
|
# The agent's X-AgentAdmit-Action-Attestation header from a Rack env:
|
|
130
156
|
# first value only, trimmed, capped at 120 characters. nil when absent
|
|
@@ -462,6 +488,10 @@ module AgentAdmit
|
|
|
462
488
|
# typed, so the agent can hand the link to the human. A malformed
|
|
463
489
|
# ceremony block degrades to the generic denial -- fail closed rather
|
|
464
490
|
# than relay an unusable confirmation.
|
|
491
|
+
# - confirmation_declined: the user declined this exact action on the
|
|
492
|
+
# hosted page; the hold rides along, strictly typed, so the agent can
|
|
493
|
+
# relay the decline to the user. A malformed block degrades to the
|
|
494
|
+
# generic denial.
|
|
465
495
|
# - anything else: unknown refusal -> generic typed denial. Fail closed.
|
|
466
496
|
#
|
|
467
497
|
def raise_active_denial!(data, scope_used)
|
|
@@ -499,6 +529,28 @@ module AgentAdmit
|
|
|
499
529
|
"Call refused by the authorization service.",
|
|
500
530
|
code: "confirmation_required", data: data
|
|
501
531
|
)
|
|
532
|
+
when "confirmation_declined"
|
|
533
|
+
# SDK 1.12.0: the user declined exactly this action on the hosted
|
|
534
|
+
# page and the hold still runs. Relay the decline so the agent can
|
|
535
|
+
# tell the user instead of nagging with a link.
|
|
536
|
+
declined = self.class.parse_action_decline(data["declined"])
|
|
537
|
+
if declined
|
|
538
|
+
description = data["error_description"]
|
|
539
|
+
description = ConfirmationDeclinedError::DESCRIPTION unless
|
|
540
|
+
description.is_a?(String) && !description.empty?
|
|
541
|
+
status = data["attestation_status"]
|
|
542
|
+
raise ConfirmationDeclinedError.new(
|
|
543
|
+
description,
|
|
544
|
+
declined: declined,
|
|
545
|
+
attestation_status: status.is_a?(String) ? status : nil,
|
|
546
|
+
data: data
|
|
547
|
+
)
|
|
548
|
+
end
|
|
549
|
+
# Malformed decline: generic denial, fail closed, no block.
|
|
550
|
+
raise ActiveDenialError.new(
|
|
551
|
+
"Call refused by the authorization service.",
|
|
552
|
+
code: "confirmation_declined", data: data
|
|
553
|
+
)
|
|
502
554
|
else
|
|
503
555
|
raise ActiveDenialError.new(
|
|
504
556
|
"Call refused by the authorization service.",
|
data/lib/agentadmit/version.rb
CHANGED
data/lib/agentadmit.rb
CHANGED
|
@@ -154,6 +154,58 @@ module AgentAdmit
|
|
|
154
154
|
end
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
+
##
|
|
158
|
+
# `active: true` + `error: "confirmation_declined"` (SDK 1.12.0,
|
|
159
|
+
# confirm-each-time) -- the user declined exactly this action on the hosted
|
|
160
|
+
# confirmation page, and the hosted service holds that answer until
|
|
161
|
+
# `declined["hold_until"]`. No new ceremony is staged and the user is not
|
|
162
|
+
# notified again while the hold runs. {#declined} is the strictly-typed
|
|
163
|
+
# block naming the declined session, when, how long, and the exact action.
|
|
164
|
+
#
|
|
165
|
+
# Agents should relay the decline to the user and not retry unless the user
|
|
166
|
+
# asks; only the user can lift a decline. After the hold ends, a retry
|
|
167
|
+
# stages a fresh confirmation.
|
|
168
|
+
#
|
|
169
|
+
# {#attestation_status} explains why a presented attestation was NOT
|
|
170
|
+
# accepted, when one was presented (e.g. declined).
|
|
171
|
+
#
|
|
172
|
+
# A malformed declined block never reaches this class: the client falls
|
|
173
|
+
# back to a generic {ActiveDenialError} (fail closed, 403, no declined
|
|
174
|
+
# block).
|
|
175
|
+
#
|
|
176
|
+
class ConfirmationDeclinedError < ActiveDenialError
|
|
177
|
+
# The canonical agent-facing description. The hosted `error_description`
|
|
178
|
+
# wins when the service sends one.
|
|
179
|
+
DESCRIPTION = "The user declined this action on the hosted confirmation page. " \
|
|
180
|
+
"Do not retry it unless the user asks you to."
|
|
181
|
+
|
|
182
|
+
# @return [Hash] the strictly-typed decline: action_session_id,
|
|
183
|
+
# declined_at, hold_until, scope (Strings), and method, endpoint,
|
|
184
|
+
# request_digest, summary (String or nil).
|
|
185
|
+
attr_reader :declined
|
|
186
|
+
# @return [String, nil] why a presented attestation was not accepted.
|
|
187
|
+
attr_reader :attestation_status
|
|
188
|
+
|
|
189
|
+
def initialize(message = DESCRIPTION, declined:, attestation_status: nil, data: {})
|
|
190
|
+
super(message, code: "confirmation_declined", data: data)
|
|
191
|
+
@declined = declined
|
|
192
|
+
@attestation_status = attestation_status
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# The 403 body: the refusal, the human-readable description, the decline,
|
|
196
|
+
# and the hosted attestation diagnostics when present. Nothing else from
|
|
197
|
+
# the wire -- a refusal must not leak identity or scope state.
|
|
198
|
+
def denial_body
|
|
199
|
+
body = { "error" => "confirmation_declined",
|
|
200
|
+
"error_description" => message,
|
|
201
|
+
"declined" => declined }
|
|
202
|
+
%w[attestation_status attestation_description renewal].each do |field|
|
|
203
|
+
body[field] = data[field] if data[field].is_a?(String)
|
|
204
|
+
end
|
|
205
|
+
body
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
157
209
|
class IntrospectionError < Error; end
|
|
158
210
|
class ConfigurationError < Error; end
|
|
159
211
|
|
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.12.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-
|
|
11
|
+
date: 2026-09-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|