Commit Graph

50172 Commits

Author SHA1 Message Date
IAvecilla
e574b02570 fix(gateway): a Chronos fire that lands during startup waits for the gateway's adapters instead of running with none
(cherry picked from commit cd1314e0ed830a8b5af689297dde4a434276168e)
2026-10-07 13:43:19 +05:30
IAvecilla
6cb3bab6b3 fix(cron): relay DM deliveries carry the origin user_id so a cold routing cache still egresses
(cherry picked from commit fb4351adf092586a354206cdbcadc33866e11d01)
2026-10-07 13:43:19 +05:30
kshitijk4poor
5ab15d1aaf test(providers): api_key profiles must declare their registry key var (default lane)
#134320 shipped lmstudio / tencent-tokenhub profiles with empty env_vars. The
provider-catalog e2e harness reads key vars from the profile only, so main's
`Python tests / e2e` went red (56 KeyError: '' errors + 2 switch failures);
#134385 restored the env vars. The PR's own e2e lane had been skipped by the
change classifier, which is how it merged green.

This invariant runs on the default test lane, which is never skipped: every
api_key profile backed by an api_key PROVIDER_REGISTRY row declares that row's
key variable first in env_vars. It fails on the pre-#134385 tree naming both
profiles and passes on main.
2026-10-07 13:40:31 +05:30
Teknium
3824188376 fix(desktop): a branched chat no longer gets stuck on a loading spinner
Branching the open chat calls navigate(child) and then resumeSession(child)
in the same tick. resumeSession captures the route token at entry, but
the router only publishes the new pathname on the next render, so the
token it captured still named the PARENT. When that render landed while
resume was awaiting resolveStoredSession/gateway, isCurrentResume() saw
the token change and returned, abandoning the resume before it bound the
warm branch runtime. The pane was left with the child selected, no
active runtime, an empty transcript and a loader for a row with history;
use-route-resume saw nothing to heal (selection already matched the
route, creatingSessionRef had suppressed the pathname edge), so the
child stayed on the spinner with no composer until the user switched
away and back.

isCurrentResume now treats the route arriving at the session being
resumed as still current (resumeRouteStillCurrent in
session-context-drift.ts); a route to any other chat still supersedes
the resume exactly as before.

