Commit Graph

21297 Commits

Author SHA1 Message Date
Alex Fournier 46bae75042 feat(observability): add Relay active install metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-29 12:20:34 -07:00
Alex Fournier 5607d09e03 feat(observability): add Relay client resource metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-29 12:20:34 -07:00
Alex Fournier f1fd678e44 feat(observability): add Relay skill metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-29 12:20:34 -07:00
Alex Fournier 8502e464a8 fix(observability): harden tool lifecycle metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-29 12:20:17 -07:00
Teknium 8714040954 fix(computer_use): resolve gateway session-key namespace in permission-mode lookup
Follow-up to the #68246 salvage. The backend permission-mode resolution
only checked the DB session_id the tool path passes, but gateway /yolo
keys approval bypass off the gateway session_key (contextvar). Consult
both namespaces so /yolo works on messaging platforms, not just CLI/TUI.
Adds a regression test driving the real approval contextvar + yolo
toggle path E2E.
2026-07-29 12:19:37 -07:00
Francesco Bonacci d59974fea5 test: update session yolo approval query 2026-07-29 12:19:37 -07:00
Francesco Bonacci c268397752 feat(computer_use): align cua-driver 0.10 permission modes 2026-07-29 12:19:37 -07:00
Francesco Bonacci 847e401b74 feat(computer_use): align cua-driver 0.9 contracts
Salvaged from PR #67807 by @f-trycua onto current main.

- Foreground gate: discover delivery_mode support from the live tools/list
  inputSchema.properties (fail closed), not the never-shipped
  input.delivery_mode capability token
- bring_to_front: standalone strict-schema MCP tool (inject_session=False),
  separate approval scope, requires foreground
- Verdict precedence: confirmed > unverifiable (verify before retry) >
  suspected_noop/refusal (escalate); surfaced as explicit verdict field
- Typed cua_browser_* route inside computer_use (browser_route.py) with
  exact-binding, adapter-injected session, snapshot-scoped refs
- Per-Hermes-session backend isolation + release_computer_use_session seam
  wired into AIAgent.close()
- Recorded 0.9 tools/list fixture replaces fabricated capability tokens
2026-07-29 12:19:37 -07:00
Teknium 3dd8059a05 fix(tests): eliminate flaky/broken tests — shadow sys.path inserts, unmocked network in compressor tests, stale-SDK feishu pin guard, quadratic redact regexes
- Remove tests/-shadowing sys.path.insert(dirname/'..') from 11 test files:
  it prepended the tests/ dir itself to sys.path, so 'import agent' /
  'import hermes_cli' resolved to the test packages and collection died
  with ModuleNotFoundError depending on import order (2 files failed in
  every full-suite run; 9 more were latent).
- Patch call_llm in 5 context-compressor tests that called compress()
  unmocked: each burned ~50s attempting live LLM traffic through the
  relay before falling back (572s file — the slowest in the suite, and
  flaky under the 300s per-file timeout). File now runs in ~5s.
- agent/redact.py: fix two catastrophically-backtracking regexes hit by
  the compressor's redaction pass on large payloads —
  _STRICT_URL_USERINFO_RE anchors on the mandatory '//' (optional-scheme
  prefix backtracked O(n^2): ~55s on a 320KB payload, now sub-ms;
  output-equivalence fuzz-verified on 20k random strings), and the
  _CFG_DOTTED_RE/_CFG_ANCHORED_RE subs gain an exact linear keyword
  pre-gate so secret-free text skips the quadratic pattern entirely.
- tests/gateway/test_feishu.py: version-guard the extra_ua_tags SDK
  signature check; the repo pins lark-oapi==1.6.8 but stale local
  installs (1.5.3) fail the assertion — skip below the pin.
- tests/tools/test_managed_browserbase_and_modal.py: stub
  agent.redact + agent.credential_persistence in the fake agent package
  (empty __path__ blocks all real agent.* imports added since the fake
  was written).
- tests/gateway/test_startup_restart_race.py: raise wait_for timeouts
  2s -> 30s; 2s wall-clock on a loaded 40-worker box flaked in the
  baseline run (passes instantly when the box is quiet).
