Class: AgentAdmit::IntrospectionClient
- Inherits:
-
Object
- Object
- AgentAdmit::IntrospectionClient
- Defined in:
- lib/agentadmit/introspection_client.rb
Overview
Mandatory introspection client — validates tokens via AgentAdmit hosted service. No local JWT decode. Every verification call goes through AgentAdmit.
Defined Under Namespace
Classes: IntrospectionResult
Instance Method Summary collapse
-
#initialize(config = nil) ⇒ IntrospectionClient
constructor
A new instance of IntrospectionClient.
-
#verify(token) ⇒ IntrospectionResult
Validate an ag_at_ token via introspection.
Constructor Details
#initialize(config = nil) ⇒ IntrospectionClient
Returns a new instance of IntrospectionClient.
19 20 21 |
# File 'lib/agentadmit/introspection_client.rb', line 19 def initialize(config = nil) @config = config || AgentAdmit.configuration || Config.new end |
Instance Method Details
#verify(token) ⇒ IntrospectionResult
Validate an ag_at_ token via introspection.
Automatically retries on HTTP 429 with exponential backoff + jitter. Raises RateLimitError when retries are exhausted.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/agentadmit/introspection_client.rb', line 35 def verify(token) unless token.start_with?(@config.token_prefix_access) raise InvalidTokenError, "Not an AgentAdmit access token" end max_retries = @config.respond_to?(:max_retries) ? @config.max_retries.to_i : 3 delay_ms = 1000 # initial backoff in milliseconds uri = URI.parse(@config.verify_url) http = build_http(uri) (0..max_retries).each do |attempt| request = build_request(uri, token) begin response = http.request(request) rescue StandardError => e raise IntrospectionError, "Introspection failed: #{e.}" end status = response.code.to_i if status == 429 retry_after = parse_float_header(response, "Retry-After") rl_limit = parse_int_header(response, "X-RateLimit-Limit") rl_remaining = parse_int_header(response, "X-RateLimit-Remaining") rl_reset = parse_int_header(response, "X-RateLimit-Reset") if attempt >= max_retries raise RateLimitError.new( "AgentAdmit rate limit exceeded. Max retries (#{max_retries}) exhausted.", retry_after: retry_after, limit: rl_limit, remaining: rl_remaining, reset: rl_reset ) end # Compute wait: honor Retry-After header or use exponential backoff, cap at 30s wait_ms = retry_after ? (retry_after * 1000).ceil : [delay_ms, 30_000].min jitter_ms = rand(0..500) total_ms = wait_ms + jitter_ms warn "[AgentAdmit] Rate-limited (attempt #{attempt + 1}/#{max_retries}). " \ "Retrying in #{total_ms}ms." sleep(total_ms / 1000.0) delay_ms = [delay_ms * 2, 30_000].min next end # Non-429 response — process normally case status when 200 data = JSON.parse(response.body) # Check active flag (RFC 7662 introspection pattern). # The verify endpoint returns {active: false} with HTTP 200 for invalid/ # expired/revoked tokens. Without this check, we'd read empty scopes. unless data["active"] reason = data["error"] || "invalid_token" raise InvalidTokenError, "Token is not active: #{reason}" end raise InvalidTokenError, "Introspection returned no user" if data["user_id"].nil? return IntrospectionResult.new( user_id: data["user_id"], connection_id: data["connection_id"], scopes: data["scopes"] || [], agent_label: data["agent_label"] || "Unknown Agent" ) when 401 data = JSON.parse(response.body) rescue {} raise InvalidTokenError, data["error_description"] || "Token validation failed" else raise IntrospectionError, "Verification service returned #{response.code}" end end # Should never be reached raise IntrospectionError, "Unexpected exit from retry loop" end |