agentadmit 1.8.0 → 1.9.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: 9903cf0e0365cc82696a35748e5736582e4d2e9aad33e9c956c62df61c4c5eea
4
- data.tar.gz: e1cbbc38ef3ca551b003574383e759056f3c6f90c2224fc4cd8dc68af836c270
3
+ metadata.gz: 677087b98e731945e7485c16db8c8a858dabc54fa346565b3697948db26f9fcb
4
+ data.tar.gz: 189ff20ab78520f0ae0ce4a7db1cd32ba8067153c5c5787f4cd04984730eece3
5
5
  SHA512:
6
- metadata.gz: c3089318cad6fd8c1914a3f087b9e9e2351ee0131b81d6bd288a9fa736868ce6c64937de4a248c0c3c4162c2d4b481e044ef2c642d917d368499e42dd6a72e93
7
- data.tar.gz: 2f2d55a5ea98bc761ccabd6cce0e3e5ac417b0b5b659094c05ca74178abd77752e392c6b6104621ab750ba97aa1264f5b4d541e47c4a243e20b037b1417b69ab
6
+ metadata.gz: 4e374766292936272c50ca95fd2b63ae97f5cb044d194a879dc268009e453630406e090fb57227dcb3dc19f964a04ec3c12dd7b40500cbfd1f1f6b218771e7cc
7
+ data.tar.gz: c18398441c0d63526cb6fce609907eea627830a7dc71803688542a29950d8b0cf7ed1bd9d30dc3c0be3c154f53ba4f568707b855c3e01e001f5cdbc7ed9a1ff6
data/README.md CHANGED
@@ -349,3 +349,22 @@ result.user_intent # => "Book my flights to the Austin offsite in October" or ni
349
349
  ```
350
350
 
351
351
  `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.
352
+
353
+ ## App-Attested Presence
354
+
355
+ 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:
356
+
357
+ ```ruby
358
+ issued = tokens.issue_token(
359
+ user_id: "user_42",
360
+ scopes: ["read:orders"],
361
+ presence: AgentAdmit::AppAttestedPresence.new(
362
+ method: "my_webauthn", # lowercase alphanumeric/underscore
363
+ verified_at: attestation.created_at # Time or DateTime
364
+ )
365
+ )
366
+ ```
367
+
368
+ The SDK sends it as `presence: {verified: true, uv: true, method, verified_at}` -- `verified`/`uv` are literal true by construction and the class cannot represent anything else; a raw Hash is rejected so the wire contract stays owned by the typed class. The hosted service validates freshness (10-minute window, 60 s future clock-skew slack) and stores the method provenance-marked `app:<method>` so app-attested facts stay distinct from ceremonies AgentAdmit witnessed itself. Introspection, the grant-event ledger, and the evidence API then carry `presence.verified: true` for the connection.
369
+
370
+ Honesty ceiling: this is your app's attestation, recorded and provenance-marked. It is not witnessed by AgentAdmit and not independently verifiable. Only attest a ceremony that verified the user with UV (biometric or PIN user verification); a ceremony without UV carries no presence fact, so pass `nil` (the default). An out-of-contract method (`^[a-z0-9_]+$`, 1-60) raises `ArgumentError` at construction, before any request; Ruby `Time`/`DateTime` always carry an offset, so `verified_at` serializes RFC 3339 with an explicit offset by construction.
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module AgentAdmit
6
+ ##
7
+ # App-attested presence: a ceremony fact your app attests at token issuance.
8
+ #
9
+ # Pass an instance to TokensClient#issue_token AFTER verifying and consuming
10
+ # your app's own fresh, purpose-bound WebAuthn/passkey attestation for the
11
+ # mint. The SDK forwards it to the hosted mint as
12
+ # presence {verified: true, uv: true, method, verified_at}; the hosted
13
+ # service stores it method-prefixed "app:<method>" — the provenance marker
14
+ # that keeps app-attested facts distinct from hosted-witnessed ceremonies.
15
+ #
16
+ # Honesty ceiling: this is YOUR attestation, recorded and provenance-marked.
17
+ # It is not witnessed by AgentAdmit and not independently verifiable. Only
18
+ # construct one for a ceremony that verified the user with UV (biometric or
19
+ # PIN user verification); verified/uv serialize as literal true and cannot
20
+ # represent anything else — a ceremony without UV carries no presence fact,
21
+ # so simply pass nil.
22
+ #
23
+ # verified_at must be recent: the hosted service enforces a 10-minute
24
+ # freshness window with 60 seconds of future clock-skew slack. Ruby Time and
25
+ # DateTime always carry an offset, so #iso8601 serializes RFC 3339 with an
26
+ # explicit offset by construction (the hosted contract; offset-less
27
+ # timestamps are rejected with 400).
28
+ #
29
+ class AppAttestedPresence
30
+ METHOD_PATTERN = /\A[a-z0-9_]+\z/
31
+ METHOD_MAX_LENGTH = 60
32
+
33
+ # NOTE: a +method+ reader shadows Object#method on instances — the same
34
+ # trade stdlib's Net::HTTPGenericRequest makes; the name matches the wire
35
+ # field.
36
+ attr_reader :method, :verified_at
37
+
38
+ ##
39
+ # @param method [String] your ceremony mechanism, 1-60 lowercase
40
+ # alphanumeric/underscore characters (e.g. "my_webauthn")
41
+ # @param verified_at [Time, DateTime] when the ceremony completed
42
+ # @raise [ArgumentError] when method is out of contract or verified_at is
43
+ # not a timestamp — validated at construction, before any request,
44
+ # where the fix is obvious
45
+ #
46
+ def initialize(method:, verified_at:)
47
+ unless method.is_a?(String) && !method.empty? &&
48
+ method.length <= METHOD_MAX_LENGTH && METHOD_PATTERN.match?(method)
49
+ raise ArgumentError,
50
+ "method must be 1-#{METHOD_MAX_LENGTH} lowercase alphanumeric/underscore " \
51
+ "characters (e.g. 'my_webauthn')"
52
+ end
53
+ unless verified_at.respond_to?(:iso8601)
54
+ raise ArgumentError,
55
+ "verified_at must be a Time or DateTime (the ceremony that authorized " \
56
+ "this mint just happened)"
57
+ end
58
+
59
+ @method = method
60
+ @verified_at = verified_at
61
+ end
62
+
63
+ ##
64
+ # The exact JSON object forwarded to the hosted mint.
65
+ #
66
+ # @return [Hash]
67
+ #
68
+ def to_wire
69
+ {
70
+ "verified" => true,
71
+ "uv" => true,
72
+ "method" => @method,
73
+ "verified_at" => @verified_at.iso8601
74
+ }
75
+ end
76
+ end
77
+ end
@@ -54,14 +54,21 @@ module AgentAdmit
54
54
  # user's typed words would be data loss. Empty/whitespace-only strings
