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
2 changes: 2 additions & 0 deletions python/everruns_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
Agent,
AgentCapabilityConfig,
CapabilityInfo,
Connection,
ContentPart,
Controls,
DeleteFileResponse,
Expand Down Expand Up @@ -51,6 +52,7 @@
"Agent",
"AgentCapabilityConfig",
"CapabilityInfo",
"Connection",
"DeleteFileResponse",
"FileInfo",
"FileStat",
Expand Down
39 changes: 39 additions & 0 deletions python/everruns_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Agent,
AgentCapabilityConfig,
CapabilityInfo,
Connection,
ContentPart,
Controls,
CreateAgentRequest,
Expand Down Expand Up @@ -120,6 +121,11 @@ def session_files(self) -> "SessionFilesClient":
"""Get the session files client."""
return SessionFilesClient(self)

@property
def connections(self) -> "ConnectionsClient":
"""Get the connections client."""
return ConnectionsClient(self)

def _url(self, path: str) -> str:
# Use relative path (no leading slash) for correct joining with base URL.
# The path parameter starts with "/" (e.g., "/agents"), so we strip it.
Expand Down Expand Up @@ -722,3 +728,36 @@ async def stat(self, session_id: str, path: str) -> FileStat:
"""
resp = await self._client._post(f"/sessions/{session_id}/fs/_/stat", {"path": path})
return FileStat(**resp)


class ConnectionsClient:
"""Client for user connection operations."""

def __init__(self, client: Everruns):
self._client = client

async def set(self, provider: str, api_key: str) -> Connection:
"""Set an API key connection for a provider.

Args:
provider: Provider name (e.g. "daytona").
api_key: API key for the provider.
"""
resp = await self._client._post(
f"/user/connections/{provider}",
{"api_key": api_key},
)
return Connection(**resp)

async def list(self) -> list[Connection]:
"""List all connections."""
resp = await self._client._get("/user/connections")
return [Connection(**c) for c in resp.get("data", [])]

async def remove(self, provider: str) -> None:
"""Remove a connection.