De-flakes Desktop core E2E lineage-sidebar ("a branch child is titled
and is its own row"), which hit this ~2 in 10 runs.
2026-10-07 01:07:46 -07:00
Teknium
4ec96b82c2 refactor(desktop): move resume's pending-approval restore into its own sibling
restorePendingApproval joins restore-pending-clarify and
restore-pending-connection as a topical sibling of use-session-actions,
so the resume module sheds lines before the next fix touches it
(code-health file cap). No behaviour change.
2026-10-07 01:07:46 -07:00
kshitijk4poor
de116d8832 test(cron): pin that a booted worker drops the marker before any job child
Review (greptile): after the boot moved before the ack, the tests
checked that a relaunch keeps the marker but none checked that a boot
without a relaunch consumes it. Job children would otherwise inherit it
and repeat the dependency boot.
2026-10-07 13:33:18 +05:30
kshitijk4poor
73ccdeee5e docs(cron): name hermes_bootstrap as the pre-ack relaunch in the worker docstrings
After the previous commit the worker's pre-ack recovery is the import of
hermes_bootstrap, not run_agent's copy; the module docstring and the
relaunch test still credited run_agent. Also restore the scheduler
comment's original wrap (whitespace-only churn from the first pick).
(Lead fold: quality + thermo + reuse lenses, Low.)
2026-10-07 13:33:18 +05:30
kshitijk4poor
27cc633d24 refactor(cron): finish_worker_boot relies on hermes_bootstrap's own interrupted-pull repair
Importing hermes_bootstrap already runs _settle_interrupted_update
(restore_interrupted_pull + relaunch_after_restore) at module import, so the
explicit second call in finish_worker_boot was a duplicate. The worker never
imports hermes_bootstrap before __main__ (probed: not in sys.modules after
`import cron.scheduler`, marked or not), so the import in finish_worker_boot
is what runs the repair, before the payload is read and the ack published.
The interrupted_pull case of test_relaunch_replays_the_worker_before_its_ack
stays green.
2026-10-07 13:33:18 +05:30
mzyas
d038b19927 fix(cron): run the interrupted-pull recovery before the external worker's ack
run_agent's import also re-execs the process when it restores a tree a killed
`hermes update` half-wrote (_early_recovery.relaunch_after_restore). The
external worker never loads hermes_cli.main, so that check first ran inside
run_one_job, after the ack: the re-exec found its payload deleted, refused the
execution, and the adopted run was left `unknown`, the same class as the
source-update relaunch. finish_worker_boot() now runs the recovery before the
payload is read, so its re-exec replays the whole worker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b28779fd3bbaa1b92272448d17ccbb47f2794e0f)
2026-10-07 13:33:18 +05:30
mzyas
2d3dc08ba7 fix(cron): a source-update relaunch replays the external worker before its ack
The external worker first imported hermes_bootstrap through run_agent inside
run_one_job, after it had read (and deleted) its payload and published its
ownership ack. When the dependency inputs were not current, prepare_launch
synced and relaunched the worker into an -I interpreter that ignores the
pinned PYTHONPATH, and worker_bootstrap had already popped the boot marker:
the new process died on `import ruamel` and the adopted run was left
`unknown` (#122290 follow-up).

The worker's __main__ now runs hermes_bootstrap (finish_worker_boot) before
it touches the payload, and the marker is dropped only after that, so a
relaunch replays the whole worker and boots its dependencies again. The
call cannot live in worker_bootstrap(): during `-m cron.scheduler`'s package
import sys.argv[0] is still "-m" and __main__ has no spec, so the relaunch
command could not name the module. Covers the POSIX execv and Windows
subprocess.call relaunch branches alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b537b59787acf3b3f5828c0fb6ce82482de40820)
2026-10-07 13:33:18 +05:30
kshitijk4poor
a1c7d85738 chore: map mzyas for salvage of #133720 2026-10-07 13:33:18 +05:30
Teknium
bc246e1f8a fix(providers): LM Studio and TokenHub profiles declare their credential env vars
#134320 registered ProviderProfiles for lmstudio and tencent-tokenhub without env_vars. That put
both into the provider-catalog E2E matrix for the first time with key_env=None. The shard fixture
then died on keys[""] (KeyError, every row of the shard errored), TokenHub turns could not find a
key, and LM Studio's /model switch could not reach the native /api/v1/models listing the fake
never served.

The profiles now name the same env vars their hermes_cli/auth.py registry rows use (that row stays
the lookup source of truth). The catalog fake also answers LM Studio's native listing.
2026-10-07 13:21:46 +05:30
kshitijk4poor
4887aba342 fix(aux): stop republishing the legacy mirrors from a key rotation
The rotation tried to decide whether the current runtime was the one
the legacy mirrors were published from, first by key equality and then
by route-tuple equality. Neither identifies the publisher: a scoped
copy with an identical route still matched, and another session could
publish between the comparison and the write (both reproduced by the
second-model verifier). Nothing needs the republish: no production
code reads the legacy globals directly, and _compat_runtime_main()
ignores them while they equal the compat snapshot, which an unchanged
mirror always does. Rotate the published runtime dict in place only,
as before this PR's mirror handling, and pin that an unchanged mirror
never becomes a runtime input.
2026-10-07 13:20:42 +05:30
kshitijk4poor
dd31854898 fix(aux): republish the legacy mirrors only from the runtime they were published from
The scoped-runtime guard compared the mirror key with the old token. A scoped
runtime that shares the still-published old key (same account, other model or
endpoint) passed that check and overwrote the mirrors with its own route, which
outlived the scope. The guard now requires the runtime's fields to equal the
published compat snapshot, so only the publishing runtime republishes.
2026-10-07 13:20:42 +05:30
kshitijk4poor
5b3c8b3240 refactor(compression): import the codex cooldown lookup at module level
The split-out conversation_compression_codex imports only logging/typing, so there is no cycle to
dodge: move the function-local import into the top-level block beside
conversation_compression_telemetry and fix the sibling's docstring to match. Drop the
client_lifecycle comment that repeated rotate_runtime_main_api_key's own docstring.
_refresh_main_credential keeps its api_mode pre-check and _swallow wrapper: the callee returns
False off anthropic_messages but does not swallow exceptions from its holder moves / aux rotation.