55
55
  # normalize to nil and are omitted. Like purpose, it is a review-time
56
56
  # record, never an enforcement input.
57
+ # @param presence [AppAttestedPresence, nil] app-attested ceremony fact:
58
+ # set it AFTER verifying and consuming your app's own fresh,
59
+ # purpose-bound WebAuthn/passkey attestation for this mint. Forwarded
60
+ # as presence {verified: true, uv: true, method, verified_at} and
61
+ # stored provenance-marked "app:<method>"; omitted when nil (omitting
62
+ # the field is the only way to say "no ceremony").
57
63
  # @return [Hash] the issue response — "token" is the self-describing
58
64
  # ag_ct_… connection token to hand to the user's agent
59
- # @raise [ArgumentError] if purpose exceeds 300 characters, or if
60
- # user_intent is a non-String (other than nil) or exceeds 300 characters
65
+ # @raise [ArgumentError] if purpose exceeds 300 characters, if
66
+ # user_intent is a non-String (other than nil) or exceeds 300
67
+ # characters, or if presence is neither nil nor an AppAttestedPresence
61
68
  # @raise [IntrospectionError] if issuance fails
62
69
  #
63
70
  def issue_token(user_id:, scopes:, role: nil, duration_seconds: UNSET, purpose: nil,
64
- user_intent: nil)
71
+ user_intent: nil, presence: nil)
65
72
  if purpose && purpose.length > PURPOSE_MAX_LENGTH
66
73
  raise ArgumentError, "purpose must be at most #{PURPOSE_MAX_LENGTH} characters"
67
74
  end
@@ -77,10 +84,18 @@ module AgentAdmit
77
84
  end
78
85
  user_intent = nil if user_intent && user_intent.strip.empty?
79
86
 
87
+ # Presence is typed-only: a raw Hash is rejected so the wire contract
88
+ # (literal-true verified/uv, offset-carrying verified_at) stays owned
89
+ # by AppAttestedPresence, never hand-rolled at call sites.
90
+ unless presence.nil? || presence.is_a?(AppAttestedPresence)
91
+ raise ArgumentError, "presence must be an AgentAdmit::AppAttestedPresence or nil"
92
+ end
93
+
80
94
  body = { "user_id" => user_id, "scopes" => scopes }
81
95
  body["role"] = role if role
82
96
  body["purpose"] = purpose if purpose
83
97
  body["user_intent"] = user_intent if user_intent
98
+ body["presence"] = presence.to_wire if presence
84
99
  # Tri-state: the UNSET sentinel omits the key entirely; nil survives
85
100
  # JSON.generate as explicit JSON null (no compact, no nil-guard).
86
101
  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.8.0"
4
+ VERSION = "1.9.0"
5
5
  end
data/lib/agentadmit.rb CHANGED
@@ -88,6 +88,7 @@ end
88
88
 
89
89
  require_relative "agentadmit/config"
90
90
  require_relative "agentadmit/introspection_client"
91
+ require_relative "agentadmit/app_attested_presence"
91
92
  require_relative "agentadmit/tokens_client"
92
93
  require_relative "agentadmit/alerts_client"
93
94
  require_relative "agentadmit/webhook"
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.8.0
4
+ version: 1.9.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-08-11 00:00:00.000000000 Z
11
+ date: 2026-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -35,6 +35,7 @@ files:
35
35
  - README.md
36
36
  - lib/agentadmit.rb
37
37
  - lib/agentadmit/alerts_client.rb
38
+ - lib/agentadmit/app_attested_presence.rb
38
39
  - lib/agentadmit/caller_consent.rb
39
40
  - lib/agentadmit/config.rb
40
41
  - lib/agentadmit/introspection_client.rb