Skip to content
Open
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
34 changes: 17 additions & 17 deletions hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -5470,7 +5470,7 @@ def block_task(
kind: Optional[str] = None,
expected_run_id: Optional[int] = None,
) -> bool:
"""Transition ``running``/``ready`` → ``blocked`` (or route elsewhere).
"""Transition ``running``/``ready`` → ``blocked``/``waiting``/``todo``/``triage``.

``kind`` (one of :data:`VALID_BLOCK_KINDS`, or ``None`` for a legacy
un-typed block) drives routing instead of every block landing in one
Expand All @@ -5480,22 +5480,22 @@ def block_task(
sit in ``blocked`` (where a cron would keep "unblocking" it); it goes to
``todo`` so the existing parent-gating / ``recompute_ready`` machinery
promotes it automatically once its parents finish. No human, no cron, no
retry storm. This is Dale's "Type 2 — dependency blocked".

* ``needs_input`` / ``capability`` / ``None`` — "truly blocked" (Dale's
"Type 1"). Lands in ``blocked`` for a human. BUT: each time such a task
is re-blocked for the SAME kind after having been unblocked, the
unblock-loop counter (``block_recurrences``) increments. When it reaches
:data:`BLOCK_RECURRENCE_LIMIT`, the task is routed to ``triage`` instead
of ``blocked`` — breaking the cron-unblock ↔ worker-re-block loop and
forcing a human-in-the-loop triage decision.

* ``transient`` — treated like a generic block for routing, but a worker
can use it to signal "this might clear on its own"; it still participates
in the loop breaker so a forever-flaky task eventually escalates.

Returns True on any successful transition (to ``blocked``, ``todo``, or
``triage``), False when the task wasn't in a blockable state.
retry storm.

* ``needs_input`` / ``capability`` — human-time waits. Land in ``waiting``
(TTL-exempt, never auto-promoted) so the dispatcher never reclaims them.
An explicit ``unwait_task`` / ``unblock_task`` / ``promote_task`` is
required to return them to the work pool.

* ``transient`` / ``None`` — agent-time blocks. Land in ``blocked``
(TTL-reclaimable, auto-promotable for circuit-breaker conditions).
A transient failure may clear on retry; the loop breaker routes
the task to ``triage`` after :data:`BLOCK_RECURRENCE_LIMIT` unblock↔
re-block cycles.

Returns True on any successful transition (to ``blocked``, ``waiting``,
``todo``, or ``triage``), False when the task wasn't in a blockable
state.
"""
if kind is not None and kind not in VALID_BLOCK_KINDS:
raise ValueError(
Expand Down
1 change: 1 addition & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14722,6 +14722,7 @@ def cmd_dashboard(args):
allow_public=getattr(args, "insecure", False),
initial_profile=getattr(args, "open_profile", "") or "",
headless=_headless_backend,
isolated=getattr(args, "isolated", False),
ssh_session_token=_ssh_session_token,
ssh_owner_nonce=_ssh_owner_nonce,
)
Expand Down
28 changes: 28 additions & 0 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11707,6 +11707,7 @@ def _open_session_db_for_profile(profile: Optional[str]):
from hermes_state import SessionDB
if not profile:
return SessionDB()
_check_isolated_profile_access(profile)
_name, home = _cron_profile_home(profile)
return SessionDB(db_path=Path(home) / "state.db")

Expand Down Expand Up @@ -15546,6 +15547,7 @@ def _profile_scope(profile: Optional[str]):
imported the modules before a HERMES_HOME override, or under test
isolation).
"""
_check_isolated_profile_access(profile)
requested = (profile or "").strip()

from hermes_constants import (
Expand Down Expand Up @@ -15599,6 +15601,7 @@ def _config_profile_scope(profile: Optional[str]):

None/""/"current" means the dashboard's own profile — no override.
"""
_check_isolated_profile_access(profile)
requested = (profile or "").strip()
if not requested or requested.lower() == "current":
yield None
Expand All @@ -15617,6 +15620,29 @@ def _config_profile_scope(profile: Optional[str]):
reset_hermes_home_override(token)


def _check_isolated_profile_access(profile: Optional[str]) -> None:
if not getattr(app.state, "isolated", False):
return
requested = (profile or "").strip()
if not requested or requested.lower() == "current":
return
from hermes_constants import get_hermes_home

isolated_home = Path(str(get_hermes_home()))
try:
requested_dir = _resolve_profile_dir(requested)
except HTTPException:
return
if requested_dir.resolve() != isolated_home.resolve():
raise HTTPException(
status_code=403,
detail=(
"Isolated mode: profile '" + requested + "' is not accessible. "
"This dashboard is scoped to its own profile only."
),
)


class SkillToggle(BaseModel):
name: str
enabled: bool
Expand Down Expand Up @@ -20021,6 +20047,7 @@ def start_server(
allow_public: bool = False,
initial_profile: str = "",
headless: bool = False,
isolated: bool = False,
ssh_session_token: Optional[str] = None,
ssh_owner_nonce: Optional[str] = None,
):
Expand Down Expand Up @@ -20055,6 +20082,7 @@ def start_server(
# uses this to decide whether to refuse the bind, log the gate-on
# banner, and enable uvicorn proxy_headers.
app.state.auth_required = should_require_auth(host)
app.state.isolated = isolated

# ``--insecure`` no longer disables the auth gate (June 2026 hardening:
# the hermes-0day MCP-persistence campaign abused unauthenticated public
Expand Down
29 changes: 29 additions & 0 deletions plugins/model-providers/kimi-oauth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Kimi OAuth provider profile — reuse Kimi Code CLI OAuth tokens.

The Kimi Code CLI stores its OAuth2 tokens at
``~/.kimi-code/credentials/kimi-code.json``. This provider reads those
tokens and exposes them as a Hermes provider, so Kimi subscription users
(K3, K2.7 Coding, etc.) don't need a separate platform API key.
"""

from providers import register_provider
from providers.base import ProviderProfile

kimi_oauth = ProviderProfile(
name="kimi-oauth",
aliases=(
"kimi",
"kimi-oauth-code",
"kimi-oauth-cli",
"kimi-code-oauth",
),
display_name="Kimi Code (OAuth)",
description="Kimi Code via OAuth tokens from Kimi Code CLI — no API key required",
signup_url="https://kimi.moonshot.cn/",
env_vars=(), # OAuth — tokens in ~/.kimi-code/credentials/kimi-code.json, not env
base_url="https://api.kimi.com/coding/v1",
auth_type="oauth_external",
default_max_tokens=65536,
)

register_provider(kimi_oauth)
3 changes: 2 additions & 1 deletion ui-tui/src/app/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
SessionInfo,
SlashCatalog,
SudoReq,
Usage
Usage,
WelcomeBannerConfig

Check failure on line 33 in ui-tui/src/app/interfaces.ts

View workflow job for this annotation

GitHub Actions / JS & TS checks / ui-tui / check

'WelcomeBannerConfig' is defined but never used
} from '../types.js'

export interface StateSetter<T> {
Expand Down
19 changes: 19 additions & 0 deletions ui-tui/src/gatewayTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,25 @@ export interface ConfigDisplayConfig {
/** Theme mode pin: 'light' / 'dark' beat background auto-detection; 'auto'
* (default) trusts the OSC-11 probe + env signals. */
tui_theme?: string

/**
* Welcome banner section configuration.
*
* Controls which accordion sections appear in the TUI welcome panel, their
* default open/closed state, and custom plugin-provided sections.
*
* Sections omitted from config use their built-in defaults (tools=open,
* skills=closed, system_prompt=closed, mcp_servers=closed). Set
* `enabled: false` to hide a section entirely.
*
* `plugin_sections` renders custom Accordion sections after the built-in
* ones. Data for plugin sections is expected on SessionInfo under the
* matching key (future — currently renders a placeholder label).
*/
welcome_banner?: {
sections?: Record<string, { default_open?: boolean; enabled?: boolean }>
plugin_sections?: Array<{ id: string; title: string; default_open?: boolean }>
}
}

export interface ConfigVoiceConfig {
Expand Down
34 changes: 34 additions & 0 deletions ui-tui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,40 @@ export interface SlashCatalog {
sub: Record<string, string[]>
}

/**
* Welcome banner section config as resolved from display.welcome_banner.
* Each built-in section (tools, skills, system_prompt, mcp_servers) can be
* independently enabled/disabled and default-open/closed.
*
* Plugin sections render custom Accordion entries after built-in sections.
*/
export interface WelcomeBannerSectionConfig {
default_open: boolean
enabled: boolean
}

export interface WelcomeBannerPluginSection {
id: string
title: string
default_open: boolean
}

export interface WelcomeBannerConfig {
sections: Record<string, WelcomeBannerSectionConfig>
plugin_sections: WelcomeBannerPluginSection[]
}

/**
* Welcome banner section default defaults for each built-in section.
* These match the hardcoded values in branding.tsx before this feature.
*/
export const WELCOME_BANNER_DEFAULTS: Record<string, WelcomeBannerSectionConfig> = {
tools: { default_open: true, enabled: true },
skills: { default_open: false, enabled: true },
system_prompt: { default_open: false, enabled: true },
mcp_servers: { default_open: false, enabled: true }
}

export interface SlashCategory {
name: string
pairs: [string, string][]
Expand Down
2 changes: 1 addition & 1 deletion website/docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ These are set automatically by the Docker terminal backend when `proxy.enabled:
| Variable | Description |
|----------|-------------|
| `TELEGRAM_BOT_TOKEN` | Telegram bot token (from @BotFather) |
| `TELEGRAM_ALLOWED_USERS` | Comma-separated user IDs allowed to use the bot (applies to DMs, groups, and forums) |
| `TELEGRAM_ALLOWED_USERS` | Comma-separated user IDs allowed to use the bot (applies to DMs, groups, and forums). Prefer `platforms.telegram.extra.allow_from` in `config.yaml`. |
| `TELEGRAM_ALLOW_ALL_USERS` | Allow any Telegram user to trigger the bot (dev only). |
| `TELEGRAM_GROUP_ALLOWED_USERS` | Comma-separated sender user IDs authorized in groups/forums only (does NOT grant DM access). Chat-ID-shaped values (starting with `-`) are still honored as chat IDs for backward compat with pre-#17686 configs, with a deprecation warning. |
| `TELEGRAM_GROUP_ALLOWED_CHATS` | Comma-separated group/forum chat IDs; any member is authorized |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ agent:main:{platform}:{chat_type}:{chat_id}
Gateway 使用多层授权检查,按顺序评估:

1. **平台级全量放行标志**(如 `TELEGRAM_ALLOW_ALL_USERS`)— 若设置,该平台所有用户均被授权
2. **平台白名单**(如 `TELEGRAM_ALLOWED_USERS`)— 逗号分隔的用户 ID
2. **平台白名单**——来自 `config.yaml`(`platforms.<name>.extra.allow_from`)或对应环境变量(如 `TELEGRAM_ALLOWED_USERS`)
3. **DM 配对** — 已认证用户可通过配对码为新用户授权
4. **全局放行标志**(`GATEWAY_ALLOW_ALL_USERS`)— 若设置,所有平台的所有用户均被授权
5. **默认:拒绝** — 未授权用户被拒绝
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,22 @@ hermes gateway setup

### 方式 B:手动配置

在 `~/.hermes/.env` 中添加以下内容:
将你的机器人 token(此为密钥)添加到 `~/.hermes/.env`:

```bash
# Telegram bot token from BotFather
TELEGRAM_BOT_TOKEN=7123456789:AAH1bGciOiJSUzI1NiIsInR5cCI6Ikp...
# 来自 BotFather 的 Telegram 机器人 token(此为密钥,始终放在 .env 中)
TELEGRAM_BOT_TOKEN=7123456789:***...
```

将你的用户 ID(非密钥设置)添加到 `~/.hermes/config.yaml`:

# Your Telegram user ID (numeric)
TELEGRAM_ALLOWED_USERS=123456789
```yaml
gateway:
platforms:
telegram:
extra:
# 你的 Telegram 用户 ID(数字)——多个用户用逗号分隔
allow_from: "123456789"
```

### 查找你的用户 ID
Expand All @@ -109,7 +117,7 @@ TELEGRAM_ALLOWED_USERS=123456789

1. 在 Telegram 上给 [@userinfobot](https://t.me/userinfobot) 发消息
2. 它会立即回复你的数字用户 ID
3. 将该数字填入 `TELEGRAM_ALLOWED_USERS`
3. 将该数字填入 `config.yaml` 中的 `allow_from` 字段

:::info
Telegram 用户 ID 是永久性数字,例如 `123456789`。它与可以更改的 `@username` 不同。白名单中请始终使用数字 ID。
Expand Down Expand Up @@ -191,19 +199,22 @@ hermes gateway status

现在让你的队友获得访问权限。有两种方式。

### 方式 A:静态白名单
### 方式 A:静态白名单(config.yaml)

收集每位团队成员的 Telegram 用户 ID(让他们给 [@userinfobot](https://t.me/userinfobot) 发消息),然后以逗号分隔的列表形式添加:
收集每位团队成员的 Telegram 用户 ID(让他们给 [@userinfobot](https://t.me/userinfobot) 发消息),然后添加到 `~/.hermes/config.yaml`:

```bash
# 在 ~/.hermes/.env 中
TELEGRAM_ALLOWED_USERS=123456789,987654321,555555555
```yaml
gateway:
platforms:
telegram:
extra:
allow_from: "123456789,987654321,555555555"
```

修改后重启 gateway:

```bash
hermes gateway stop && hermes gateway start
hermes gateway restart
```

### 方式 B:私信配对(推荐用于团队)
Expand Down Expand Up @@ -260,11 +271,15 @@ hermes pairing clear-pending

**方式 1:** 在机器人所在的任意 Telegram 群组或聊天中使用 `/sethome` 命令。

**方式 2:** 在 `~/.hermes/.env` 中手动设置:
**方式 2:** 在 `~/.hermes/config.yaml` 中手动设置:

```bash
TELEGRAM_HOME_CHANNEL=-1001234567890
TELEGRAM_HOME_CHANNEL_NAME="Team Updates"
```yaml
gateway:
platforms:
telegram:
home_channel:
chat_id: "-1001234567890"
name: "Team Updates"
```

要查找频道 ID,可将 [@userinfobot](https://t.me/userinfobot) 添加到群组——它会报告该群组的聊天 ID。
Expand Down Expand Up @@ -372,20 +387,11 @@ Cron 任务的 prompt 在完全全新的会话中运行,不保留任何先前

在共享团队机器人上,使用 Docker 作为终端后端,让 agent 命令在容器中运行,而非直接在宿主机上运行:

```bash
# 在 ~/.hermes/.env 中
TERMINAL_BACKEND=docker
TERMINAL_DOCKER_IMAGE=nikolaik/python-nodejs:python3.11-nodejs20
```

或在 `~/.hermes/config.yaml` 中:

```yaml
# 在 ~/.hermes/config.yaml 中
terminal:
backend: docker
container_cpu: 1
container_memory: 5120
container_persistent: true
docker_image: nikolaik/python-nodejs:python3.11-nodejs20
```

这样即使有人要求机器人执行破坏性操作,你的宿主系统也受到保护。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,15 +218,18 @@ Hermes 在执行每条命令前都会与一份精心维护的危险模式列表

### 为消息 Bot 使用白名单

永远不要在拥有终端访问权限的 bot 上设置 `GATEWAY_ALLOW_ALL_USERS=true`。始终使用平台专属白名单(`TELEGRAM_ALLOWED_USERS`、`DISCORD_ALLOWED_USERS`)或 DM 配对来控制谁可以与你的 agent 交互。

```bash
# Recommended: explicit allowlists per platform
TELEGRAM_ALLOWED_USERS=123456789,987654321
DISCORD_ALLOWED_USERS=123456789012345678

# Or use cross-platform allowlist
GATEWAY_ALLOWED_USERS=123456789,987654321
永远不要在拥有终端访问权限的 bot 上设置 `GATEWAY_ALLOW_ALL_USERS=true`。始终使用平台专属白名单或 DM 配对来控制谁可以与你的 agent 交互。

```yaml
# 推荐:在 config.yaml 中为每个平台配置显式白名单
gateway:
platforms:
telegram:
extra:
allow_from: "123456789,987654321"
discord:
extra:
allow_from: "123456789012345678"
```

---
Expand Down
Loading
Loading