Skip to content

feat: delegated auth support — token forwarding and principal-based resolver#36

Open
marcbaque wants to merge 6 commits intomainfrom
feat/delegated-auth-resolver
Open

feat: delegated auth support — token forwarding and principal-based resolver#36
marcbaque wants to merge 6 commits intomainfrom
feat/delegated-auth-resolver

Conversation

@marcbaque
Copy link
Copy Markdown

@marcbaque marcbaque commented Apr 10, 2026

🚪 Why?

Mastra agents need to authenticate with the Factorial backend when triggered by backend events (no user session). This PR adds the full delegated auth chain to ruby-ai-client: forwarding tokens to Mastra via HTTP headers, and a delegated_auth: parameter on Ai::Agent that resolves principals (Access or Company) to tokens through a configurable callback — so callers just pass the principal without manually minting tokens.

🔑 What?

Token forwarding:

  • Ai::Client base class, Ai::Clients::Mastra, and Ai::Clients::Test accept an optional delegated_token: keyword on generate and run_workflow.
  • Mastra client forwards it as X-Factorial-Delegated-Bearer HTTP header on all requests (agent generate, workflow create-run, stream, and result-fetch).
  • Tests verify the header is sent when token is provided and absent when not.

Principal-based resolver:

  • New Ai.delegated_token_resolver config accessor — the host application sets a lambda that resolves a principal (e.g. Access, Company) to a token string.
  • Ai::Agent#generate_text and #generate_object accept delegated_auth: (any principal). Internally calls the resolver to get the token, then passes it to the client layer.
  • delegated_auth: nil (the default) skips resolution entirely — agents that don't need backend auth work unchanged.
  • Raises Ai::Error if delegated_auth: is provided but no resolver is configured.
  • Full test coverage: resolver integration, nil passthrough, missing-resolver error.

📐 New interfaces

Ai::Agent#generate_text / #generate_object — new delegated_auth: parameter

Pass an Access or Company principal to authenticate Mastra's downstream calls to the backend. The agent resolves it to a Doorkeeper token internally via the configured resolver. Optional — agents that don't call the backend omit it.

# With Company auth (background jobs, Sidekiq — no user session)
agent.generate_object(messages: msgs, output_class: MyResult, delegated_auth: company)

# With Access auth (user-initiated flows)
agent.generate_text(messages: msgs, delegated_auth: access)

# No auth needed (unchanged) — agents that don't call the backend
agent.generate_text(messages: msgs)

Ai.delegated_token_resolver — new config accessor

The host application configures a lambda that maps a principal to a token string. Set once in an initializer:

# config/initializers/ai_client.rb
Ai.delegated_token_resolver = lambda { |principal|
  outcome =
    case principal
    when Access
      ApiCore::AccessInteractor::GenerateMastraDelegationToken.new(access: principal).call
    when Company
      ApiCore::CompanyInteractor::GenerateMastraDelegationToken.new(company: principal).call
    else
      raise Ai::Error, "Unsupported delegated auth principal: #{principal.class}"
    end

  raise Ai::Error, "Failed to mint delegated token" unless outcome.successful?
  outcome.value.token
}

Ai::Client#generate / #run_workflow — new delegated_token: parameter (internal)

The client layer accepts a raw token string and the Mastra client forwards it as the X-Factorial-Delegated-Bearer HTTP header. This is internal plumbing — callers should use Ai::Agent with delegated_auth: instead.

# Internal transport — not intended for direct use by callers
client.generate(agent_name, messages: msgs, options: opts, delegated_token: 'token-string')
client.run_workflow(workflow_name, input: input, delegated_token: 'token-string')

🏡 Context

Ai::Agent now accepts delegated_auth: (Access, Company, or any principal)
instead of delegated_token:. The principal is resolved to a token string
via the configurable Ai.delegated_token_resolver callback.

delegated_token: remains on the Ai::Client layer (internal transport).
@marcbaque marcbaque marked this pull request as ready for review April 12, 2026 18:26
@marcbaque marcbaque requested a review from a team as a code owner April 12, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant