Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
MDM manages the device, but each AI developer tool has its own settings, MCP
connections, skills, and gateway configuration. agentdesktop manages those
tools as a fleet: see what is installed, apply managed configuration, and
connect each device to your inference gateway.
connect each device to your LLM gateway.

agentdesktop brings discovery, policy, identity, gateway access, and telemetry
into one fully open-source system. Keep developers in Claude, Codex, OpenCode,
Expand All @@ -24,9 +24,9 @@ built for AI developer tools, not retrofitted from device management scripts.
- Inventory MCP servers and skills without collecting MCP command arguments,
environment variables, HTTP headers, or skill bodies.
- Preview and reconcile managed settings for supported tools.
- Connect supported tools directly to a shared inference gateway.
- Connect supported tools directly to a shared LLM gateway.
- Enroll devices through OIDC and associate them with the signed-in user.
- Issue short-lived controller-signed JWTs for an inference gateway such as
- Issue short-lived controller-signed JWTs for an LLM gateway such as
agentgateway.
- Collect selected session and tool-use events when telemetry is enabled.

Expand Down Expand Up @@ -54,7 +54,7 @@ The project targets Linux, macOS, and Windows.
The daemon runs on each device and reconciles developer-tool configuration. It
can receive desired configuration from the controller, or read the same YAML
directly in standalone mode. Developer tools continue to run locally and can
request short-lived credentials for the inference gateway through the daemon.
request short-lived credentials for the LLM gateway through the daemon.

![agentdesktop architecture](images/overview.png)

