feat: credential store auto sync

This commit is contained in:
Timothy
2026-01-29 14:37:20 -08:00
parent aa0fff8ac5
commit 248716c093
14 changed files with 674 additions and 1770 deletions
+9 -2
View File
@@ -29,7 +29,14 @@
"mcp__agent-builder__store_credential",
"mcp__agent-builder__list_stored_credentials",
"mcp__agent-builder__delete_stored_credential",
"mcp__agent-builder__verify_credentials"
"mcp__agent-builder__verify_credentials",
"Bash(PYTHONPATH=/home/timothy/oss/hive/core:/home/timothy/oss/hive/exports python:*)",
"Bash(PYTHONPATH=core:exports:tools/src python -m hubspot_input:*)",
"mcp__agent-builder__export_graph"
]
}
},
"enabledMcpjsonServers": [
"agent-builder",
"tools"
]
}
File diff suppressed because it is too large Load Diff
@@ -1,13 +1,30 @@
"""Runtime configuration."""
from dataclasses import dataclass
import json
from dataclasses import dataclass, field
from pathlib import Path
def _load_preferred_model() -> str:
"""Load preferred model from ~/.hive/configuration.json."""
config_path = Path.home() / ".hive" / "configuration.json"
if config_path.exists():
try:
with open(config_path) as f:
config = json.load(f)
llm = config.get("llm", {})
if llm.get("provider") and llm.get("model"):
return f"{llm['provider']}/{llm['model']}"
except Exception:
pass
return "anthropic/claude-sonnet-4-20250514"
@dataclass
class RuntimeConfig:
model: str = "groq/moonshotai/kimi-k2-instruct-0905"
model: str = field(default_factory=_load_preferred_model)
temperature: float = 0.7
max_tokens: int = 16384
max_tokens: int = 8192
api_key: str | None = None
api_base: str | None = None
+150 -54
View File
@@ -4,7 +4,7 @@ description: Set up and install credentials for an agent. Detects missing creden
license: Apache-2.0
metadata:
author: hive
version: "2.0"
version: "2.1"
type: utility
---
@@ -45,18 +45,32 @@ validation = runner.validate()
# validation.warnings contains detailed messages with help URLs
```
Alternatively, inspect manually:
Alternatively, check the credential store directly:
```python
from aden_tools.credentials import CredentialManager
from core.framework.credentials import CredentialStore
creds = CredentialManager()
# Use encrypted storage (default: ~/.hive/credentials)
store = CredentialStore.with_encrypted_storage()
# For tool-based credentials
missing_tools = creds.get_missing_for_tools(agent_tool_names)
# Check what's available
available = store.list_credentials()
print(f"Available credentials: {available}")
# For node-type credentials (e.g., LLM nodes need ANTHROPIC_API_KEY)
missing_nodes = creds.get_missing_for_node_types(agent_node_types)
# Check if specific credential exists
if store.is_available("hubspot"):
print("HubSpot credential found")
else:
print("HubSpot credential missing")
```
To see all known credential specs (for help URLs and setup instructions):
```python
from aden_tools.credentials import CREDENTIAL_SPECS
for name, spec in CREDENTIAL_SPECS.items():
print(f"{name}: env_var={spec.env_var}, aden={spec.aden_supported}")
```
### Step 3: Present Auth Options for Each Missing Credential
@@ -64,11 +78,25 @@ missing_nodes = creds.get_missing_for_node_types(agent_node_types)
For each missing credential, check what authentication methods are available:
```python
from aden_tools.credentials import CredentialManager
from aden_tools.credentials import CREDENTIAL_SPECS
creds = CredentialManager()
auth_options = creds.get_auth_options("hubspot") # Returns: ['aden', 'direct', 'custom']
setup_info = creds.get_setup_instructions("hubspot")
spec = CREDENTIAL_SPECS.get("hubspot")
if spec:
# Determine available auth options
auth_options = []
if spec.aden_supported:
auth_options.append("aden")
if spec.direct_api_key_supported:
auth_options.append("direct")
auth_options.append("custom") # Always available
# Get setup info
setup_info = {
"env_var": spec.env_var,
"description": spec.description,
"help_url": spec.help_url,
"api_key_instructions": spec.api_key_instructions,
}
```
Present the available options using AskUserQuestion:
@@ -98,6 +126,14 @@ Choose how to configure HUBSPOT_ACCESS_TOKEN:
This is the recommended flow for supported integrations (HubSpot, etc.).
**How Aden OAuth Works:**
The ADEN_API_KEY represents a user who has already completed OAuth authorization on Aden's platform. When users sign up and connect integrations on Aden, those OAuth tokens are stored server-side. Having an ADEN_API_KEY means:
1. User has an Aden account
2. User has already authorized integrations (HubSpot, etc.) via OAuth on Aden
3. We just need to sync those credentials down to the local credential store
**4.1a. Check for ADEN_API_KEY**
```python
@@ -105,14 +141,17 @@ import os
aden_key = os.environ.get("ADEN_API_KEY")
```
If not set, guide user to get one:
If not set, guide user to get one from Aden (this is where they do OAuth):
```python
from aden_tools.credentials import open_browser, get_aden_setup_url
# Open browser to get Aden API key
# Open browser to Aden - user will sign up and connect integrations there
url = get_aden_setup_url() # https://integration.adenhq.com/setup
success, msg = open_browser(url)
print("Please sign in to Aden and connect your integrations (HubSpot, etc.).")
print("Once done, copy your API key and return here.")
```
Ask user to provide the ADEN_API_KEY they received.
@@ -155,49 +194,64 @@ config = json.loads(config_path.read_text()) if config_path.exists() else {}
config["aden"] = {
"api_key_configured": True,
"api_url": "https://hive.adenhq.com"
"api_url": "https://staging-hive.adenhq.com"
}
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(json.dumps(config, indent=2))
```
**4.1c. Open Browser for OAuth2 Authorization**
**4.1c. Sync Credentials from Aden Server**
```python
from aden_tools.credentials import open_browser, get_aden_auth_url
# Get integration ID from credential spec
setup_info = creds.get_setup_instructions("hubspot")
provider_name = setup_info["aden_provider_name"] # "hubspot"
auth_url = get_aden_auth_url(provider_name) # https://integration.adenhq.com/connect/hubspot
success, msg = open_browser(auth_url)
print("Please complete the OAuth2 authorization in your browser.")
print("Once done, return here to continue.")
```
Wait for user to confirm they've completed authorization.
**4.1d. Sync Credentials from Aden Server**
Since the user has already authorized integrations on Aden, use the one-liner factory method:
```python
from core.framework.credentials import CredentialStore
from core.framework.credentials.aden import AdenCredentialClient
store = CredentialStore.with_encrypted_storage()
aden_client = AdenCredentialClient(
api_url="https://hive.adenhq.com",
api_key=aden_api_key,
# This single call handles everything:
# - Creates encrypted local storage at ~/.hive/credentials
# - Configures Aden client from ADEN_API_KEY env var
# - Syncs all credentials from Aden server automatically
store = CredentialStore.with_aden_sync(
base_url="https://staging-hive.adenhq.com",
auto_sync=True, # Syncs on creation
)
# Sync credentials from Aden server
synced = aden_client.sync_to_store(store)
print(f"Synced {len(synced)} credentials from Aden")
# Check what was synced
synced = store.list_credentials()
print(f"Synced credentials: {synced}")
# If the required credential wasn't synced, the user hasn't authorized it on Aden yet
if "hubspot" not in synced:
print("HubSpot not found in your Aden account.")
print("Please visit https://integration.adenhq.com to connect HubSpot, then try again.")
```
**4.1e. Run Health Check**
For more control over the sync process:
```python
from core.framework.credentials import CredentialStore
from core.framework.credentials.aden import (
AdenCredentialClient,
AdenClientConfig,
AdenSyncProvider,
)
# Create client (API key loaded from ADEN_API_KEY env var)
client = AdenCredentialClient(AdenClientConfig(
base_url="https://staging-hive.adenhq.com",
))
# Create provider and store
provider = AdenSyncProvider(client=client)
store = CredentialStore.with_encrypted_storage()
# Manual sync
synced_count = provider.sync_all(store)
print(f"Synced {synced_count} credentials from Aden")
```
**4.1d. Run Health Check**
```python
from aden_tools.credentials import check_credential_health
@@ -221,14 +275,20 @@ For users who prefer manual API key management.
**4.2a. Show Setup Instructions**
```python
setup_info = creds.get_setup_instructions("hubspot")
print(setup_info["api_key_instructions"])
from aden_tools.credentials import CREDENTIAL_SPECS
spec = CREDENTIAL_SPECS.get("hubspot")
if spec and spec.api_key_instructions:
print(spec.api_key_instructions)
# Output:
# To get a HubSpot Private App token:
# 1. Go to HubSpot Settings > Integrations > Private Apps
# 2. Click "Create a private app"
# 3. Name your app (e.g., "Hive Agent")
# ...
if spec and spec.help_url:
print(f"More info: {spec.help_url}")
```
**4.2b. Collect API Key from User**
@@ -394,10 +454,14 @@ All credential specs are defined in `tools/src/aden_tools/credentials/`:
| File | Category | Credentials | Aden Supported |
| ----------------- | ------------- | --------------------------------------------- | -------------- |
| `llm.py` | LLM Providers | `anthropic`, `openai`, `cerebras`, `groq` | No |
| `llm.py` | LLM Providers | `anthropic` | No |
| `search.py` | Search Tools | `brave_search`, `google_search`, `google_cse` | No |
| `integrations.py` | Integrations | `hubspot` | Yes |
**Note:** Additional LLM providers (Cerebras, Groq, OpenAI) are handled by LiteLLM via environment
variables (`CEREBRAS_API_KEY`, `GROQ_API_KEY`, `OPENAI_API_KEY`) but are not yet in CREDENTIAL_SPECS.
Add them to `llm.py` as needed.
To check what's registered:
```python
@@ -406,6 +470,40 @@ for name, spec in CREDENTIAL_SPECS.items():
print(f"{name}: aden={spec.aden_supported}, direct={spec.direct_api_key_supported}")
```
## Migration: CredentialManager → CredentialStore
**CredentialManager is deprecated.** Use CredentialStore instead.
| Old (Deprecated) | New (Recommended) |
| --------------------------------------------- | ---------------------------------------------------- |
| `CredentialManager()` | `CredentialStore.with_encrypted_storage()` |
| `creds.get("hubspot")` | `store.get("hubspot")` or `store.get_key("hubspot", "access_token")` |
| `creds.validate_for_tools(tools)` | Use `store.is_available(cred_id)` per credential |
| `creds.get_auth_options("hubspot")` | Check `CREDENTIAL_SPECS["hubspot"].aden_supported` |
| `creds.get_setup_instructions("hubspot")` | Access `CREDENTIAL_SPECS["hubspot"]` directly |
**Why migrate?**
- **CredentialStore** supports encrypted storage, multi-key credentials, template resolution, and automatic token refresh
- **CredentialManager** only reads from environment variables and .env files (no encryption, no refresh)
- **CredentialStoreAdapter** exists for backward compatibility during migration
```python
# Old way (deprecated)
from aden_tools.credentials import CredentialManager
creds = CredentialManager()
token = creds.get("hubspot")
# New way (recommended)
from core.framework.credentials import CredentialStore
store = CredentialStore.with_encrypted_storage()
token = store.get("hubspot")
# With Aden sync (recommended for OAuth integrations)
store = CredentialStore.with_aden_sync()
token = store.get_key("hubspot", "access_token")
```
## Example Session
```
@@ -447,7 +545,10 @@ Agent: Great! Let me check if you have an Aden API key configured...
[Checks for ADEN_API_KEY, not found]
[Opens browser to integration.adenhq.com/setup]
Agent: I've opened the Aden setup page. Please sign in and copy your API key.
Agent: I've opened Aden's setup page. Please:
1. Sign in or create an account
2. Connect your HubSpot account (OAuth happens on Aden's site)
3. Copy the API key shown after setup
[AskUserQuestion: "Please provide your Aden API key:"]
[User provides key]
@@ -457,20 +558,15 @@ Agent: Would you like me to save this to your shell config (~/.zshrc) for future
[User approves]
[Saves to ~/.zshrc and ~/.hive/configuration.json]
Agent: Now let's connect your HubSpot account.
Agent: Let me sync your credentials from Aden...
[Opens browser to integration.adenhq.com/connect/hubspot]
Agent: Please complete the OAuth2 authorization in your browser, then return here.
[User confirms completion]
[Syncs credentials from Aden server]
[Syncs credentials from Aden server - OAuth already done on Aden's side]
[Runs health check]
Agent: HubSpot credentials validated successfully!
All credentials are now configured:
- ANTHROPIC_API_KEY: Stored in encrypted credential store
- HUBSPOT_ACCESS_TOKEN: Connected via Aden OAuth2
- HUBSPOT_ACCESS_TOKEN: Synced from Aden (OAuth completed on Aden)
- Validation passed - your agent is ready to run!
```
+2 -2
View File
@@ -1,7 +1,7 @@
{
"mcpServers": {
"agent-builder": {
"command": "python",
"command": ".venv/bin/python",
"args": ["-m", "framework.mcp.agent_builder_server"],
"cwd": "core",
"env": {
@@ -9,7 +9,7 @@
}
},
"tools": {
"command": "python",
"command": ".venv/bin/python",
"args": ["mcp_server.py", "--stdio"],
"cwd": "tools",
"env": {
+4 -4
View File
@@ -8,12 +8,12 @@ This client fetches tokens and delegates refresh operations to Aden.
Usage:
# API key loaded from ADEN_API_KEY environment variable by default
client = AdenCredentialClient(AdenClientConfig(
base_url="https://hive.adenhq.com",
base_url="https://staging-hive.adenhq.com",
))
# Or explicitly provide the API key
client = AdenCredentialClient(AdenClientConfig(
base_url="https://hive.adenhq.com",
base_url="https://staging-hive.adenhq.com",
api_key="your-api-key",
))
@@ -85,7 +85,7 @@ class AdenClientConfig:
"""Configuration for Aden API client."""
base_url: str
"""Base URL of the Aden server (e.g., 'https://hive.adenhq.com')."""
"""Base URL of the Aden server (e.g., 'https://staging-hive.adenhq.com')."""
api_key: str | None = None
"""Agent's API key for authenticating with Aden.
@@ -199,7 +199,7 @@ class AdenCredentialClient:
Usage:
# API key loaded from ADEN_API_KEY environment variable
config = AdenClientConfig(
base_url="https://hive.adenhq.com",
base_url="https://staging-hive.adenhq.com",
)
client = AdenCredentialClient(config)
+1 -1
View File
@@ -79,7 +79,7 @@ class AdenSyncProvider(CredentialProvider):
Usage:
client = AdenCredentialClient(AdenClientConfig(
base_url="https://hive.adenhq.com",
base_url="https://staging-hive.adenhq.com",
api_key=os.environ["ADEN_API_KEY"],
))
+2 -2
View File
@@ -616,7 +616,7 @@ class CredentialStore:
@classmethod
def with_aden_sync(
cls,
base_url: str = "https://hive.adenhq.com",
base_url: str = "https://staging-hive.adenhq.com",
cache_ttl_seconds: int = 300,
local_path: str | None = None,
auto_sync: bool = True,
@@ -630,7 +630,7 @@ class CredentialStore:
is unreachable.
Args:
base_url: Aden server URL (default: https://hive.adenhq.com)
base_url: Aden server URL (default: https://staging-hive.adenhq.com)
cache_ttl_seconds: How long to cache credentials locally (default: 5 min)
local_path: Path for local credential storage (default: ~/.hive/credentials)
auto_sync: Whether to sync all credentials on startup (default: True)
+2 -3
View File
@@ -1,5 +1,4 @@
"""MCP servers for worker-bee."""
from framework.mcp.agent_builder_server import mcp as agent_builder_server
__all__ = ["agent_builder_server"]
# Don't auto-import servers to avoid double-import issues when running with -m
__all__ = []
+88 -45
View File
@@ -457,14 +457,27 @@ def _validate_tool_credentials(tools_list: list[str]) -> dict | None:
return None
try:
from aden_tools.credentials import CredentialManager
from aden_tools.credentials import CREDENTIAL_SPECS
cred_manager = CredentialManager()
missing_creds = cred_manager.get_missing_for_tools(tools_list)
store = _get_credential_store()
if missing_creds:
cred_errors = []
for cred_name, spec in missing_creds:
# Build tool -> credential mapping
tool_to_cred: dict[str, str] = {}
for cred_name, spec in CREDENTIAL_SPECS.items():
for tool_name in spec.tools:
tool_to_cred[tool_name] = cred_name
# Find missing credentials
cred_errors = []
checked: set[str] = set()
for tool_name in tools_list:
cred_name = tool_to_cred.get(tool_name)
if cred_name is None or cred_name in checked:
continue
checked.add(cred_name)
spec = CREDENTIAL_SPECS[cred_name]
cred_id = spec.credential_id or cred_name
if spec.required and not store.is_available(cred_id):
affected_tools = [t for t in tools_list if t in spec.tools]
cred_errors.append(
{
@@ -476,15 +489,16 @@ def _validate_tool_credentials(tools_list: list[str]) -> dict | None:
}
)
if cred_errors:
return {
"valid": False,
"errors": [f"Missing credentials for tools: {[e['env_var'] for e in cred_errors]}"],
"missing_credentials": cred_errors,
"action_required": "Add the credentials to your .env file and retry",
"action_required": "Store credentials via store_credential and retry",
"example": f"Add to .env:\n{cred_errors[0]['env_var']}=your_key_here",
"message": (
"Cannot add node: missing API credentials. "
"Add them to .env and retry this command."
"Store them via store_credential and retry this command."
),
}
except ImportError as e:
@@ -492,7 +506,7 @@ def _validate_tool_credentials(tools_list: list[str]) -> dict | None:
return {
"valid": True,
"warnings": [
f"⚠️ Credential validation SKIPPED: aden_tools not available ({e}). "
f"Credential validation SKIPPED: aden_tools not available ({e}). "
"Tools may fail at runtime if credentials are missing. "
"Add tools/src to PYTHONPATH to enable validation."
],
@@ -3234,18 +3248,31 @@ def load_exported_plan(
# =============================================================================
def _get_credential_manager():
"""Get a CredentialManager instance."""
from aden_tools.credentials import CredentialManager
return CredentialManager()
def _get_credential_store():
"""Get a CredentialStore with encrypted file storage at ~/.hive/credentials."""
from framework.credentials import CredentialStore
"""Get a CredentialStore that checks encrypted files and env vars.
return CredentialStore.with_encrypted_storage()
Uses CompositeStorage: encrypted file storage (primary) with env var fallback.
This ensures credentials stored via `store_credential` AND env vars are both found.
"""
from framework.credentials import CredentialStore
from framework.credentials.storage import CompositeStorage, EncryptedFileStorage, EnvVarStorage
# Build env var mapping from CREDENTIAL_SPECS for the fallback
env_mapping: dict[str, str] = {}
try:
from aden_tools.credentials import CREDENTIAL_SPECS
for name, spec in CREDENTIAL_SPECS.items():
cred_id = spec.credential_id or name
env_mapping[cred_id] = spec.env_var
except ImportError:
pass
storage = CompositeStorage(
primary=EncryptedFileStorage(),
fallbacks=[EnvVarStorage(env_mapping=env_mapping)],
)
return CredentialStore(storage=storage)
@mcp.tool()
@@ -3259,43 +3286,59 @@ def check_missing_credentials(
Use this before running or testing an agent to identify what needs to be configured.
"""
try:
from aden_tools.credentials import CREDENTIAL_SPECS
from framework.runner import AgentRunner
runner = AgentRunner.load(agent_path)
runner.validate()
cred_manager = _get_credential_manager()
store = _get_credential_store()
info = runner.info()
# Gather missing tool credentials
missing_tools = cred_manager.get_missing_for_tools(info.required_tools)
# Gather missing node-type credentials
node_types = list({node.node_type for node in runner.graph.nodes})
missing_nodes = cred_manager.get_missing_for_node_types(node_types)
# Deduplicate
seen = set()
# Build reverse mappings: tool/node_type -> credential name
tool_to_cred: dict[str, str] = {}
node_type_to_cred: dict[str, str] = {}
for cred_name, spec in CREDENTIAL_SPECS.items():
for tool_name in spec.tools:
tool_to_cred[tool_name] = cred_name
for nt in spec.node_types:
node_type_to_cred[nt] = cred_name
# Gather missing credentials (tools + node types), deduplicated
seen: set[str] = set()
all_missing = []
for name, spec in missing_tools + missing_nodes:
if spec.env_var not in seen:
seen.add(spec.env_var)
all_missing.append(
{
"credential_name": name,
"env_var": spec.env_var,
"description": spec.description,
"help_url": spec.help_url,
"tools": spec.tools,
}
)
for name_list, mapping in [
(info.required_tools, tool_to_cred),
(node_types, node_type_to_cred),
]:
for item_name in name_list:
cred_name = mapping.get(item_name)
if cred_name is None or cred_name in seen:
continue
seen.add(cred_name)
spec = CREDENTIAL_SPECS[cred_name]
cred_id = spec.credential_id or cred_name
if spec.required and not store.is_available(cred_id):
all_missing.append(
{
"credential_name": cred_name,
"env_var": spec.env_var,
"description": spec.description,
"help_url": spec.help_url,
"tools": spec.tools,
}
)
# Also check what's already set
all_specs = cred_manager._specs
available = []
for name, spec in all_specs.items():
if spec.env_var not in seen and cred_manager.is_available(name):
# Only include if relevant to this agent's tools/nodes
for name, spec in CREDENTIAL_SPECS.items():
if name in seen:
continue
cred_id = spec.credential_id or name
if store.is_available(cred_id):
relevant_tools = [t for t in spec.tools if t in info.required_tools]
relevant_nodes = [n for n in spec.node_types if n in node_types]
if relevant_tools or relevant_nodes:
@@ -3469,4 +3512,4 @@ def verify_credentials(
# =============================================================================
if __name__ == "__main__":
mcp.run()
mcp.run(transport="stdio")
+114 -23
View File
@@ -23,6 +23,42 @@ if TYPE_CHECKING:
from framework.runner.protocol import AgentMessage, CapabilityResponse
# Configuration paths
HIVE_CONFIG_FILE = Path.home() / ".hive" / "configuration.json"
CLAUDE_CREDENTIALS_FILE = Path.home() / ".claude" / ".credentials.json"
def get_hive_config() -> dict[str, Any]:
"""Load hive configuration from ~/.hive/configuration.json."""
if not HIVE_CONFIG_FILE.exists():
return {}
try:
with open(HIVE_CONFIG_FILE) as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return {}
def get_claude_code_token() -> str | None:
"""
Get the OAuth token from Claude Code subscription.
Reads from ~/.claude/.credentials.json which is created by the
Claude Code CLI when users authenticate with their subscription.
Returns:
The access token if available, None otherwise.
"""
if not CLAUDE_CREDENTIALS_FILE.exists():
return None
try:
with open(CLAUDE_CREDENTIALS_FILE) as f:
creds = json.load(f)
return creds.get("claudeAiOauth", {}).get("accessToken")
except (json.JSONDecodeError, OSError):
return None
@dataclass
class AgentInfo:
"""Information about an exported agent."""
@@ -426,15 +462,32 @@ class AgentRunner:
self._llm = MockLLMProvider(model=self.model)
else:
# Detect required API key from model name
api_key_env = self._get_api_key_env_var(self.model)
if api_key_env and os.environ.get(api_key_env):
from framework.llm.litellm import LiteLLMProvider
from framework.llm.litellm import LiteLLMProvider
self._llm = LiteLLMProvider(model=self.model)
elif api_key_env:
print(f"Warning: {api_key_env} not set. LLM calls will fail.")
print(f"Set it with: export {api_key_env}=your-api-key")
# Check if Claude Code subscription is configured
config = get_hive_config()
llm_config = config.get("llm", {})
use_claude_code = llm_config.get("use_claude_code_subscription", False)
api_key = None
if use_claude_code:
# Get OAuth token from Claude Code subscription
api_key = get_claude_code_token()
if not api_key:
print("Warning: Claude Code subscription configured but no token found.")
print("Run 'claude' to authenticate, then try again.")
if api_key:
# Use Claude Code subscription token
self._llm = LiteLLMProvider(model=self.model, api_key=api_key)
else:
# Fall back to environment variable
api_key_env = self._get_api_key_env_var(self.model)
if api_key_env and os.environ.get(api_key_env):
self._llm = LiteLLMProvider(model=self.model)
elif api_key_env:
print(f"Warning: {api_key_env} not set. LLM calls will fail.")
print(f"Set it with: export {api_key_env}=your-api-key")
# Get tools for executor/runtime
tools = list(self._tool_registry.get_tools().values())
@@ -840,28 +893,66 @@ class AgentRunner:
warnings.append(f"Missing tool implementations: {', '.join(missing_tools)}")
# Check credentials for required tools and node types
# Uses CredentialStore (encrypted files + env var fallback)
missing_credentials = []
try:
from aden_tools.credentials import CredentialManager
from aden_tools.credentials import CREDENTIAL_SPECS
cred_manager = CredentialManager()
from framework.credentials import CredentialStore
from framework.credentials.storage import (
CompositeStorage,
EncryptedFileStorage,
EnvVarStorage,
)
# Check tool credentials (Tier 2)
missing_creds = cred_manager.get_missing_for_tools(info.required_tools)
for _, spec in missing_creds:
missing_credentials.append(spec.env_var)
affected_tools = [t for t in info.required_tools if t in spec.tools]
tools_str = ", ".join(affected_tools)
warning_msg = f"Missing {spec.env_var} for {tools_str}"
if spec.help_url:
warning_msg += f"\n Get it at: {spec.help_url}"
warnings.append(warning_msg)
# Build env mapping for fallback
env_mapping = {
(spec.credential_id or name): spec.env_var
for name, spec in CREDENTIAL_SPECS.items()
}
storage = CompositeStorage(
primary=EncryptedFileStorage(),
fallbacks=[EnvVarStorage(env_mapping=env_mapping)],
)
store = CredentialStore(storage=storage)
# Build reverse mappings
tool_to_cred: dict[str, str] = {}
node_type_to_cred: dict[str, str] = {}
for cred_name, spec in CREDENTIAL_SPECS.items():
for tool_name in spec.tools:
tool_to_cred[tool_name] = cred_name
for nt in spec.node_types:
node_type_to_cred[nt] = cred_name
# Check tool credentials
checked: set[str] = set()
for tool_name in info.required_tools:
cred_name = tool_to_cred.get(tool_name)
if cred_name is None or cred_name in checked:
continue
checked.add(cred_name)
spec = CREDENTIAL_SPECS[cred_name]
cred_id = spec.credential_id or cred_name
if spec.required and not store.is_available(cred_id):
missing_credentials.append(spec.env_var)
affected_tools = [t for t in info.required_tools if t in spec.tools]
tools_str = ", ".join(affected_tools)
warning_msg = f"Missing {spec.env_var} for {tools_str}"
if spec.help_url:
warning_msg += f"\n Get it at: {spec.help_url}"
warnings.append(warning_msg)
# Check node type credentials (e.g., ANTHROPIC_API_KEY for LLM nodes)
node_types = list({node.node_type for node in self.graph.nodes})
missing_node_creds = cred_manager.get_missing_for_node_types(node_types)
for _, spec in missing_node_creds:
if spec.env_var not in missing_credentials: # Avoid duplicates
for nt in node_types:
cred_name = node_type_to_cred.get(nt)
if cred_name is None or cred_name in checked:
continue
checked.add(cred_name)
spec = CREDENTIAL_SPECS[cred_name]
cred_id = spec.credential_id or cred_name
if spec.required and not store.is_available(cred_id):
missing_credentials.append(spec.env_var)
affected_types = [t for t in node_types if t in spec.node_types]
types_str = ", ".join(affected_types)
+5 -5
View File
@@ -120,7 +120,7 @@ Response 400 Bad Request:
"error": "refresh_failed",
"message": "Refresh token is invalid or revoked. User must re-authorize.",
"requires_reauthorization": true,
"reauthorization_url": "https://hive.adenhq.com/integrations/hubspot/connect"
"reauthorization_url": "https://staging-hive.adenhq.com/integrations/hubspot/connect"
}
Response 429 Too Many Requests:
@@ -196,7 +196,7 @@ Response 200 OK (needs reauth):
"valid": false,
"reason": "refresh_token_revoked",
"requires_reauthorization": true,
"reauthorization_url": "https://hive.adenhq.com/integrations/hubspot/connect"
"reauthorization_url": "https://staging-hive.adenhq.com/integrations/hubspot/connect"
}
```
@@ -266,7 +266,7 @@ HTTP client for communicating with the Aden server.
@dataclass
class AdenClientConfig:
"""Configuration for Aden API client."""
base_url: str # e.g., "https://hive.adenhq.com"
base_url: str # e.g., "https://staging-hive.adenhq.com"
api_key: str | None = None # Loaded from ADEN_API_KEY env var if not provided
tenant_id: str | None = None # For multi-tenant
timeout: float = 30.0
@@ -322,7 +322,7 @@ class AdenSyncProvider(CredentialProvider):
Usage:
# API key loaded from ADEN_API_KEY env var by default
client = AdenCredentialClient(AdenClientConfig(
base_url="https://hive.adenhq.com",
base_url="https://staging-hive.adenhq.com",
))
provider = AdenSyncProvider(client=client)
@@ -573,7 +573,7 @@ provider = HubSpotOAuth2Provider(
# After: Delegate to Aden
provider = AdenSyncProvider(
client=AdenCredentialClient(AdenClientConfig(
base_url="https://hive.adenhq.com",
base_url="https://staging-hive.adenhq.com",
api_key="...",
))
)
+7 -43
View File
@@ -224,19 +224,17 @@ declare -A PROVIDER_IDS=(
["MISTRAL_API_KEY"]="mistral"
["TOGETHER_API_KEY"]="together"
["DEEPSEEK_API_KEY"]="deepseek"
["CLAUDE_CODE"]="anthropic"
)
declare -A DEFAULT_MODELS=(
["anthropic"]="claude-sonnet-4-20250514"
["anthropic"]="claude-sonnet-4-5-20250929"
["openai"]="gpt-4o"
["gemini"]="gemini/gemini-2.0-flash"
["google"]="gemini/gemini-2.0-flash"
["groq"]="groq/llama-3.3-70b-versatile"
["cerebras"]="cerebras/llama-3.3-70b"
["mistral"]="mistral/mistral-large-latest"
["together"]="together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo"
["deepseek"]="deepseek/deepseek-chat"
["gemini"]="gemini-3.0-flash-preview"
["groq"]="moonshotai/kimi-k2-instruct-0905"
["cerebras"]="zai-glm-4.7"
["mistral"]="mistral-large-latest"
["together_ai"]="meta-llama/Llama-3.3-70B-Instruct-Turbo"
["deepseek"]="deepseek-chat"
)
# Configuration directory
@@ -280,34 +278,12 @@ if [ -f "$HOME/.env" ]; then
set +a
fi
# Check for Claude Code subscription
CLAUDE_CREDS_FILE="$HOME/.claude/.credentials.json"
CLAUDE_TOKEN=""
if [ -f "$CLAUDE_CREDS_FILE" ]; then
CLAUDE_TOKEN=$($PYTHON_CMD -c "
import json
try:
with open('$CLAUDE_CREDS_FILE') as f:
creds = json.load(f)
token = creds.get('claudeAiOauth', {}).get('accessToken', '')
if token:
print(token)
except:
pass
" 2>/dev/null)
fi
# Find all available API keys
FOUND_PROVIDERS=() # Display names for UI
FOUND_ENV_VARS=() # Corresponding env var names
SELECTED_PROVIDER_ID="" # Will hold the chosen provider ID
SELECTED_ENV_VAR="" # Will hold the chosen env var
if [ -n "$CLAUDE_TOKEN" ]; then
FOUND_PROVIDERS+=("Claude Code subscription")
FOUND_ENV_VARS+=("CLAUDE_CODE")
fi
for env_var in "${!PROVIDER_NAMES[@]}"; do
value="${!env_var}"
if [ -n "$value" ]; then
@@ -330,12 +306,6 @@ if [ ${#FOUND_PROVIDERS[@]} -gt 0 ]; then
SELECTED_ENV_VAR="${FOUND_ENV_VARS[0]}"
SELECTED_PROVIDER_ID="${PROVIDER_IDS[$SELECTED_ENV_VAR]}"
# If Claude token, export it as ANTHROPIC_API_KEY
if [ "$SELECTED_ENV_VAR" = "CLAUDE_CODE" ]; then
export ANTHROPIC_API_KEY="$CLAUDE_TOKEN"
SELECTED_ENV_VAR="ANTHROPIC_API_KEY"
fi
echo ""
echo -e "${GREEN}${NC} Using ${FOUND_PROVIDERS[0]}"
fi
@@ -359,12 +329,6 @@ if [ ${#FOUND_PROVIDERS[@]} -gt 0 ]; then
SELECTED_ENV_VAR="${FOUND_ENV_VARS[$idx]}"
SELECTED_PROVIDER_ID="${PROVIDER_IDS[$SELECTED_ENV_VAR]}"
# If Claude token, export it as ANTHROPIC_API_KEY
if [ "$SELECTED_ENV_VAR" = "CLAUDE_CODE" ]; then
export ANTHROPIC_API_KEY="$CLAUDE_TOKEN"
SELECTED_ENV_VAR="ANTHROPIC_API_KEY"
fi
echo ""
echo -e "${GREEN}${NC} Selected: ${FOUND_PROVIDERS[$idx]}"
break
+13
View File
@@ -0,0 +1,13 @@
# MCP Server
fastmcp
# Tool dependencies
diff-match-patch
pypdf
beautifulsoup4
lxml
playwright
playwright-stealth
requests
# Note: After installing, run `playwright install` to download browser binaries