Capability-set targeting + Claude Code headless provider + resilience fixes
Capability targeting (new feature): - loop --capability <SET|ALL> scopes which capability library the loop reasons against. data/capability_sets/<name>.json (same schema as capabilities.json) holds one-off targeted products; drop a file in and pass its name, no code changes. First set: agentminder (Broadcom AgentMinder, agentic-AI identity). - capabilities.set_active_set() swaps the active library; keyword pre-filter is bypassed for small targeted sets so every signal is evaluated. New 'capability-sets' command lists them; run banner shows the active set. LLM provider — the '429' was a disguised policy block: - Anthropic rejects Claude Code OAuth tokens on /v1/messages for non-Claude-Code traffic with a fake rate_limit_error (no anthropic-ratelimit-* headers). New 'claude-cli' provider runs inference through the sanctioned headless path (claude -p --append-system-prompt --model), now the default provider. - OAuth keychain token auto-refresh via headless claude when expired. Resilience: - STATE_DIR is overridable (macOS endpoint-security locked output/state via com.apple.macl); load_memory tolerates PermissionError; exa usage counter tolerates malformed files; run.py handles Ctrl-C cleanly (no traceback). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,6 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = ROOT / "data"
|
||||
OUTPUT_DIR = ROOT / "output"
|
||||
STATE_DIR = OUTPUT_DIR / "state"
|
||||
BRIEFS_DIR = OUTPUT_DIR / "briefs"
|
||||
|
||||
|
||||
def _load_dotenv() -> None:
|
||||
@@ -27,6 +24,15 @@ def _load_dotenv() -> None:
|
||||
|
||||
_load_dotenv()
|
||||
|
||||
# Paths (after dotenv so OUTPUT_DIR/STATE_DIR overrides in .env apply).
|
||||
# STATE_DIR is overridable because macOS TCC/endpoint-security software can
|
||||
# permanently lock a directory tree (com.apple.macl xattr) — pointing at a
|
||||
# fresh dir is the recovery path (state is regenerable). Relative values are
|
||||
# resolved against the sales-agent root.
|
||||
OUTPUT_DIR = ROOT / os.environ.get("OUTPUT_DIR", "output")
|
||||
STATE_DIR = (ROOT / os.environ.get("STATE_DIR", "")) if os.environ.get("STATE_DIR") else OUTPUT_DIR / "state"
|
||||
BRIEFS_DIR = OUTPUT_DIR / "briefs"
|
||||
|
||||
# --- LLM ---
|
||||
# Provider abstraction: openai | gemini | anthropic. If LLM_PROVIDER is unset we
|
||||
# auto-detect from whichever key is present (OpenAI-compatible first). There is NO
|
||||
@@ -66,6 +72,16 @@ LLM_MODEL = os.environ.get("LLM_MODEL", "claude-sonnet-5")
|
||||
# keychain ("Claude Code-credentials"), read at runtime so refreshes are picked up.
|
||||
ANTHROPIC_OAUTH_TOKEN = os.environ.get("ANTHROPIC_OAUTH_TOKEN", "").strip()
|
||||
ANTHROPIC_USE_KEYCHAIN = os.environ.get("ANTHROPIC_USE_KEYCHAIN", "1").strip() == "1"
|
||||
# When the keychain token is expired, run a headless `claude` command to make
|
||||
# Claude Code refresh it, then re-read — the loop self-heals with no human step.
|
||||
ANTHROPIC_AUTOREFRESH = os.environ.get("ANTHROPIC_AUTOREFRESH", "1").strip() == "1"
|
||||
|
||||
# "claude-cli" provider: run inference through Claude Code's official headless
|
||||
# mode (`claude -p`). This is the sanctioned way to use a Claude subscription
|
||||
# programmatically — direct API calls with the Claude Code OAuth token are
|
||||
# policy-blocked (they return a fake 429 "Error"). Requires `claude` on PATH.
|
||||
CLAUDE_CLI_MODEL = os.environ.get("CLAUDE_CLI_MODEL", "sonnet").strip()
|
||||
CLAUDE_CLI_TIMEOUT = int(os.environ.get("CLAUDE_CLI_TIMEOUT", "300"))
|
||||
# Effort level for reasoning models (Sonnet 5 etc.): low | medium | high | max.
|
||||
ANTHROPIC_EFFORT = os.environ.get("ANTHROPIC_EFFORT", "low").strip()
|
||||
|
||||
@@ -74,14 +90,14 @@ GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
|
||||
|
||||
_provider = os.environ.get("LLM_PROVIDER", "").strip().lower()
|
||||
if not _provider:
|
||||
if ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY:
|
||||
if ANTHROPIC_API_KEY:
|
||||
_provider = "anthropic"
|
||||
elif OPENAI_API_KEY:
|
||||
_provider = "openai"
|
||||
elif GEMINI_API_KEY:
|
||||
_provider = "gemini"
|
||||
else:
|
||||
_provider = "none"
|
||||
_provider = "claude-cli" # subscription via Claude Code headless mode
|
||||
# Selecting a provider whose key is missing => no usable provider.
|
||||
# ("anthropic" is exempt here: the token may come from the keychain at runtime.)
|
||||
if _provider == "openai" and not OPENAI_API_KEY:
|
||||
@@ -100,6 +116,8 @@ LLM_CONFIGURED = LLM_PROVIDER != "none"
|
||||
def llm_label() -> str:
|
||||
if LLM_PROVIDER == "none":
|
||||
return "NONE (no LLM provider configured)"
|
||||
if LLM_PROVIDER == "claude-cli":
|
||||
return f"Claude Code headless ({CLAUDE_CLI_MODEL}, subscription)"
|
||||
if LLM_PROVIDER == "openai":
|
||||
host = OPENAI_BASE_URL.split("//")[-1].split("/")[0]
|
||||
return f"OpenAI-compatible ({OPENAI_MODEL} @ {host})"
|
||||
|
||||
Reference in New Issue
Block a user