cred update in quickstart, sample agent check before agent run, agent has welcome msg

This commit is contained in:
bryan
2026-02-10 11:27:17 -08:00
parent a12163d63f
commit 21e7554cdb
13 changed files with 137 additions and 18 deletions
+2 -1
View File
@@ -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()
+9 -3
View File
@@ -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: │
│ │ │ │
+8
View File
@@ -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
+85
View File
@@ -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,
+9 -4
View File
@@ -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()
+1 -3
View File
@@ -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
View File
@@ -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}"