35829 Commits

Author SHA1 Message Date
m4 fb13457f6f feat: consolidate desktop and provider updates
CI / Supply-chain scan (push) Has been cancelled
Docker Build, Test, and Publish / Detect affected areas (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
Nix flake check / Detect affected areas (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
CI / Detect affected areas (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / Profile artifact check (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
CI / Check no case-colliding filenames (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / OS-specific tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Installer tests (push) Has been cancelled
CI / Rust tests (push) Has been cancelled
Nix flake check / nix flake check (push) Has been cancelled
2026-09-27 17:37:19 +08:00
m4 226350cd04 feat(desktop): open workspace files with system app 2026-09-27 17:32:11 +08:00
m4 3272102fe9 feat(desktop): 登录记住用户名密码 + 用户名不可改(2026-09-20 用户裁定)
记住凭据:登录成功自动把 {site, email, password} 存进 safeStorage 独立槽
(复刻 freemodel2api remembered 模式),登录页启动预填;登出不清,
U-6 解绑全清时一并抹掉。renderer 只有读口(rememberedLoad)。

用户名不可改:/me 资料页 username 从可编辑表单降为只读展示行,
可改字段只剩 nickname/phone。

测试:electron 侧 login 自动存/登出保留/换号覆盖/解绑抹除;renderer 侧
预填、读取失败兜底、username 只读。聚焦 59 passed;全量 10408 passed,
仅 4 个既有失败(clean HEAD 同现,与本次无关)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-20 17:39:12 +08:00
m4 5832cbbe9c fix(desktop): DB-T6 recordBind 收窄改写——TS 6.0.3 对布尔判别式真值收窄不生效
改显式 === 比较;门禁补 tsconfig.electron.json(根 tsconfig 不覆盖 electron/)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 22:59:55 +08:00
m4 10a46a00a9 feat(desktop): DB-T6 登录后自动绑定 + 机器码/pending 可查 + 失败可见(§21 U-7)
登录成功/恢复会话后 main 自动发起免口令绑定(每进程一次),复用 U-1
通路,失败绝不阻塞登录;binding-status 扩展机器码(插件 GET status)
与 pending/失败码回查;/me 绑定区单源展示 + 重试,侧栏卡片 amber 角标。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 22:50:04 +08:00
m4 55868dc7b1 feat(desktop): DB-T4c/T5 /me 绑定区 UI + token 脱敏静态断言(§21 U-1/U-6)
- /me 新增机器绑定区:未绑定一键免口令绑定,binding_pending 展示确认码,
  已绑定显示 site+installationId,解绑走破坏性确认框(U-6 全清语义写明)
- i18n binding 18 键 × 六语言
- DB-T5:user-account-hygiene 测试行级 grep——日志/诊断行零 token,
  preload 桥不回传 token 字段
2026-09-19 18:12:55 +08:00
m4 a45c35ce54 feat(desktop): DB-T4b 绑定/解绑 IPC + U-6 全清(§21 U-1/U-5/U-6)
- binding-status:只读插件 state.json 非敏感二字段,state 损坏 fail-closed
- bind-machine:session_token 免口令绑定(U-1),401 接入 refresh-retry 链,
  binding_pending 透传确认码;email/password 不出 main
- unbind-machine:尽力服务端撤销 → 插件 purge → 删所有会话;purge 失败
  结构化返回且不动本地会话(不留半清假象);登出≠解绑硬断言
2026-09-19 18:00:34 +08:00
m4 9d606be4f1 feat(desktop): DB-T3 侧栏用户卡 + /me 资料页(§21 U-4)
- 侧栏底部用户卡:U-4 展示名 + 邮箱,离线 amber 圆点,点击进 /me
- /me 资料页(ROUTES_AREA workspace 页):三字段白名单编辑
  (username/nickname/phone,只提交改动项),离线缓存数据时间横幅,
  登出入口(登出≠解绑 U-1)
- electron:PATCH /api/v2/me 客户端 + update-profile IPC(email 等
  非白名单键一律丢弃;响应四字段合并进完整快照,不覆盖 plan 等)
- i18n userAccount.me 20 键 × 6 语言;electron +3 / ui +8 测试全绿

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 13:35:00 +08:00
m4 357ac45281 feat(desktop): DB-T2 强制登录门 + 注册/找回 + U-5 机器-用户锁(§21)
- 登录门:非 signed_in 全屏盖 app,登录/注册/找回三页,只盖 UI
  不断机器通道(U-1);status 通道异常落登录页不砖(U-3)
- U-5:登录成功当场校验本机 installation ∈ 会话用户列表,不符即
  销毁会话并提示绑定者邮箱;绑定状态损坏 fail-closed
- relay 客户端补注册/找回四通道 + installations/binding-owner 查询;
  IPC 预期失败全部结构化 {ok:false, code} 返回
- i18n userAccount 48 键 × 6 语言;electron +11 / ui +7 测试全绿

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 13:12:49 +08:00
m4 a48a81a5f0 feat(desktop): DB-T1 用户账号会话层(§21 U-1/U-3)
relay /auth/* 客户端 + safeStorage 落盘 + IPC 边界:token 只活 main
进程;401 反应式刷新,refresh 被拒为唯一硬终点(本地清零 +
sessionExpired);relay 不可达标离线并回缓存 profile + 数据时间。
登出≠解绑,机器绑定不经此层。

tests: +19(electron 15 / ui 4);typecheck 绿;electron/ui 全量仅
既有环境性失败(干净树同败,已对照)。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-19 12:42:31 +08:00
m4 05be76016a feat(desktop): workspace bar carries the full path as the breadcrumb
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
Nix flake check / Detect affected areas (push) Has been cancelled
CI / Detect affected areas (push) Has been cancelled
Docker Build, Test, and Publish / Detect affected areas (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Installer tests (push) Has been cancelled
CI / Rust tests (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / Profile artifact check (push) Has been cancelled
CI / Check no case-colliding filenames (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Supply-chain scan (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / OS-specific tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Nix flake check / nix flake check (push) Has been cancelled
The chat header is display:none inside the pane tree (title lives in the
zone tab), so the header cwd segment never shows in the main layout.
Append displayPath(cwd) to the always-visible workspace strip instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 19:32:46 +08:00
m4 7025584b9f feat(desktop): pin AI-content disclaimer under the composer
Registered as a core composer.underside contribution so the chrome-free
strip auto-hides when empty and plugins can sit beside the line.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 19:17:06 +08:00
m4 6946e37f96 feat(desktop): surface produced files, session artifacts fold, header cwd
Completed diff-less writes (write_file creates) were invisible in the
transcript. They now appear in the per-turn files card with a "new" tag
(preview on click, system-app open on hover), a collapsible per-session
artifacts fold sits above the usage footer, and the chat header shows
the session's workspace path ahead of the title.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 19:16:57 +08:00
m4 9845e0b262 fix(desktop): resolve session usage footer by stored id with lineage match
CI / Check no case-colliding filenames (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Supply-chain scan (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / Profile artifact check (push) Has been cancelled
CI / Detect affected areas (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / OS-specific tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Installer tests (push) Has been cancelled
CI / Rust tests (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
Docker Build, Test, and Publish / Detect affected areas (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
Nix flake check / Detect affected areas (push) Has been cancelled
Nix flake check / nix flake check (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Build Skills Index / build-index (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CDP verification on the live app showed the footer never rendered: ChatView
passed the runtime id (view.$runtimeId) while $sessions rows key off stored
ids, and compression chains hand the view a lineage id rather than the row's
own id. Look up with selectedSessionId || routedSessionId and
sessionMatchesStoredId, same as ChatHeader.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 17:34:54 +08:00
m4 71b37dd42d feat(desktop): render session usage footer at transcript end
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 17:08:49 +08:00
m4 917290bec2 feat(desktop): add SessionUsageFooter component and i18n keys
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-18 16:50:10 +08:00
m4 74fa6881ea style(desktop): fix import order in office-open touched files
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-17 23:08:53 +08:00
m4 526b13170d test(desktop): satisfy Translations typing and lint rules for office-open tests
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-17 23:08:24 +08:00
m4 45dc96b139 feat(desktop): binary refusal page offers the system app first
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-17 22:57:33 +08:00
m4 a1f9e97782 feat(desktop): double-click office files opens them in the system app
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-17 22:51:07 +08:00
m4 4379bf6d0b feat(desktop): openFileWithSystemApp entry with office whitelist
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-17 22:41:23 +08:00
m4 c25a3748bb feat(desktop): hermes:fs:openExternal IPC with receipt-returning OS open
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-17 22:35:34 +08:00
m4 3ca91c55f3 feat(desktop): openExternalFileForIpc opens files via OS handler
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-09-17 22:28:07 +08:00
brooklyn! fb56a7e06d docs(desktop): the wake-word ear and speaker live in the mic's fan
The desktop guide and the wake-word page told users to click an ear in the
composer row; it now fans out of the microphone on hover. The folded voice
menu, not the ear, carries the silent-mic hint.
2026-09-16 02:56:57 -05:00
brooklyn! 1331053fcf style(desktop): anchor composer icon tooltips beside the control
Tips on the composer's row controls open to the left so they never cover
the fanned discs or the input; the model and reasoning pills keep theirs
centred above. A left-anchored Tip rags its wrapped lines toward the
trigger, in the primitive rather than per call site.
2026-09-16 02:56:57 -05:00
brooklyn! bc722dcb85 feat(desktop): fan the composer's voice toggles out of the mic
The docked composer spent three icon buttons on toggles that are set once
and rarely touched. The mic is now the one button in the row; hovering it
fans spoken-replies and the wake word out above it. The mic reports
dictation only — the wake word carries its own on-state on its own disc,
so mirroring it onto the mic read as dictation being on.

The wake disc's tip names the phrase and nothing else; the pressed state
says on/off. The folded HUD/narrow-tile VoiceMenu is unchanged.
2026-09-16 02:56:57 -05:00
brooklyn! c7580f0e06 feat(desktop): FanMenu primitive and a floating Button variant
One hub control that fans its sibling toggles out on hover — a column, a
row split around the hub, or an arc — with the discs portalled past any
overflow-hidden parent and re-anchored on scroll and resize. Hover state,
geometry and open/close timing live in the primitive so a consumer only
re-renders when its own items change.

A document-level pointer watchdog closes the fan when enter/leave is
skipped: a disc that goes disabled under the pointer (a toggle turning
pending right after its click) stops receiving pointer events, so its
pointerleave never fires and the fan stayed open.

Discs wear the new Button `floating` variant off (popover fill +
shadow-md, glyph-only hover) and `default` on, so a toggled state is an
opaque primary disc rather than a translucent tint.
2026-09-16 02:56:57 -05:00
brooklyn! 65b622da0d fix(desktop): transcript directives survive markdown, and the guide keeps its reasoning to itself
A handoff directive whose brief read as markdown (*by week*, a_b c_d, ~/x)
split the paragraph into element children, so the card never claimed it and
the raw ::onboarding{task="…"} line painted as the user's own message.
Directive lines are now backslash-shielded in preprocessMarkdown, next to
the inline-code and math shields, and reach the renderer as one text node.

The guide's reasoning is it reading its own runbook ("Now step 4: offer the
tour with ::ask"); shown under the greeting it breaks the conversation the
guide is trying to have. Reasoning disclosures stay hidden on the guide
thread only.
2026-09-16 02:49:21 -05:00
teknium1 287c56e95a test(kanban): archive-terminates-worker fixture records a verified spawn fingerprint
_set_worker_pid on a fake PID (54321) now persists the 'unverified' marker, under which
the archive path correctly refuses to signal; the test pins the verified-spawn behaviour,
so it stubs the fingerprint capture.
2026-09-16 00:35:00 -07:00
teknium1 92d64465bf docs(multiplex): routed-child credential rule and the frozen launch env restart requirement
kvnloo (#111620 review) asked for the operator-visible statement that once a serve /
dashboard process hosts a second profile, the launch profile's env-only credentials are
frozen at activation and a rotation in the process env needs a restart. Also states the
routed-child rule with and without the multiplex flag (#111617). Adds encoding='utf-8'
to the probe child in the child-env authority test (windows-footguns lint).
2026-09-16 00:35:00 -07:00
teknium1 db54f5448d fix(kanban): worker fingerprint carries a boot witness; an uncaptured fingerprint never authorizes a signal
#111617 review (andrexibiza P1 #3/#4, kvnloo nit):

- worker_started_at persisted only gateway.status.get_process_start_time(): on Linux that
  is /proc/<pid>/stat field 22, clock ticks since THIS boot. The threat is a row surviving
  a reboot, and that counter does not, so an unrelated process on a later boot with the
  same PID and the same tick value passed _start_times_agree(). The fingerprint is now
  "<gateway.drain_control.current_instantiation_epoch()>|<start>" (boot_id + PID-1 start,
  the witness the drain marker already uses); both halves must match. Integer values on
  rows written before this change keep the start-time-only comparison.
- A failed capture persisted NULL, which _pid_recycled treats as the legacy pre-fingerprint
  row and falls back to bare PID existence - a new spawn silently recreated the #89614/
  #99558 kill authority. A failed capture now persists UNVERIFIED_WORKER_FINGERPRINT: the
  claim is held while the PID is live (never released beside it, never SIGTERM/SIGKILLed
  by timeout, stale-claim, manual reclaim, archive or the terminal reaper) and reclaimed
  once it is gone. NULL stays legacy-only.
- Every tasks UPDATE that nulls worker_pid nulls worker_started_at too (archive_task and
  the reclaim/timeout/reopen paths): the fingerprint is part of the kill-authority tuple
  and must not outlive its pid.

Live (real sleeper child): reboot-shaped row (same pid, same tick, other boot id) ->
reclaimed to ready, child untouched; matching fingerprint -> SIGTERM delivered, exit -15.
tests/hermes_cli/test_kanban_worker_pid_fingerprint.py: +2 hostile tests, both red on base.

Not changed: the check-then-act window between _pid_recycled and kill (kvnloo P2) is
real but needs pidfd_open/pidfd_send_signal (Linux 5.3+) to close atomically; left as
the documented residual of "never kills a DETECTED recycled PID".
2026-09-16 00:35:00 -07:00
teknium1 8609743389 fix(serve): launch-profile scope decided at entry; send keeps scope authority; per-reset release
Three edges of the fail-closed multi-profile host (#111620 review, andrexibiza P1 + P2,
kvnloo finding 1):

- `send` under a routed profile's scope `update()`d the installed scope from raw `.env`,
  reversing build_profile_secret_scope's precedence (user .env, then external secret
  sources) for the rest of the request; a stale user value beat the secret-manager one.
  The installed scope is authoritative as-is; only the config.yaml setdefault bridge runs.
- The launch profile's body was scoped only when `is_multiplex_active()` was already true
  at entry, while get_secret consults that global on every read. A launch RPC / dashboard
  request entering single-profile and resuming after a concurrent first `?profile=B`
  activation raised UnscopedSecretError mid-request. The launch profile's secret scope
  (its .env + external sources over the launch env: live while single-profile, the frozen
  snapshot once multiplexing is active) is now bound for every launch-profile body, so the
  credential source is fixed at entry. The terminal policy overlay stays multiplex-only
  (standalone terminal execution keeps its os.environ bridge). _publish_env_value mirrors a
  same-request .env write into that scope AND os.environ for the launch profile, only into
  the scope for a routed one (serves_routed_profile).
- _release_profile_runtime_scope_tokens reset terminal → secret → home in sequence under
  one outer suppress; a failing terminal reset left the previous profile's secrets and
  HERMES_HOME installed for the next body in that context. Each reset is now independent;
  the first failure is re-raised after every scope is released.

tests/tui_gateway/test_multi_profile_hosting_transitions.py: manager-vs-dotenv precedence
through _load_hermes_env, TUI-RPC and dashboard barrier tests (launch enters single-profile,
B activates on another thread, launch resumes and still resolves its injected credential,
never B's), forced terminal-reset failure still releases secret + home. 4/4 red on base.
2026-09-16 00:35:00 -07:00
teknium1 3fe8e5e443 fix(multiplex): routed children never inherit launch-only credentials, with or without the multiplex flag
Two authority gaps in served_profile_child_env (#111617 review, andrexibiza P1 #1/#2,
kvnloo finding 1):

- The base was hermes_subprocess_env(inherit_credentials=True) = the launch environ's
  provider credentials; strip_launch_profile_env only knows names with .env/source
  provenance, so a key systemd/Compose/the shell injected into the launch process
  survived into profile B's child whenever B did not define the same name. Now a ROUTED
  target scrubs every Tier-1/Tier-2 credential from the base regardless of provenance
  before B's own scope is overlaid (the child boundary gets get_secret's contract: a
  scoped miss is no credential, never ambient fallback). The launch profile's own child
  keeps its env. bot_relay's base=os.environ goes through the same scrub.
- strip_launch_profile_env / the scrub keyed on is_multiplex_active(); the Desktop and
  dashboard backends serve ?profile=B by installing the HERMES_HOME override without
  that flag, so B's slash worker / helper children kept A's .env and settings. The
  authority test is now "is the target a routed home" (target != process home).
- _build_browser_env resolved the passthrough keys via get_secret, which falls through
  to os.environ on a scoped miss while multiplexing is inactive: a routed B with no
  Firecrawl key got A's. Under serves_routed_profile() the bound scope is the only source.
- served_profile_child_env(inherit_credentials=True) with no target and no scope bound
  under multiplex minted with the launch credentials (key_cmd TTL refresh on a worker
  thread); it now raises UnscopedSecretError like get_secret.

tests/tui_gateway/test_served_profile_child_env_authority.py: ambient-only A key + B
missing it (mux on), flag-off routed B (helper child + browser), real child observation.
3/3 red on base.
2026-09-16 00:35:00 -07:00
teknium1 7dde7a2424 fix(gateway): migrate --multiplex resumes from live state, compensates the whole destructive phase, and refuses an unknown default principal
Three P1 findings from the review of #111062 (fixes #110850 remainder):

- interrupted-detection keyed off `plan.default.has_gateway`, which is true for an
  installed-but-dead unit; `systemd_install` writes the unit before the start that
  can still be killed, so an apply interrupted at default/start (or mid-restart of
  a stopped default) was reported as "already multiplexed" and never recovered.
  `interrupted` is now derived from the manifest vs the LIVE default's recorded
  served_profiles: flag on + manifest + not every migrated profile served = resume.

- the compensation `try` began after `_remove_secondary_gateways` and the flag
  write, so a later secondary's stop, a unit's daemon-reload or the config write
  failing left the first secondary removed with no rollback. The flag write,
  removals and default bring-up now all sit inside one boundary that rolls back
  through the manifest; `_preflight_apply` refuses failures knowable from the plan
  (root system unit without a recorded User=, unresolvable recorded user, an
  unwritable config.yaml) before any working gateway is stopped.

- `_guard_unix_user` blocked an unknown SECONDARY principal but accepted
  `default_uid is None`; a default system unit whose User= this host cannot
  resolve is the same boundary from the other side and now blocks the update
  hook (same known uid still folds, different known uid still refuses).
2026-09-16 00:33:03 -07:00
Sora-bluesky 9085ef967c fix(profiles): sweep the remaining pre-write mkdirs under the deleted-profile guard
A long-lived serve process keeps a deleted profile as the context home of threads
that outlive the delete. A bare `mkdir(parents=True)` right before an atomic write
brings `profiles/<name>/` back after `hermes profile delete` has written the
tombstone and removed the tree.

The writers in `utils` and the seven callers named in #112592 are guarded by the
preceding commits; this one applies the same `mkdir_under_hermes_home` idiom to the
other pre-write directory creations found by the same mechanical rule (auth,
personality, plugin catalog, skills sync, tool discovery cache, platform adapters,
memory plugins, local runtime supervisor, process identity, breadcrumbs). The two
sites that pass `mode=` keep their mkdir behind `assert_named_profile_home_live`.
The guard is a no-op unless the target has a provable `profiles/<name>` ancestor.

Salvaged from #112596 (30-file sweep) on top of #112594 / #112601; the overlapping
files were resolved to the already-landed versions.
2026-09-16 00:32:15 -07:00
teknium1 5d97d5ed6d refactor(profiles): move rename identity migration off the facades; trim tests to invariants
- hermes_cli/profiles.py was already past the 2,000-line gate; the rename identity
  migration (migrate_profile_identity, _migrate_profile_identity, control-answer helpers)
  now lives in hermes_cli/profile_identity.py, imported late from rename_profile and the
  profile subcommand.
- The migrate-profile-identity control-verb handler moves out of gateway/run.py into
  gateway/run_profile_reconcile.py (migrate_profile_identity_verb), beside the other
  hot-serve control-verb logic.
- tests/utils/ is not a mirrored source dir: the atomic-writer tests move to
  tests/test_utils_atomic_writers_deleted_profile.py (root-module test placement).
- Drop duplicate no-op/idempotent/raw-answer tests so each fix carries invariant tests only.

Behaviour unchanged; authored commits from @xielevi, @KoNit-K and @kokhlo are preserved.
2026-09-16 00:32:15 -07:00
Konstantin Khlopkov 7f7d229f83 fix(utils): atomic writers refuse to resurrect a deleted named profile home
Background writers that still carry a tombstoned profile as their Hermes
home (reasoning-caps warm thread, models cache, models.dev ETag, gateway
lifecycle ledger, MCP OAuth tokens, memory store) re-created
profiles/<name>/ with a bare mkdir right before an atomic write. Route the
parent-dir creation through mkdir_under_hermes_home so a deleted named
profile raises FileNotFoundError and stays gone, matching the tombstone
contract already enforced for logging and state.
2026-09-16 00:32:15 -07:00
KoNit-K 91a38622db fix: guard atomic writers after profile deletion 2026-09-16 00:32:15 -07:00
xielevi 81140e4546 fix(profiles): ship a retry path for the rename identity migration
A rename under a live multiplexer that could not reach the control verb warned and
stopped there, leaving the operator with no way to finish: the rename cannot be
repeated (profiles/<old> is gone) and the CLI deliberately never rewrites the
routing DB a live gateway holds in memory.

- `hermes profile migrate-identity <old> <new>`: retries the migration —
  delegates to the gateway control verb while a multiplexer is live, performs the
  durable rewrite of both state DBs when none is. Idempotent, and exits non-zero
  naming the offending database on a collision, a lock, or a partial failure. Only
  the name format and the existence of the new profile are checked; the old profile
  directory is expected to be gone.
- An older gateway that does not implement the verb is reported as such (`identify`
  answers while the migrate verb does not), not as "no gateway".
- `_migrate_profile_identity` returns an explicit success/failure result so the
  command can set its exit code; the rename warning now names the exact invocation.
- A failed control answer keeps the raw payload when it carries no reason field.
- The offline failure branch called `click.echo` in a module that never imports
  `click`: a failed second database raised NameError instead of printing its warning.
2026-09-16 00:32:15 -07:00
xielevi 4ba717df12 fix(profiles): migrate session/routing identity on profile rename
Renaming a profile moved profiles/<old>/ to profiles/<new>/, so the row DATA
travelled with the directory, but the profile name is also baked into
keys/values the move left untouched: session keys (agent:<old>:* namespace),
sessions.profile_name (fail-closed owner ladder / Desktop sidebar scope /
@session: deep links), sessions.origin_json.profile,
gateway_heartbeats.profile, delivery_obligations (session_key +
adapter_profile), telegram_dm_topic_* profile_name bindings, and the
gateway_routing index. Left stale, every inbound event on a chat keyed to the
old name resolved to a profile that no longer exists — flooding errors.log
with "Profile <old> does not exist ... falling back to global HERMES_HOME"
every few seconds — and renamed sessions dropped out of the sidebar / broke
their deep links.

The routing index is held in memory by a live multiplexer and written back
periodically, so a CLI-side DB rewrite alone is clobbered. Fix in layers:

- SessionDB.rekey_profile_state: atomic durable rewrite of the state.db
  tables, matching the agent:<name>: namespace by exact prefix (substr, not
  LIKE — '_' is a legal profile-name character and a LIKE wildcard), rewriting
  the profile inside routing/origin JSON, and REFUSING on a target collision
  (routing rows or telegram bindings) instead of silently merging.
- SessionStore.rekey_profile_routing: rekey the in-memory routing index
  (keys + origin.profile) then persist — the half a DB write cannot reach.
  Raises on a target-key collision before mutating.
- Control verb migrate-profile-identity (params-carrying; the socket passes
  params only to handlers that declare them, bare handlers unchanged) so a
  live gateway rekeys its in-memory copy AND both durable stores (routing home
  + the renamed profile's own state.db).
- rename_profile calls the verb when a multiplexer is live and, if it fails,
  does NOT fall back to a racing CLI-side write: it prints a warning telling
  the operator to restart the gateway and retry. With no live gateway it
  performs the durable rewrite itself (safe: nothing else holds the store
  open).

Checkpoints keyed by the profile's workdir path are a known related gap,
tracked separately, not addressed here.

Tests: rekey_profile_state (all tables, routing/origin JSON, collisions,
idempotent, no-op), rekey_profile_routing (namespace + origin, no-op, no
overwrite), control verb param passing, and rename end-to-end for both the
live-gateway (delegates, refuses unsafe fallback) and no-gateway (durable
rewrite) paths.
2026-09-16 00:32:15 -07:00
ouyangbo 012c9c6bed chore: anchor fresh-start history to upstream 2026-09-16 15:06:54 +08:00
brooklyn! 2e320e6d1a fix(desktop): keep inline edit placeholders off entered text 2026-09-16 02:04:58 -05:00
brooklyn! 64a9b43261 docs: run worktree desktops side by side without port eviction 2026-09-16 01:54:36 -05:00
brooklyn! 2b7292ff0f style(desktop): frame inline subagents with a subtle outline 2026-09-16 01:44:33 -05:00
brooklyn! 3bdd4cc5fd fix(desktop): keep background continuations in one visual response 2026-09-16 01:44:33 -05:00
brooklyn! 57d34b14c2 fix(desktop): keep completed subagents from reviving stale activity 2026-09-16 01:44:33 -05:00
kshitijk4poor f0b2ba4160 test(tui_gateway): teardown tests prove agent.close() reached the row finalizer
_teardown_session swallows agent.close() failures, so a "row stays open"
assertion alone is green even when close() aborted before
_finalize_owned_session_row. Assert the one side effect only that finalizer
produces (_owns_session_db cleared), give the stub the _memory_manager attr
close() reads directly, and assert the flag is untouched on the control
paths. Rename the sole-owner cross-process test to what it now asserts.
2026-09-16 12:13:05 +05:30
kshitijk4poor dfc40a50a9 fix(tui_gateway): a spared durable row survives agent.close() too
_finalize_session decides whether the TUI backend ends the state.db row
(gateway-owned source → no; another backend holds the lease → no; automatic
Desktop reclaim → no, #105588). _teardown_session then calls agent.close(),
whose _finalize_owned_session_row ends the same row as "agent_close" through
the agent's own handle whenever _end_session_on_close is still True — so every
spare decision was undone one call later. Verified with real SessionDB +
AIAgent.close(): with only the #105620 guard a Desktop ws_orphan_reap still
lands ended_at + end_reason="agent_close" (the #105588 follow-up report).

One seam after the lifecycle decision now clears agent._end_session_on_close
whenever the row was spared, replacing the gateway-owned-only assignment from
#111145 so the lease-held and Desktop-reclaim cases get the same treatment.
The five mocked _finalize_session tests from #105620 are replaced by two
real-DB _teardown_session tests beside the #111145 harness (red on both
origin/main and the bare cherry-picks, green on the stack).
2026-09-16 12:13:05 +05:30
Eva a8b8556108 fix(tui): a gateway-owned session survives agent.close() 2026-09-16 12:13:05 +05:30
ClintonEmok d3a753df30 fix(gateway): don't end durable session row on automatic Desktop cleanup
ws_orphan_reap, idle_timeout, lru_evict, ws_disconnect, and tui_shutdown
are runtime/connection GC — not user intent. Ending the durable session
row during these automatic cleanup reasons confuses GC with user action,
causing secondary bot chats to vanish from the sidebar even though the
transcript is intact in state.db.

The canonical Bot Chat already has resurrection paths for accidental
ws_orphan_reap ends, but non-canonical secondary chats do not, so they
are hit harder.

Skip db.end_session() when _desktop_automatic_cleanup is True (automatic
cleanup reason + Desktop source). Runtime is still reclaimed, the
session.reclaimed event still fires, and explicit user close/archive/reset
still ends the durable row normally.

Fixes #105588
2026-09-16 12:13:05 +05:30