diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 813011e316dd..add78e95c419 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -984,8 +984,8 @@ "js-sys_0.3.85": "{\"dependencies\":[{\"default_features\":false,\"name\":\"once_cell\",\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.108\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"wasm-bindgen/std\"]}}", "jsonwebtoken_9.3.1": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\")))))\"},{\"name\":\"js-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"pem\",\"optional\":true,\"req\":\"^3\"},{\"features\":[\"std\"],\"name\":\"ring\",\"req\":\"^0.17.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"std\",\"wasm32_unknown_unknown_js\"],\"name\":\"ring\",\"req\":\"^0.17.4\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"simple_asn1\",\"optional\":true,\"req\":\"^0.6\"},{\"features\":[\"wasm-bindgen\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\",\"target\":\"cfg(not(all(target_arch = \\\"wasm32\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\")))))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.1\"}],\"features\":{\"default\":[\"use_pem\"],\"use_pem\":[\"pem\",\"simple_asn1\"]}}", "keyring_3.6.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"byteorder\",\"optional\":true,\"req\":\"^1.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"derive\",\"wrap_help\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.0-rc.1\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.0-rc.2\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"dbus-secret-service\",\"optional\":true,\"req\":\"^4.0.1\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11.5\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2\"},{\"features\":[\"std\"],\"name\":\"linux-keyutils\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"log\",\"req\":\"^0.4.22\"},{\"name\":\"openssl\",\"optional\":true,\"req\":\"^0.10.66\"},{\"kind\":\"dev\",\"name\":\"rpassword\",\"req\":\"^7\"},{\"kind\":\"dev\",\"name\":\"rprompt\",\"req\":\"^2\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"secret-service\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"security-framework\",\"optional\":true,\"req\":\"^2\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"name\":\"security-framework\",\"optional\":true,\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"whoami\",\"req\":\"^1.5\"},{\"features\":[\"Win32_Foundation\",\"Win32_Security_Credentials\"],\"name\":\"windows-sys\",\"optional\":true,\"req\":\"^0.60\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"linux\\\")\"},{\"name\":\"zbus\",\"optional\":true,\"req\":\"^4\",\"target\":\"cfg(target_os = \\\"openbsd\\\")\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"apple-native\":[\"dep:security-framework\"],\"async-io\":[\"zbus?/async-io\"],\"async-secret-service\":[\"dep:secret-service\",\"dep:zbus\"],\"crypto-openssl\":[\"dbus-secret-service?/crypto-openssl\",\"secret-service?/crypto-openssl\"],\"crypto-rust\":[\"dbus-secret-service?/crypto-rust\",\"secret-service?/crypto-rust\"],\"linux-native\":[\"dep:linux-keyutils\"],\"linux-native-async-persistent\":[\"linux-native\",\"async-secret-service\"],\"linux-native-sync-persistent\":[\"linux-native\",\"sync-secret-service\"],\"sync-secret-service\":[\"dep:dbus-secret-service\"],\"tokio\":[\"zbus?/tokio\"],\"vendored\":[\"dbus-secret-service?/vendored\",\"openssl?/vendored\"],\"windows-native\":[\"dep:windows-sys\",\"dep:byteorder\"]}}", - "kontext-dev-sdk-core_0.1.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"name\":\"url\",\"req\":\"^2\"}],\"features\":{}}", - "kontext-dev-sdk_0.1.4": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"dirs\",\"req\":\"^6\"},{\"name\":\"jsonwebtoken\",\"req\":\"^9\"},{\"name\":\"kontext-dev-sdk-core\",\"req\":\"^0.1.4\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"json\"],\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"name\":\"tiny_http\",\"req\":\"^0.12\"},{\"features\":[\"sync\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"url\",\"req\":\"^2\"},{\"name\":\"urlencoding\",\"req\":\"^2\"},{\"name\":\"webbrowser\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{}}", + "kontext-dev-sdk-core_0.2.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"name\":\"url\",\"req\":\"^2\"}],\"features\":{}}", + "kontext-dev-sdk_0.2.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"dirs\",\"req\":\"^6\"},{\"name\":\"jsonwebtoken\",\"req\":\"^9\"},{\"name\":\"kontext-dev-sdk-core\",\"req\":\"^0.2.0\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"json\"],\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"name\":\"tiny_http\",\"req\":\"^0.12\"},{\"features\":[\"sync\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"url\",\"req\":\"^2\"},{\"name\":\"urlencoding\",\"req\":\"^2\"},{\"name\":\"webbrowser\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"wiremock\",\"req\":\"^0.6\"}],\"features\":{}}", "kqueue-sys_1.0.4": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^1.2.1\"},{\"name\":\"libc\",\"req\":\"^0.2.74\"}],\"features\":{}}", "kqueue_1.1.1": "{\"dependencies\":[{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"dhat\",\"req\":\"^0.3.2\"},{\"name\":\"kqueue-sys\",\"req\":\"^1.0.4\"},{\"name\":\"libc\",\"req\":\"^0.2.17\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"}],\"features\":{}}", "lalrpop-util_0.19.12": "{\"dependencies\":[{\"name\":\"regex\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"std\"],\"lexer\":[\"regex/std\",\"std\"],\"std\":[]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 9fd405e73e29..17dfbe5776cd 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -5448,9 +5448,9 @@ dependencies = [ [[package]] name = "kontext-dev-sdk" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e12135cfbce0eec97e0dcd4f7260bde91719f9334b2a1ac469349d98a54cd04" +checksum = "e00dc5a53668bd2b1a7c6155925ee42ec7d9b8400647d3b76addc20b88300e5f" dependencies = [ "base64 0.22.1", "dirs", @@ -5471,9 +5471,9 @@ dependencies = [ [[package]] name = "kontext-dev-sdk-core" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a339ba9aee72bac72ea1facbc60f2b5ddda616e5fa1d3a2a1e0e1b0dc83dd8" +checksum = "bdb2298d4b6743803df3acf384db64dd9d2090400b9ca2186c55be6bc3bb0981" dependencies = [ "serde", "serde_json", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 4ca423789b6a..39d1ca62e799 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -96,7 +96,7 @@ codex-client = { path = "codex-client" } codex-cloud-requirements = { path = "cloud-requirements" } codex-config = { path = "config" } codex-core = { path = "core" } -kontext-dev-sdk = { version = "=0.1.4" } +kontext-dev-sdk = { version = "0.2.0" } codex-exec = { path = "exec" } codex-execpolicy = { path = "execpolicy" } codex-experimental-api-macros = { path = "codex-experimental-api-macros" } diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index e8073bcd4a13..a07c0967ff21 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -8,6 +8,10 @@ license.workspace = true name = "codex-kontext" path = "src/main.rs" +[[bin]] +name = "codex" +path = "src/main.rs" + [lib] name = "codex_cli" path = "src/lib.rs" diff --git a/codex-rs/core/src/kontext_dev.rs b/codex-rs/core/src/kontext_dev.rs index e487211ff02b..697cbe02c3db 100644 --- a/codex-rs/core/src/kontext_dev.rs +++ b/codex-rs/core/src/kontext_dev.rs @@ -9,6 +9,7 @@ use kontext_dev_sdk::KontextDevError; use kontext_dev_sdk::build_kontext_prompt_guidance; use kontext_dev_sdk::create_kontext_orchestrator; use kontext_dev_sdk::mcp::KontextTool; +use kontext_dev_sdk::mcp::RuntimeIntegrationConnectType; use kontext_dev_sdk::orchestrator::KontextOrchestrator; use serde_json::Map; use serde_json::Value; @@ -46,6 +47,7 @@ pub(crate) struct InjectedKontextToolSpec { struct DisconnectedCapability { name: String, connect_url: Option, + connect_type: Option, } #[derive(Clone, Debug, Default)] @@ -116,6 +118,24 @@ impl KontextDevRuntime { Vec::new() } }; + let runtime_integrations = match self.client.mcp().list_integrations().await { + Ok(integrations) => integrations, + Err(err) if is_url_elicitation_required_error(&err) => { + warn!( + "Kontext runtime integrations require URL elicitation before listing status. Continuing startup and showing connect guidance." + ); + needs_connect_page = true; + Vec::new() + } + Err(err) => { + warn!("Unable to list Kontext runtime integrations: {err}"); + Vec::new() + } + }; + let connect_type_by_id = runtime_integrations + .into_iter() + .map(|integration| (integration.id, integration.connect_type)) + .collect::>(); let mut disconnected_capabilities = Vec::new(); for integration in integrations.as_slice() { @@ -131,6 +151,7 @@ impl KontextDevRuntime { self.settings.integration_ui_url.as_deref(), ) }), + connect_type: connect_type_by_id.get(&integration.id).cloned(), }); } @@ -260,11 +281,20 @@ impl KontextDevRuntime { disconnected.as_slice(), needs_connect_page, ) { + let has_user_token_disconnected = disconnected.iter().any(|capability| { + capability.connect_type == Some(RuntimeIntegrationConnectType::UserToken) + }); match self.connect_page_url().await { Ok(connect_url) => { - info!( - "Kontext integrations are disconnected. Open this URL to connect: {connect_url}" - ); + if has_user_token_disconnected { + info!( + "Kontext integrations are disconnected and at least one requires a per-user token/API key. Open this URL to connect: {connect_url}" + ); + } else { + info!( + "Kontext integrations are disconnected. Open this URL to connect: {connect_url}" + ); + } if let Err(err) = webbrowser::open(&connect_url) { warn!( "Failed to open Kontext connect URL in browser (showing URL instead): {err}" @@ -281,20 +311,30 @@ impl KontextDevRuntime { } if let Some(first) = disconnected.first() { + let connect_requirement = match first.connect_type.as_ref() { + Some(RuntimeIntegrationConnectType::Oauth) => "requires OAuth authorization.", + Some(RuntimeIntegrationConnectType::UserToken) => { + "requires a per-user token/API key." + } + Some(RuntimeIntegrationConnectType::Credentials) => { + "requires internal credentials." + } + Some(RuntimeIntegrationConnectType::None) | None => "is disconnected.", + }; if let Some(connect_url) = first.connect_url.as_deref() { warn!( - "Kontext integration `{}` is disconnected. Open this URL to connect: {connect_url}", - first.name + "Kontext integration `{}` {connect_requirement} Open this URL to connect: {connect_url}", + first.name, ); } else { match self.connect_page_url().await { Ok(connect_url) => warn!( - "Kontext integration `{}` is disconnected. Open this URL to connect/manage integrations: {connect_url}", - first.name + "Kontext integration `{}` {connect_requirement} Open this URL to connect/manage integrations: {connect_url}", + first.name, ), Err(err) => warn!( - "Kontext integration `{}` is disconnected and generating a connect URL failed: {err}", - first.name + "Kontext integration `{}` {connect_requirement} Generating a connect URL failed: {err}", + first.name, ), } } @@ -573,6 +613,7 @@ fn sha1_hex(value: &str) -> String { mod tests { use super::*; use pretty_assertions::assert_eq; + use serde::Deserialize; #[test] fn unique_tool_name_sanitizes_and_dedupes() { @@ -600,6 +641,7 @@ mod tests { let disconnected = vec![DisconnectedCapability { name: "Linear".to_string(), connect_url: None, + connect_type: None, }]; assert!(should_auto_open_connect_page( @@ -748,4 +790,54 @@ mod tests { "Always call `REQUEST_CAPABILITY` for fresh integration links." ); } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct RuntimeIntegrationsPayload { + items: Vec, + } + + #[test] + fn runtime_integrations_payload_accepts_user_token_connect_type() { + let payload = serde_json::from_value::(json!({ + "items": [{ + "id": "github", + "name": "GitHub", + "url": "https://mcp.kontext.dev/github", + "category": "gateway_remote_mcp", + "connectType": "user_token", + "connection": { + "connected": false, + "status": "disconnected" + } + }] + })) + .expect("user_token should deserialize as a valid connect type"); + + assert_eq!(payload.items.len(), 1); + assert_eq!( + payload.items[0].connect_type, + RuntimeIntegrationConnectType::UserToken + ); + } + + #[test] + fn runtime_integrations_payload_rejects_unknown_connect_type() { + let err = serde_json::from_value::(json!({ + "items": [{ + "id": "github", + "name": "GitHub", + "url": "https://mcp.kontext.dev/github", + "category": "gateway_remote_mcp", + "connectType": "api_key", + "connection": { + "connected": false, + "status": "disconnected" + } + }] + })) + .expect_err("unknown connect type should fail payload parsing"); + + assert!(err.to_string().contains("unknown variant")); + } }