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
8 changes: 5 additions & 3 deletions src/paude/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,25 @@
}


def get_agent(name: str) -> Agent:
def get_agent(name: str, provider: str | None = None) -> Agent:
"""Get an agent instance by name.

Args:
name: Agent name (e.g., "claude").
provider: Inference provider name (e.g., "vertex", "openai"),
or None for the agent's default provider.

Returns:
Agent instance.

Raises:
ValueError: If agent name is not registered.
ValueError: If agent name is not registered or provider is invalid.
"""
cls = _REGISTRY.get(name)
if cls is None:
available = ", ".join(sorted(_REGISTRY.keys()))
raise ValueError(f"Unknown agent '{name}'. Available: {available}")
return cls() # type: ignore[no-any-return]
return cls(provider=provider) # type: ignore[no-any-return]


def list_agents() -> list[str]:
Expand Down
50 changes: 49 additions & 1 deletion src/paude/agents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,64 @@ class AgentConfig:
extra_domain_aliases: list[str] = field(default_factory=lambda: ["claude"])
exposed_ports: list[tuple[int, int]] = field(default_factory=list)
default_base_image: str | None = None
provider: str | None = None


@dataclass
class ProviderCredentials:
"""Resolved provider credentials for an agent."""

passthrough_env_vars: list[str] = field(default_factory=list)
secret_env_vars: list[str] = field(default_factory=list)
passthrough_env_prefixes: list[str] = field(default_factory=list)
extra_env_vars: dict[str, str] = field(default_factory=dict)
resolved_provider_name: str = ""
model_config: dict[str, str] = field(default_factory=dict)


def build_provider_credentials(
agent_name: str, provider: str | None
) -> ProviderCredentials:
"""Build credential lists from provider configuration."""
from paude.providers.agent_providers import DEFAULT_PROVIDER, resolve_agent_provider

resolved_name = (
provider if provider is not None else DEFAULT_PROVIDER.get(agent_name)
)
if resolved_name is None:
return ProviderCredentials()

provider_config, agent_config = resolve_agent_provider(agent_name, resolved_name)

passthrough = list(provider_config.passthrough_env_vars)
passthrough.extend(agent_config.extra_passthrough_env_vars)

secret = list(provider_config.secret_env_vars)
secret.extend(agent_config.extra_secret_env_vars)

prefixes = list(provider_config.passthrough_env_prefixes)

extra_env = dict(agent_config.extra_env_vars)

return ProviderCredentials(
passthrough_env_vars=passthrough,
secret_env_vars=secret,
passthrough_env_prefixes=prefixes,
extra_env_vars=extra_env,
resolved_provider_name=resolved_name,
model_config=dict(agent_config.model_config),
)


def build_environment_from_config(config: AgentConfig) -> dict[str, str]:
"""Build environment dict by collecting passthrough vars from os.environ.
"""Build environment dict from static env_vars and passthrough vars from os.environ.

