Class: AgentAdmit::CallerConsent

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

Overview

Caller-Identity Consent middleware: the "classify caller, then gate the right independent path" recipe as one Rack middleware, so an app owner does not have to hand-roll it.

One endpoint serves every caller class. On each request the middleware:

  1. classifies the caller from the STRUCTURE of the credential (a class the caller cannot self-select), before any consent check;
  2. routes to that class's ISOLATED consent path; no path reads or inherits another class's preference;
  3. permits or denies, and sets env variables (agentadmit.caller_class, agentadmit.consent, plus the standard agent env variables on the external-agent path).
  • external_agent: an ag_at_ access token -> hosted introspection, which returns the external-agent consent verdict inline plus the granted scopes. Enforced here directly.
  • in_app_ai: your application's own server-side AI code path -> the Consent Ledger /consent/check for the in-app-AI class.
  • human_session: your application's own permission model (sharing, roles, grants). Deferred to your existing authorization by default; opt in to a stored human-session switch with gate_human: true.

The three decisions are independent: granting one never grants another.

SECURITY: this is a consent gate, not an authenticator. It classifies the caller and enforces the per-class CONSENT decision; it does not by itself authenticate a human session. Mount it AFTER your own authentication. On the human_session path it defers to your application's permission model and calls the app without re-authenticating. The external_agent path is always authenticated (hosted introspection); the in_app_ai path always evaluates the ledger.

Usage:

use AgentAdmit::CallerConsent,
  # derive the class from your own credential structure, never caller input
  classify_non_agent: ->(env) {
    env["HTTP_X_INTERNAL_AI"] == ENV["INTERNAL_AI_SECRET"] ? "in_app_ai" : "human_session"
  },
  resolve_data_owner_id: ->(env) { Rack::Request.new(env).params["owner_id"] },
  required_scope: "read:records"

Constant Summary collapse

BEARER_AGENT_RE =

RFC 7235: the auth-scheme token is case-insensitive.

/\Abearer ag_at_/i
HUMAN_SESSION =
"human_session"
IN_APP_AI =
"in_app_ai"
EXTERNAL_AGENT =
"external_agent"

Instance Method Summary collapse

Constructor Details

#initialize(app, resolve_data_owner_id: nil, classify_non_agent: nil, required_scope: nil, scope_group: nil, gate_human: false) ⇒ CallerConsent

Returns a new instance of CallerConsent.

Parameters:

  • app (#call)

    the downstream Rack app

  • resolve_data_owner_id (Proc, nil) (defaults to: nil)

    env -> your app's identifier for the data owner whose resource is accessed. Required for the in_app_ai path, and for human_session when gate_human is set. The external-agent owner comes from the token, so it is not used there.

  • classify_non_agent (Proc, nil) (defaults to: nil)

    env -> "in_app_ai" or "human_session", derived from the STRUCTURE of the credential (for example an internal service token), never a value the caller can set. Defaults to treating non-agent callers as human sessions.

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

    for the external_agent path, require this scope (403 insufficient_scope if not granted).

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

    optional finer-than-class consent group for ledger checks.

  • gate_human (Boolean) (defaults to: false)

    also gate the human_session class against a stored switch. Off by default: the human path belongs to your own permission model.



73
74
75
76
77
78
79
80
81
82
# File 'lib/agentadmit/caller_consent.rb', line 73

def initialize(app, resolve_data_owner_id: nil, classify_non_agent: nil,
               required_scope: nil, scope_group: nil, gate_human: false)
  @app = app
  @client = IntrospectionClient.new
  @resolve_data_owner_id = resolve_data_owner_id
  @classify_non_agent = classify_non_agent
  @required_scope = required_scope
  @scope_group = scope_group
  @gate_human = gate_human
end

Instance Method Details

#call(env) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/agentadmit/caller_consent.rb', line 98

def call(env)
  caller_class = classify_caller(env)
  env["agentadmit.caller_class"] = caller_class

  case caller_class
  when EXTERNAL_AGENT
    call_external_agent(env)
  when IN_APP_AI
    call_ledger_gated(env, IN_APP_AI, "in_app_ai",
                      "The data owner has not enabled in-app AI analysis.")
  else
    if @gate_human
      call_ledger_gated(env, HUMAN_SESSION, "user",
                        "The data owner has not enabled this access.")
    else
      # Defer the human path to the app's existing authorization.
      env["agentadmit.auth_type"] = "user"
      @app.call(env)
    end
  end
end

#classify_caller(env) ⇒ Object

Classify the caller from credential structure, before any consent check. An ag_at_ bearer token is an external agent; anything else is resolved by classify_non_agent (default: human_session). The class is derived, never self-selected by the caller.



90
91
92
93
94
95
96
# File 'lib/agentadmit/caller_consent.rb', line 90

def classify_caller(env)
  auth = env["HTTP_AUTHORIZATION"] || ""
  return EXTERNAL_AGENT if BEARER_AGENT_RE.match?(auth)
  return IN_APP_AI if @classify_non_agent && @classify_non_agent.call(env) == IN_APP_AI

  HUMAN_SESSION
end