Skip to content

fix(pairing): make the listed pending request approvable - #74427

Merged
OutThisLife merged 4 commits into
mainfrom
bb/pairing-approve-entry
Jul 29, 2026
Merged

fix(pairing): make the listed pending request approvable#74427
OutThisLife merged 4 commits into
mainfrom
bb/pairing-approve-entry

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Approving a pending pairing request from an admin surface could not work. list_pending returns the first 8 hex of the code's salted SHA-256 as a field named code — deliberately, so reading the store never reveals the real code — and the dashboard posted that value straight back to /api/pairing/approve, which hashes its input and compares. It never matched. Each click also counted toward the brute-force lockout, so five of them locked the platform for an hour and rejected the correct code at the CLI too.

Verified against the store on main before touching anything:

real code:      MCZNHUZY
list_pending(): [{'code': '07e7d183', 'user_id': '12345', ...}]
approve_code('telegram', '07e7d183') -> None      # what the dashboard sends
approve_code('telegram', 'MCZNHUZY') -> {'user_id': '12345', ...}

[pairing] Platform telegram locked out for 3600s after 5 failed attempts
approve_code('telegram', 'MCZNHUZY') -> None      # correct code, now refused

The code hashing landed in 2e509422e; the dashboard pairing page arrived nine days later in #36704 against the already-hashed shape. The only coverage asserted a 404 on a garbage code — which is also what a correct code returns now, so the suite stayed green.

Pending entries now expose a request_id (the server-side entry key) and PairingStore.approve_request grants on it. That fits how the two surfaces actually authenticate: the DM code proves the channel belongs to the requester, which is why approve_code keeps its proof-of-possession model untouched; a request id is only ever obtained by an admin already authenticated to the store, so it identifies a row rather than proving a secret. Consequently a request-id miss is a stale row, not an attack, and does not touch the lockout counter — otherwise the GUI DoSes the CLI. hermes pairing approve accepts either form and dispatches on shape.

Consolidation

Three open PRs target this defect. Both code-bearing ones are cherry-picked, so authorship survives in git history rather than being reimplemented:

PR Author Disposition
#46584 @itsflownium Cherry-picked as the base — the request-id direction is theirs
#62145 @solyanviktor-star Cherry-picked; the reset moves into the shared _finish_approval so both grant paths get it
#66761 @lhridley Intent served structurally — a non-approvable value is no longer printed, so there is nothing to relabel or reject

On top of the salvaged base: dropped the code/code_hash_prefix compat fields (the prefix is what surfaces mistook for a credential; re-exporting the id under the old code key kept the ambiguity alive), gave the duplicated 16-hex sniffing one owner in looks_like_request_id, and fixed the endpoint's chained conditional, which tested that flag against the wrong field and reported a bogus 429 on the request-id path.

Verification

hermes pairing list → copy Request ID → hermes pairing approve telegram <id> → user approved, disappears from pending. Endpoint exercised through TestClient against a temp HERMES_HOME.

Regressions added for the two properties that made this silent: the listing exposes nothing derived from the code and neither grant path accepts the digest prefix; repeated approvals of a stale request id past the lockout threshold leave the code path usable.

60 passed across tests/gateway/test_pairing.py, tests/hermes_cli/test_pairing.py, tests/hermes_cli/test_dashboard_admin_endpoints.py; 111 passed across the gateway pairing/authz/multiplex suites; tsc --noEmit clean in web.


Closes #46580
Supersedes #46584
Supersedes #62145
Supersedes #66761

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 37d0766

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 visual diff.

inline evidence upload failed.

Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso)

itsflownium and others added 4 commits July 29, 2026 17:49
approve_code()'s success path never cleared _failures:{platform}. The
counter is incremented on every non-matching code, persisted in
_rate_limits.json, and only ever reset to 0 when it reaches
MAX_FAILED_ATTEMPTS (firing the lockout). So it counts failures over the
gateway's entire lifetime, not consecutive ones.

An owner who mistypes a pairing code on a handful of separate occasions
— each time immediately retyping it correctly and successfully pairing —
accumulates those isolated typos. A later single fresh typo then hits
MAX_FAILED_ATTEMPTS and locks the whole platform out for an hour, at
which point _is_locked_out gates approve_code and even the *correct*
code is rejected.

Reset the counter on a successful approval, matching standard
brute-force-guard semantics (the counter tracks consecutive failures).
This does not weaken protection: an attacker cannot produce a success
without a valid code, and 5 consecutive wrong attempts still lock out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up hardening on the request-id grant path.

approve_request took the same lockout treatment as approve_code: gated by
it, and recording a miss toward it. But the two paths defend different
things. The lockout exists to stop guessing at the 8-char code space over a
messaging channel; a request id is only ever obtained by an admin already
authenticated to the store, so a miss means the row they clicked went stale.
Counting those let a handful of clicks on a stale list lock the operator out
of `hermes pairing approve` for an hour — the GUI DoSing the CLI.

Also drops the `code`/`code_hash_prefix` compat fields from list_pending.
The hash prefix is what admin surfaces mistook for an approvable code in the
first place, and re-exporting the request id under the old `code` key just
preserves the ambiguity; both consumers in the tree read `request_id` now.
The 16-hex sniffing that had been copy-pasted into the CLI and the endpoint
(where a chained conditional consulted it against the wrong field) moves to
one owner, PairingStore.looks_like_request_id.

The endpoint no longer reports a 429 on the request-id path, where lockout
can't apply — a stale id surfaced as a bogus "locked out" while the platform
sat locked for something else entirely.
@OutThisLife
OutThisLife force-pushed the bb/pairing-approve-entry branch from 9686738 to 37d0766 Compare July 29, 2026 22:53
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) area/auth Authentication, OAuth, credential pools sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jul 29, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #46584 and #46580. This uses the same request-id approval direction as #46584 but adds request-id shape dispatch, both CLI paths, and lockout-streak handling; maintainers should choose or consolidate the overlapping patches.

@OutThisLife
OutThisLife merged commit 7002ed9 into main Jul 29, 2026
51 checks passed
@OutThisLife
OutThisLife deleted the bb/pairing-approve-entry branch July 29, 2026 23:06
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…prove-entry

fix(pairing): make the listed pending request approvable
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
…prove-entry

fix(pairing): make the listed pending request approvable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard comp/dashboard Web dashboard / control panel UI (dashboard/, landing) comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telegram pairing list shows non-approvable code in CLI

4 participants