cred update in quickstart, sample agent check before agent run, agent has welcome msg
This commit is contained in:
@@ -669,6 +669,7 @@ AskUserQuestion(questions=[{
|
|||||||
|------|---------------|
|
|------|---------------|
|
||||||
| `config.py` | `AgentMetadata.name` — the display name shown in TUI agent selection |
|
| `config.py` | `AgentMetadata.name` — the display name shown in TUI agent selection |
|
||||||
| `config.py` | `AgentMetadata.description` — agent description |
|
| `config.py` | `AgentMetadata.description` — agent description |
|
||||||
|
| `config.py` | `AgentMetadata.intro_message` — greeting shown to user when TUI loads |
|
||||||
| `agent.py` | Module docstring (line 1) |
|
| `agent.py` | Module docstring (line 1) |
|
||||||
| `agent.py` | `class OldNameAgent:` → `class NewNameAgent:` |
|
| `agent.py` | `class OldNameAgent:` → `class NewNameAgent:` |
|
||||||
| `agent.py` | `GraphSpec(id="old-name-graph")` → `GraphSpec(id="new-name-graph")` — shown in TUI status bar |
|
| `agent.py` | `GraphSpec(id="old-name-graph")` → `GraphSpec(id="new-name-graph")` — shown in TUI status bar |
|
||||||
@@ -735,7 +736,7 @@ mcp__agent-builder__export_graph()
|
|||||||
|
|
||||||
**THEN write the Python package files** using the exported data. Create these files in `exports/AGENT_NAME/`:
|
**THEN write the Python package files** using the exported data. Create these files in `exports/AGENT_NAME/`:
|
||||||
|
|
||||||
1. `config.py` - Runtime configuration with model settings
|
1. `config.py` - Runtime configuration with model settings and `AgentMetadata` (including `intro_message` — the greeting shown when TUI loads)
|
||||||
2. `nodes/__init__.py` - All NodeSpec definitions
|
2. `nodes/__init__.py` - All NodeSpec definitions
|
||||||
3. `agent.py` - Goal, edges, graph config, and agent class
|
3. `agent.py` - Goal, edges, graph config, and agent class
|
||||||
4. `__init__.py` - Package exports
|
4. `__init__.py` - Package exports
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ class AgentMetadata:
|
|||||||
"multi-source search, quality evaluation, and synthesis - with TUI conversation "
|
"multi-source search, quality evaluation, and synthesis - with TUI conversation "
|
||||||
"at key checkpoints for user guidance and feedback."
|
"at key checkpoints for user guidance and feedback."
|
||||||
)
|
)
|
||||||
|
intro_message: str = (
|
||||||
|
"Hi! I'm your deep research assistant. Tell me a topic and I'll investigate it "
|
||||||
|
"thoroughly — searching multiple sources, evaluating quality, and synthesizing "
|
||||||
|
"a comprehensive report. What would you like me to research?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
metadata = AgentMetadata()
|
metadata = AgentMetadata()
|
||||||
|
|||||||
@@ -460,9 +460,14 @@ result: HealthCheckResult = check_credential_health("hubspot", token_value)
|
|||||||
The local encrypted store requires `HIVE_CREDENTIAL_KEY` to encrypt/decrypt credentials.
|
The local encrypted store requires `HIVE_CREDENTIAL_KEY` to encrypt/decrypt credentials.
|
||||||
|
|
||||||
- If the user doesn't have one, `EncryptedFileStorage` will auto-generate one and log it
|
- If the user doesn't have one, `EncryptedFileStorage` will auto-generate one and log it
|
||||||
- The user MUST persist this key (e.g., in `~/.bashrc` or a secrets manager)
|
- The user MUST persist this key (e.g., in `~/.bashrc`/`~/.zshrc` or a secrets manager)
|
||||||
- Without this key, stored credentials cannot be decrypted
|
- Without this key, stored credentials cannot be decrypted
|
||||||
- This is the ONLY secret that should live in `~/.bashrc` or environment config
|
|
||||||
|
**Shell config rule:** Only TWO keys belong in shell config (`~/.zshrc`/`~/.bashrc`):
|
||||||
|
- `HIVE_CREDENTIAL_KEY` — encryption key for the credential store
|
||||||
|
- `ADEN_API_KEY` — Aden platform auth key (needed before the store can sync)
|
||||||
|
|
||||||
|
All other API keys (Brave, Google, HubSpot, etc.) must go in the encrypted store only. **Never offer to add them to shell config.**
|
||||||
|
|
||||||
If `HIVE_CREDENTIAL_KEY` is not set:
|
If `HIVE_CREDENTIAL_KEY` is not set:
|
||||||
|
|
||||||
@@ -475,6 +480,7 @@ If `HIVE_CREDENTIAL_KEY` is not set:
|
|||||||
- **NEVER** log, print, or echo credential values in tool output
|
- **NEVER** log, print, or echo credential values in tool output
|
||||||
- **NEVER** store credentials in plaintext files, git-tracked files, or agent configs
|
- **NEVER** store credentials in plaintext files, git-tracked files, or agent configs
|
||||||
- **NEVER** hardcode credentials in source code
|
- **NEVER** hardcode credentials in source code
|
||||||
|
- **NEVER** offer to save API keys to shell config (`~/.zshrc`/`~/.bashrc`) — the **only** keys that belong in shell config are `HIVE_CREDENTIAL_KEY` and `ADEN_API_KEY`. All other credentials (Brave, Google, HubSpot, GitHub, Resend, etc.) go in the encrypted store only.
|
||||||
- **ALWAYS** use `SecretStr` from Pydantic when handling credential values in Python
|
- **ALWAYS** use `SecretStr` from Pydantic when handling credential values in Python
|
||||||
- **ALWAYS** use the local encrypted store (`~/.hive/credentials`) for persistence
|
- **ALWAYS** use the local encrypted store (`~/.hive/credentials`) for persistence
|
||||||
- **ALWAYS** run health checks before storing credentials (when possible)
|
- **ALWAYS** run health checks before storing credentials (when possible)
|
||||||
@@ -605,7 +611,7 @@ All credentials are now configured:
|
|||||||
│ │
|
│ │
|
||||||
│ 1. RUN YOUR AGENT: │
|
│ 1. RUN YOUR AGENT: │
|
||||||
│ │
|
│ │
|
||||||
│ PYTHONPATH=core:exports python -m research-agent tui │
|
│ hive tui │
|
||||||
│ │
|
│ │
|
||||||
│ 2. IF YOU ENCOUNTER ISSUES, USE THE DEBUGGER: │
|
│ 2. IF YOU ENCOUNTER ISSUES, USE THE DEBUGGER: │
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|||||||
"""Run an exported agent."""
|
"""Run an exported agent."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from framework.credentials.models import CredentialError
|
||||||
from framework.runner import AgentRunner
|
from framework.runner import AgentRunner
|
||||||
|
|
||||||
# Set logging level (quiet by default for cleaner output)
|
# Set logging level (quiet by default for cleaner output)
|
||||||
@@ -376,6 +377,9 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|||||||
model=args.model,
|
model=args.model,
|
||||||
enable_tui=True,
|
enable_tui=True,
|
||||||
)
|
)
|
||||||
|
except CredentialError as e:
|
||||||
|
print(f"\n{e}", file=sys.stderr)
|
||||||
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading agent: {e}")
|
print(f"Error loading agent: {e}")
|
||||||
return
|
return
|
||||||
@@ -1136,6 +1140,7 @@ def cmd_tui(args: argparse.Namespace) -> int:
|
|||||||
"""Browse agents and launch the interactive TUI dashboard."""
|
"""Browse agents and launch the interactive TUI dashboard."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from framework.credentials.models import CredentialError
|
||||||
from framework.runner import AgentRunner
|
from framework.runner import AgentRunner
|
||||||
from framework.tui.app import AdenTUI
|
from framework.tui.app import AdenTUI
|
||||||
|
|
||||||
@@ -1187,6 +1192,9 @@ def cmd_tui(args: argparse.Namespace) -> int:
|
|||||||
model=args.model,
|
model=args.model,
|
||||||
enable_tui=True,
|
enable_tui=True,
|
||||||
)
|
)
|
||||||
|
except CredentialError as e:
|
||||||
|
print(f"\n{e}", file=sys.stderr)
|
||||||
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading agent: {e}")
|
print(f"Error loading agent: {e}")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ class AgentRunner:
|
|||||||
storage_path: Path | None = None,
|
storage_path: Path | None = None,
|
||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
enable_tui: bool = False,
|
enable_tui: bool = False,
|
||||||
|
intro_message: str = "",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize the runner (use AgentRunner.load() instead).
|
Initialize the runner (use AgentRunner.load() instead).
|
||||||
@@ -284,6 +285,7 @@ class AgentRunner:
|
|||||||
storage_path: Path for runtime storage (defaults to temp)
|
storage_path: Path for runtime storage (defaults to temp)
|
||||||
model: Model to use (reads from agent config or ~/.hive/configuration.json if None)
|
model: Model to use (reads from agent config or ~/.hive/configuration.json if None)
|
||||||
enable_tui: If True, forces use of AgentRuntime with EventBus
|
enable_tui: If True, forces use of AgentRuntime with EventBus
|
||||||
|
intro_message: Optional greeting shown to user on TUI load
|
||||||
"""
|
"""
|
||||||
self.agent_path = agent_path
|
self.agent_path = agent_path
|
||||||
self.graph = graph
|
self.graph = graph
|
||||||
@@ -291,6 +293,7 @@ class AgentRunner:
|
|||||||
self.mock_mode = mock_mode
|
self.mock_mode = mock_mode
|
||||||
self.model = model or self._resolve_default_model()
|
self.model = model or self._resolve_default_model()
|
||||||
self.enable_tui = enable_tui
|
self.enable_tui = enable_tui
|
||||||
|
self.intro_message = intro_message
|
||||||
|
|
||||||
# Set up storage
|
# Set up storage
|
||||||
if storage_path:
|
if storage_path:
|
||||||
@@ -319,6 +322,10 @@ class AgentRunner:
|
|||||||
self._agent_runtime: AgentRuntime | None = None
|
self._agent_runtime: AgentRuntime | None = None
|
||||||
self._uses_async_entry_points = self.graph.has_async_entry_points()
|
self._uses_async_entry_points = self.graph.has_async_entry_points()
|
||||||
|
|
||||||
|
# Validate credentials before spawning MCP servers.
|
||||||
|
# Fails fast with actionable guidance — no MCP noise on screen.
|
||||||
|
self._validate_credentials()
|
||||||
|
|
||||||
# Auto-discover tools from tools.py
|
# Auto-discover tools from tools.py
|
||||||
tools_path = agent_path / "tools.py"
|
tools_path = agent_path / "tools.py"
|
||||||
if tools_path.exists():
|
if tools_path.exists():
|
||||||
@@ -329,6 +336,74 @@ class AgentRunner:
|
|||||||
if mcp_config_path.exists():
|
if mcp_config_path.exists():
|
||||||
self._load_mcp_servers_from_config(mcp_config_path)
|
self._load_mcp_servers_from_config(mcp_config_path)
|
||||||
|
|
||||||
|
def _validate_credentials(self) -> None:
|
||||||
|
"""Check that required credentials are available before spawning MCP servers.
|
||||||
|
|
||||||
|
Raises CredentialError with actionable guidance if any are missing.
|
||||||
|
Uses graph node specs + CREDENTIAL_SPECS — no tool registry needed.
|
||||||
|
"""
|
||||||
|
required_tools: set[str] = set()
|
||||||
|
for node in self.graph.nodes:
|
||||||
|
if node.tools:
|
||||||
|
required_tools.update(node.tools)
|
||||||
|
if not required_tools:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from aden_tools.credentials import CREDENTIAL_SPECS
|
||||||
|
|
||||||
|
from framework.credentials import CredentialStore
|
||||||
|
from framework.credentials.storage import (
|
||||||
|
CompositeStorage,
|
||||||
|
EncryptedFileStorage,
|
||||||
|
EnvVarStorage,
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
return # aden_tools not installed, skip check
|
||||||
|
|
||||||
|
# Build credential store (same logic as validate())
|
||||||
|
env_mapping = {
|
||||||
|
(spec.credential_id or name): spec.env_var for name, spec in CREDENTIAL_SPECS.items()
|
||||||
|
}
|
||||||
|
storages: list = [EnvVarStorage(env_mapping=env_mapping)]
|
||||||
|
if os.environ.get("HIVE_CREDENTIAL_KEY"):
|
||||||
|
storages.insert(0, EncryptedFileStorage())
|
||||||
|
if len(storages) == 1:
|
||||||
|
storage = storages[0]
|
||||||
|
else:
|
||||||
|
storage = CompositeStorage(primary=storages[0], fallbacks=storages[1:])
|
||||||
|
store = CredentialStore(storage=storage)
|
||||||
|
|
||||||
|
# Build tool→credential mapping and check
|
||||||
|
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
|
||||||
|
|
||||||
|
missing: list[str] = []
|
||||||
|
checked: set[str] = set()
|
||||||
|
for tool_name in sorted(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):
|
||||||
|
affected = sorted(t for t in required_tools if t in spec.tools)
|
||||||
|
entry = f" {spec.env_var} for {', '.join(affected)}"
|
||||||
|
if spec.help_url:
|
||||||
|
entry += f"\n Get it at: {spec.help_url}"
|
||||||
|
missing.append(entry)
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
from framework.credentials.models import CredentialError
|
||||||
|
|
||||||
|
lines = ["Missing required credentials:\n"]
|
||||||
|
lines.extend(missing)
|
||||||
|
lines.append("\nTo fix: run /hive-credentials in Claude Code.")
|
||||||
|
raise CredentialError("\n".join(lines))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _import_agent_module(agent_path: Path):
|
def _import_agent_module(agent_path: Path):
|
||||||
"""Import an agent package from its directory path.
|
"""Import an agent package from its directory path.
|
||||||
@@ -420,6 +495,12 @@ class AgentRunner:
|
|||||||
hive_config = get_hive_config()
|
hive_config = get_hive_config()
|
||||||
max_tokens = hive_config.get("llm", {}).get("max_tokens", DEFAULT_MAX_TOKENS)
|
max_tokens = hive_config.get("llm", {}).get("max_tokens", DEFAULT_MAX_TOKENS)
|
||||||
|
|
||||||
|
# Read intro_message from agent metadata (shown on TUI load)
|
||||||
|
agent_metadata = getattr(agent_module, "metadata", None)
|
||||||
|
intro_message = ""
|
||||||
|
if agent_metadata and hasattr(agent_metadata, "intro_message"):
|
||||||
|
intro_message = agent_metadata.intro_message
|
||||||
|
|
||||||
# Build GraphSpec from module-level variables
|
# Build GraphSpec from module-level variables
|
||||||
graph = GraphSpec(
|
graph = GraphSpec(
|
||||||
id=f"{agent_path.name}-graph",
|
id=f"{agent_path.name}-graph",
|
||||||
@@ -442,6 +523,7 @@ class AgentRunner:
|
|||||||
storage_path=storage_path,
|
storage_path=storage_path,
|
||||||
model=model,
|
model=model,
|
||||||
enable_tui=enable_tui,
|
enable_tui=enable_tui,
|
||||||
|
intro_message=intro_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fallback: load from agent.json (legacy JSON-based agents)
|
# Fallback: load from agent.json (legacy JSON-based agents)
|
||||||
@@ -762,6 +844,9 @@ class AgentRunner:
|
|||||||
checkpoint_config=checkpoint_config,
|
checkpoint_config=checkpoint_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Pass intro_message through for TUI display
|
||||||
|
self._agent_runtime.intro_message = self.intro_message
|
||||||
|
|
||||||
async def run(
|
async def run(
|
||||||
self,
|
self,
|
||||||
input_data: dict | None = None,
|
input_data: dict | None = None,
|
||||||
|
|||||||
@@ -638,10 +638,15 @@ class ChatRepl(Vertical):
|
|||||||
# Check for resumable sessions
|
# Check for resumable sessions
|
||||||
self._check_and_show_resumable_sessions()
|
self._check_and_show_resumable_sessions()
|
||||||
|
|
||||||
history.write(
|
# Show agent intro message if available
|
||||||
"[dim]Quick start: /sessions to see previous sessions, "
|
intro = getattr(self.runtime, "intro_message", "")
|
||||||
"/pause to pause execution[/dim]\n"
|
if intro:
|
||||||
)
|
history.write(f"[bold blue]Agent:[/bold blue] {intro}\n")
|
||||||
|
else:
|
||||||
|
history.write(
|
||||||
|
"[dim]Quick start: /sessions to see previous sessions, "
|
||||||
|
"/pause to pause execution[/dim]\n"
|
||||||
|
)
|
||||||
|
|
||||||
def _check_and_show_resumable_sessions(self) -> None:
|
def _check_and_show_resumable_sessions(self) -> None:
|
||||||
"""Check for non-terminated sessions and prompt user."""
|
"""Check for non-terminated sessions and prompt user."""
|
||||||
|
|||||||
@@ -241,9 +241,7 @@ class DeepResearchAgent:
|
|||||||
session_state=session_state,
|
session_state=session_state,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run(
|
async def run(self, context: dict, session_state=None) -> ExecutionResult:
|
||||||
self, context: dict, session_state=None
|
|
||||||
) -> ExecutionResult:
|
|
||||||
"""Run the agent (convenience method for single execution)."""
|
"""Run the agent (convenience method for single execution)."""
|
||||||
await self.start()
|
await self.start()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ class AgentMetadata:
|
|||||||
"multi-source search, quality evaluation, and synthesis - with TUI conversation "
|
"multi-source search, quality evaluation, and synthesis - with TUI conversation "
|
||||||
"at key checkpoints for user guidance and feedback."
|
"at key checkpoints for user guidance and feedback."
|
||||||
)
|
)
|
||||||
|
intro_message: str = (
|
||||||
|
"Hi! I'm your deep research assistant. Tell me a topic and I'll investigate it "
|
||||||
|
"thoroughly — searching multiple sources, evaluating quality, and synthesizing "
|
||||||
|
"a comprehensive report. What would you like me to research?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
metadata = AgentMetadata()
|
metadata = AgentMetadata()
|
||||||
|
|||||||
@@ -225,9 +225,7 @@ class TechNewsReporterAgent:
|
|||||||
session_state=session_state,
|
session_state=session_state,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run(
|
async def run(self, context: dict, session_state=None) -> ExecutionResult:
|
||||||
self, context: dict, session_state=None
|
|
||||||
) -> ExecutionResult:
|
|
||||||
"""Run the agent (convenience method for single execution)."""
|
"""Run the agent (convenience method for single execution)."""
|
||||||
await self.start()
|
await self.start()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ class AgentMetadata:
|
|||||||
"summarize key stories, and produce a well-organized report "
|
"summarize key stories, and produce a well-organized report "
|
||||||
"for the user to read."
|
"for the user to read."
|
||||||
)
|
)
|
||||||
|
intro_message: str = (
|
||||||
|
"Hi! I'm your tech news reporter. I'll search the web for the latest technology "
|
||||||
|
"and AI news, then put together a clear summary for you. What topic or area "
|
||||||
|
"should I cover?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
metadata = AgentMetadata()
|
metadata = AgentMetadata()
|
||||||
|
|||||||
@@ -240,9 +240,7 @@ class TwitterOutreachAgent:
|
|||||||
session_state=session_state,
|
session_state=session_state,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run(
|
async def run(self, context: dict, session_state=None) -> ExecutionResult:
|
||||||
self, context: dict, session_state=None
|
|
||||||
) -> ExecutionResult:
|
|
||||||
"""Run the agent (convenience method for single execution)."""
|
"""Run the agent (convenience method for single execution)."""
|
||||||
await self.start()
|
await self.start()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ class AgentMetadata:
|
|||||||
"Reads a target's Twitter/X profile, crafts a personalized outreach email "
|
"Reads a target's Twitter/X profile, crafts a personalized outreach email "
|
||||||
"referencing their specific activity, and sends it after user approval."
|
"referencing their specific activity, and sends it after user approval."
|
||||||
)
|
)
|
||||||
|
intro_message: str = (
|
||||||
|
"Hi! I can help you with personalized Twitter outreach. Give me a Twitter/X "
|
||||||
|
"handle and I'll analyze their profile, then craft a tailored outreach email "
|
||||||
|
"for your approval."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
metadata = AgentMetadata()
|
metadata = AgentMetadata()
|
||||||
|
|||||||
+1
-1
@@ -878,7 +878,7 @@ if [ -n "$HIVE_CREDENTIAL_KEY" ]; then
|
|||||||
|
|
||||||
# Initialize the metadata index
|
# Initialize the metadata index
|
||||||
if [ ! -f "$HIVE_CRED_DIR/metadata/index.json" ]; then
|
if [ ! -f "$HIVE_CRED_DIR/metadata/index.json" ]; then
|
||||||
echo '{}' > "$HIVE_CRED_DIR/metadata/index.json"
|
echo '{"credentials": {}, "version": "1.0"}' > "$HIVE_CRED_DIR/metadata/index.json"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN} ✓ Credential store initialized at ~/.hive/credentials/${NC}"
|
echo -e "${GREEN} ✓ Credential store initialized at ~/.hive/credentials/${NC}"
|
||||||
|
|||||||
Reference in New Issue
Block a user