feat: credential store auto sync
This commit is contained in:
@@ -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
+20
-3
@@ -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
|
||||
|
||||
|
||||
@@ -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!
|
||||
```
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"],
|
||||
))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__ = []
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user