Class: AgentAdmit::TokensClient

Inherits:
Object
  • Object
show all
Defined in:
lib/agentadmit/tokens_client.rb

Overview

TokensClient — issue, exchange, and revoke connection tokens via the AgentAdmit hosted service.

Constant Summary collapse

UNSET =

Sentinel for issue_token's duration_seconds: leave the field out of the request entirely, so AgentAdmit applies its default (30 days). Pass nil instead for an until-revoked connection (explicit JSON null).

Object.new.freeze
PURPOSE_MAX_LENGTH =

Maximum length of a declared purpose (matches the hosted API contract).

300
USER_INTENT_MAX_LENGTH =

Maximum length of a user-declared intent (matches the hosted API contract: optional string, 1..300 characters).

300

Instance Method Summary collapse

Constructor Details

#initialize(config = nil) ⇒ TokensClient

Returns a new instance of TokensClient.



25
26
27
28
# File 'lib/agentadmit/tokens_client.rb', line 25

def initialize(config = nil)
  @config = config || AgentAdmit.configuration || Config.new
  @config.validate_api_key!
end

Instance Method Details

#exchange(connection_token, agent_label: nil, agent_id: nil) ⇒ Hash

Exchange a single-use connection token for an access token. Calls POST /api/v1/exchange — unauthenticated by design: the connection token itself is the credential, so the operator API key is NOT sent.

Parameters:

  • connection_token (String)

    the ag_ct_… connection token

  • agent_label (String, nil) (defaults to: nil)

    human-readable agent name

  • agent_id (String, nil) (defaults to: nil)

    agent identifier

Returns:

  • (Hash)

    the exchange response — "access_token" is the ag_at_… token

Raises:



102
103
104
105
106
107
108
# File 'lib/agentadmit/tokens_client.rb', line 102

def exchange(connection_token, agent_label: nil, agent_id: nil)
  body = { "token" => connection_token }
  body["agent_label"] = agent_label if agent_label
  body["agent_id"] = agent_id if agent_id

  post("/api/v1/exchange", body, authenticated: false, op: "exchange")
end

#issue_token(user_id:, scopes:, role: nil, duration_seconds: UNSET, purpose: nil, user_intent: nil) ⇒ Hash

Issue a connection token for one of your users. Calls POST /api/v1/apps/app_id/token.

The duration is tri-state:

  • omit the argument — field omitted; AgentAdmit applies its default (30 days)
  • nil — explicit JSON null; the connection lasts until revoked
  • Integer — explicit duration in seconds (60–31536000)

Parameters:

  • user_id (String)

    your app's identifier for the user

  • scopes (Array<String>)

    scopes the connection grants

  • role (String, nil) (defaults to: nil)

    the user's role on the connection

  • duration_seconds (Integer, nil, UNSET) (defaults to: UNSET)

    see above

  • purpose (String, nil) (defaults to: nil)

    declared purpose: the user-facing reason recorded on the grant at the consent moment. Review-time record only, never an enforcement input; authorization decisions ride scopes, connection status, and consent. Max 300 characters; omitted from the request when nil.

  • user_intent (String, nil) (defaults to: nil)

    user-declared intent: the user's OWN words, typed at the consent moment (distinct from purpose, which is the app's words). Optional, 1-300 characters. Validated like purpose: a non-String, non-nil value or a string over 300 characters raises ArgumentError before any request is sent — silently discarding the user's typed words would be data loss. Empty/whitespace-only strings normalize to nil and are omitted. Like purpose, it is a review-time record, never an enforcement input.

Returns:

  • (Hash)

    the issue response — "token" is the self-describing ag_ct_… connection token to hand to the user's agent

Raises:

  • (ArgumentError)

    if purpose exceeds 300 characters, or if user_intent is a non-String (other than nil) or exceeds 300 characters

  • (IntrospectionError)

    if issuance fails



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
# File 'lib/agentadmit/tokens_client.rb', line 63

def issue_token(user_id:, scopes:, role: nil, duration_seconds: UNSET, purpose: nil,
                user_intent: nil)
  if purpose && purpose.length > PURPOSE_MAX_LENGTH
    raise ArgumentError, "purpose must be at most #{PURPOSE_MAX_LENGTH} characters"
  end

  # User-declared intent is validated like purpose: reject out-of-contract
  # values before any request rather than silently discarding the user's
  # typed words (data loss). Empty/whitespace-only normalizes to nil-omit.
  unless user_intent.nil? || user_intent.is_a?(String)
    raise ArgumentError, "user_intent must be a String or nil"
  end
  if user_intent && user_intent.length > USER_INTENT_MAX_LENGTH
    raise ArgumentError, "user_intent must be at most #{USER_INTENT_MAX_LENGTH} characters"
  end
  user_intent = nil if user_intent && user_intent.strip.empty?

  body = { "user_id" => user_id, "scopes" => scopes }
  body["role"] = role if role
  body["purpose"] = purpose if purpose
  body["user_intent"] = user_intent if user_intent
  # Tri-state: the UNSET sentinel omits the key entirely; nil survives
  # JSON.generate as explicit JSON null (no compact, no nil-guard).
  body["duration_seconds"] = duration_seconds unless duration_seconds.equal?(UNSET)

  post("/api/v1/apps/#{@config.app_id}/token", body, authenticated: true, op: "issue_token")
end

#revoke(connection_id, reason: nil) ⇒ Hash

Revoke a connection (and its access tokens). Calls POST /api/v1/revoke.

Parameters:

  • connection_id (String)

    the connection to revoke

  • reason (String, nil) (defaults to: nil)

    optional human-readable reason

Returns:

  • (Hash)

    the revoke response — { "ok" => true, ... }

Raises:



119
120
121
122
123
124
# File 'lib/agentadmit/tokens_client.rb', line 119

def revoke(connection_id, reason: nil)
  body = { "connection_id" => connection_id }
  body["reason"] = reason if reason

  post("/api/v1/revoke", body, authenticated: true, op: "revoke")
end