Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions .github/workflows/supply-chain-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:

permissions:
pull-requests: write
issues: write

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep workflow permissions minimal. Current main's redesigned audit workflow retains pull-requests: write and contents: read; any added issues: write scope needs a concrete operation that cannot use the existing permission.

contents: read

jobs:
Expand Down Expand Up @@ -164,8 +165,28 @@ jobs:
echo "critical=false" >> "$GITHUB_OUTPUT"
fi

- name: Post warning comment
- name: Write findings to job summary
if: steps.scan.outputs.found == 'true'
run: |
SEVERITY="⚠️ Supply Chain Risk Detected"
if [ "${{ steps.scan.outputs.critical }}" = "true" ]; then
SEVERITY="🚨 CRITICAL Supply Chain Risk Detected"
fi

{
echo "## ${SEVERITY}"
echo
echo "This PR contains patterns commonly associated with supply chain attacks. This does **not** mean the PR is malicious — but these patterns require careful human review before merging."
echo
cat /tmp/findings.md
echo
echo "---"
echo "*Automated scan triggered by [supply-chain-audit](/.github/workflows/supply-chain-audit.yml). If this is a false positive, a maintainer can approve after manual review.*"
} >> "$GITHUB_STEP_SUMMARY"

- name: Post warning comment
if: steps.scan.outputs.found == 'true' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
Expand All @@ -188,5 +209,5 @@ jobs:
- name: Fail on critical findings
if: steps.scan.outputs.critical == 'true'
run: |
echo "::error::CRITICAL supply chain risk patterns detected in this PR. See the PR comment for details."
echo "::error::CRITICAL supply chain risk patterns detected in this PR. See the job summary for details."
exit 1
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
# Install system dependencies in one layer, clear APT cache
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps && \
build-essential git nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps && \
rm -rf /var/lib/apt/lists/*

# Non-root user for runtime; UID can be overridden via HERMES_UID at runtime
Expand Down
10 changes: 4 additions & 6 deletions agent/credential_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
_resolve_zai_base_url,
_save_auth_store,
_save_provider_state,
read_credential_pool,
write_credential_pool,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -316,7 +314,7 @@ def get_custom_provider_pool_key(base_url: str) -> Optional[str]:

def list_custom_pool_providers() -> List[str]:
"""Return all 'custom:*' pool keys that have entries in auth.json."""
pool_data = read_credential_pool(None)
pool_data = auth_mod.read_credential_pool(None)
return sorted(
key for key in pool_data
if key.startswith(CUSTOM_POOL_PREFIX)
Expand Down Expand Up @@ -388,7 +386,7 @@ def _replace_entry(self, old: PooledCredential, new: PooledCredential) -> None:
return

def _persist(self) -> None:
write_credential_pool(
auth_mod.write_credential_pool(
self.provider,
[entry.to_dict() for entry in self._entries],
)
Expand Down Expand Up @@ -1333,7 +1331,7 @@ def _seed_custom_pool(pool_key: str, entries: List[PooledCredential]) -> Tuple[b

def load_pool(provider: str) -> CredentialPool:
provider = (provider or "").strip().lower()
raw_entries = read_credential_pool(provider)
raw_entries = auth_mod.read_credential_pool(provider)
entries = [PooledCredential.from_dict(provider, payload) for payload in raw_entries]

if provider.startswith(CUSTOM_POOL_PREFIX):
Expand All @@ -1349,7 +1347,7 @@ def load_pool(provider: str) -> CredentialPool:
changed |= _normalize_pool_priorities(provider, entries)

if changed:
write_credential_pool(
auth_mod.write_credential_pool(
provider,
[entry.to_dict() for entry in sorted(entries, key=lambda item: item.priority)],
)
Expand Down
3 changes: 3 additions & 0 deletions agent/model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def _strip_provider_prefix(model: str) -> str:
# all miss. Replaced the previous 80+ entry dict.
# For provider-specific context lengths, models.dev is the primary source.
DEFAULT_CONTEXT_LENGTHS = {
"accounts/fireworks/routers/kimi-k2p5-turbo": 256000,
# Anthropic Claude 4.6 (1M context) — bare IDs only to avoid
# fuzzy-match collisions (e.g. "anthropic/claude-sonnet-4" is a
# substring of "anthropic/claude-sonnet-4.6").
Expand Down Expand Up @@ -156,6 +157,7 @@ def _strip_provider_prefix(model: str) -> str:
"moonshotai/Kimi-K2.5": 262144,
"moonshotai/Kimi-K2-Thinking": 262144,
"MiniMaxAI/MiniMax-M2.5": 204800,
"minimaxai/minimax-m2.5": 204800,
"XiaomiMiMo/MiMo-V2-Flash": 256000,
"mimo-v2-pro": 1000000,
"mimo-v2-omni": 256000,
Expand Down Expand Up @@ -209,6 +211,7 @@ def _is_custom_endpoint(base_url: str) -> bool:
"api.openai.com": "openai",
"chatgpt.com": "openai",
"api.anthropic.com": "anthropic",
"api.fireworks.ai": "fireworks",
"api.z.ai": "zai",
"api.moonshot.ai": "kimi-coding",
"api.kimi.com": "kimi-coding",
Expand Down
29 changes: 27 additions & 2 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,14 @@ class ProviderConfig:
api_key_env_vars=("HF_TOKEN",),
base_url_env_var="HF_BASE_URL",
),
"fireworks": ProviderConfig(
id="fireworks",
name="Fireworks AI",
auth_type="api_key",
inference_base_url="https://api.fireworks.ai/inference/v1",
api_key_env_vars=("FIREWORKS_API_KEY",),
base_url_env_var="FIREWORKS_BASE_URL",
),
"xiaomi": ProviderConfig(
id="xiaomi",
name="Xiaomi MiMo",
Expand Down Expand Up @@ -934,6 +942,7 @@ def resolve_provider(
"github": "copilot", "github-copilot": "copilot",
"github-models": "copilot", "github-model": "copilot",
"github-copilot-acp": "copilot-acp", "copilot-acp-agent": "copilot-acp",
"fireworks-ai": "fireworks",
"aigateway": "ai-gateway", "vercel": "ai-gateway", "vercel-ai-gateway": "ai-gateway",
"opencode": "opencode-zen", "zen": "opencode-zen",
"qwen-portal": "qwen-oauth", "qwen-cli": "qwen-oauth", "qwen-oauth": "qwen-oauth",
Expand Down Expand Up @@ -983,8 +992,24 @@ def resolve_provider(
return "openrouter"

# Auto-detect API-key providers by checking their env vars
for pid, pconfig in PROVIDER_REGISTRY.items():
if pconfig.auth_type != "api_key":
# Keep this order deliberate: tests and UX expect established providers
# like Xiaomi to outrank newly-added providers when both secrets exist.
_AUTO_PROVIDER_PRIORITY = [
"anthropic",
"gemini",
"zai",
"kimi-coding",
"minimax",
"minimax-cn",
"ai-gateway",
"kilocode",
"huggingface",
"xiaomi",
"fireworks",
]
for pid in _AUTO_PROVIDER_PRIORITY:
pconfig = PROVIDER_REGISTRY.get(pid)
if not pconfig or pconfig.auth_type != "api_key":
continue
# GitHub tokens are commonly present for repo/tool access but should not
# hijack inference auto-selection unless the user explicitly chooses
Expand Down
16 changes: 16 additions & 0 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,14 @@ def _ensure_hermes_home_managed(home: Path):
"category": "provider",
"advanced": True,
},
"FIREWORKS_API_KEY": {
"description": "Fireworks API key for direct Fireworks model access",
"prompt": "Fireworks API key",
"url": "https://app.fireworks.ai/api-keys",
"password": True,
"category": "provider",
"advanced": True,
},
"GEMINI_API_KEY": {
"description": "Google AI Studio API key (alias for GOOGLE_API_KEY)",
"prompt": "Gemini API key",
Expand All @@ -784,6 +792,14 @@ def _ensure_hermes_home_managed(home: Path):
"category": "provider",
"advanced": True,
},
"FIREWORKS_BASE_URL": {
"description": "Fireworks base URL override",
"prompt": "Fireworks base URL (leave empty for default)",
"url": None,
"password": False,
"category": "provider",
"advanced": True,
},
"GLM_API_KEY": {
"description": "Z.AI / GLM API key (also recognized as ZAI_API_KEY / Z_AI_API_KEY)",
"prompt": "Z.AI / GLM API key",
Expand Down
45 changes: 34 additions & 11 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,7 @@ def select_provider_and_model(args=None):
"copilot": "GitHub Copilot",
"anthropic": "Anthropic",
"gemini": "Google AI Studio",
"fireworks": "Fireworks AI",
"zai": "Z.AI / GLM",
"kimi-coding": "Kimi / Moonshot",
"minimax": "MiniMax",
Expand Down Expand Up @@ -1086,6 +1087,7 @@ def select_provider_and_model(args=None):
("opencode-go", "OpenCode Go (open models, $10/month subscription)"),
("ai-gateway", "AI Gateway (Vercel — 200+ models, pay-per-use)"),
("alibaba", "Alibaba Cloud / DashScope Coding (Qwen + multi-provider)"),
("fireworks", "Fireworks AI (open models + Fire Pass)"),
("xiaomi", "Xiaomi MiMo (MiMo-V2 models — pro, omni, flash)"),
]

Expand Down Expand Up @@ -1144,9 +1146,12 @@ def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]:
ordered.append(("more", "More providers..."))
ordered.append(("cancel", "Cancel"))

provider_idx = _prompt_provider_choice(
[label for _, label in ordered], default=default_idx,
)
try:
provider_idx = _prompt_provider_choice(
[label for _, label in ordered], default=default_idx,
)
except TypeError:
provider_idx = _prompt_provider_choice([label for _, label in ordered])
if provider_idx is None or ordered[provider_idx][0] == "cancel":
print("No change.")
return
Expand All @@ -1161,9 +1166,12 @@ def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]:
ext_ordered.append(("remove-custom", "Remove a saved custom provider"))
ext_ordered.append(("cancel", "Cancel"))

ext_idx = _prompt_provider_choice(
[label for _, label in ext_ordered], default=0,
)
try:
ext_idx = _prompt_provider_choice(
[label for _, label in ext_ordered], default=0,
)
except TypeError:
ext_idx = _prompt_provider_choice([label for _, label in ext_ordered])
if ext_idx is None or ext_ordered[ext_idx][0] == "cancel":
print("No change.")
return
Expand Down Expand Up @@ -1199,7 +1207,7 @@ def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]:
_model_flow_anthropic(config, current_model)
elif selected_provider == "kimi-coding":
_model_flow_kimi(config, current_model)
elif selected_provider in ("gemini", "zai", "minimax", "minimax-cn", "kilocode", "opencode-zen", "opencode-go", "ai-gateway", "alibaba", "huggingface", "xiaomi"):
elif selected_provider in ("gemini", "fireworks", "zai", "minimax", "minimax-cn", "kilocode", "opencode-zen", "opencode-go", "ai-gateway", "alibaba", "huggingface", "xiaomi"):
_model_flow_api_key_provider(config, selected_provider, current_model)

# ── Post-switch cleanup: clear stale OPENAI_BASE_URL ──────────────
Expand Down Expand Up @@ -1239,7 +1247,7 @@ def _clear_stale_openai_base_url():
else f"Cleared stale OPENAI_BASE_URL from .env (was: {stale_url})")


def _prompt_provider_choice(choices, *, default=0):
def _prompt_provider_choice(choices, default=0):
"""Show provider selection menu with curses arrow-key navigation.

Falls back to a numbered list when curses is unavailable (e.g. piped
Expand Down Expand Up @@ -1608,8 +1616,13 @@ def _model_flow_custom(config):

try:
base_url = input(f"API base URL [{current_url or 'e.g. https://api.example.com/v1'}]: ").strip()
if not sys.stdin.isatty():
print("Error: interactive terminal required for API key entry.")
return
import getpass
api_key = getpass.getpass(f"API key [{current_key[:8] + '...' if current_key else 'optional'}]: ").strip()
api_key = getpass.getpass(
f"API key [{current_key[:8] + '...' if current_key else 'optional'}]: "
).strip()
except (KeyboardInterrupt, EOFError):
print("\nCancelled.")
return
Expand Down Expand Up @@ -2519,7 +2532,17 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
except Exception:
pass

if mdev_models:
if provider_id == "fireworks":
from hermes_cli.models import provider_model_ids

model_list = provider_model_ids("fireworks")
if model_list:
print(f" Found {len(model_list)} Fireworks model(s)")
else:
model_list = curated
if model_list:
print(f" Showing {len(model_list)} curated models — use \"Enter custom model name\" for others.")
elif mdev_models:
model_list = mdev_models
print(f" Found {len(model_list)} model(s) from models.dev registry")
elif curated and len(curated) >= 8:
Expand Down Expand Up @@ -4516,7 +4539,7 @@ def main():
)
chat_parser.add_argument(
"--provider",
choices=["auto", "openrouter", "nous", "openai-codex", "copilot-acp", "copilot", "anthropic", "gemini", "huggingface", "zai", "kimi-coding", "minimax", "minimax-cn", "kilocode", "xiaomi"],
choices=["auto", "openrouter", "nous", "openai-codex", "copilot-acp", "copilot", "anthropic", "gemini", "huggingface", "fireworks", "zai", "kimi-coding", "minimax", "minimax-cn", "kilocode", "xiaomi"],
default=None,
help="Inference provider (default: auto)"
)
Expand Down
Loading