Expand All @@ -64,7 +64,7 @@ Start with [Build and install](https://agentdesktop.dev/docs/getting-started/bui
when working from source, then choose the setup that fits your environment:

- [Standalone mode](https://agentdesktop.dev/docs/getting-started/standalone/)
reads local YAML and can authenticate directly to an inference gateway with
reads local YAML and can authenticate directly to an LLM gateway with
OIDC. It needs no controller or device identity. The repository includes a
[local standalone example](./examples/standalone).
- [Controller-managed mode](https://agentdesktop.dev/docs/getting-started/managed/)
Expand All @@ -84,7 +84,7 @@ standalone daemon can apply it from a local file.
A small configuration can manage a shared gateway, telemetry, and agents. For example:

```yaml
inferenceGateway:
llmGateway:
url: https://gateway.example.com
authentication:
type: controllerJwt
Expand All @@ -108,7 +108,7 @@ programs:
For a controller-free setup, omit `controller` and configure OIDC directly:

```yaml
inferenceGateway:
llmGateway:
url: https://gateway.example.com
authentication:
type: oidc
Expand Down Expand Up @@ -146,7 +146,7 @@ is used to authenticate the device to the controller.

OIDC also authenticates the device user. Standalone mode uses a simpler native
OIDC flow and sends the resulting access token directly to the configured
inference gateway; it creates no device key or certificate.
LLM gateway; it creates no device key or certificate.

## Telemetry

Expand Down
10 changes: 5 additions & 5 deletions crates/agent/src/anthropic_oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{
sync::atomic::{AtomicBool, Ordering},
};

use agentdesktop_core::model::InferenceGatewayCredential;
use agentdesktop_core::model::LlmGatewayCredential;
use anyhow::{Context, bail};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
Expand Down Expand Up @@ -56,7 +56,7 @@ pub async fn credential(
state_dir: &Path,
callback_listen: Option<SocketAddr>,
open_browser: bool,
) -> anyhow::Result<Option<InferenceGatewayCredential>> {
) -> anyhow::Result<Option<LlmGatewayCredential>> {
let state_dir = state_dir.to_owned();
tokio::spawn(async move { credential_inner(&state_dir, callback_listen, open_browser).await })
.await
Expand All @@ -67,7 +67,7 @@ async fn credential_inner(
state_dir: &Path,
callback_listen: Option<SocketAddr>,
open_browser: bool,
) -> anyhow::Result<Option<InferenceGatewayCredential>> {
) -> anyhow::Result<Option<LlmGatewayCredential>> {
let _login = LOGIN.lock().await;
let store = SecretStore::new(state_dir)?;
let redirect_uri = Url::parse(REDIRECT_URI).expect("static Anthropic redirect URI is valid");
Expand Down Expand Up @@ -171,8 +171,8 @@ fn save(store: &SecretStore, token: &StoredToken) -> anyhow::Result<()> {
)
}

fn as_credential(token: &StoredToken) -> InferenceGatewayCredential {
InferenceGatewayCredential {
fn as_credential(token: &StoredToken) -> LlmGatewayCredential {
LlmGatewayCredential {
credential: token.access_token.clone(),
expires_at_unix_seconds: token.expires_at_unix_seconds,
}
Expand Down
41 changes: 18 additions & 23 deletions crates/agent/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ use serde::{Deserialize, Serialize};
use tokio::sync::{mpsc, oneshot};

use agentdesktop_core::{
config::{
DaemonConfig, InferenceGatewayAuthentication, ProgramAuthentication, valid_client_id,
},
config::{DaemonConfig, LlmGatewayAuthentication, ProgramAuthentication, valid_client_id},
model::{
Discovery, EnrollmentStatus, InferenceGatewayCredential, TelemetryEvent, TelemetryEventKind,
Discovery, EnrollmentStatus, LlmGatewayCredential, TelemetryEvent, TelemetryEventKind,
},
};

Expand Down Expand Up @@ -51,10 +49,7 @@ pub fn router(state: AppState) -> Router {
.route("/v1/enrollment", get(enrollment))
.route("/v1/logout", post(logout))
.route("/v1/telemetry", post(telemetry))
.route(
"/v1/inference-gateway/credential",
get(inference_gateway_credential),
)
.route("/v1/llm-gateway/credential", get(llm_gateway_credential))
.with_state(state)
}

Expand Down Expand Up @@ -217,10 +212,10 @@ async fn logout(State(state): State<AppState>) -> Result<StatusCode, (StatusCode
Ok(StatusCode::NO_CONTENT)
}

async fn inference_gateway_credential(
async fn llm_gateway_credential(
State(state): State<AppState>,
Query(query): Query<CredentialQuery>,
) -> Result<Json<InferenceGatewayCredential>, (StatusCode, String)> {
) -> Result<Json<LlmGatewayCredential>, (StatusCode, String)> {
if !valid_client_id(&query.client_id) {
return Err((StatusCode::BAD_REQUEST, "invalid client ID".to_owned()));
}
Expand All @@ -230,15 +225,15 @@ async fn inference_gateway_credential(
format!("read applied configuration: {error:#}"),
)
})?;
let gateway = effective.inference_gateway.as_ref().ok_or_else(|| {
let gateway = effective.llm_gateway.as_ref().ok_or_else(|| {
(
StatusCode::FAILED_DEPENDENCY,
"daemon has no inference gateway configured".to_owned(),
"daemon has no LLM gateway configured".to_owned(),
)
})?;
let uses_subscription = program_uses_subscription(&effective, &query.client_id);
let (identity, continue_in_browser) = match gateway.authentication.as_ref() {
Some(InferenceGatewayAuthentication::ControllerJwt { .. }) => {
Some(LlmGatewayAuthentication::ControllerJwt { .. }) => {
let controller = state.config.controller.as_ref().ok_or_else(|| {
(
StatusCode::FAILED_DEPENDENCY,
Expand All @@ -247,11 +242,11 @@ async fn inference_gateway_credential(
})?;
// Local transport permissions authenticate the user, not the calling
// process. The client ID selects an allowed policy within that boundary.
remote::inference_gateway_credential(controller, &state.state_dir, &query.client_id)
remote::llm_gateway_credential(controller, &state.state_dir, &query.client_id)
.await
.map(|credential| (credential, false))
}
Some(InferenceGatewayAuthentication::Oidc {
Some(LlmGatewayAuthentication::Oidc {
issuer,
client_id,
redirect_uri,
Expand All @@ -277,7 +272,7 @@ async fn inference_gateway_credential(
)
}),
None => Err(anyhow::anyhow!(
"inference gateway has no authentication configured"
"LLM gateway has no authentication configured"
)),
}
.map_err(|error| (StatusCode::BAD_GATEWAY, format!("{error:#}")))?;
Expand Down Expand Up @@ -324,7 +319,7 @@ mod tests {
fn subscription_is_selected_by_requesting_agent() {
let config = parse_daemon(
r#"
inferenceGateway:
llmGateway:
url: https://gateway.example.com
authentication:
type: oidc
Expand Down Expand Up @@ -360,7 +355,7 @@ controller:
fs::write(
root.join("remote-config.yaml"),
r#"
inferenceGateway:
llmGateway:
url: https://gateway.example.com
authentication:
type: controllerJwt
Expand All @@ -373,7 +368,7 @@ inferenceGateway:
let effective = load_effective_config(&local, &root).unwrap();

assert_eq!(
effective.inference_gateway.unwrap().url.as_str(),
effective.llm_gateway.unwrap().url.as_str(),
"https://gateway.example.com/"
);
fs::remove_dir_all(root).unwrap();
Expand All @@ -389,7 +384,7 @@ inferenceGateway:
fs::create_dir_all(&root).unwrap();
let local = parse_daemon(
r#"
inferenceGateway:
llmGateway:
url: http://127.0.0.1:4001
authentication:
type: oidc
Expand All @@ -402,7 +397,7 @@ inferenceGateway:
fs::write(
root.join("remote-config.yaml"),
r#"
inferenceGateway:
llmGateway:
url: https://stale.example.com
authentication:
type: controllerJwt
Expand All @@ -414,11 +409,11 @@ inferenceGateway:

let effective = load_effective_config(&local, &root).unwrap();

let gateway = effective.inference_gateway.unwrap();
let gateway = effective.llm_gateway.unwrap();
assert_eq!(gateway.url.as_str(), "http://127.0.0.1:4001/");
assert!(matches!(
gateway.authentication,
Some(agentdesktop_core::config::InferenceGatewayAuthentication::Oidc { .. })
Some(agentdesktop_core::config::LlmGatewayAuthentication::Oidc { .. })
));
fs::remove_dir_all(root).unwrap();
}
Expand Down
8 changes: 4 additions & 4 deletions crates/agent/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{io::Read, path::PathBuf};
use agentdesktop_client as client;
use agentdesktop_core::{
config::DaemonConfig,
model::{Discovery, Health, InferenceGatewayCredential, TelemetryEventKind},
model::{Discovery, Health, LlmGatewayCredential, TelemetryEventKind},
};
use clap::Subcommand;
use serde::Deserialize;
Expand All @@ -18,7 +18,7 @@ pub enum ClientCommand {
Discover,
/// Print the daemon's local startup configuration.
Config,
/// Print a short-lived credential for an inference gateway.
/// Print a short-lived credential for an LLM gateway.
Credential {
/// Developer tool requesting the credential.
#[arg(long, default_value = "agentdesktop")]
Expand Down Expand Up @@ -93,9 +93,9 @@ pub async fn run(command: ClientCommand, socket: PathBuf) -> anyhow::Result<()>
ClientCommand::Credential { client_id } => {
let client_id: String =
url::form_urlencoded::byte_serialize(client_id.as_bytes()).collect();
let response: InferenceGatewayCredential = client::get(
let response: LlmGatewayCredential = client::get(
&socket,
&format!("/v1/inference-gateway/credential?client_id={client_id}"),
&format!("/v1/llm-gateway/credential?client_id={client_id}"),
)
.await?;
println!("{}", response.credential);
Expand Down
24 changes: 12 additions & 12 deletions crates/agent/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ fn start_gateway_authentication(
state_dir: PathBuf,
callback_listen: Option<SocketAddr>,
) {
let Some(gateway) = config.inference_gateway.as_ref() else {
let Some(gateway) = config.llm_gateway.as_ref() else {
return;
};
let authentication = gateway.authentication.clone();
Expand All @@ -403,15 +403,15 @@ fn start_gateway_authentication(
tokio::spawn(async move {
let result: anyhow::Result<()> = async {
let mut continue_in_browser = false;
if let Some(agentdesktop_core::config::InferenceGatewayAuthentication::Oidc {
if let Some(agentdesktop_core::config::LlmGatewayAuthentication::Oidc {
issuer,
client_id,
redirect_uri,
scopes,
allow_insecure,
}) = authentication
{
tracing::info!(%issuer, "starting inference gateway OIDC authentication");
tracing::info!(%issuer, "starting LLM gateway OIDC authentication");
let acquired = gateway_oidc::credential(
&issuer,
&client_id,
Expand All @@ -426,7 +426,7 @@ fn start_gateway_authentication(
)
.await?;
continue_in_browser = acquired.interactive && subscription;
tracing::info!(%issuer, "inference gateway OIDC authentication ready");
tracing::info!(%issuer, "LLM gateway OIDC authentication ready");
}
if subscription {
tracing::info!("starting Anthropic subscription authentication");
Expand All @@ -444,7 +444,7 @@ fn start_gateway_authentication(
if let Err(error) = result {
tracing::error!(
error = %format!("{error:#}"),
"inference gateway authentication failed"
"LLM gateway authentication failed"
);
}
});
Expand All @@ -469,36 +469,36 @@ fn validate_one_shot(config: &agentdesktop_core::config::DaemonConfig) -> anyhow
bail!("--once cannot collect telemetry because hooks require the daemon to remain running");
}
let authenticated_gateway_is_used = config
.inference_gateway
.llm_gateway
.as_ref()
.is_some_and(|gateway| gateway.authentication.is_some())
&& [
config
.programs
.claude_code
.as_ref()
.is_some_and(|program| program.use_inference_gateway),
.is_some_and(|program| program.use_llm_gateway),
config
.programs
.claude_desktop
.as_ref()
.is_some_and(|program| program.use_inference_gateway),
.is_some_and(|program| program.use_llm_gateway),
config
.programs
.codex
.as_ref()
.is_some_and(|program| program.use_inference_gateway),
.is_some_and(|program| program.use_llm_gateway),
config
.programs
.open_code
.as_ref()
.is_some_and(|program| program.use_inference_gateway),
.is_some_and(|program| program.use_llm_gateway),
]
.into_iter()
.any(|used| used);
if authenticated_gateway_is_used {
bail!(
"--once cannot configure an authenticated inference gateway because credential helpers require the daemon to remain running"
"--once cannot configure an authenticated LLM gateway because credential helpers require the daemon to remain running"
);
}
Ok(())
Expand Down Expand Up @@ -841,7 +841,7 @@ programs:

let oidc = parse_daemon(
r#"
inferenceGateway:
llmGateway:
url: https://gateway.example.com
authentication:
type: oidc
Expand Down
8 changes: 4 additions & 4 deletions crates/agent/src/gateway_oidc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{net::SocketAddr, path::Path};

use agentdesktop_core::model::InferenceGatewayCredential;
use agentdesktop_core::model::LlmGatewayCredential;
use anyhow::{Context, bail};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use serde::{Deserialize, Serialize};
Expand All @@ -21,7 +21,7 @@ pub struct LoginOptions {
}

pub struct CredentialAcquisition {
pub credential: InferenceGatewayCredential,
pub credential: LlmGatewayCredential,
pub interactive: bool,
}

Expand Down Expand Up @@ -228,8 +228,8 @@ fn account(issuer: &Url, client_id: &str) -> String {
URL_SAFE_NO_PAD.encode(digest)
}

fn as_credential(tokens: &StoredTokens) -> InferenceGatewayCredential {
InferenceGatewayCredential {
fn as_credential(tokens: &StoredTokens) -> LlmGatewayCredential {
LlmGatewayCredential {
credential: tokens.access_token.clone(),
expires_at_unix_seconds: tokens.expires_at_unix_seconds,
}
Expand Down
Loading
Loading