blob: 72d16d830992e0146262073db02001af99242e2a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Compiled provider policy for authentication, discovery and model adapters.
//! Credentials and connections belong to Host, not to the contribution catalog.
pub mod authentication;
mod refresh;
pub use refresh::RefreshCall;
mod inspection;
pub use inspection::{Discovery, Verification};
pub mod configuration;
use crate::model::Credentials;
use crate::model::Transport;
use authentication::{Authenticate, Credential, Interaction};
pub use configuration::{Connection, Descriptor, Model, Resolve};
use futures_util::future::BoxFuture;
use maka_agent_facts::configuration::ModelInfo;
pub use maka_agent_facts::provider::Identity;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
#[derive(Clone)]
pub struct Context {
pub transport: Arc<dyn Transport>,
pub cancellation: CancellationToken,
/// Present only for a Host-admitted interactive login.
pub interaction: Option<Arc<dyn Interaction>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
#[serde(tag = "kind", content = "message", rename_all = "snake_case")]
pub enum Error {
#[error("provider registration is unavailable")]
Unavailable,
#[error("provider operation was cancelled")]
Cancelled,
#[error("provider requires authentication")]
AuthenticationRequired,
/// Caller-facing explanation supplied by the provider. Must not include
/// credentials or unfiltered remote response bodies.
#[error("provider rejected the request: {0}")]
Rejected(String),
#[error("invalid provider input or output: {0}")]
Invalid(String),
#[error("invalid provider configuration")]
InvalidConfiguration,
#[error("invalid selected model")]
InvalidModel,
#[error("invalid prepared model")]
InvalidPreparedModel,
#[error("provider changed the selected model identity")]
SelectedModelChanged,
#[error("unsupported model thinking level")]
UnsupportedThinkingLevel,
#[error("provider execution profile or semantic features are not supported")]
UnsupportedExecutionProfile,
#[error("provider adapter or protocol is not supported")]
UnsupportedProtocol,
#[error("invalid provider endpoint")]
InvalidEndpoint,
#[error("Responses contract is not declared")]
MissingResponsesContract,
#[error("provider protocol does not expose parallel-call policy")]
UnsupportedParallelPolicy,
#[error("model output limit does not leave a positive text budget")]
InsufficientOutputBudget,
#[error("provider facts are unavailable")]
ProviderFactsUnavailable,
#[error("provider probe authentication is not supported")]
UnsupportedProbeAuthentication,
#[error("provider request outcome is unknown")]
OutcomeUnknown,
#[error("provider transport failed: {0}")]
Transport(String),
/// Status only; provider error bodies can contain credentials.
#[error("provider HTTP request failed with status {0}")]
Http(u16),
}
/// Pure request policy is separate from credential resolution. Host can freeze
/// policy without disclosing tokens; authorization follows execution admission.
pub trait Provider: Send + Sync {
fn resolve(&self, request: Resolve) -> BoxFuture<'_, Result<Model, Error>>;
fn authorize(
&self,
connection: Connection,
credential: Option<Credential>,
session_id: String,
) -> BoxFuture<'_, Result<Credentials, Error>>;
fn authenticate(
&self,
_request: Authenticate,
_context: Context,
) -> BoxFuture<'_, Result<Credential, Error>> {
Box::pin(async { Err(Error::AuthenticationRequired) })
}
fn refresh(
&self,
_connection: Connection,
_credential: Credential,
_context: Context,
) -> BoxFuture<'_, Result<Credential, Error>> {
Box::pin(async { Err(Error::AuthenticationRequired) })
}
fn discover(
&self,
_request: Discovery,
_context: Context,
) -> BoxFuture<'_, Result<Vec<ModelInfo>, Error>> {
Box::pin(async { Err(Error::Unavailable) })
}
/// The provider chooses its meaningful readiness check. Host does not
/// infer subscription behavior from a vendor name or fabricate a probe.
fn verify(
&self,
_request: Verification,
_context: Context,
) -> BoxFuture<'_, Result<(), Error>> {
Box::pin(async { Err(Error::Unavailable) })
}
}
pub struct Definition {
descriptor: Descriptor,
implementation: Arc<dyn Provider>,
configuration: jsonschema::Validator,
authentication: std::collections::BTreeMap<String, jsonschema::Validator>,
}
impl Definition {
pub fn new(descriptor: Descriptor, implementation: Arc<dyn Provider>) -> Result<Self, Error> {
descriptor.validate()?;
let compile = |schema: &serde_json::Value| {
jsonschema::options()
.offline()
.should_validate_formats(false)
.build(schema)
.map_err(|_| Error::Invalid("invalid provider schema".into()))
};
let configuration = compile(&descriptor.configuration_schema)?;
let authentication = descriptor
.authentication
.iter()
.map(|method| Ok((method.id.clone(), compile(&method.input_schema)?)))
.collect::<Result<_, Error>>()?;
Ok(Self {
descriptor,
implementation,
configuration,
authentication,
})
}
pub fn descriptor(&self) -> &Descriptor {
&self.descriptor
}
/// Defaults are materialized once, when creating the connection. Reading a
/// stored connection never substitutes a newer plugin's endpoint or defaults.
pub fn configure(&self, input: serde_json::Value) -> Result<serde_json::Value, Error> {
let serde_json::Value::Object(input) = input else {
return Err(Error::InvalidConfiguration);
};
let mut configuration = self
.descriptor
.configuration_defaults
.as_object()
.expect("validated descriptor")
.clone();
configuration.extend(input);
let configuration = serde_json::Value::Object(configuration);
self.validate_configuration(&configuration)?;
Ok(configuration)
}
/// Validated policy for a trusted embedding. The embedding must retain and
/// drain its actual implementation owner; this value is not an admission grant.
pub async fn prepare(&self, request: Resolve) -> Result<Model, Error> {
self.validate_configuration(&request.connection.configuration)?;
request.model.validate().map_err(|_| Error::InvalidModel)?;
let expected = request.model.id.clone();
let model = self.implementation.resolve(request).await?;
model.validate().map_err(|_| Error::InvalidPreparedModel)?;
if model.info.id != expected {
return Err(Error::SelectedModelChanged);
}
Ok(model)
}
/// Private credential resolution after the embedding's execution admission.
pub async fn authorize(
&self,
connection: Connection,
credential: Option<Credential>,
session_id: String,
) -> Result<Credentials, Error> {
self.validate_configuration(&connection.configuration)?;
if let Some(credential) = &credential {
credential.validate().map_err(Error::Invalid)?;
}
let credentials = self
.implementation
.authorize(connection, credential, session_id)
.await?;
credentials.validate().map_err(Error::Invalid)?;
Ok(credentials)
}
/// Prepare refresh for a trusted native owner, which retains the actual
/// implementation and drains settlement independently of request waiters.
pub fn prepare_refresh(
&self,
connection: Connection,
credential: Credential,
) -> Result<RefreshCall, Error> {
self.validate_configuration(&connection.configuration)?;
credential.validate().map_err(Error::Invalid)?;
Ok(RefreshCall {
implementation: self.implementation.clone(),
connection,
credential,
})
}
/// Authentication after the trusted embedding's actual execution admission.
/// The embedding owns cancellation and physical settlement, as for authorize.
pub async fn authenticate(
&self,
request: Authenticate,
context: Context,
) -> Result<Credential, Error> {
self.validate_authentication(&request, &context)?;
let credential = self.implementation.authenticate(request, context).await?;
credential.validate().map_err(Error::Invalid)?;
Ok(credential)
}
fn validate_authentication(
&self,
request: &Authenticate,
context: &Context,
) -> Result<(), Error> {
self.validate_configuration(&request.connection.configuration)?;
let validator = self
.authentication
.get(&request.method)
.ok_or_else(|| Error::Invalid("unknown authentication method".into()))?;
// Validation errors must never echo submitted credentials.
if serde_json::to_vec(&request.input)
.map_err(|_| Error::Invalid("invalid authentication input".into()))?
.len()
> 64 * 1024
|| !validator.is_valid(&request.input)
{
return Err(Error::Invalid(
"authentication input does not satisfy its schema".into(),
));
}
let method = self
.descriptor
.authentication
.iter()
.find(|method| method.id == request.method)
.expect("compiled authentication schema");
if method.interactive && context.interaction.is_none() {
return Err(Error::Unavailable);
}
Ok(())
}
pub fn validate_configuration(&self, configuration: &serde_json::Value) -> Result<(), Error> {
if serde_json::to_vec(configuration)
.map_err(|_| Error::InvalidConfiguration)?
.len()
> 64 * 1024
|| !self.configuration.is_valid(configuration)
{
return Err(Error::InvalidConfiguration);
}
Ok(())
}
}