2026-07-29 12:18:07 -07:00
teknium1 bcb352eeab refactor: registry-owned execute() on CommandDef — informational commands unified (thin slice) 2026-07-29 12:11:24 -07:00
teknium1 ab08e8fc76 refactor(gateway): consolidate 19 session-keyed dicts into SessionState (turn/conversation/persistent scopes; eliminates wholesale-reset races)
GatewayRunner carried ~19 separate Dict[str, ...] attributes keyed by
session_key, each with an ad-hoc lifecycle. They now live in one
`self._sessions: dict[str, SessionState]` (gateway/session_state.py) with
three lifecycle scopes and a `_session_state(key)` get-or-create accessor.
Mechanical refactor: same state, same semantics, new container.

Migration table (dict -> old decl line -> current clear path -> new home):

| legacy dict                              | decl  | cleared by (before)                              | SessionState field                     |
|------------------------------------------|-------|--------------------------------------------------|----------------------------------------|
| _running_agents                          | 3505  | _release_running_agent_state; stop() .clear()    | turn.agent                             |
| _running_agents_ts                       | 3506  | same                                             | turn.started_ts                        |
| _active_session_leases                   | 3507  | same (+ lease.release())                         | turn.lease                             |
| _busy_ack_ts                             | 3539  | same                                             | turn.busy_ack_ts                       |
| _turn_lease_tokens ((key, gen)-keyed)    | 3518  | _release_turn_lease (generation-guarded)         | turn.lease_token + turn.lease_generation |
| _session_model_overrides                 | 3585  | _CONVERSATION_SCOPED_STATE funnel                | conversation.model_override            |
| _pending_one_turn_model_restores         | 3586  | funnel; one-shot pop in turn finally             | conversation.one_turn_restore          |
| _session_reasoning_overrides             | 3589  | funnel; lazy-init dict swap ~5692 (RACE)         | conversation.reasoning_override        |
| _session_service_tier_overrides          | 3592  | funnel; lazy-init dict swap ~5738 (RACE)         | conversation.service_tier_override     |
| _last_resolved_model ("*" = process-wide)| 3527  | funnel                                           | conversation.last_resolved_model       |
| _queued_events                           | 3537  | funnel; lazy-init dict swap ~5204 (RACE)         | conversation.queued_events             |
| _pending_turn_sidecar_notes              | 3597  | funnel; lazy-init dict swap ~19832 (RACE)        | conversation.sidecar_notes             |
| _session_ephemeral_pin                   | 3601  | agent-cache evict pop; lazy swap ~19885 (RACE)   | conversation.ephemeral_pin             |
| _session_vc_last                         | 3604  | agent-cache evict pop; lazy swap ~19864 (RACE)   | conversation.vc_last                   |
| _pending_approvals                       | 3611  | boundary security funnel; stop() .clear()        | persistent.approvals                   |
| _update_prompt_pending                   | 3623  | security funnel; update watcher pops             | persistent.update_prompt_pending       |
| _pending_native_image_paths_by_session   | 3538  | one-shot consume; lazy swap ~12944 (RACE)        | persistent.native_image_paths          |
| _pending_messages (runner-level, str)    | 3519  | _interrupt_and_clear_session pop; stop() flush   | persistent.pending_command_text        |
| _session_run_generation                  | 3540  | NEVER (monotonic, #28686)                        | persistent.run_generation (never reset)|

Races eliminated: every `self._X = {}` lazy-init/reset replaced the WHOLE
dict, so a writer on session A racing a lazy init triggered by session B
could lose its entry. All six such sites (_session_reasoning_overrides
~5692, _session_service_tier_overrides ~5738, _queued_events ~5204,
_pending_turn_sidecar_notes ~19832, _session_ephemeral_pin ~19885,
_session_vc_last ~19864, _pending_native_image_paths_by_session ~12944,
_turn_lease_tokens ~13644) are now per-session field writes on an existing
SessionState; a reset can no longer cross sessions structurally.

Registry successors:
- _release_running_agent_state -> state.turn.clear() (one structured reset
  instead of the drifting pop-list; still pops the slot lease and calls
  lease.release() first; still generation-guarded).
- _CONVERSATION_SCOPED_STATE funnel -> state.conversation.clear(); the
  tuple is retained for legacy plain-dict stores not yet folded in
  (_pending_model_notes) and for the public test contract.
- _turn_lease_tokens' (key, generation) tuple key -> lease_token +
  lease_generation fields; release/rebind only match when the generation is
  current, preserving the #28686/#64934 ownership check.
- _session_run_generation stays monotonic on persistent.run_generation and
  is never cleared (conversation boundaries and turn releases don't touch it).

Compatibility adapters: tests (and a few mixin call sites) access the old
dict names directly (137 direct assignments to _running_agents alone), so
each legacy name is kept as a thin @property returning a live MutableMapping
view over the corresponding SessionState field (legacy_dict_property /
legacy_lease_token_property in session_state.py). Setter accepts a plain
dict (the `runner._X = {...}` test pattern); views support ==, in, len,
.get/.pop/.clear. The shutdown path in _stop_impl deliberately keeps
duck-typed legacy-attribute access because test fakes borrow it with plain
dicts.

Name collision noted (NOT touched, out of scope): gateway/platforms/base.py
has its own _pending_messages Dict[str, MessageEvent] (adapter-level slot);
the runner-level Dict[str, str] of the same name is what moved to
persistent.pending_command_text.

Entry leaks preserved (follow-up, no new eviction in this PR): SessionState
entries in self._sessions are never evicted, matching the old dicts — e.g.
_last_resolved_model, _session_run_generation, _session_vc_last entries for
dead sessions leaked before and their fields still occupy a SessionState now.

Verification: 123 tests/gateway files referencing the old names +
_release_running_agent_state + _CONVERSATION_SCOPED_STATE all pass (sole
failure test_feishu.py::test_websocket_sdk_accepts_channel_ua_tag is
pre-existing, stash-verified); 8 non-gateway test files touching the names
pass (266 tests); `import gateway.run` subprocess smoke OK; ruff clean;
post-migration grep shows zero non-comment `self._<oldname>` references in
run.py outside the property adapters and the duck-typed shutdown block.
2026-07-29 12:11:15 -07:00
Ben Barclay b521fd9dc9 docs(relay): finish the QA-N scrub in the new relay test files
The earlier scrub covered adapter.py; nine internal QA-campaign tracker IDs
remained in the two new test files, including a module docstring and an
assertion message. They mean nothing to a future reader — describe the
behavior instead. Comments only, no assertion changes.
2026-07-29 12:03:01 -07:00
Ben Barclay 0dc293f4f7 fix(relay): coerce Slack behavior flags exactly as the native adapter does
Both relay Slack knobs read their value through bool(), while the native
adapter they mirror uses str(raw).strip().lower() in {"1","true","yes","on"}.
A YAML-quoted string diverges:

    dm_top_level_threads_as_sessions: "false"   → relay True, native False

Non-empty strings are truthy, so the escape hatch is silently ignored in
exactly the shape an operator writes to switch it OFF. reply_in_thread has the
same defect and gates reply placement, session keying and run.py's progress
resolver, so one quoted "false" misfires three ways.

Route both through a shared _coerce_flag mirroring native's predicate. Real
booleans pass through untouched; None falls back to the default. Contract §8
documents the accepted spellings.

Tests: both knobs parametrized over the true/false spellings native accepts,
plus the absent-key default.
2026-07-29 12:03:01 -07:00
Ben Barclay 33833e232e fix(relay): apply the Slack thread anchor on the media lane too
The DM thread-anchor contract was resolved only in send(). _send_media() —
backing send_image, send_image_file, send_voice, send_video and send_document
— passed reply_to straight to the frame and never touched metadata, so
attachments egressing through the same connector-side Slack sender got both
failure shapes this branch set out to remove:

  flat mode   → reply_to survives, the image threads UNDER the user's DM
                message (the original reported symptom)
  thread mode → no metadata.thread_id, and threadTs() never reads reply_to,
                so the image lands in the home channel instead of the
                per-message thread

Both are reachable: gateway/run.py delivers agent artifacts through
send_voice/send_document.

Extract the three steps that must always happen together (mode gate, mirrored
reply_to_message_id strip, metadata promotion) into
_apply_slack_thread_anchor and route BOTH lanes through it, so text and media
cannot drift again. The media lane copies caller metadata rather than mutating
it — these helpers are called in loops with a shared mapping.

Also fold send_typing/stop_typing's duplicated status-anchor blocks into
_with_status_thread_anchor. They had already drifted (stop_typing omitted the
platform check) and the clear must target the thread the heartbeat set or the
status line sticks until Slack's own timeout.

Tests: media lane pinned in both modes plus the channel and
no-caller-mutation cases; verified as real by reverting the fix and watching
them fail.
2026-07-29 12:03:00 -07:00
brooklyn! 3bb239750d Merge pull request #74286 from NousResearch/bb/tool-ticker-clip
fix(desktop): let an opened tool row escape the live run's one-line window
2026-07-29 14:01:51 -05:00
teknium1 30c783589c perf(agent): cursor/memo optimizations for per-iteration full-history walks (byte-parity proven)
Three provably-safe optimizations for O(n)-per-iteration history walks:

1. sanitize_tool_call_arguments: optional identity-keyed cursor (strong
   refs to the exact validated message objects) skips re-json.loads-ing
   already-validated history each loop iteration. Any list rewrite
   (compression, repair, undo, steer) breaks the identity prefix match
   and forces re-scan from the divergence point. Wired via a per-agent
   cursor dict in conversation_loop.

2. estimate_messages_tokens_rough: per-message memo keyed on a deep
   identity fingerprint (strings pinned by strong reference so id()
   aliasing is impossible; scalars by value; dicts/lists structurally
   with key order). Equal fingerprints imply identical str(shadow)
   bytes, hence identical estimates. Unfingerprintable shapes fall
   through to direct compute. Bounded FIFO cache (4096 entries).

3. _flush_messages_to_session_db_unlocked: bounded scan that skips the
   identity-matched prefix of the previous successful flush's snapshot.
   Snapshot only taken on full success; cleared on exception. Compression
   rewrites use fresh copies, breaking identity and forcing full re-scan.

Parity proven in tests/agent/test_cursor_optimizations_parity.py:
500-message synthetic histories with tool calls, malformed args, unicode,
element-wise old==new across 3 iterations incl. simulated compression.

Measured (median of 5): sanitize 0.097ms->0.011ms, tokens 1.145ms->0.853ms,
persist-scan 179.5us->10.0us at 500 messages.
2026-07-29 11:54:18 -07:00
teknium1 ba7da1332c refactor: single-owner model switch parsing + effective-model resolution (kills the api_server/run.py divergence class) 2026-07-29 11:54:09 -07:00
teknium1 bd93ccb890 refactor(gateway): shared fence-aware markdown chunker core (yuanbao-derived) + canonical table-row splitter 2026-07-29 11:53:59 -07:00
teknium1 5b751dc0ad chore: remove unused imports and dead locals (ruff F401/F841 sweep)
Cleans F401 unused imports and F841 dead local assignments across
root *.py, agent/, hermes_cli/, tools/, gateway/, cron/, tui_gateway/
(tests/, plugins/, skills/ excluded).

Intentionally KEPT (false positives / test-patch surfaces):
- agent/transports/__init__.py package re-exports
- cli.py browser_connect re-exports (DEFAULT_BROWSER_CDP_URL area,
  used by tests/cli/test_cli_browser_connect.py)
- hermes_cli/main.py _prompt_auth_credentials_choice /
  _model_flow_bedrock_api_key (accessed via main_mod attr in tests)
- gateway/run.py aliased replay_cleanup + whatsapp_identity re-exports
  and _PORT_BINDING_PLATFORM_VALUES (test-referenced)
- hermes_cli/web_server.py get_running_pid (tests monkeypatch it) and
  _OAUTH_TOKEN_URL availability probe
- hermes_cli/config.py get_process_hermes_home re-export (noqa'd F811
  chain) and yaml availability-probe import
- hermes_cli/nous_subscription.py managed_nous_tools_enabled
  (tests patch hermes_cli.nous_subscription.managed_nous_tools_enabled)
- try/except ImportError availability probes (env_loader, tts_tool,
  mcp_tool, web_server anthropic OAuth block)
- tools/web_tools.py noqa F401 re-exports
- hermes_cli/setup_whatsapp_cloud.py:263 'proceed' skipped: possible
  missing-guard bug, flagged for separate review
- unused function parameters (signature changes out of scope)

Side-effect RHS calls preserved where only the binding was dead
(e.g. web_server proc = _spawn_hermes_action -> bare call).
2026-07-29 11:53:39 -07:00
Alex Fournier 4ad78a98fb fix(observability): derive tool metrics from runtime metadata
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-29 11:49:45 -07:00
teknium1 f67ca220ab refactor(tui): split @method handlers into methods_* modules (mechanical move, registry set-equality verified) 2026-07-29 11:46:31 -07:00
Teknium c3ffe27383 test: align doctor spawn fixture with stricter health_report validation
The #74187 doctor fallback requires a checks list in schema_version=1
payloads; add it to the salvaged Windows console-hiding test fixture.
2026-07-29 11:35:33 -07:00
Teknium 28cc6599df test: pin cua-driver resolver in CLI-fallback sanitization test
The test passed on dev boxes where a cua-driver binary is on PATH but
failed in hermetic CI: _call_tool_via_cli hit resolve_cua_driver_cmd()'s
install-hint early exit before reaching the spawn-env assertion. Pin the
resolver so the test exercises the code path it actually asserts.
2026-07-29 11:35:33 -07:00
Teknium 16720dc45b fix(computer-use): hide the --no-overlay help probe console too
Follow-up to the #62821 salvage: the _cua_driver_supports_no_overlay
--help probe is another Windows-reachable spawn; give it the same
windows_hide_flags() treatment. Also adjust the status test to stub
_resolve_driver_cmd (permissions.py resolves via that helper, not
shutil.which).
2026-07-29 11:35:33 -07:00
Teknium b0a8775d66 chore: map salvage contributors 2026-07-29 11:35:33 -07:00
motoblurr 7bc9956660 fix(computer-use): normalize Windows manifest paths in WSL
A Windows-installed cua-driver can return an absolute
``C:\Users\...\cua-driver.exe`` mcp_invocation.command to a Hermes
process running inside WSL. POSIX spawning can't use the raw Windows
string even though the binary is reachable through DrvFS. Translate
``<drive>:\...`` to ``/mnt/<drive>/...`` in _resolve_mcp_invocation
(before the path-separator check, since backslash is not a separator
on POSIX), only when actually running under WSL.

Salvaged from #63532 by @motoblurr (original commit carried a
placeholder 'Hermes Agent <hermes@local>' identity; re-authored).
Fixes #63938 premise.
2026-07-29 11:35:33 -07:00
ZundamonnoVRChatkaisetu 50e27abdd3 fix(computer-use): hide Windows cua-driver subprocess consoles
Apply windows_hide_flags() (CREATE_NO_WINDOW; 0 on POSIX) at the
Windows-reachable cua-driver subprocess boundaries: manifest probe,
update checker, CLI fallback transport, doctor health-report spawn,
and the permissions/status runner. Prevents OpenConsole/Windows
Terminal windows flashing into the foreground when spawned from
GUI-backed Gateway/Desktop processes.

The env-probe half of the original PR was already implemented on main
and is not re-applied here.

Salvaged from #62821 by @ZundamonnoVRChatkaisetu (original commits
carried a placeholder 'Claude Code Enterprise' identity; re-authored
to the contributor's GitHub identity).
2026-07-29 11:35:33 -07:00
embwl0x 88c19a4edf fix(cli): repair Windows cua-driver autostart registration for paths with spaces
Detect a missing cua-driver-serve scheduled task after the installer runs
and retry registration via Start-Process -FilePath/-ArgumentList instead of
interpolating the binary path into a PowerShell command string (which splits
at the first space in a username-space path).

Salvaged from #60880 by @embwl0x. Related: #60808.
2026-07-29 11:35:33 -07:00
teknium1 2164e548b1 test: retarget nemo-relay telemetry stub at read_raw_config_readonly
Same sibling-mock class as the relay-metrics runtime file — this test
stubs the telemetry gate's config read, which now goes through
read_raw_config_readonly(). Swept the whole test tree for remaining
read_raw_config stubs: all others target consumers that still use the
mutable reader (browser config, url_safety, inventory) and carry no
telemetry keys.
2026-07-29 11:33:41 -07:00
teknium1 44d5a2df5a test: point relay-metrics mocks at read_raw_config_readonly
enabled() now reads via read_raw_config_readonly(); the 7 monkeypatch/
patch sites in test_relay_shared_metrics_runtime.py that stubbed
hermes_cli.config.read_raw_config no longer intercepted the read,
failing 18 tests on CI slice 7/8. Repro'd locally, retargeted the
mocks; 147 passed + 2 skipped across both relay metrics files.
2026-07-29 11:33:41 -07:00
teknium1 c2eda92fd0 perf(config): stop deepcopying config on per-turn read-only paths
Four hot-path consumers paid a full config deepcopy per read:

- telemetry gate relay_shared_metrics.enabled() — runs 2-3x per agent
  turn (2x per API call from lifecycle hooks + 1x per tool call) and
  called read_raw_config(), which deepcopies the whole raw config every
  call. New read_raw_config_readonly() serves the cached dict directly:
  248 us -> 4.6 us per call (54x) on Teknium's real 77-key config.
- interruptible_streaming_api_call local-endpoint stale-timeout branch
  called load_config() once per API call for every local-model user.
- gateway get_inbound_media_max_bytes() + _get_ephemeral_system_ttl_default()
  called load_config() on per-message paths. All three switched to
  load_config_readonly() (345 us -> 12 us; PR #28866 lineage).

Together these account for ~90% of the ~1,900 deepcopy primitives per
turn measured in the 26-call stubbed-LLM profile.

read_raw_config_readonly() keeps the (mtime_ns, size) freshness key so
config edits are picked up next call, and preserves the identity
invariant (cache-miss returns the same object later hits serve) —
regression-tested with 'is', per the PR #28866 identity-bug lesson.
The mutable read_raw_config() is unchanged for save-path callers.

581 targeted tests green (config, relay metrics x2, ephemeral reply,
platform base, new readonly suite).
2026-07-29 11:33:41 -07:00
Gille 6d292fd5eb fix(discord): retry slash sync after failed fingerprint 2026-07-29 11:33:38 -07:00
Brooklyn Nicholson 53e4f87017 fix(desktop): let an opened tool row escape the live run's one-line window
A live tool run renders its rows inside a ticker — a window exactly one
line tall that offsets to the newest row. Expanding a row inside it left
the row's output clipped to that line, and the next call ticked it away
entirely.

Opening a row is a request to read it, so the run gives up the window
until it settles: the ticker unmounts and the rows render at full
height, the same escape an approval already had.
2026-07-29 13:30:06 -05:00
brooklyn! b84389c625 Merge pull request #74277 from NousResearch/bb/remembered-route-per-profile
fix(desktop): scope the remembered route per profile
2026-07-29 13:29:48 -05:00
Alex Fournier 8b0c3da8c0 feat(observability): aggregate bounded tool metrics 2026-07-29 11:25:55 -07:00
Alex Fournier a0476b3605 fix(observability): preserve configured model attribution
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-29 11:24:13 -07:00
Alex Fournier dc4714b1e0 feat(observability): report model and provider usage 2026-07-29 11:24:13 -07:00
teknium1 c5d37bb95c test(tui): pin approval-mode fixtures to env HERMES_HOME for canonical resolver
_load_approval_mode now delegates to tools.approval._get_approval_mode,
which reads config via hermes_cli.config.load_config — that path resolves
HERMES_HOME from the environment, not the server module's _hermes_home
attribute. The four approval-mode tests only monkeypatched the module
attribute, so the resolver silently read the developer/CI real config
(default 'smart') instead of the temp fixture. Behavior assertions are
unchanged (invalid → manual fail-safe, YAML off-bool → 'off', three-way
persist + live emit); the fixtures now also set HERMES_HOME so the
canonical read path sees the same temp config.
2026-07-29 11:19:24 -07:00
teknium1 eff3b11eb2 refactor: complete approval mode/timeout resolution migration to tools/approval.py core (TUI + codex surfaces)
TUI (tui_gateway/server.py _load_approval_mode): now delegates to
tools.approval._get_approval_mode instead of re-reading config raw via
_load_cfg + _deep_merge(DEFAULT_CONFIG, ...) and normalizing locally.
Behavior fix, not pure refactor: the canonical load_config path applies
managed-scope config overlays and ${VAR} env expansion, plus a legacy
max_turns lift, which the TUI's raw YAML read bypassed — under a managed
config that sets approvals.mode, the TUI previously reported/toggled a
different mode than the approval gate actually enforced. Both surfaces
now agree by construction. Name/signature and the mode-vocabulary clamp
are preserved.

Codex (agent/transports/codex_app_server_session.py): read confirmed the
_decide_exec_approval/_decide_apply_patch_approval paths carry NO
Hermes-side mode/timeout reads — the Hermes resolution already flows in
from agent/codex_runtime.py via tools.approval.is_approval_bypass_active()
(auto_approve_* routing) and via the shared approval-gate callback. So no
code extraction was needed; added docstrings pinning that invariant and a
cross-reference on the protocol-semantic choice mapping
(_approval_choice_to_codex_decision), which intentionally stays local.

Adds tests/tools/test_approval_mode_parity.py: cross-surface invariant
test asserting the core resolver, the TUI path, and the codex bypass
derivation agree for synthetic configs (unset defaults, global mode set,
YAML-bool off, malformed values, whitespace/case), plus a delegation-seam
test proving the TUI has no independent config read left.

Note: gateway/run.py has sibling raw reads but is intentionally untouched
(multiple in-flight PRs); flagged as follow-up.
2026-07-29 11:19:24 -07:00
teknium1 703fe94174 fix(gateway): keep no-patterns early return in mention compilation
The compile_mention_patterns promotion moved the 'patterns is None ->
return []' short-circuit into the shared helper, which meant the
telegram/dingtalk wrappers now evaluated self.name (via log_prefix=)
even on the no-patterns path. On main that path returned before
touching any adapter attributes; tests construct bare adapters via
object.__new__ that lack .platform, so TestTelegramGuestMentionGating
failed with AttributeError. Restore the early return in both wrappers
for exact behavior parity with main.
2026-07-29 11:19:16 -07:00
teknium1 1f45ff9e8a refactor(gateway): shared exec-approval/picker formatting cores in base adapter
- base._format_exec_approval(command, description, smart_denied): shared
  header/fence/reason/smart-deny assembly driven by _EA_* template attrs and
  an _ea_escape() hook; base._format_choice_page(options, page, per_page):
  shared pagination core returning (page_options, meta) incl. the
  ' (N-M of T)' page_info suffix; base._truncate_preview: the shared
  truncate-with-ellipsis idiom.
- telegram (HTML attrs + _html.escape hook), feishu (card markdown attrs),
  matrix (head-only; local reaction-legend tail) rewired; telegram's
  provider/model keyboard pagination and slash-confirm preview use the
  shared cores. All user-visible strings byte-identical (parity-tested).
- slack/discord/teams left untouched: their formatting interleaves
  platform-specific budget arithmetic (Slack 3000-char section budget
  subtraction, Discord mention-prefix + dual content/embed budgets, Teams
  adaptive-card blocks) beyond template params.
- tests/gateway/test_interactive_prompt_base.py covers the cores + parity.
2026-07-29 11:19:16 -07:00
teknium1 4fe5410d57 refactor(gateway): shared reaction-ack policy in base adapter
- base.on_processing_complete implements the opt-in remove-ack/add-outcome
  flow driven by _OK_EMOJI/_FAIL_EMOJI class attrs and the
  _add_reaction(chat_id, message_id, emoji)/_remove_reaction(chat_id,
  message_id) primitive shape; default stays a no-op.
- photon drops its override (exact behavioral match).
- slack/discord/feishu/matrix/telegram/google_chat keep overrides: divergent
  primitive signatures (team_id routing, raw message objects, reaction-id
  handles, replace-semantics setMessageReaction) or extra state protocols.
2026-07-29 11:19:16 -07:00
teknium1 58400a6793 refactor(gateway): promote compile_mention_patterns to helpers 2026-07-29 11:19:16 -07:00
Teknium 21d1d08a2f test: convert doctor env-sanitization check from source-inspection to behavioral
The old assertion read _drive_health_report's source text for the
_sanitized_cua_env() call — a banned source-reading test that broke when
the spawn moved into _open_mcp with identical runtime behavior. Now
intercepts subprocess.Popen at the _open_mcp seam and asserts the env it
actually receives strips secrets and applies the telemetry opt-out.
2026-07-29 11:09:08 -07:00
Teknium 57b3c1a86f chore: contributor mappings for cua doctor/key-combo salvage (#62915, #68452, #71590) 2026-07-29 11:09:08 -07:00
monerostar b9215f5bc9 fix(computer_use): surface CLI --version when health_report version lies 2026-07-29 11:09:08 -07:00
camaleonidas c6db7b0f4f fix(computer-use): fallback doctor when cua-driver health_report is unclassified
cua-driver 0.10.0 marks health_report as risk.class=unclassified and denies
the MCP call with isError. Hermes doctor previously treated the bare
{exit_code:1} structuredContent as a real report and printed
"cua-driver ? on ? — ?" with exit 1.

Detect isError / non-schema payloads, raise HealthReportUnavailable, and
compose a schema_version=1 report from working probes (check_permissions,
list_apps, CLI --version/doctor). Prefer real health_report when present.

Tests cover unclassified denial, schema preference, and overall mapping.
2026-07-29 11:09:08 -07:00
Andrew Chen 0411869503 fix(computer-use): block destructive key combos in hyphen notation
`_canon_key_combo` (the `_BLOCKED_KEY_COMBOS` gate in
`handle_computer_use`) split key strings on `+` only, but the cua-driver
backend's `_parse_key_combo` splits on both `+` and `-`. So a model could
issue `{"action":"key","keys":"ctrl-alt-delete"}` (or `alt-f4`,
`cmd-shift-q`): the gate saw a single unknown token and let it through
while the backend executed the real destructive shortcut.

Split the gate on both `+` and `-` so it canonicalizes combos the same
way the backend does. Non-destructive hyphen combos (`cmd-c`) and the
literal `-` zoom key (`cmd+-`) are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 11:09:08 -07:00
Teknium 8eb5e2cd20 chore: map contributor email for umi008 2026-07-29 11:08:58 -07:00
Koho Zheng f2a4ca9637 fix(computer-use): normalize cua-driver result envelopes
cua-driver 0.7.x can return list_windows/list_apps payloads under
structuredContent.windows, data.windows, data._legacy_windows, or
top-level windows/_legacy_windows (direct CLI responses). The wrapper
only read structuredContent.windows, so discovery came back empty
(capture 0x0, list_apps []) while raw cua-driver calls worked.

- add _windows_from_tool_result(): walks the known envelope shapes in
  priority order, skipping empty higher-priority envelopes
- route _load_windows() (MCP + CLI re-fetch paths) through the helper,
  covering capture() and focus_app()
- harden _ingest_windows(): skip non-dict members, normalize untrusted
  app_name/title/z_index fields
- list_apps(): prefer structuredContent.apps, fall through populated
  data/top-level envelopes, derive unique apps from window-shaped
  payloads via _apps_from_windows(), keep the text-line fallback last
- tests for every envelope shape, precedence, malformed records, and
  app derivation

Salvaged from #63037 (Reaper-Legion), which itself preserved the
original implementation from #57961 (kohoj); #73007 (umi008)
independently proposed the same normalization later.

Fixes #57905

Co-authored-by: Reaper <248977840+Reaper-Forge@users.noreply.github.com>
Co-authored-by: Ulises Millan Guerrero <ulises.millanguerrero@gmail.com>
2026-07-29 11:08:58 -07:00