agentadmit 1.0.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 +7 -0
- data/LICENSE +56 -0
- data/README.md +142 -0
- data/lib/agentadmit/config.rb +25 -0
- data/lib/agentadmit/introspection_client.rb +155 -0
- data/lib/agentadmit/middleware.rb +47 -0
- data/lib/agentadmit/railtie.rb +15 -0
- data/lib/agentadmit/scope_enforcement.rb +70 -0
- data/lib/agentadmit/version.rb +5 -0
- data/lib/agentadmit.rb +55 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 71a63361a9ebd638c907411b5dcbce487f000d940c2618887d68ffe90a9bc5fa
|
|
4
|
+
data.tar.gz: c48534a09f1d07e05d56b34c88dfe2ecc64fcdc61564e880f2c1e948e18c9e67
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fc1d745572d89c57767e20bb6320d47816580d8d7a1a07d75333564d2b32ed562843a467483799f793d81b12d5ed185915d8afd376a206ee82029cebd5eb7d9c
|
|
7
|
+
data.tar.gz: 8f1f126ad0d27e99fac5e3e58fb43a6064a3078bae8b0b921ce8b3a8499caeaa16a249d7142180c84a76b8d26bc76cfcafaf073965b1e5f3591a83d4493b0889
|
data/LICENSE
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
AgentAdmit Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentAdmit LLC. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Patent Pending — U.S. Application No. 19/660,916
|
|
6
|
+
|
|
7
|
+
TERMS OF USE
|
|
8
|
+
|
|
9
|
+
1. GRANT OF LICENSE. AgentAdmit LLC ("Licensor") grants you a limited,
|
|
10
|
+
non-exclusive, non-transferable, revocable license to use this software
|
|
11
|
+
development kit ("SDK") solely for the purpose of integrating with the
|
|
12
|
+
AgentAdmit hosted service (api.agentadmit.com).
|
|
13
|
+
|
|
14
|
+
2. RESTRICTIONS. You may not:
|
|
15
|
+
(a) Use this SDK with any service other than the AgentAdmit hosted service;
|
|
16
|
+
(b) Modify, adapt, or create derivative works of this SDK for the purpose
|
|
17
|
+
of building or operating a competing service;
|
|
18
|
+
(c) Reverse engineer, decompile, or disassemble the AgentAdmit protocol
|
|
19
|
+
or service architecture;
|
|
20
|
+
(d) Remove or alter any proprietary notices, labels, or marks;
|
|
21
|
+
(e) Redistribute this SDK as a standalone product or as part of a
|
|
22
|
+
competing authorization service.
|
|
23
|
+
|
|
24
|
+
3. PERMITTED USES. You may:
|
|
25
|
+
(a) Install and use this SDK in your applications;
|
|
26
|
+
(b) Include this SDK as a dependency in your projects;
|
|
27
|
+
(c) Distribute your applications that incorporate this SDK, provided
|
|
28
|
+
those applications connect to the AgentAdmit hosted service.
|
|
29
|
+
|
|
30
|
+
4. AGENTADMIT SERVICE REQUIRED. This SDK is designed exclusively for use
|
|
31
|
+
with the AgentAdmit hosted service. An AgentAdmit account and valid API
|
|
32
|
+
keys are required. Sign up at https://agentadmit.com.
|
|
33
|
+
|
|
34
|
+
5. INTELLECTUAL PROPERTY. The AgentAdmit protocol, user-mediated token
|
|
35
|
+
delivery mechanism, mandatory introspection architecture, and related
|
|
36
|
+
inventions are protected by pending U.S. patent(s) and other intellectual
|
|
37
|
+
property rights. This license does not grant any rights to the underlying
|
|
38
|
+
patents or trade secrets.
|
|
39
|
+
|
|
40
|
+
6. NO WARRANTY. THIS SDK IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
|
41
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
|
|
42
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
43
|
+
|
|
44
|
+
7. LIMITATION OF LIABILITY. IN NO EVENT SHALL AGENTADMIT LLC BE LIABLE FOR
|
|
45
|
+
ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES
|
|
46
|
+
ARISING FROM YOUR USE OF THIS SDK.
|
|
47
|
+
|
|
48
|
+
8. TERMINATION. This license terminates automatically if you violate any
|
|
49
|
+
of its terms. Upon termination, you must cease all use and destroy all
|
|
50
|
+
copies of this SDK in your possession.
|
|
51
|
+
|
|
52
|
+
9. GOVERNING LAW. This license is governed by the laws of the State of
|
|
53
|
+
California, United States.
|
|
54
|
+
|
|
55
|
+
For licensing inquiries: legal@agentadmit.com
|
|
56
|
+
For developer support: https://agentadmit.com/docs
|
data/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# AgentAdmit SDK for Ruby (Rails)
|
|
2
|
+
|
|
3
|
+
User-mediated AI agent authorization. Plug-and-play for any Rails app.
|
|
4
|
+
|
|
5
|
+
> **Get started:** Sign up at [agentadmit.com](https://agentadmit.com) → Get your test keys → Install the SDK → Build.
|
|
6
|
+
> Test keys are available immediately after signup. Live keys become available when you subscribe an app.
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
# Gemfile
|
|
12
|
+
gem 'agentadmit'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
rails generate agentadmit:install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Add your credentials to `config/credentials.yml.enc` or `.env`:
|
|
21
|
+
|
|
22
|
+
```env
|
|
23
|
+
AGENTADMIT_APP_ID=app_yourappid
|
|
24
|
+
AGENTADMIT_API_KEY=aa_test_yourkey
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Add scope enforcement to any controller:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
class OrdersController < ApplicationController
|
|
31
|
+
before_action -> { require_scope_if_agent!('read:orders') }
|
|
32
|
+
|
|
33
|
+
def index
|
|
34
|
+
render json: current_user.orders
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Your app now supports AI agent connections with:
|
|
40
|
+
- Scoped access control (you define the scopes)
|
|
41
|
+
- User-controlled connection duration
|
|
42
|
+
- Token generation and exchange
|
|
43
|
+
- Mandatory introspection (every agent request validated through AgentAdmit)
|
|
44
|
+
- Revocation and audit logging
|
|
45
|
+
- Discovery endpoint at `/.well-known/agentadmit`
|
|
46
|
+
|
|
47
|
+
## How It Works
|
|
48
|
+
|
|
49
|
+
1. User clicks "AgentAdmit" in your app
|
|
50
|
+
2. Selects scopes and connection duration
|
|
51
|
+
3. Gets a token to give to their AI agent
|
|
52
|
+
4. Agent exchanges the token for scoped API access
|
|
53
|
+
5. User revokes anytime
|
|
54
|
+
|
|
55
|
+
The token goes to the human, not the agent. No automated delivery = no prompt injection surface.
|
|
56
|
+
|
|
57
|
+
## Important
|
|
58
|
+
|
|
59
|
+
**Mandatory introspection.** All token validation goes through api.agentadmit.com. There is no self-hosted mode. No local JWT validation. No bypass. This is required for security, audit logging, and scope enforcement.
|
|
60
|
+
|
|
61
|
+
**Admin revocation.** As the app operator, you can revoke any user's agent connection via `DELETE /agentadmit/admin/connections/{connection_id}` (requires admin role or `manage:connections` scope).
|
|
62
|
+
|
|
63
|
+
**Embeddable admin panel.** Drop the `<AgentAdmitAdminPanel>` React component into your admin section to view all agent connections, usage metrics, billing status, and revoke any connection without leaving your app. See the React SDK for details.
|
|
64
|
+
|
|
65
|
+
**In-app AI scopes.** If your app has built-in AI features (analysis, plan generation, photo recognition), do not expose those as agent scopes. The user's AI agent can read the raw data and do the analysis itself. Exposing in-app AI endpoints to agents creates double cost.
|
|
66
|
+
|
|
67
|
+
## Rate Limiting
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
### Retry behavior
|
|
72
|
+
|
|
73
|
+
| Parameter | Default | Description |
|
|
74
|
+
|-----------|---------|-------------|
|
|
75
|
+
| Initial delay | 1 second | First retry wait |
|
|
76
|
+
| Backoff multiplier | 2× | Doubles each retry |
|
|
77
|
+
| Cap | 30 seconds | Maximum wait per retry |
|
|
78
|
+
| Jitter | 0–500 ms | Random addition to each delay |
|
|
79
|
+
| Max retries | **3** | Configurable |
|
|
80
|
+
|
|
81
|
+
The SDK also respects the `Retry-After` response header — if present, it overrides the computed backoff delay.
|
|
82
|
+
|
|
83
|
+
### Configuring max retries
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
AgentAdmit.configure do |config|
|
|
87
|
+
config.max_retries = 5 # default: 3
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or via environment variable:
|
|
92
|
+
|
|
93
|
+
```env
|
|
94
|
+
AGENTADMIT_MAX_RETRIES=5
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Handling exhausted retries
|
|
98
|
+
|
|
99
|
+
When all retries are exhausted, `IntrospectionClient#verify` raises `AgentAdmit::RateLimitError`:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
begin
|
|
103
|
+
result = client.verify(token)
|
|
104
|
+
rescue AgentAdmit::RateLimitError => e
|
|
105
|
+
render json: { error: 'rate_limited', retry_after: e.retry_after }, status: 429
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`RateLimitError` attributes:
|
|
110
|
+
- `retry_after` — seconds from `Retry-After` header (`nil` if absent)
|
|
111
|
+
- `limit` — `X-RateLimit-Limit` header value (`nil` if absent)
|
|
112
|
+
- `remaining` — `X-RateLimit-Remaining` header value (`nil` if absent)
|
|
113
|
+
- `reset` — `X-RateLimit-Reset` Unix timestamp (`nil` if absent)
|
|
114
|
+
|
|
115
|
+
## Documentation
|
|
116
|
+
|
|
117
|
+
Full integration guide: https://agentadmit.com/docs/app-owner-guide
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
## Data Collection & Privacy
|
|
121
|
+
|
|
122
|
+
The AgentAdmit Ruby SDK runs server-side and does not interact with app stores or end-user devices directly.
|
|
123
|
+
|
|
124
|
+
### What the SDK does
|
|
125
|
+
- Validates AgentAdmit tokens presented by AI agents
|
|
126
|
+
- Enforces scope-based access control on your API routes
|
|
127
|
+
- Manages connection lifecycle (create, revoke, audit)
|
|
128
|
+
|
|
129
|
+
### What the SDK does NOT do
|
|
130
|
+
- Does not collect end-user data
|
|
131
|
+
- Does not send telemetry or analytics
|
|
132
|
+
- Does not phone home to AgentAdmit servers (all operations use your configured keys and storage)
|
|
133
|
+
- Does not track users or devices
|
|
134
|
+
|
|
135
|
+
### Privacy impact
|
|
136
|
+
Since this SDK runs on your server, it has no direct App Store or Play Store compliance surface. Your client-side integration (e.g., the AgentAdmit React SDK) handles privacy manifest and data safety requirements.
|
|
137
|
+
|
|
138
|
+
For complete compliance guidance, see our [compliance guide](https://agentadmit.com/docs/compliance).
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
All rights reserved. Patent pending.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# IMPORTANT: AgentAdmit uses MANDATORY hosted introspection.
|
|
4
|
+
# All token validation goes through api.agentadmit.com.
|
|
5
|
+
# There is no self-hosted mode. No local JWT validation. No bypass.
|
|
6
|
+
# This is required for security, audit logging, and scope enforcement.
|
|
7
|
+
|
|
8
|
+
module AgentAdmit
|
|
9
|
+
class Config
|
|
10
|
+
attr_accessor :app_id, :api_key, :verify_url, :api_url,
|
|
11
|
+
:token_prefix_access, :token_prefix_connection,
|
|
12
|
+
:max_retries
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@app_id = ENV.fetch("AGENTADMIT_APP_ID", "")
|
|
16
|
+
@api_key = ENV.fetch("AGENTADMIT_API_KEY", "")
|
|
17
|
+
@verify_url = ENV.fetch("AGENTADMIT_VERIFY_URL", "https://api.agentadmit.com/v1/verify")
|
|
18
|
+
@api_url = ENV.fetch("AGENTADMIT_API_URL", "https://api.agentadmit.com")
|
|
19
|
+
@token_prefix_access = "ag_at_"
|
|
20
|
+
@token_prefix_connection = "ag_ct_"
|
|
21
|
+
# Max retries on HTTP 429 before raising RateLimitError. Default: 3.
|
|
22
|
+
@max_retries = ENV.fetch("AGENTADMIT_MAX_RETRIES", "3").to_i
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module AgentAdmit
|
|
8
|
+
##
|
|
9
|
+
# Mandatory introspection client — validates tokens via AgentAdmit hosted service.
|
|
10
|
+
# No local JWT decode. Every verification call goes through AgentAdmit.
|
|
11
|
+
#
|
|
12
|
+
class IntrospectionClient
|
|
13
|
+
IntrospectionResult = Struct.new(:user_id, :connection_id, :scopes, :agent_label, keyword_init: true) do
|
|
14
|
+
def has_scope?(scope)
|
|
15
|
+
scopes.include?(scope)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(config = nil)
|
|
20
|
+
@config = config || AgentAdmit.configuration || Config.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# Validate an ag_at_ token via introspection.
|
|
25
|
+
#
|
|
26
|
+
# Automatically retries on HTTP 429 with exponential backoff + jitter.
|
|
27
|
+
# Raises {RateLimitError} when retries are exhausted.
|
|
28
|
+
#
|
|
29
|
+
# @param token [String] The full token including ag_at_ prefix
|
|
30
|
+
# @return [IntrospectionResult]
|
|
31
|
+
# @raise [InvalidTokenError] if validation fails
|
|
32
|
+
# @raise [IntrospectionError] if the service is unreachable
|
|
33
|
+
# @raise [RateLimitError] if rate-limited and retries exhausted
|
|
34
|
+
#
|
|
35
|
+
def verify(token)
|
|
36
|
+
unless token.start_with?(@config.token_prefix_access)
|
|
37
|
+
raise InvalidTokenError, "Not an AgentAdmit access token"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
max_retries = @config.respond_to?(:max_retries) ? @config.max_retries.to_i : 3
|
|
41
|
+
delay_ms = 1000 # initial backoff in milliseconds
|
|
42
|
+
|
|
43
|
+
uri = URI.parse(@config.verify_url)
|
|
44
|
+
http = build_http(uri)
|
|
45
|
+
|
|
46
|
+
(0..max_retries).each do |attempt|
|
|
47
|
+
request = build_request(uri, token)
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
response = http.request(request)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
raise IntrospectionError, "Introspection failed: #{e.message}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
status = response.code.to_i
|
|
56
|
+
|
|
57
|
+
if status == 429
|
|
58
|
+
retry_after = parse_float_header(response, "Retry-After")
|
|
59
|
+
rl_limit = parse_int_header(response, "X-RateLimit-Limit")
|
|
60
|
+
rl_remaining = parse_int_header(response, "X-RateLimit-Remaining")
|
|
61
|
+
rl_reset = parse_int_header(response, "X-RateLimit-Reset")
|
|
62
|
+
|
|
63
|
+
if attempt >= max_retries
|
|
64
|
+
raise RateLimitError.new(
|
|
65
|
+
"AgentAdmit rate limit exceeded. Max retries (#{max_retries}) exhausted.",
|
|
66
|
+
retry_after: retry_after,
|
|
67
|
+
limit: rl_limit,
|
|
68
|
+
remaining: rl_remaining,
|
|
69
|
+
reset: rl_reset
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Compute wait: honor Retry-After header or use exponential backoff, cap at 30s
|
|
74
|
+
wait_ms = retry_after ? (retry_after * 1000).ceil : [delay_ms, 30_000].min
|
|
75
|
+
jitter_ms = rand(0..500)
|
|
76
|
+
total_ms = wait_ms + jitter_ms
|
|
77
|
+
|
|
78
|
+
warn "[AgentAdmit] Rate-limited (attempt #{attempt + 1}/#{max_retries}). " \
|
|
79
|
+
"Retrying in #{total_ms}ms."
|
|
80
|
+
|
|
81
|
+
sleep(total_ms / 1000.0)
|
|
82
|
+
delay_ms = [delay_ms * 2, 30_000].min
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Non-429 response — process normally
|
|
87
|
+
case status
|
|
88
|
+
when 200
|
|
89
|
+
data = JSON.parse(response.body)
|
|
90
|
+
|
|
91
|
+
# Check active flag (RFC 7662 introspection pattern).
|
|
92
|
+
# The verify endpoint returns {active: false} with HTTP 200 for invalid/
|
|
93
|
+
# expired/revoked tokens. Without this check, we'd read empty scopes.
|
|
94
|
+
unless data["active"]
|
|
95
|
+
reason = data["error"] || "invalid_token"
|
|
96
|
+
raise InvalidTokenError, "Token is not active: #{reason}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
raise InvalidTokenError, "Introspection returned no user" if data["user_id"].nil?
|
|
100
|
+
|
|
101
|
+
return IntrospectionResult.new(
|
|
102
|
+
user_id: data["user_id"],
|
|
103
|
+
connection_id: data["connection_id"],
|
|
104
|
+
scopes: data["scopes"] || [],
|
|
105
|
+
agent_label: data["agent_label"] || "Unknown Agent"
|
|
106
|
+
)
|
|
107
|
+
when 401
|
|
108
|
+
data = JSON.parse(response.body) rescue {}
|
|
109
|
+
raise InvalidTokenError, data["error_description"] || "Token validation failed"
|
|
110
|
+
else
|
|
111
|
+
raise IntrospectionError, "Verification service returned #{response.code}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Should never be reached
|
|
116
|
+
raise IntrospectionError, "Unexpected exit from retry loop"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def build_http(uri)
|
|
122
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
123
|
+
http.use_ssl = uri.scheme == "https"
|
|
124
|
+
http.read_timeout = 5
|
|
125
|
+
http.open_timeout = 5
|
|
126
|
+
http
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_request(uri, token)
|
|
130
|
+
req = Net::HTTP::Post.new(uri.path)
|
|
131
|
+
req["Authorization"] = "Bearer #{@config.api_key}"
|
|
132
|
+
req["Content-Type"] = "application/json"
|
|
133
|
+
req.body = JSON.generate({ token: token })
|
|
134
|
+
req
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# Parse a response header as Float, returning nil if absent or non-numeric.
|
|
139
|
+
#
|
|
140
|
+
def parse_float_header(response, name)
|
|
141
|
+
val = response[name]
|
|
142
|
+
return nil if val.nil? || val.empty?
|
|
143
|
+
Float(val) rescue nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
##
|
|
147
|
+
# Parse a response header as Integer, returning nil if absent or non-numeric.
|
|
148
|
+
#
|
|
149
|
+
def parse_int_header(response, name)
|
|
150
|
+
val = response[name]
|
|
151
|
+
return nil if val.nil? || val.empty?
|
|
152
|
+
Integer(val) rescue nil
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentAdmit
|
|
4
|
+
##
|
|
5
|
+
# Rack middleware that intercepts requests with ag_at_ tokens
|
|
6
|
+
# and validates them via introspection.
|
|
7
|
+
#
|
|
8
|
+
# Sets env variables for downstream use:
|
|
9
|
+
# env['agentadmit.auth_type'] — "agent" or nil
|
|
10
|
+
# env['agentadmit.user_id'] — validated user ID
|
|
11
|
+
# env['agentadmit.scopes'] — granted scopes array
|
|
12
|
+
# env['agentadmit.connection_id'] — connection identifier
|
|
13
|
+
# env['agentadmit.agent_label'] — agent display name
|
|
14
|
+
#
|
|
15
|
+
class Middleware
|
|
16
|
+
def initialize(app)
|
|
17
|
+
@app = app
|
|
18
|
+
@client = IntrospectionClient.new
|
|
19
|
+
@config = AgentAdmit.configuration || Config.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(env)
|
|
23
|
+
auth = env["HTTP_AUTHORIZATION"] || ""
|
|
24
|
+
|
|
25
|
+
if auth.start_with?("Bearer #{@config.token_prefix_access}")
|
|
26
|
+
token = auth.sub("Bearer ", "")
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
result = @client.verify(token)
|
|
30
|
+
env["agentadmit.auth_type"] = "agent"
|
|
31
|
+
env["agentadmit.user_id"] = result.user_id
|
|
32
|
+
env["agentadmit.scopes"] = result.scopes
|
|
33
|
+
env["agentadmit.connection_id"] = result.connection_id
|
|
34
|
+
env["agentadmit.agent_label"] = result.agent_label
|
|
35
|
+
rescue InvalidTokenError => e
|
|
36
|
+
return [401, { "Content-Type" => "application/json" },
|
|
37
|
+
[{ error: "invalid_token", error_description: e.message }.to_json]]
|
|
38
|
+
rescue IntrospectionError => e
|
|
39
|
+
return [502, { "Content-Type" => "application/json" },
|
|
40
|
+
[{ error: "introspection_failed", error_description: e.message }.to_json]]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@app.call(env)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentAdmit
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
initializer "agentadmit.middleware" do |app|
|
|
6
|
+
app.middleware.use AgentAdmit::Middleware
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
initializer "agentadmit.configure" do
|
|
10
|
+
AgentAdmit.configure do |config|
|
|
11
|
+
# Config loaded from environment variables by default
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentAdmit
|
|
4
|
+
##
|
|
5
|
+
# Controller concern for scope enforcement in Rails controllers.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# class OrdersController < ApplicationController
|
|
9
|
+
# include AgentAdmit::ScopeEnforcement
|
|
10
|
+
#
|
|
11
|
+
# before_action -> { require_scope!("read:orders") }, only: [:index, :show]
|
|
12
|
+
# before_action -> { require_scope_if_agent!("create:orders") }, only: [:create]
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
module ScopeEnforcement
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Enforce scope — agent MUST have this scope or gets 403.
|
|
22
|
+
#
|
|
23
|
+
def require_scope!(scope)
|
|
24
|
+
unless request.env["agentadmit.auth_type"] == "agent"
|
|
25
|
+
render json: { error: "invalid_token", error_description: "AgentAdmit token required" }, status: :unauthorized
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
scopes = request.env["agentadmit.scopes"] || []
|
|
30
|
+
unless scopes.include?(scope)
|
|
31
|
+
render json: {
|
|
32
|
+
error: "insufficient_scope",
|
|
33
|
+
required_scope: scope,
|
|
34
|
+
granted_scopes: scopes,
|
|
35
|
+
message: "This action requires '#{scope}' scope."
|
|
36
|
+
}, status: :forbidden
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# Enforce scope only for agent tokens. Regular users pass through.
|
|
42
|
+
#
|
|
43
|
+
def require_scope_if_agent!(scope)
|
|
44
|
+
return unless request.env["agentadmit.auth_type"] == "agent"
|
|
45
|
+
|
|
46
|
+
scopes = request.env["agentadmit.scopes"] || []
|
|
47
|
+
unless scopes.include?(scope)
|
|
48
|
+
render json: {
|
|
49
|
+
error: "insufficient_scope",
|
|
50
|
+
required_scope: scope,
|
|
51
|
+
granted_scopes: scopes,
|
|
52
|
+
message: "This action requires '#{scope}' scope."
|
|
53
|
+
}, status: :forbidden
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Get the current agent/user context.
|
|
59
|
+
#
|
|
60
|
+
def agentadmit_context
|
|
61
|
+
{
|
|
62
|
+
auth_type: request.env["agentadmit.auth_type"],
|
|
63
|
+
user_id: request.env["agentadmit.user_id"],
|
|
64
|
+
scopes: request.env["agentadmit.scopes"],
|
|
65
|
+
connection_id: request.env["agentadmit.connection_id"],
|
|
66
|
+
agent_label: request.env["agentadmit.agent_label"],
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/agentadmit.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "agentadmit/version"
|
|
4
|
+
require_relative "agentadmit/config"
|
|
5
|
+
require_relative "agentadmit/introspection_client"
|
|
6
|
+
require_relative "agentadmit/middleware"
|
|
7
|
+
require_relative "agentadmit/scope_enforcement"
|
|
8
|
+
require_relative "agentadmit/railtie" if defined?(Rails)
|
|
9
|
+
|
|
10
|
+
module AgentAdmit
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
class InvalidTokenError < Error; end
|
|
13
|
+
class InsufficientScopeError < Error; end
|
|
14
|
+
class IntrospectionError < Error; end
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# Raised when the AgentAdmit introspection endpoint returns HTTP 429 and
|
|
18
|
+
# all retry attempts (with exponential backoff + jitter) have been exhausted.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# begin
|
|
22
|
+
# client.verify(token)
|
|
23
|
+
# rescue AgentAdmit::RateLimitError => e
|
|
24
|
+
# render json: { error: 'rate_limited', retry_after: e.retry_after }, status: 429
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
class RateLimitError < Error
|
|
28
|
+
# @return [Float, nil] Seconds to wait before retrying (Retry-After header), or nil.
|
|
29
|
+
attr_reader :retry_after
|
|
30
|
+
# @return [Integer, nil] X-RateLimit-Limit value, or nil.
|
|
31
|
+
attr_reader :limit
|
|
32
|
+
# @return [Integer, nil] X-RateLimit-Remaining value, or nil.
|
|
33
|
+
attr_reader :remaining
|
|
34
|
+
# @return [Integer, nil] X-RateLimit-Reset Unix timestamp, or nil.
|
|
35
|
+
attr_reader :reset
|
|
36
|
+
|
|
37
|
+
def initialize(message = "AgentAdmit rate limit exceeded. Max retries exhausted.",
|
|
38
|
+
retry_after: nil, limit: nil, remaining: nil, reset: nil)
|
|
39
|
+
super(message)
|
|
40
|
+
@retry_after = retry_after
|
|
41
|
+
@limit = limit
|
|
42
|
+
@remaining = remaining
|
|
43
|
+
@reset = reset
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
attr_accessor :configuration
|
|
49
|
+
|
|
50
|
+
def configure
|
|
51
|
+
self.configuration ||= Config.new
|
|
52
|
+
yield(configuration) if block_given?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: agentadmit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Christopher Emerson
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: json
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description: Integrate AgentAdmit into your Rails app. Mandatory introspection, scope
|
|
28
|
+
enforcement, and secure AI agent connections.
|
|
29
|
+
email:
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- lib/agentadmit.rb
|
|
37
|
+
- lib/agentadmit/config.rb
|
|
38
|
+
- lib/agentadmit/introspection_client.rb
|
|
39
|
+
- lib/agentadmit/middleware.rb
|
|
40
|
+
- lib/agentadmit/railtie.rb
|
|
41
|
+
- lib/agentadmit/scope_enforcement.rb
|
|
42
|
+
- lib/agentadmit/version.rb
|
|
43
|
+
homepage: https://agentadmit.com/docs
|
|
44
|
+
licenses:
|
|
45
|
+
- Nonstandard
|
|
46
|
+
metadata: {}
|
|
47
|
+
post_install_message:
|
|
48
|
+
rdoc_options: []
|
|
49
|
+
require_paths:
|
|
50
|
+
- lib
|
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 3.1.0
|
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
requirements: []
|
|
62
|
+
rubygems_version: 3.0.3.1
|
|
63
|
+
signing_key:
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: AgentAdmit SDK for Ruby on Rails — User-mediated AI agent authorization
|
|
66
|
+
test_files: []
|