Commit Graph

852 Commits

Author SHA1 Message Date
teknium1
4c4ad0d387 feat(discord): setup checks the bot token with Discord and prints the invite link
Discord was the hardest gateway to set up: the .env template had no Discord
section at all, the wizard saved whatever was pasted, and users had to find
the Message Content Intent toggle, build an OAuth2 invite URL by hand and turn
on Developer Mode just to learn their own user ID.

The wizard now reads GET /applications/@me with the pasted token: a rejected
token is re-prompted instead of saved, an intent that is off gets a direct
link to the toggle and a re-check, the invite link is printed with the
permissions the adapter uses, and the allowlist defaults to the application's
owner (or accepted team members). Offline, the token is still saved with the
manual intent reminder. The wizard moves from the 7k-line adapter into
plugins/platforms/discord/onboarding.py.
2026-10-03 23:40:33 -07:00
kshitijk4poor
483a8b3af2 test(gateway): pin the clarify text-fallback push and trim to two invariants
Add the clarify text-fallback invariant. It drives the real
TelegramAdapter send through text_fallback_coro and fails on base, where
the send carries disable_notification=True. Drop the runner metadata
test: it only pins that the metadata did not change from base. The
_notification_kwargs parametrize already fails on base for
is_approval_prompt, which keeps the stack at two invariant tests.
2026-10-04 11:58:00 +05:30
kshitijk4poor
943d4caa47 fix(gateway): keep approval prompts interim instead of tagging them notify
The salvaged change also put notify=True on both approval sends.
On A2A, `notify` marks the turn-final reply, and A2A has no native
approval renderer. A prompt that needs a human decision would therefore
complete the caller's task with the prompt text while the turn is still
waiting (@ehz0ah's review on #132527). Telegram already pushes the
prompt through the approval-aware _notification_kwargs, so only the
is_approval_prompt marker is needed. The card send goes back to the
verbatim thread metadata: Telegram's control-message path never silences
cards.

Tests: the runner test pins that the fallback metadata stays interim and
does not carry notify. The adapter tests become a single parametrize.

Fold the _notification_kwargs change back into the original one-line
shape, so adapter.py grows by one line instead of six.
2026-10-04 11:58:00 +05:30
liuhao1024
e26d4cb3e3 fix(telegram): push approval prompts instead of silently delivering them
In the default Telegram notification mode ("important") the plain-text
approval fallback marked only is_approval_prompt, which no notification
decision consults, so _notification_kwargs sent the prompt with
disable_notification=True: the phone got no push, the user never learned
a dangerous command was waiting, and the tool sat the full approvals.timeout
(default 300s) before failing closed (#132516).

Mirror the cron-delivery convention on both approval sends in
_approval_notify_sync: notify=True on the button card and the text
fallback, and make _notification_kwargs itself approval-aware
(is_approval_prompt defeats disable_notification) so every surface that
marks the prompt is covered in one place. Loosens no guard and
auto-approves nothing.

(cherry picked from commit 576fec2ab6a6cdca08f96968574ba741c47568c0)
2026-10-04 11:58:00 +05:30
kshitijk4poor
37dd129a76 test(telegram): pin per-chat FIFO when a waiting update is cancelled 2026-10-03 20:50:15 +05:30
kshitijk4poor
c03cca2763 refactor(telegram): parse max_concurrent_updates with coerce_port
Gate BR3 r1 reuse (Low): build_update_processor hand-rolled the int-or-
default parse that gateway.platforms._shared.coerce_port already does.
Neither caught OverflowError, so `max_concurrent_updates: .inf` in YAML
aborted connect(). coerce_port now treats non-finite values as unparseable
(as gateway.config._coerce_num already does), which also hardens its other
port/int callers; the telegram setting warns and falls back to 32.
2026-10-03 20:50:15 +05:30
kshitijk4poor
03395f9de7 refactor(telegram): build PerChatUpdateProcessor on SimpleUpdateProcessor
Gate BR3 r1 quality (Low): PTB's SimpleUpdateProcessor already provides the
no-op initialize/shutdown the class copied, and config.extra is always a
dict, so the `(extra or {})` guard was dead. The wiring test only compared
PTB's concurrent_updates property with itself (true for any processor);
assert the PerChatUpdateProcessor type and DEFAULT_MAX_CONCURRENT_UPDATES
instead - now red when a plain SimpleUpdateProcessor(32) is wired in.
2026-10-03 20:50:15 +05:30
kshitijk4poor
ff90457267 fix(telegram): keep per-chat chain intact when a waiting update is cancelled
PerChatUpdateProcessor awaited the predecessor's tail future directly, so
cancelling a waiting same-chat update also cancelled that future (gate
BR3 r1 2c + quality, Medium). The running predecessor then raised
InvalidStateError from done.set_result() after its handler succeeded, and
the next waiter started while the predecessor still ran (probe order:
start A, start C). Shield the await and, for a cancelled waiter, release
its turn only once its predecessor finishes.
2026-10-03 20:50:15 +05:30
kshitijk4poor
a6d7e08fd1 fix(telegram): keep same-chat updates in order under concurrent dispatch
Plain PTB concurrency (concurrent_updates=32) unblocks other chats while one
chat's handler is slow, but also lets that chat's NEXT update dispatch and
deliver before the first one finishes (download/vision/callback work happens
inline, before text batching can merge them).

PerChatUpdateProcessor (update_admission.py, not the 7k-line adapter) chains
each update behind the previous update of the same chat while keeping the
bounded cross-chat pool. The builder keeps the instance, so the connect-retry
rebuild in _initialize_app_with_retries reuses it. An invalid
max_concurrent_updates now logs a warning instead of silently falling back
(as #74652 did), and the dispatch-stall watchdog comment no longer claims PTB
dispatches sequentially.

Co-authored-by: fortona-space <299084105+fortona-space@users.noreply.github.com>
2026-10-03 20:50:15 +05:30
Yuan Li
7766d4627f test(telegram): regression tests for concurrent update processing
(cherry picked from commit 64cc98fb15e1a4f6643ff4ae4b8aae0b566ee74b)
2026-10-03 20:50:15 +05:30
kshitijk4poor
bb5b4f4ca6 test(telegram): run the ingress admission case for the full stall threshold
Fold of gate finding (2c Medium regression): raising _INGRESS_DISPATCH_STALL_HEARTBEATS to 4
left the `ingress` row of test_only_pre_handoff_failure_reopens_admission looping 3 heartbeats,
which can never reach the threshold, so its "no healthy-but-deaf report" assertion was disarmed.
Loop the threshold itself so the leg tracks future bumps. Verified: with update_admission's
dispatch counting removed, the heartbeat leg now fails (it passed with range(3)).
2026-10-03 18:40:38 +05:30
Froraut
b015e59fb9 fix(matrix): address review — budget edit mentions and move plugin tests
An edit carries m.mentions twice: once inside m.new_content and once at the
top level. When a budget-sized edit names many distinct Matrix IDs, that
fixed part alone could pass the content budget. Shortening the fallback body
then had no room to work with, and the edit still went out at about 54 KB,
which an encrypted room would reject with M_TOO_LARGE.

When the edit is still over budget with an empty fallback, it now drops the
top-level mentions copy. That copy only drives the edit's own notifications,
and m.new_content keeps the full list.

The event-size tests move to tests/plugins/platforms/matrix/test_event_size.py,
mirroring the plugin source tree, and gain a mention-heavy text. The
tests/gateway/test_matrix_message_length.py file is back to main apart from the
env-override value that the new ceiling requires.

(cherry picked from commit 4b06bcb7dbffb732bef6e877cb66dea2fbff8c73)
2026-10-03 16:31:35 +05:30
Benjamin Brumbaugh
d5cda9135c fix(disk-cleanup): fail closed on unresolved track paths 2026-10-02 21:21:36 -05:00
Benjamin Brumbaugh
cc2dd7016e test: harden disk cleanup path safety 2026-10-02 21:21:36 -05:00
Benjamin Brumbaugh
0ba4992c52 fix(disk-cleanup): accept macOS temp aliases 2026-10-02 21:21:36 -05:00
liuhao1024
e0daebeb6b fix(web/firecrawl): pass timeout to scrape API to match asyncio deadline
The Firecrawl scrape() call was missing the `timeout` parameter, so the
server-side used its 30s default.  The client-side asyncio.wait_for
expects 60s, causing SCRAPE_TIMEOUT on slow pages before the Python
deadline expires.

Fix: pass timeout=60000 (ms) to scrape() so both deadlines align.

Fixes #43272
2026-10-02 20:24:48 -05:00
teknium1
80455093bf test: drop the bundled supermemory provider's tests
The provider's own suite and the supermemory cases in the shared memory
suites (BOM-tolerant config reads, PM SDK admission, multiplex identity
scope, provider threads inheriting the profile, plugin-family secret scope,
cron agent_context) imported plugins.memory.supermemory, which is gone.
The other providers' cases in those suites are unchanged; the SDK-admission
test keeps its mem0 case.
2026-10-02 05:10:06 -07:00
teknium1
bbff6cdf68 test(memory): host-block router tests use a fixture provider, not the removed bundled honcho 2026-10-02 04:39:28 -07:00
teknium1
7e53b3ef82 chore(memory): remove bundled honcho provider; install from the plugin catalog 2026-10-02 04:39:28 -07:00
finn763
823385e5cb fix(agent): keep generated images/videos out of swept media caches (Closes #126445)
Generated image/video output now writes under cache/generated/images|videos
instead of the swept inbound caches (cache/images, cache/videos). The hourly
gateway housekeeping deletes 24h-old files in those dirs; for base64-returning
providers the swept file was the only copy, breaking transcript references,
the Artifacts panel and later edits.

Rebased onto current main, where the writers delegate to agent/provider_media,
so the media kind is now the generated/ subpath. cache/generated is added to
tools/credential_files._CACHE_DIRS: without a mount/sync entry the Docker,
SSH and Modal backends never receive the bytes and the provider result loses
agent_visible_image. All callers consume the returned absolute paths, so no
call-site changes were needed; inbound download paths (get_image_cache_dir /
get_video_cache_dir) and the sweeper are untouched.

RED: tests/agent/test_generated_media_durable.py (9 failed on main) — 25h-backdated
generated files deleted by the real cleanup_image_cache / cleanup_video_cache, and
no mount mapping for cache/generated. tests/tools/test_image_generation_artifacts.py
::test_real_generated_image_is_agent_visible_under_ssh_env covers the sandbox
regression with the real writer. Green: 533 passed, 19 skipped.

Co-authored-by: Froraut <79215537+Froraut@users.noreply.github.com>
(cherry picked from commit fd23181f22232f66aec7076d6bf0868ed45661f2)
2026-10-02 01:18:03 +05:30
kshitijk4poor
647ab0daad test(disk-cleanup): cover kanban exclusion on a call-created file; tidy imports
The kanban test's hook leg used to rewrite the pre-existing attachment, which
the created-paths snapshot already ignores, so the kanban exclusion was never
reached on that path. It now creates a NEW kanban file via the call and
asserts it survives session end. Without the kanban entry in
_NEVER_TRACK_TOP_LEVEL that file gets tracked and deleted.

Also moves the function-local `import os` to module level and replaces
iter(range(1, 10**6)) with itertools.count(1) for call IDs.
2026-10-02 00:55:59 +05:30
kshitijk4poor
91b68e3acc test(disk-cleanup): trim salvage tests to two invariants
Keep the headline invariant (a call never tracks paths it did not
create) and the cross-session snapshot isolation case; the rewritten-args
case exercises the same fail-closed branch as the headline test.
2026-10-02 00:55:59 +05:30
Froraut
c467053f52 fix(disk-cleanup): address review — key snapshots per session, fail closed on unsnapshotted paths
The pre/post snapshot handshake had two ways to mark a user's existing
file as created, which queues it for deletion when the turn ends:

- Snapshots were keyed by tool_call_id alone. llama.cpp sends one
  constant id for every call. When session B's pre hook ran between
  session A's pre and post, A consumed B's snapshot. A's
  already-existing test_user.py then looked "not in existing", and A
  tracked it.
- Every pre_tool_call hook sees the original args. Another plugin's
  `modify` directive is merged afterwards, and post_tool_call sees the
  final args. A path swapped in by that hook was never snapshotted, so
  "not in existing" held for it too. The file was tracked even when the
  write was refused.

Snapshots are now keyed by (task_id or session_id, tool_call_id), the
identity both hooks receive from the dispatcher. They store the paths the
call named that did not exist yet, instead of the ones that did. The post
hook tracks a path only if its own snapshot saw it absent and it exists
now. Any path the snapshot did not cover fails closed.

I reproduced both end to end through model_tools.handle_function_call
with a second plugin's modify hook and two interleaved sessions. The
user's file was deleted before this fix and survives after it, and a
file the call really created is still cleaned up. The README hook
contract is updated to match.

(cherry picked from commit 8d0b853981f1ab0775549f4831a3d4f733a46106)
2026-10-02 00:55:59 +05:30
Froraut
17d55a2032 fix(disk-cleanup): track only files a tool call created, never pre-existing ones
post_tool_call sent write_file/patch paths, every absolute path in a
terminal command AND every path in its output to dg.track(), gated only
on `p.exists()` plus the test_*/tmp_* name rule. Category 'test' is
deleted at age 0 by quick(), which on_session_end runs after every turn,
and directories are rmtree'd. So a user's cron script
$HERMES_HOME/scripts/test_uptime.py the agent merely patched, or a hook
dir $HERMES_HOME/hooks/test_notify it listed (`ls -la`, `find ... -name
'test_*'`), was irreversibly deleted when the turn ended — then the
emptied scripts/ and hooks/ dirs too. The plugin's contract is files
"created during Hermes sessions".

A pre_tool_call hook now snapshots which of the call's argument paths
already exist (keyed by tool_call_id; it never raises, since pre hooks
fail closed; entries expire after an hour if the post hook never
fires). post_tool_call tracks a path only when the call named it, it
did not exist before and exists now. Paths seen only in terminal output
carry no such proof and are no longer tracked; no snapshot means
nothing is tracked. Existing tests now drive the real hook order.

(cherry picked from commit e0e0e1f7450df0c75a8499fe57b63dc46c6d2af2)
2026-10-02 00:55:59 +05:30
kshitijk4poor
66ed823b5c fix(a2a): keep no-token localhost peers trusted when an allow-list is set
The fail-closed change dropped the localhost_only() short-circuit from
is_trusted_peer. In no-token mode authenticate() always yields ip:<addr>,
so any install with A2A_TRUSTED_PEERS / a2a.trusted_peers configured
started 403-ing every local dispatch. Restore the short-circuit and pin it
in test_localhost_trusts_all.

Also:
- log the non-loopback/no-allow-list misconfiguration once in
  A2AAdapter.connect() instead of an ERROR on every refused request;
- is_loopback_bind reuses agent.proxy_bypass.is_loopback_host instead of a
  hand-rolled loopback check;
- fold the loopback backward-compat test into
  test_no_allowlist_trusts_authenticated (now explicit A2A_HOST=127.0.0.1)
  and drop implementation asserts on is_loopback_bind().
2026-10-02 00:45:30 +05:30
kshitijk4poor
4da1943d69 test(a2a): trim fail-closed tests to the two invariants
Keep non-loopback+empty allowlist -> closed (the #126756 headline) and
loopback+token+empty allowlist -> open (backward compat). Allowlist
membership and allow-all are already covered by existing TestTrustedPeers
cases.
2026-10-02 00:45:30 +05:30
JerryLiu369
a0cad39f5d fix(a2a): fail closed on non-loopback bind with empty trusted-peers allowlist (#126756)
(cherry picked from commit 26a8dca575cd86a693e6f3be57caa449f9e8a032)
2026-10-02 00:45:30 +05:30
kshitijk4poor
de479f6469 fix(image_gen): keep legacy non-dict Krea refs passing through and cap the bytes actually read
The inlining step called ref.get() on every reference, so a legacy
image_style_references entry that is neither a string nor a dict raised
AttributeError out of generate() (main forwarded it to Krea). It now passes
through untouched.

The 3 MB budget was checked against stat() and the file read separately,
so a file that grew in between was sent in full. Each file is now read once,
at most one byte past the remaining budget, and the cap applies to those
bytes. The refusal test states each expected error_type and message
directly.
2026-10-01 16:22:28 +05:30
kshitijk4poor
68655270c7 fix(image_gen): run the credential guard before probing a local Krea style reference
The salvaged inlining checked is_file() and summed st_size before
raise_if_read_blocked(), so a denied path (e.g. HERMES_HOME/.env) answered
"not found" or "over 3 MB" and revealed whether it exists and how big it
is. The guard now runs first, and its denial comes back as the plugin's
normal error dict instead of an exception escaping generate().

Local files are inlined after the managed-gateway LoRA/moodboard fail-fast,
so a request about to be refused no longer reads and base64s up to 3 MB.
The size message is built from the constant. The four salvaged tests
become one embed test and one parametrized refusal test (missing file,
total over the cap, guard-denied path).
2026-10-01 16:22:28 +05:30
Mariano Nicolini
bf1bc78e63 feat(image_gen): expose the Krea 2 sliders to the agent only when the active backend declares them
(cherry picked from commit cbbd7f2b7b961fc822b47f4783b29fbc581f06b6)
2026-10-01 16:22:28 +05:30
Mariano Nicolini
87f3fa8881 fix(image_gen): cap local Krea style references by total size so multi-file requests fit the gateway limit
(cherry picked from commit 3e4e2bc4dbcd3143099455a9b604fb36af35f34a)
2026-10-01 16:22:28 +05:30
Mariano Nicolini
f5928fe003 fix(image_gen): embed local Krea style references so files from disk work outside a sandbox
(cherry picked from commit de6fe8e09002386786f6e64755d948a33c17bd89)
2026-10-01 16:22:28 +05:30
kshitijk4poor
a042567296 test: tighten the audit's new assertions after review
- relay-deliver-budget: the ceiling/timeout composition is checked
  against the imported constants (which the mirror tests already tie to
  the Python defaults) instead of re-reading config_defaults.py.
- terminal env sync: the key recorder also traps get()/[] and is an
  instance, the probe fails loudly if the bridge looks up no keys (so the
  gateway-only-key check cannot go vacuous), one _cli_env_map() helper
  serves both tests, and the docstring says what is actually driven.
- live_system_guard bypass reuses _live_system_guard_is_active().
- retaindb no-client prefetch asserts no thread is spawned through the
  module's spawn_context_thread seam, not a private list.

Each changed assertion re-proven red by its production mutation.
2026-09-29 15:58:55 +05:30
kshitijk4poor
b65972c1a5 test: give four vacuous tests an assertion that can fail
Each test passed with the behaviour it names removed (proven by mutating
the production owner on the old and new test):

- s6 hooks test_register_noop_on_host: the hook swallows the
  AssertionError into a printed warning, so reaching the manager never
  failed the test. Assert nothing was printed, as the sibling test does.
- live_system_guard bypass: signal 0 to our own PID passes the guard too.
  Assert os.kill is the unpatched builtin.
- retaindb queue_prefetch without a client: a missing guard fails only in
  background threads. Assert no prefetch thread was started.
- test_config golden migration parity: drop the `_config_version == 33`
  exact-match branch, which cannot run again now the version is 49; the
  subset check it fell through to is unchanged.

test_terminal_config_env_sync now drives gateway.run's
_bridge_terminal_config_to_env with every known terminal key instead of
parsing gateway/run.py's source for a dict literal. It asserts each key
reaches the same TERMINAL_* var as the CLI, which the key-set comparison
could not check, and removes the last inspect.getsource in tests/.
2026-09-29 15:58:55 +05:30
unsupportedpastels
99b24916ce fix(photon): don't read a signal-raced sidecar exit as a fatal crash
Same race as the WhatsApp bridge (#127047): a container/supervisor stop
signals the whole process tree, so the Photon sidecar (its own session)
can die before the gateway's stop flow reaches disconnect(), while
_inbound_running is still True. _supervise_sidecar() then reported
SIDECAR_CRASHED and queued a reconnect during shutdown.

Consult the runner's _stop_requested_by_signal, which the signal handler
sets before any stop work runs. A sidecar exit while the gateway keeps
running is unchanged: still a retryable fatal error.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-09-28 21:06:08 -04:00
liuhao1024
11598aebe3 fix(kanban): dashboard Specify/Decompose resolve the profile's aux-LLM key (#123372, salvage #123384)
WHAT: regression test mounting the kanban plugin router exactly as
`_mount_plugin_api_routes` does (behind `_plugin_route_secret_scope`) and driving
Specify under `set_multiplex_active(True)` for launch profile A -> `?profile=workerb`
-> A, asserting the aux call's `get_secret` sees each profile's own key.

WHY: `_run_aux` / `_run_estimate` only pinned the board, never a profile scope, so the
aux client's provider-key read failed closed under multi-profile hosting. The scope is
now bound once at the plugin mount (previous commit) rather than per aux call inside the
plugin: one mechanism for every plugin router, and the `?profile=` request convention
works for the kanban routes too.

Fixes #123372

(cherry picked from commit 6f01ddc9e455b3eee7046d5b1ac969df3499cbeb)
2026-09-28 05:37:55 -07:00
teknium1
75b6d946cd fix(a2a): scope the client-tools A2A_PORT gate and prove A→B→A under multiplex (#122126, salvage #122128)
Follow-up trim to @JE4NVRG's salvaged commit (is_connected → env_is_connected("A2A_PORT")):

- plugins/platforms/a2a/tools.py::_a2a_tools_available read os.getenv("A2A_PORT") too — the
  same unscoped gate in the same plugin, so every secondary profile also paid for the a2a
  toolset whenever the launch profile set A2A_PORT. Now get_scoped_secret, same policy as
  the adapter's port read (scoped miss fails closed; default/T1 reads its own environ).
- Tests trimmed to two invariants: A→B→A over two temp profile homes under
  set_multiplex_active(True) with the launch value in os.environ (profile B with no
  A2A_PORT of its own is NOT connected and gets no tools; red on origin/main at index 1),
  and the standalone T1 case where os.environ IS the profile.

Row L1 (boot-time platform enablement probe), topologies T2/T3 (multiplexed host serving
secondary profiles). No other plugins/platforms/*/__init__.py is_connected/check_requirements
does an unscoped env read (buzz #125985 is handled separately).

Co-authored-by: JE4NVRG <jean.v1803@gmail.com>
2026-09-28 05:24:26 -07:00
JE4NVRG
68c205b5b9 fix(a2a): resolve the inbound enablement gate through the profile-scoped reader
is_connected() gated on a raw os.getenv("A2A_PORT"), which under multiplexing
holds the launch (default) profile's value. Every secondary profile therefore
instantiated the inbound A2A server, all of them fell back to the module
default port 9900, and all but one died with "could not bind ... Address
already in use" (fatal / bind_failed in gateway_state.json) on every boot.

Resolve the gate through gateway.platforms._shared.env_is_connected, the same
scope-aware idiom the sibling adapters use -- the adapter's own port read
already goes through _get_scoped_secret for exactly this reason.

Tests: tests/plugins/test_a2a_plugin.py pins the behavior. A profile with no
A2A_PORT in its own scope is not connected and never reads os.environ (a raw
os.getenv fails the test), a scoped port connects, and extra.enabled still
short-circuits.

Fixes #122126
2026-09-28 05:24:26 -07:00
teknium1
b63c138d78 fix: Hermes never falls back to the user's node/npm/npx/uv
Maintainer ruling: only Hermes and only its packaged package managers
(uv/pip/node/npm) are ever used; no PATH fallback when the managed tool is
missing, no "prefer the user's if new enough".

- hermes_constants.find_node_executable: node/npm/npx resolve to PM's
  installed copy or None. Every caller already pm.ensure()s on None, so a
  missing runtime is now provisioned instead of silently borrowing the
  user's Node (native-addon ABI / npm cache mismatches).
- agent/lsp/install._install_npm: pm.ensure('npm') when PM npm is absent,
  instead of failing over to whatever npm is on PATH.
- gateway._append_node_dir_for_service: stop baking the invoker's PATH node
  dir into generated systemd/launchd units.
- main_install_repair._resolve_node_runtime_npm: drop the PATH re-scan for
  another npm.
- source_build.source_product_current: run the freshness reader only with
  PM's node.
- doctor: Node/npm rows and npm audit use PM's copies (Termux APT distro
  keeps its system Node).
- install.sh ensure_uv / install.ps1 Get-Uv: always stage the pinned uv
  artifact; delete the "uv on PATH if new enough" developer shortcut.
2026-09-27 22:04:26 -07:00
Kevin Rajan
0ebaf162cc fix(kanban): run clock ticks from the current run, not the task's first start
RunClock ticked from tasks.started_at (the task's first-ever start), so after
a review timeout + retry a healthy current run showed cumulative card age
(e.g. working · 2h for a minutes-old run).

_task_dict() now emits current_run_started_at from the run row
tasks.current_run_id points at (batched single query, same as latest
summaries), and RunClock prefers it, falling back to started_at for older
backends. Fixes #99819.
2026-09-26 16:51:19 -05:00
kshitijk4poor
064f2f6b09 fix(source-check): strip inherited pathspec modes from read-only git probes
GIT_LITERAL_PATHSPECS=1 turns disk-cleanup's ':(literal)' pathspec into filename text, so ls-files misses tracked files and the plugin deletes them.
2026-09-27 01:36:34 +05:30
kshitijk4poor
6e8a0a0f99 test(disk-cleanup): fold the untracked-scratch control into the checkout test
The control test only added one thing over the first new test: a quick()
deletion check for untracked scratch in a git-init'ed HERMES_HOME. It also
passed on base, so it pinned no new behaviour. Save both entries in the
first test's quick() call and assert the tracked file survives while the
scratch beside it is deleted, then drop the control. The stack now adds
one test, and that test is still red on base.

Update test_scratch_outside_git_trees_still_cleaned's docstring. It said
only a .git below HERMES_HOME marks a file git-owned, which stopped being
true once an enclosing repo that tracks the file counts too.
2026-09-27 01:36:34 +05:30
kshitijk4poor
b22c36d3bf test(disk-cleanup): fold late-commit and enclosing-repo cases into one test
Keep the stack at two invariant tests: the tracked-file test now also
covers a file committed after first classification in the same process
and a HERMES_HOME nested in an enclosing repo; the separate stale-cache
test is redundant now that there is no cache.

Co-authored-by: David Crandall <david@convergentdesign.dev>
2026-09-27 01:36:34 +05:30
David Crandall
9d6609a14b fix(disk-cleanup): the ls-files cache must not outlive the git index
`_git_tracked_index()` cached one `git ls-files` set per process, so a
long-lived process could keep classifying a file as untracked after git had
taken ownership of it. `guess_category()` runs on every post-tool-call, so the
gateway can cache the index while `test_scratch.py` is still untracked; an
auto-snapshot then stages and commits it, `quick()` re-validates the stored
"test" entry through `guess_category()`, the stale cache still reports the file
as untracked, and `_delete_item()` unlinks a file git now owns (`git status`
shows `D test_scratch.py`).

Key the cached set on the git index's stat signature as well as the home, so a
staged, committed or unstaged change is a new cache key and no invalidation
hook is needed anywhere. The index path comes from `git rev-parse --git-path
index`, cached per home — so the per-call cost stays a single `os.stat`, which
also covers a linked worktree (where `.git` is a pointer file) and an explicit
`GIT_INDEX_FILE`. Fail-open behaviour is unchanged: no index and no git mean the
empty set, and the guard never raises.

Regression test: classify an untracked `test_scratch.py`, then `git add` and
`git commit` it in the same process with no `cache_clear()`, and assert
`quick()` leaves it on disk while an untracked control beside it is still
cleaned.

(cherry picked from commit a3271750608ca4e02e5205311a09b906e16d33d5)
2026-09-27 01:36:34 +05:30
David Crandall
0e799da223 fix(disk-cleanup): a git-tracked file is never disposable, even when HERMES_HOME is the checkout
The `_inside_git_worktree()` guard sliced its parent chain at HERMES_HOME, so
`.git` entries strictly below the home were the only ones consulted. That misses
the case where HERMES_HOME IS a git checkout: on this install `~/.hermes` is the
userfiles repo, so `~/.hermes/scripts/` and any top-level `tmp_*` file sit inside
a worktree yet resolve to no `.git` below the home.

Observed live: the bundled disk-cleanup plugin classified two COMMITTED
regression tests in `~/.hermes/scripts/` as disposable session scratch and
unlinked them — `scripts/test_analyze_upstream_opportunities.py` and
`scripts/test_customization_protocol_v2.py`; the deletion was then committed by
the next auto-snapshot (ce8dc99), so the suite that would have caught a scanner
regression was silently gone. 13 tracked `test_*` files under `scripts/` plus 17
tracked top-level `tmp_*` files were in the same blast radius.

Fix: when HERMES_HOME itself carries `.git`, ask git whether it TRACKS the exact
path (`git ls-files`, one cached subprocess per process keyed on the home) rather
than treating the whole home as protected — a scratch file merely living beside
tracked ones must still be cleanable, or cleanup would be disabled entirely.

Tests: two regression tests — a git-tracked `test_*` file is never classified
disposable and a stale pre-fix tracked.json entry is dropped by quick()'s
re-validation instead of deleted; plus the control that an UNTRACKED `test_*`
file in the same repo is still cleaned. Existing suite 30 -> 32 passed.

(cherry picked from commit c5555940108286c4c1070e84d6d694ad9a35bb76)
(cherry picked from commit c7c39e60d4dc467bf9681c3dcb5781ac96557724)
2026-09-27 01:36:34 +05:30
kshitijk4poor
d24aadfdd1 refactor(copilot): one GitHub effort clamp for the profile and main agent
Follow-up to the #103857 salvage. The Copilot provider profile and the
main-agent GitHub reasoning path each carried the same clamp-then-medium
fallback. Both now call hermes_cli.models.clamp_github_reasoning_effort.

The offline Astra tests move next to the other Copilot effort tests, along
with a check that a structured catalog entry still wins. The main-agent
clamp now has a test pinning max/ultra -> high on the GPT-5 ladder. The
transport test no longer writes config.yaml. The two effort comments now
say the same thing.
2026-09-26 22:21:47 +05:30
brooklyn!
bfb1d952c0 fix(kanban): start the events stream at the board tail
Desktop opened /events with no since, and a missing cursor was read as
0, so every open replayed task_events history. Seed the socket from the
snapshot or this connection's last frame, and start a cursorless stream
at MAX(id). An explicit since still replays from there.

Fixes #81537
2026-09-25 11:26:40 -05:00
kshitijk4poor
e326520d50 refactor(codex): one credential/route authority for aux + image paths
Gate round-1 follow-ups on the #121486 fix:
- auxiliary_client: inline the pool route lookup (no dead try/except or
  fallbacks; HERMES_CODEX_BASE_URL short-circuits once) and read auth.json
  directly when the pool yields no token (no second uncached pool load,
  no re-select race pairing a new pool key with chatgpt.com).
- image plugin: _read_codex_credential() is the single source for both
  is_available() and generate(); _post_image_request requires base_url.
- auth_codex: drop the unused _pool_codex_access_token wrapper; the route
  helper's error fallback reads the profile-scoped override, not the raw
  process env.
- model setup flow: the confirm guards get the resolved Codex base, not
  the chatgpt.com constant.
- cli_model_switch_mixin: self.base_url is always set.
2026-09-25 21:27:06 +05:30
kshitijk4poor
e87f673faa fix(codex): send catalog/image credentials only to their own route
Follow-up to the two contributor commits for #121486. The picker, the
image plugin and the auxiliary Codex client still composed a pooled
gateway key with a base re-read from ambient state (HERMES_CODEX_BASE_URL
or the chatgpt.com default), so a model.base_url-only gateway (env unset)
still sent its key to chatgpt.com.

- auth_codex: resolve_codex_runtime_credentials reports the host a pooled
  credential actually routes to (runtime_provider._pool_entry_mode_and_url:
  env > model.base_url while the row is canonical > row URL) instead of the
  ambient default; get_codex_auth_status carries the same bound base_url.
- picker: get_codex_model_ids(access_token, base_url=) now receives the base
  resolved with the token from hermes_cli/models.py, the CLI default-model
  swap (self.base_url) and the `hermes model` Codex flow.
- aux/image: _resolve_codex_credential_and_base() returns (token, base) from
  one pool selection; the image plugin, _build_codex_client and the raw
  Codex client use it (profile-scoped override from #121497 still wins).
- model_metadata: the non-JWT refusal now applies only when the target is
  chatgpt.com; a gateway key may probe its own gateway's /models.

Adversarial regressions: model.base_url with env unset, env/route mismatch,
opaque + JWT gateway keys, pool-selected credential, pool row with its own
gateway URL, direct-ChatGPT positive control.

Addresses @andrexibiza's review on #121508.
2026-09-25 21:27:06 +05:30
liuhao1024
602aa9a55b fix(agent): keep custom-Codex-base credentials off chatgpt.com
Behind a custom Codex base URL (HERMES_CODEX_BASE_URL / model.base_url
gateway) three paths still hit the hard-coded chatgpt.com host with the
gateway's credential-pool key (#121486):

- the OAuth context-length probe (agent/model_metadata.py) and the
  /model picker's live discovery (hermes_cli/codex_models.py) both GET
  https://chatgpt.com/backend-api/codex/models with
  Authorization: Bearer <gateway key> whenever model.context_length is
  not pinned — the key is sent to a service it does not belong to and
  cannot answer for;
- the openai-codex image_gen plugin posts to the same hard-coded base.

Fix, mirroring the quota probe's existing gate in auth_codex:

- both catalog sites now decline to probe non-JWT credentials (real
  Codex access tokens are JWTs; a gateway key is not one) and fall
  back to the static table / offline sources — same outcome as the
  doomed request today, minus the credential leak;
- a JWT reached through a custom base now probes that base's own
  /models instead of chatgpt.com (catalog URLs are built from the
  resolved base; the per-token cache key includes the base);
- the image plugin resolves its base from HERMES_CODEX_BASE_URL the
  same way the text client does.

Fast-mode host gating in the /fast picker is intentionally left
untouched: lifting it needs an explicit opt-in design decision, not a
bug fix.

(cherry picked from commit 5d76ec525674d7b103ab53955ba5279605457ca1)
[salvage: plugins/image_gen/openai-codex/__init__.py hunk dropped in favour of #121497 (first submitter, profile-scoped override + base-aware Cloudflare headers)]
2026-09-25 21:27:06 +05:30