Fold finding C15 #4 (gate quality review).
2026-10-07 13:20:42 +05:30
kshitijk4poor
e91abaf863 fix(compression): announce compaction before the pre-compression credential refresh
compress_context ran _refresh_main_credential before _announce_compression_start, so an expired
Claude Code token's blocking network refresh left the Desktop working row a bare spinner -- the
exact delay #111294 moved the announce first to avoid. Refresh right after the announce instead;
nothing between the two touches the client, and it still precedes the feasibility probe and the
summary call. The kept compression test now also asserts the announce saw the pre-refresh state.

Fold finding C15 #3 (gate efficiency review).
2026-10-07 13:20:42 +05:30
kshitijk4poor
4c8d4148ca test(aux): pin that a scoped runtime's key rotation stays out of the legacy mirrors
e9a77f4188's `_RUNTIME_MAIN_API_KEY == old` guard had no test: reverting it left every kept test
green, though rotating under scoped_runtime_main then raises KeyError 'base_url' (or, with the
.get() default, overwrites the turn's mirrors with the scoped copy). Extend the existing aux-route
test with the scoped rotate step; red with the guard removed (mirror model becomes 'm') and red on
the pre-guard code (KeyError), green on HEAD.

Fold finding C15 #2 (gate 2ab toothless guard).
2026-10-07 13:20:42 +05:30
kshitijk4poor
b6a19d6262 fix(anthropic): keep the fallback-restore snapshot's OAuth flag paired with its refreshed key
The credential refresh moved _primary_runtime["anthropic_api_key"] to the new token but left
_primary_runtime["is_anthropic_oauth"] at the old token's value, while self._is_anthropic_oauth is
recomputed from the new token. _build_anthropic_client_from_runtime restores the key and flag
together after a fallback, so a refresh that changed token type (Console key -> OAuth) would
restore the new key with the stale flag. Update the flag under the same identity guard.

Fold finding C15 #1 (gate quality review).
2026-10-07 13:20:42 +05:30
kshitijk4poor
03101500d0 refactor(compression): move _codex_compaction_cooldown_remaining into conversation_compression_codex to keep conversation_compression.py under its size cap
Code-health ratchet: the pre-compression credential refresh grew agent/conversation_compression.py
to 4591 lines, over its 4579 cap on main. Pure move (body byte-identical), late-imported at its one
call site; no test or doc references the old path.
2026-10-07 13:20:42 +05:30
kshitijk4poor
3ad035cb2e test(anthropic): compression before any request sends the refreshed token
Manual /compress and turn-start compaction run before the next main request, so the
holder sync inside the main-request refresh never fired and the summary call carried the
revoked token. Replaces the bare holder-move test (its assertions are now checked through
compress_context), keeping the stack at two tests. Red without the pre-compression refresh.

Co-authored-by: Atul <atul@trycoral.ai>
2026-10-07 13:20:42 +05:30
Atul
aa27574648 fix(agent): refresh a rotating Anthropic token before compressing
The main loop re-resolves the Anthropic OAuth token before each request, but a compression that runs
before the next request (manual /compress, preflight compaction) still summarised with the token the
last request used. compress_context now runs the same refresh first.

(cherry picked from commit 63e22d597340aa042c3c736062e1190e31a9f96e, agent/conversation_compression.py hunk only)
2026-10-07 13:20:42 +05:30
kshitijk4poor
0adafc01c9 chore: map atul-trycoral for salvage of #130334 2026-10-07 13:20:42 +05:30
kshitijk4poor
5018787e7b refactor(aux): move rotate_runtime_main_api_key into auxiliary_key_rotation to keep auxiliary_client.py under its size cap
Code-health ratchet: agent/auxiliary_client.py was 8421 > its 8407-line cap on main (+14) after this
stack added the key-rotation helper. Pure move of the existing function into a topical sibling;
the body is unchanged and it late-imports the facade's runtime-main state, so it still reads the
live legacy mirrors at call time. The only caller (client_lifecycle) now imports from the sibling.
2026-10-07 13:20:42 +05:30
kshitijk4poor
325c9a2f26 fix(aux): keep a scoped runtime's key rotation out of the legacy mirrors
scoped_runtime_main binds a normalized copy that drops empty fields, so rotating under it raised KeyError and would have written a scoped runtime into the mirrors. Refresh the mirrors only when they still hold the revoked key. Gate r2 quality finding; probe: KeyError 'base_url' before, clean after.
2026-10-07 13:20:42 +05:30
kshitijk4poor
d79480f128 fix(anthropic): rotate the published aux key in place so the turn thread sees it
Gate finding B15-1 (Medium): the pre-dispatch refresh runs inside the request
worker's copied Context (_context_thread_target -> contextvars.copy_context().run),
so re-calling _publish_runtime_main rebound the ContextVar only in that copy; the
turn thread's later `auto` aux calls (approvals, goal judge, plugin llm) still read
the revoked token. rotate_runtime_main_api_key mutates the already-published dict
(shared by both contexts) when it still holds the old key, then refreshes the
legacy mirrors. No cross-session leak: set_runtime_main builds a fresh dict per
turn, so the shared object is this turn's only.

The kept e2e test now runs the refresh on a thread under _context_thread_target;
red on the previous head, green here.
2026-10-07 13:20:42 +05:30
kshitijk4poor
e8f524d7ea test(anthropic): drop the stray third blank line after the aux-route test
Gate finding B15-2 (Low): three blank lines before TestOAuthFlagOnCredentialSwap
where the file uses two between classes.
2026-10-07 13:20:42 +05:30
kshitijk4poor
44c269f1eb fix(anthropic): republish the same-turn aux runtime after a token refresh
turn_context publishes the auxiliary main runtime at turn start, before the
first request can trigger the silent Anthropic refresh. Syncing the agent-side
holders alone left that already-published runtime on the revoked token, so
same-turn `auto` aux calls (approvals, goal judge, plugin llm) still 401d.
Re-call _publish_runtime_main when the published key is still the old token;
identity-gated so a fallback or other-route publish is never overwritten.

Also keep #121757's end-to-end aux-route test, rewritten to production
order (publish, then refresh, then read).

Co-authored-by: SoyMarketing <yo@soymarketing.org>
Co-authored-by: mahdi <mahdi.nikfallah@yarnhub.org>
2026-10-07 13:20:42 +05:30
mahdi
4b842d3f25 fix(anthropic): move every holder of the revoked token on credential refresh
Claude Code revokes the old OAuth token when it refreshes, but
_try_refresh_anthropic_client_credentials only updated _anthropic_api_key.
agent.api_key, context_compressor.api_key (sent as the aux main_runtime for
compression) and the _primary_runtime restore snapshot kept the revoked
token, so every compression summary 401d for the rest of the session.
Sync each holder that still carries the old token (identity-matched, so a
compressor or snapshot on another route keeps its own key).

Rebuilt by hand on main from #121859 (conflicted in the test file).

Fixes #121756

Co-authored-by: SoyMarketing <yo@soymarketing.org>
2026-10-07 13:20:42 +05:30
kshitijk4poor
a3f67a473d chore: map MNIK-YH and soymarketing for salvage of #121859/#121757
Attribution for the salvage of the Anthropic refresh holder-sync fix
(#121859 author, #121757 co-author).
2026-10-07 13:20:42 +05:30
kshitijk4poor
b278d6528f fix(line): a non-string channel secret or token reads as unset
_credentials coerced every value with str(), so YAML channel_secret: true
keyed the HMAC with the guessable "True" (likewise 123, lists, maps).
Read a value only when it is a string, matching the webhook adapter's
_is_usable_secret policy; scoped-env values stay first. (Lead fold: Sol
r4/r5 + quality lens, converged; probed: true/0 -> "" on head.)
2026-10-07 13:19:35 +05:30
kshitijk4poor
e8b53518ea fix(webhook): an all-NUL whsec_ key fails closed like the empty key
HMAC zero-pads its key, so a whsec_ secret that decodes to NUL bytes
(whsec_AA==) signs exactly like the empty key b"" the previous fold
refuses; the whitespace-only guard let it through. Strip NULs with the
ASCII whitespace before the emptiness check. (Lead fold, 2c lens Low.)
2026-10-07 13:19:35 +05:30
kshitijk4poor
b0ea19a9cd fix(webhook): a whsec_ Svix secret that decodes to an empty or blank key fails closed
_is_usable_secret only sees the encoded string, so "whsec_" (decodes to b"")
and "whsec_ICAg" (decodes to three spaces) passed it and keyed the Svix HMAC
with a public value: any caller could sign a request. Apply the same
blank-key rule to the decoded key before computing the HMAC.
2026-10-07 13:19:35 +05:30
kshitijk4poor
d28c513904 fix(raft): a null or blank bridge token is unset, not a guessable inbound token
`bridge_token:` with an empty YAML value became str(None) == "None" (and a
whitespace-only value stayed "   "). Both are truthy, so connect() skipped
the auto-generated token and _authorized() accepted any wake/activity POST
carrying that header. Same class as the null-secret fixes for LINE and
WhatsApp Cloud in this stack.
2026-10-07 13:19:35 +05:30
kshitijk4poor
a32358068f test(line): pin the blank-token refusal in the standalone sender
Reading the standalone-send token through _credentials (earlier in this
stack) strips it, but no test failed when that read was reverted: the only
standalone test used an absent token. A whitespace-only token now has to
stop at "missing token" before the push API.
2026-10-07 13:19:35 +05:30
kshitijk4poor
adf4a4b48c fix(sms): a non-string or blank standalone-send key fails closed instead of raising
PlatformConfig keeps a numeric YAML api_key as an int, so the .strip() this
stack added to _standalone_send raised AttributeError where origin/main
returned an error dict. Coerce through str(... or "") like the LINE reader,
and pin both the int key and a whitespace-only key (which also gives the
standalone strip a test that fails without it).
2026-10-07 13:19:35 +05:30
kshitijk4poor
53f84cc716 fix(whatsapp_cloud): keep a null app secret unset instead of keying HMAC with "None"
An empty YAML value (`app_secret:` / `verify_token:`) arrives as None, and
str(None) made the inbound signing key the literal "None". The truthiness
guard in _handle_webhook then accepted it, so anyone could sign a POST with
HMAC("None", body). Same class as the LINE null-secret fix in this stack;
`or ""` lets a null read as unset and reach the existing 503 refusal.
2026-10-07 13:19:35 +05:30
kshitijk4poor
ae083f8a22 fix(line): read the standalone-send token through _credentials and pin both strips
_standalone_send still read the access token raw, so a blank or newline-padded token went out as the Bearer header; reuse the hardened _credentials. Pin the token half of the null/blank guard and the env presence strip (both reverted green before). Gate r4 Lows.
2026-10-07 13:19:35 +05:30
kshitijk4poor
62df46fdc4 test(sms): pin the stripped Twilio token in the requirements check
Reverting only the check_sms_requirements strip left the suite green; also drop the dead 'or ""' after reads that already pass a default. Gate r3 Lows.
2026-10-07 13:19:35 +05:30
kshitijk4poor
c5caff330d fix(line): keep a null channel secret unset instead of keying HMAC with "None"
c92ed7e000 wrapped the extra fallback in str(), so a present-but-null config value (an empty YAML key) became the guessable literal "None" and connect passed. Fall through to "" before str(); strip the env presence check too so check_requirements agrees with connect. Gate r3 High (2ab, 2c, quality converge).
2026-10-07 13:19:35 +05:30
kshitijk4poor
caa63dfc44 test(webhook): pin the dynamic-route secret guard
Gate r2 findings 3 and 4. Mutating _dynamic_route_allowed to
`if not effective_secret:` stayed green, because no test reached the
hot-reload guard with a blank or non-string secret. The existing
parametrized connect case now asserts the dynamic-route guard rejects
the same secrets, and it goes red under that mutation (4/4). Also
rename the test and its param to match what they cover (unusable, not
only whitespace-only), and refresh the stale docstring to name
_is_usable_secret.
2026-10-07 13:19:35 +05:30
kshitijk4poor
84cb790e64 fix(sms): fail closed on a whitespace-only Twilio auth token
Gate r2 finding 2 (same class as the webhook blank-secret fix):
TWILIO_AUTH_TOKEN was read unstripped, so a whitespace-only token
passed check_sms_requirements and keyed the X-Twilio-Signature HMAC
with blanks, letting anyone forge inbound SMS. Strip the token at
every read (adapter init, standalone send, requirements check).

Stripping alone would leave an empty key, and HMAC with an empty key
is equally public (verified by probe: a signature forged with b''
was accepted). So _check_signature now refuses outright when no
auth token is set. This goes one line past the reviewer's FIX, which
only asked for the strip.
2026-10-07 13:19:35 +05:30
kshitijk4poor
a560ba0b08 fix(line): fail closed on a whitespace-only channel secret
Gate r2 finding 1 (same class as the webhook blank-secret fix):
_credentials returned LINE_CHANNEL_SECRET / extra['channel_secret']
unstripped, so a whitespace-only secret passed the `not channel_secret`
check at connect and in verify_line_signature, and the webhook HMAC
was keyed with blanks, making signatures forgeable. Strip both values
in _credentials (matching whatsapp_cloud) so a blank secret reads as
unset and connect refuses with config_missing.
2026-10-07 13:19:35 +05:30
kshitijk4poor
5a527de775 fix(webhook): say a non-string secret is rejected in the secret messages
Fold finding 2 (gate): _is_usable_secret now also rejects non-string
secrets (a YAML int like `secret: 123` fails closed), but the startup
ValueError, the dynamic-route warning, the per-request log and the
comment in _read_authenticated_body still said "no secret" / "missing
or empty". An operator with an int secret would be told it is absent.
All three now say "missing, blank, or not a string"; the connect()
test gains an int row and matches the new wording.
2026-10-07 13:19:35 +05:30
kshitijk4poor
6e6657e1ab refactor(webhook): keep the secret classifier private to the webhook adapter
Fold finding 1 (gate, 3 reviewers): is_usable_secret lived in the
experimental gateway/relay/auth.py, so every webhook import pulled in
the relay package (~2.5ms) for a one-line predicate only the webhook
adapter uses. Move it next to _hex_hmac as module-private
_is_usable_secret, revert both relay/auth.py hunks, and move the
parametrized classifier test into tests/gateway/test_webhook_adapter.py.
No behaviour change.
2026-10-07 13:19:35 +05:30
kshitijk4poor
e123550d43 test(webhook): pin the per-request blank-secret guard
The salvaged tests only covered connect(); extend the existing
missing-secret request test with a whitespace-only route secret signed
with that same secret, which BASE authenticated. Drop the
real-secret-accepted connect case, already covered elsewhere, to keep the
stack at two invariant tests.
2026-10-07 13:19:35 +05:30
Alex Honecker
8ec668b3cc fix(security): treat whitespace-only HMAC secrets as unset
A blank or whitespace-only secret now fails closed at webhook startup,
on the request path, and when a dynamic route is admitted.

(cherry picked from commit 05e918d52c41e905d88e6c0a0f55e7612f4d899d)
2026-10-07 13:19:35 +05:30
kshitijk4poor
1a7779d10e chore: map ahadvice for salvage of #102069
Attribute the cherry-picked webhook blank-secret fix to its author.
2026-10-07 13:19:35 +05:30
kshitijk4poor
149f39fc9f fix(photon): log the sent-text lookup failure instead of swallowing it
The code-health ratchet (BLE001) rejects a blind except Exception that
neither re-raises nor logs the traceback. The record path already logs
at debug with exc_info; give the lookup path the same.
2026-10-07 13:18:15 +05:30
kshitijk4poor
cb71296ebd fix(photon): a threaded reply to our bubble stays "own" after a restart
Cloud spectrum targets carry no direction and _sent_message_ids is
in-memory, so after a gateway restart a swipe-reply to one of our
bubbles hydrated the quoted text from the sent-text index but was
marked as not ours: the agent saw [Replying to: "..."] instead of
[Replying to your previous message: "..."]. Only our sends are indexed,
so an index hit is itself proof the target was ours.
2026-10-07 13:18:15 +05:30
David Uche
aba1f72ef2 fix(photon): record sent text at the _post_send choke point
Polls (clarify), the plain-text fallback resend and standalone attachment
sends reached the sidecar without recording what they sent, so a threaded
reply to those bubbles lost its quoted context. Record once in _post_send
(plus the standalone attachment leg, which has its own HTTP client) and
reuse _normalize_chat_key instead of a duplicate regex.
2026-10-07 13:18:15 +05:30