Args:
provider: Provider name (e.g. "daytona").
"""
await self._client._delete(f"/user/connections/{provider}")
11 changes: 11 additions & 0 deletions python/everruns_sdk/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,17 @@ class DeleteFileResponse(BaseModel):
deleted: bool


# --- Connections Models ---


class Connection(BaseModel):
"""A user connection to an external provider."""

provider: str
created_at: str
updated_at: str


def extract_tool_calls(data: dict[str, Any]) -> list[ToolCallInfo]:
"""Extract tool call info from event data (``data.message.content``)."""
message = data.get("message")
Expand Down
70 changes: 70 additions & 0 deletions python/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -950,3 +950,73 @@ def test_list_response_with_pagination_fields():
assert resp.total == 10
assert resp.offset == 5
assert resp.limit == 25


# --- Connections Tests ---

CONN_RESPONSE = {
"provider": "daytona",
"created_at": "2026-03-31T00:00:00Z",
"updated_at": "2026-03-31T00:00:00Z",
}


@pytest.mark.asyncio
@respx.mock
async def test_connections_set():
route = respx.post("https://custom.example.com/api/v1/user/connections/daytona").mock(
return_value=httpx.Response(200, json=CONN_RESPONSE)
)

client = Everruns(api_key="evr_test_key")
try:
conn = await client.connections.set("daytona", "dtn_secret_key")
finally:
await client.close()

assert conn.provider == "daytona"
assert route.called
body = json.loads(route.calls[0].request.content)
assert body["api_key"] == "dtn_secret_key"


@pytest.mark.asyncio
@respx.mock
async def test_connections_list():
route = respx.get("https://custom.example.com/api/v1/user/connections").mock(
return_value=httpx.Response(
200,
json={
"data": [CONN_RESPONSE],
"total": 1,
"offset": 0,
"limit": 100,
},
)
)

client = Everruns(api_key="evr_test_key")
try:
connections = await client.connections.list()
finally:
await client.close()

assert len(connections) == 1
assert connections[0].provider == "daytona"
assert route.called


@pytest.mark.asyncio
@respx.mock
async def test_connections_remove():
route = respx.delete("https://custom.example.com/api/v1/user/connections/daytona").mock(
return_value=httpx.Response(204)
)

client = Everruns(api_key="evr_test_key")
try:
await client.connections.remove("daytona")
finally:
await client.close()

assert route.called
32 changes: 32 additions & 0 deletions rust/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ impl Everruns {
SessionFilesClient { client: self }
}

/// Get the connections client
pub fn connections(&self) -> ConnectionsClient<'_> {
ConnectionsClient { client: self }
}

pub(crate) fn url(&self, path: &str) -> Url {
// Use relative path (no leading slash) for correct joining with base URL.
// The path parameter starts with "/" (e.g., "/agents"), so we strip it.
Expand Down Expand Up @@ -763,6 +768,33 @@ impl<'a> SessionFilesClient<'a> {
}
}

/// Client for user connection operations
pub struct ConnectionsClient<'a> {
client: &'a Everruns,
}

impl<'a> ConnectionsClient<'a> {
/// Set an API key connection for a provider
pub async fn set(&self, provider: &str, api_key: &str) -> Result<Connection> {
let req = SetConnectionRequest::new(api_key);
self.client
.post(&format!("/user/connections/{}", provider), &req)
.await
}

/// List all connections
pub async fn list(&self) -> Result<ListResponse<Connection>> {
self.client.get("/user/connections").await
}

/// Remove a connection
pub async fn remove(&self, provider: &str) -> Result<()> {
self.client
.delete(&format!("/user/connections/{}", provider))
.await
}
}

impl std::fmt::Debug for Everruns {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Everruns")
Expand Down
25 changes: 25 additions & 0 deletions rust/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,31 @@ pub struct DeleteResponse {
pub deleted: bool,
}

// --- Connections Models ---

/// A user connection to an external provider
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Connection {
pub provider: String,
pub created_at: String,
pub updated_at: String,
}

/// Request to set a connection API key
#[derive(Debug, Clone, Serialize)]
pub struct SetConnectionRequest {
pub api_key: String,
}

impl SetConnectionRequest {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
79 changes: 78 additions & 1 deletion rust/tests/client_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use everruns_sdk::{
CreateAgentRequest, CreateFileRequest, CreateSessionRequest, Everruns, InitialFile,
UpdateFileRequest,
SetConnectionRequest, UpdateFileRequest,
};
use wiremock::{
Mock, MockServer, ResponseTemplate,
Expand Down Expand Up @@ -547,3 +547,80 @@ async fn test_session_files_stat() {
assert_eq!(stat.size_bytes, 5);
assert!(!stat.is_directory);
}

// --- Connections Tests ---

#[tokio::test]
async fn test_connections_set() {
let server = MockServer::start().await;
let client = Everruns::with_base_url("evr_test_key", &server.uri()).expect("client");

Mock::given(method("POST"))
.and(path("/v1/user/connections/daytona"))
.and(body_json(serde_json::json!({
"api_key": "dtn_secret_key"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"provider": "daytona",
"created_at": "2026-03-31T00:00:00Z",
"updated_at": "2026-03-31T00:00:00Z"
})))
.mount(&server)
.await;

let conn = client
.connections()
.set("daytona", "dtn_secret_key")
.await
.expect("set connection should succeed");

assert_eq!(conn.provider, "daytona");
}

#[tokio::test]
async fn test_connections_list() {
let server = MockServer::start().await;
let client = Everruns::with_base_url("evr_test_key", &server.uri()).expect("client");

Mock::given(method("GET"))
.and(path("/v1/user/connections"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"data": [{
"provider": "daytona",
"created_at": "2026-03-31T00:00:00Z",
"updated_at": "2026-03-31T00:00:00Z"
}],
"total": 1,
"offset": 0,
"limit": 100
})))
.mount(&server)
.await;

let connections = client
.connections()
.list()
.await
.expect("list connections should succeed");

assert_eq!(connections.data.len(), 1);
assert_eq!(connections.data[0].provider, "daytona");
}

#[tokio::test]
async fn test_connections_remove() {
let server = MockServer::start().await;
let client = Everruns::with_base_url("evr_test_key", &server.uri()).expect("client");

Mock::given(method("DELETE"))
.and(path("/v1/user/connections/daytona"))
.respond_with(ResponseTemplate::new(204))
.mount(&server)
.await;

client
.connections()
.remove("daytona")
.await
.expect("remove connection should succeed");
}
30 changes: 30 additions & 0 deletions typescript/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ApiKey } from "./auth.js";
import {
Agent,
CapabilityInfo,
Connection,
ContentPart,
CreateAgentRequest,
DeleteFileResponse,
Expand Down Expand Up @@ -44,6 +45,7 @@ export class Everruns {
readonly events: EventsClient;
readonly capabilities: CapabilitiesClient;
readonly sessionFiles: SessionFilesClient;
readonly connections: ConnectionsClient;

constructor(options: EverrunsOptions = {}) {
if (options.apiKey instanceof ApiKey) {
Expand All @@ -66,6 +68,7 @@ export class Everruns {
this.events = new EventsClient(this);
this.capabilities = new CapabilitiesClient(this);
this.sessionFiles = new SessionFilesClient(this);
this.connections = new ConnectionsClient(this);
}

/**
Expand Down Expand Up @@ -578,6 +581,33 @@ class SessionFilesClient {
}
}

class ConnectionsClient {
constructor(private readonly client: Everruns) {}

/** Set an API key connection for a provider. */
async set(provider: string, apiKey: string): Promise<Connection> {
return this.client.fetch(`/user/connections/${provider}`, {
method: "POST",
body: JSON.stringify({ api_key: apiKey }),
});
}

/** List all connections. */
async list(): Promise<Connection[]> {
const response = await this.client.fetch<{ data: Connection[] }>(
"/user/connections",
);
return response.data;
}

/** Remove a connection. */
async remove(provider: string): Promise<void> {
await this.client.fetch(`/user/connections/${provider}`, {
method: "DELETE",
});
}
}

/** Build the JSON body for agent creation from a CreateAgentRequest. */
function toAgentBody(request: CreateAgentRequest): Record<string, unknown> {
const body: Record<string, unknown> = {
Expand Down
Loading
Loading