Secret env vars (listed in config.secret_env_vars) are excluded from
this output. Use build_secret_environment_from_config() for those.
"""
secret_set = set(config.secret_env_vars)
env: dict[str, str] = {}
env.update(config.env_vars)
for var in config.passthrough_env_vars:
if var in secret_set:
continue
Expand Down
22 changes: 8 additions & 14 deletions src/paude/agents/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from paude.agents.base import (
AgentConfig,
build_environment_from_config,
build_provider_credentials,
pipefail_install_lines,
)
from paude.mounts import resolve_path
Expand Down Expand Up @@ -38,39 +39,32 @@
"debug/*",
]

_CLAUDE_PASSTHROUGH_VARS = [
"CLAUDE_CODE_USE_VERTEX",
"ANTHROPIC_VERTEX_PROJECT_ID",
"GOOGLE_CLOUD_PROJECT",
]

_CLAUDE_PASSTHROUGH_PREFIXES = [
"CLOUDSDK_AUTH_",
]


class ClaudeAgent:
"""Claude Code agent implementation."""

def __init__(self) -> None:
def __init__(self, provider: str | None = None) -> None:
creds = build_provider_credentials("claude", provider)
self._config = AgentConfig(
name="claude",
display_name="Claude Code",
process_name="claude",
session_name="claude",
install_script="curl -fsSL https://claude.ai/install.sh | bash",
install_dir=".local/bin",
env_vars={},
env_vars=creds.extra_env_vars,
skip_install_env_var="PAUDE_SKIP_AGENT_INSTALL",
passthrough_env_vars=list(_CLAUDE_PASSTHROUGH_VARS),
passthrough_env_prefixes=list(_CLAUDE_PASSTHROUGH_PREFIXES),
passthrough_env_vars=creds.passthrough_env_vars,
secret_env_vars=creds.secret_env_vars,
passthrough_env_prefixes=creds.passthrough_env_prefixes,
config_dir_name=".claude",
config_file_name=".claude.json",
config_excludes=list(_CLAUDE_CONFIG_EXCLUDES),
activity_files=list(_CLAUDE_ACTIVITY_FILES),
yolo_flag="--dangerously-skip-permissions",
clear_command="/clear",
args_env_var="PAUDE_AGENT_ARGS",
provider=creds.resolved_provider_name,
)

@property
Expand Down
22 changes: 10 additions & 12 deletions src/paude/agents/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,30 @@
from paude.agents.base import (
AgentConfig,
build_environment_from_config,
build_provider_credentials,
pipefail_install_lines,
)
from paude.mounts import resolve_path

_CURSOR_SECRET_VARS = [
"CURSOR_API_KEY",
]


class CursorAgent:
"""Cursor CLI agent implementation."""

def __init__(self) -> None:
def __init__(self, provider: str | None = None) -> None:
creds = build_provider_credentials("cursor", provider)
creds.extra_env_vars["APPIMAGE_EXTRACT_AND_RUN"] = "1"
creds.extra_env_vars["NODE_USE_ENV_PROXY"] = "1"
self._config = AgentConfig(
name="cursor",
display_name="Cursor",
process_name="agent",
session_name="cursor",
install_script="curl https://cursor.com/install -fsS | bash",
install_dir=".local/bin",
env_vars={
"APPIMAGE_EXTRACT_AND_RUN": "1",
"NODE_USE_ENV_PROXY": "1",
},
passthrough_env_vars=[],
secret_env_vars=list(_CURSOR_SECRET_VARS),
passthrough_env_prefixes=[],
env_vars=creds.extra_env_vars,
passthrough_env_vars=creds.passthrough_env_vars,
secret_env_vars=creds.secret_env_vars,
passthrough_env_prefixes=creds.passthrough_env_prefixes,
config_dir_name=".cursor",
config_file_name=None,
config_excludes=[],
Expand All @@ -42,6 +39,7 @@ def __init__(self) -> None:
yolo_flag="--yolo",
clear_command="/clear",
extra_domain_aliases=["cursor"],
provider=creds.resolved_provider_name,
)

@property
Expand Down
25 changes: 12 additions & 13 deletions src/paude/agents/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,19 @@

from pathlib import Path

from paude.agents.base import AgentConfig, build_environment_from_config
from paude.agents.base import (
AgentConfig,
build_environment_from_config,
build_provider_credentials,
)
from paude.mounts import resolve_path

_GEMINI_PASSTHROUGH_VARS = [
"GOOGLE_CLOUD_PROJECT",
]

_GEMINI_PASSTHROUGH_PREFIXES = [
"CLOUDSDK_AUTH_",
]


class GeminiAgent:
"""Gemini CLI agent implementation."""

def __init__(self) -> None:
def __init__(self, provider: str | None = None) -> None:
creds = build_provider_credentials("gemini", provider)
self._config = AgentConfig(
name="gemini",
display_name="Gemini CLI",
Expand All @@ -30,16 +27,18 @@ def __init__(self) -> None:
# and install_agent() skips via `command -v gemini`.
install_script="npm install -g @google/gemini-cli",
install_dir=".local/bin",
env_vars={},
passthrough_env_vars=list(_GEMINI_PASSTHROUGH_VARS),
passthrough_env_prefixes=list(_GEMINI_PASSTHROUGH_PREFIXES),
env_vars=creds.extra_env_vars,
passthrough_env_vars=creds.passthrough_env_vars,
secret_env_vars=creds.secret_env_vars,
passthrough_env_prefixes=creds.passthrough_env_prefixes,
config_dir_name=".gemini",
config_file_name=None,
config_excludes=[],
activity_files=[],
yolo_flag="--yolo",
clear_command="/clear",
extra_domain_aliases=["gemini", "nodejs"],
provider=creds.resolved_provider_name,
)

@property
Expand Down
46 changes: 19 additions & 27 deletions src/paude/agents/openclaw.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,13 @@

from pathlib import Path

from paude.agents.base import AgentConfig, build_environment_from_config
from paude.agents.base import (
AgentConfig,
build_environment_from_config,
build_provider_credentials,
)
from paude.mounts import resolve_path

_OPENCLAW_SECRET_VARS = [
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"GROQ_API_KEY",
]

_OPENCLAW_PASSTHROUGH_VARS = [
"ANTHROPIC_VERTEX_PROJECT_ID",
"GOOGLE_CLOUD_PROJECT",
"GOOGLE_CLOUD_PROJECT_ID",
"GOOGLE_CLOUD_LOCATION",
"CLOUD_ML_REGION",
]

_OPENCLAW_PASSTHROUGH_PREFIXES = [
"CLOUDSDK_AUTH_",
]


class OpenClawAgent:
"""OpenClaw agent implementation.
Expand All @@ -34,20 +20,22 @@ class OpenClawAgent:
The tmux session shows server logs.
"""

def __init__(self) -> None:
def __init__(self, provider: str | None = None) -> None:
creds = build_provider_credentials("openclaw", provider)
creds.extra_env_vars["NODE_USE_ENV_PROXY"] = "1"
self._model_config = creds.model_config

self._config = AgentConfig(
name="openclaw",
display_name="OpenClaw",
process_name="node",
session_name="openclaw",
install_script="",
install_dir=".local/bin",
env_vars={
"NODE_USE_ENV_PROXY": "1",
},
passthrough_env_vars=list(_OPENCLAW_PASSTHROUGH_VARS),
secret_env_vars=list(_OPENCLAW_SECRET_VARS),
passthrough_env_prefixes=list(_OPENCLAW_PASSTHROUGH_PREFIXES),
env_vars=creds.extra_env_vars,
passthrough_env_vars=creds.passthrough_env_vars,
secret_env_vars=creds.secret_env_vars,
passthrough_env_prefixes=creds.passthrough_env_prefixes,
config_dir_name=".openclaw",
config_file_name=None,
config_excludes=[],
Expand All @@ -58,6 +46,7 @@ def __init__(self) -> None:
extra_domain_aliases=["openclaw"],
exposed_ports=[(18789, 18789)],
default_base_image="ghcr.io/openclaw/openclaw:latest",
provider=creds.resolved_provider_name,
)

@property
Expand Down Expand Up @@ -121,6 +110,9 @@ def apply_sandbox_config(self, home: str, workspace: str, args: str) -> str:
/credentials/env/ and are loaded by the entrypoint into the
process environment before the agent launches.
"""
primary_model = self._model_config.get(
"primary", "anthropic-vertex/claude-opus-4-6"
)
return f"""\
#!/bin/bash
# Pre-configure OpenClaw for containerized operation
Expand All @@ -140,7 +132,7 @@ def apply_sandbox_config(self, home: str, workspace: str, args: str) -> str:
"defaults": {{
"workspace": "{workspace}",
"model": {{
"primary": "anthropic-vertex/claude-opus-4-6"
"primary": "{primary_model}"
}}
}}
}}
Expand Down
2 changes: 2 additions & 0 deletions src/paude/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Session:
container_id: str | None = None
volume_name: str | None = None
agent: str = "claude"
provider: str | None = None
version: str | None = None


Expand Down Expand Up @@ -82,6 +83,7 @@ class SessionConfig:
credential_timeout: int = 60 # minutes of inactivity before credential removal
wait_for_ready: bool = True
agent: str = "claude"
provider: str | None = None
gpu: str | None = None
reuse_volume: bool = False
ports: list[tuple[int, int]] = field(default_factory=list)
Expand Down
6 changes: 6 additions & 0 deletions src/paude/backends/openshift/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from paude.backends.shared import (
PAUDE_LABEL_AGENT,
PAUDE_LABEL_GPU,
PAUDE_LABEL_PROVIDER,
PAUDE_LABEL_VERSION,
PAUDE_LABEL_YOLO,
encode_path,
Expand Down Expand Up @@ -54,6 +55,7 @@ def __init__(
image: str,
resources: dict[str, dict[str, str]],
agent: str = "claude",
provider: str | None = None,
gpu: str | None = None,
yolo: bool = False,
) -> None:
Expand All @@ -65,6 +67,7 @@ def __init__(
image: Container image to use.
resources: Resource requests/limits for the container.
agent: Agent name (e.g., "claude").
provider: Inference provider name (e.g., "vertex", "openai").
gpu: GPU spec (e.g., "all", "device=0,1", "2").
yolo: Whether YOLO mode is enabled.
"""
Expand All @@ -73,6 +76,7 @@ def __init__(
self._image = image
self._resources = resources
self._agent = agent
self._provider = provider
self._gpu = gpu
self._yolo = yolo
self._env: dict[str, str] = {}
Expand Down Expand Up @@ -133,6 +137,8 @@ def _build_metadata(self, created_at: str) -> dict[str, Any]:
PAUDE_LABEL_AGENT: self._agent,
PAUDE_LABEL_VERSION: __version__,
}
if self._provider:
labels[PAUDE_LABEL_PROVIDER] = self._provider
if self._gpu:
labels[PAUDE_LABEL_GPU] = self._gpu
if self._yolo:
Expand Down
Loading
Loading