Commit Graph

47078 Commits

Author SHA1 Message Date
kshitijk4poor
6c51075929 test(security): pass repo_root to _worktree_is_dirty after the include-symlink change
#130595 made repo_root a required argument; the filter-injection test still called the
one-argument form.
2026-10-02 01:24:41 +05:30
kshitijk4poor
8fc2de8906 test(delegation): read the owner's git state from a throwaway repo
The recovery-hint test used the hermes checkout as the owner cwd. CI's
actions/checkout writes a repo-local includeIf credential include, and
noninteractive_repo_git_env now refuses any includeIf, so the hint was
(correctly) withheld and the test read a missing key.
2026-10-02 01:24:41 +05:30
kshitijk4poor
0d5b75a6dd test(security): correct the broken_config exit-code comment
git config dies with rc 128 on a malformed .git/config, not 3; the
row is caught by the not-in-(0, 1) gate either way.
2026-10-02 01:24:41 +05:30
kshitijk4poor
0c0230e33e test(security): cover the filter-discovery rc gate and fold the ssh reclaim test
A malformed .git/config makes `git config --get-regexp` exit 3; the
rc gate in noninteractive_repo_git_env must refuse rather than treat it
as "no filters", but no row exercised it, so removing the gate stayed
green. Add a broken_config row.

Parametrize the filter rows with their data (attrs, config, refused)
instead of a string-keyed dict plus a restated id tuple, hoist
_CLEAN_GIT_ENV above its first use and use it everywhere.

Fold the core.sshCommand reclaim test into the index-reading test: the
same malicious repo gets an ssh origin, a hostile core.sshCommand and a
.git/shallow file, so ls-remote and fetch --unshallow are exercised
without a separate two-commit source repo and depth-1 clone. Keeps the
stack at two new tests.
2026-10-02 01:24:41 +05:30
kshitijk4poor
157c3c1d0b refactor(git): drop dead fallbacks in noninteractive_repo_git_env
bounded_probe_run returns a CompletedProcess from communicate() with
text=True, so stdout is always a str once proc is not None; the
`or ""` guard could never fire. GIT_CONFIG_COUNT is always written by
noninteractive_git_env (it pops any caller value and sets
str(len(overrides))), so the defensive `.get(..., "0") or 0` parse
only hid that invariant.
2026-10-02 01:24:41 +05:30
kshitijk4poor
21c4b228b1 fix(git): make filter discovery case-exact, fail closed on any includeIf, bound the probe
Three gaps in noninteractive_repo_git_env, two probe-verified:

- Dedup was on key.lower(), but git keeps the subsection's case:
  [filter "evil"] + [filter "Evil"] with `filter=Evil` overrode only the
  lowercase driver and the hostile smudge ran. Dedup on the exact name
  `git config --name-only` prints.
- Only includeIf "onbranch:" failed closed. includeIf "gitdir:<repo>/.git/
  worktrees/" matches the new worktree's git dir during `worktree add`, so it
  loads a filter discovery never saw. Refuse on ANY repo-local includeIf
  (global/system config is already /dev/null).
- The discovery probe used subprocess.run(timeout=2), which on Windows can
  deadlock in the post-timeout communicate() and flashes a console. Use
  bounded_probe_run (rc 1 = "no filters" stays a verdict, so not
  bounded_git_probe).

Also share the refusal stderr as FILTER_DISCOVERY_FAILED across the kanban
and worktree-gc call sites and the test, and fold the fail-closed test into
the filter test (shared repo/env helpers) with case_collision and
gitdir_include rows.
2026-10-02 01:24:41 +05:30
kshitijk4poor
5dbd816f7e fix(git): cap discovered filter keys so filter discovery cannot hit E2BIG
Every discovered filter key adds GIT_CONFIG_KEY/VALUE env entries. A repository
with tens of thousands of filter sections makes the spawned git fail with
OSError(7, "Argument list too long") inside kanban/worktree-gc `_git`, which do
not expect it. Past 256 keys, fail closed like any other untrusted discovery.

One parametrized invariant covers both fail-closed paths (onbranch include and
filter flood) through the kanban `worktree add` call.

Fixes #126017
2026-10-02 01:24:41 +05:30
kshitijk4poor
a085553695 fix(git): fail closed on branch-conditional includes in filter discovery
noninteractive_repo_git_env discovers repository-named filter keys with
`git config --includes`, which evaluates includeIf "onbranch:..." against the
branch checked out NOW. A `worktree add -b X` then checks out X, git loads the
X-only include, and its smudge filter runs with no override pinned. Any
onbranch include in the effective config means the inventory cannot be trusted,
so return None and let callers skip the automatic git call.

Refs #126017
2026-10-02 01:24:41 +05:30
John Paul Soliva
f3767103e8 fix(git): delete worktree branches under noninteractive_git_env
The reclaim sweep (_reap_prune_verdicts), the orphaned-branch pass
(_prune_orphaned_branches) and the cleanup after a failed worktree add
(_cleanup_failed_worktree_add) ran `git branch -D` with the inherited
environment. A ref update runs the repository's reference-transaction hook
through core.hooksPath, so the unattended sweep executed whatever the
checkout's own config named. The three calls now use the same hardened env
as the other worktree_ops calls, with stdin closed.

(cherry picked from commit 31eec2f67e47a39c1b715f55d4fa649208076473)
2026-10-02 01:24:41 +05:30
John Paul Soliva
13ea517514 fix(git): run the worktree reclaim ls-remote, unshallow fetch and prune under noninteractive_git_env
The reclaim sweep's ls-remote, the shallow-repo fetch --unshallow and both worktree prune calls ran with the inherited environment, so a repository-level core.sshCommand or credential.helper ran unattended. They now use the same pinned env as the base fetch.

(cherry picked from commit a0d8e2a592790a90f6b666445017ea73f88cbc63)
2026-10-02 01:24:41 +05:30
John Paul Soliva
00e781e193 docs(worktrees): hermes -w checks out without the repository's hooks, fsmonitor and filters
Note the Git LFS consequence (pointer files) and the fix (git lfs pull in the worktree), EN and zh-Hans.

(cherry picked from commit 9870e29a42a77865eac79d01d3b99d516e5ebf0f)
2026-10-02 01:24:41 +05:30
John Paul Soliva
1ff01f4cf7 fix(git): neutralize repository-named filters in subagent finalization, worktree reclaim and hermes -w
finalize_subagent_worktree's status probe re-hashes files the child touched, so it ran a
repository-named clean filter after every delegated subagent. worktree_ops._worktree_is_dirty, the
dirty probe kanban teardown and the stale-worktree sweep run unattended, called git status with no
hardened env at all: core.fsmonitor ran as well. worktree_ops._worktree_add (hermes -w) had the
fixed pins only, so its checkout ran a named smudge filter.

All three now use noninteractive_repo_git_env. When filter discovery fails, finalization keeps the
worktree as unproven, the dirty probe reports dirty (the reclaimers keep the tree), and hermes -w
refuses to create the worktree. worktree remove --force at prune runs no filter once the status
probe is neutralized.

(cherry picked from commit e727281137a495602457fe4a86b51212af2fdbb7)
2026-10-02 01:24:41 +05:30
John Paul Soliva
62803f720d test(security): one fixture test covers the four index-reading sites
The recovery-hint/completion test and the kanban/worktree-gc test ran the same malicious-repo fixture; one test now runs all four sites. Reverting any one site alone still turns it red, so the PR keeps two invariant tests.

(cherry picked from commit 0b1a5da4055b26aa278bcba444635aeb05a943a1)
2026-10-02 01:24:41 +05:30
John Paul Soliva
803cd03521 fix(git): neutralize repository-named filters in the session snapshot and subagent worktrees
The session-start workspace snapshot runs git status, which re-hashes a
re-timestamped file through the repo-named clean filter, and a delegated
subagent's worktree add runs the smudge filter. Both now use
noninteractive_repo_git_env and skip the call when filter discovery fails.

(cherry picked from commit 04170a83d19a026fe6c5f968ce6e61b61f53c418)
2026-10-02 01:24:41 +05:30
John Paul Soliva
9c589b91bd fix(git): neutralize repository-named filters at the kanban, worktree-gc and recovery-hint calls
worktree add runs a repo-configured smudge filter and status runs its clean filter;
the fixed env pins cannot name the driver. These three sites now use
noninteractive_repo_git_env and skip the call when filter discovery fails. The Cmd-P
ls-files probe stays on the plain env: it never hashes or checks out content.

(cherry picked from commit 8d1233886a477f8fc623caee65dd28ce5e080eeb)
2026-10-02 01:24:41 +05:30
JoaoMarcos44
c77a2dda9f fix(git): add noninteractive_repo_git_env to neutralize repository-named filters
A repository names its clean/smudge/process filter drivers in .gitattributes, so the
fixed GIT_CONFIG_KEY pins in noninteractive_git_env cannot enumerate them. Discover the
checkout's filter command keys under the isolated env and append empty command overrides
plus required=false. Discovery failure returns None so callers skip the git call.

Ported from #126075.

(cherry picked from commit ebdb7feb263512a2e76f06d9ee20d483af6adc24)
2026-10-02 01:24:41 +05:30
John Paul Soliva
bba715b42d fix(git): run the remaining index-reading git calls under noninteractive_git_env
The recovery-hint git status, the TUI fuzzy-completion ls-files, the kanban dispatcher's worktree add and the worktree gc wrapper spawned git with the inherited environment, unlike the other Hermes-initiated git calls hardened in #101483.

(cherry picked from commit 3a8a79e6815f802b62195a24a05bc4239947fe17)
2026-10-02 01:24:41 +05:30
kshitijk4poor
c67b0a1dc4 refactor(gateway): name the media-cache max age and lstat cache files once
The hourly housekeeping loop hard-coded 24 while every cache accessor
defaults to MEDIA_CACHE_MAX_AGE_HOURS; use the constant so the two cannot
drift (docstring too). iter_cache_files did is_symlink + is_file + stat
per item behind an Optional-cutoff helper; one lstat per item with
S_ISREG (symlinks still excluded, unreadable entries skipped) and a
hoisted generated-root string give identical results with fewer syscalls.
2026-10-02 01:18:03 +05:30
kshitijk4poor
0ecc5c644a fix(backup): keep cache/generated in hermes backup archives
cache/generated holds provider-generated images/videos that are now
deliberately never swept (they are the only copy of the user's output).
_KEPT_CACHE_SUBDIRS did not list it, so `hermes backup` treated it as a
regenerable cache and dropped it — for the root home and profile homes.
Add GENERATED_SUBDIR (stdlib-only agent.provider_media) to the kept set
and pin both locations in the existing cache-pruning backup test.
2026-10-02 01:18:03 +05:30
kshitijk4poor
266499524e fix(gateway): deliver and bound-sync generated media in cache/generated
The salvage moved generated images/videos to cache/generated/<kind>/ so the
24h sweep stops deleting the only copy (#126445), but two consumers still
only knew the old swept dirs:

- Strict media delivery (HERMES_MEDIA_DELIVERY_STRICT=1) allowlists cache
  roots via _MEDIA_DELIVERY_CACHE_SUBDIRS; without "generated" a day-old
  generated image was rejected once the recency window lapsed, i.e. the
  move broke re-sending artifacts on public gateways.
- iter_cache_files (Modal/remote resync) walked the new never-swept tree in
  full, so every sync re-uploaded the whole generation history. Only files
  within the sweep window (MEDIA_CACHE_MAX_AGE_HOURS) are synced now.

GENERATED_SUBDIR and MEDIA_CACHE_MAX_AGE_HOURS live in agent/provider_media
as the single source for the image/video writers, the gateway allowlist and
cleanup default, and credential_files. Also trims duplicated comments and
fixes the pet orchestrate comments (nothing prunes cache/generated/images).
Tests use direct function refs instead of importlib strings.
2026-10-02 01:18:03 +05:30
kshitijk4poor
64f81b18be test(agent): trim generated-media salvage tests to two invariants
Keep sweep survival (parametrized over image/video writers) and the
cache/generated mount mapping; the outside-dir, per-file sync and SSH
artifact cases restate those two invariants.
2026-10-02 01:18:03 +05:30
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
6111d347f0 test(gateway): drop redundant read_record assert from the drift test
`hr.read_record(hr.ROLE_GATEWAY)` re-reads the on-disk, undrifted record,
so it never exercises the `drifted` copy the test is about; the
record_is_stale/liveness_is_proven asserts on `drifted` already cover the
invariant. Removing it keeps the test focused on drift tolerance.
2026-10-02 01:17:03 +05:30
kshitijk4poor
a10b0d4cae refactor(gateway): drop unreachable refusal in standalone lock-loss path
The block is entered only when profile_is_standalone(home) is True, and
standalone_attach_decision returns None only when that same check is False,
so the trailing _refuse_second_host_gateway(owner) could never run. The
function's final refusal still covers every non-standalone path.
2026-10-02 01:17:03 +05:30
kshitijk4poor
3ab993432b test(gateway): prove host-record drift on real POSIX process data
The WSL create_time drift test was linux-only and monkeypatched
_pid_incarnation_matches, so it never exercised the real start-fingerprint
path and was skipped on macOS. Drift the record's create_time with
dataclasses.replace instead: the live PID's stable start fingerprint must
still prove liveness. Red on the pre-fix host_rendezvous.py, green on POSIX.

Also pin the standalone lock-loss probe sequence exactly
(calls == [None, None]) rather than a loose length/last-element check.
2026-10-02 01:17:03 +05:30
JoaoMarcos44
42fb720c73 fix(gateway): stabilize host rendezvous process identity
(cherry picked from commit 3a9043b3dc8d6602a876a97f1b6da18d4812be50)
2026-10-02 01:17:03 +05:30
Yuan Li
d9487be621 test(gateway): cover standalone lock-loss probe gap
(cherry picked from commit ae1773274aaeac6e6234dfc22dfb7e0122e44055)
2026-10-02 01:17:03 +05:30
Yuan Li
ff6122e6f8 fix(gateway): allow standalone starts when owner probe is unavailable
(cherry picked from commit 7167be14e488edf977aa8ea7060d9f2ae9391371)
2026-10-02 01:17:03 +05:30
kshitijk4poor
5d579c5144 refactor(tools): pass the Unicode edit plan explicitly; tighten JSON constant scan
Behaviour-neutral review cleanups for the stack:

- fuzzy_match: _preserve_unicode_in_replacement now requires the plan
  (it is always precomputed by the caller), so the unused old_string param
  and the plan-less fallback go away. _apply_replacements takes the
  precomputed unicode_plan instead of a preserve_unicode flag, keeping the
  strategy decision in fuzzy_find_and_replace where it is made.
- file_operations: _json_nonstandard_constant skips the full json.loads
  when the text cannot contain NaN/Infinity (every JSON write pays this
  scan), strips the BOM via the shared _strip_bom helper, and catches only
  the errors json.loads can raise on str input instead of a blanket
  Exception.
- backup: the --keep validator comment now states why it exists (a
  negative keep slices away the newest archives).
2026-10-02 01:15:39 +05:30
kshitijk4poor
992f7807bf refactor(tools): pass each region to the Unicode-preserving replacement
_preserve_unicode_in_replacement now takes the matched region directly
instead of (content, [span]) wrapped per call, and the normalized
old_string plus its old->new opcodes - identical for every match - are
computed once per replace_all rather than once per match.

The replace_all Unicode regression test moves to a pure
fuzzy_find_and_replace test in tests/tools/test_fuzzy_match.py, replacing
the registry-driven test_patch_unicode_replace_all.py.
2026-10-02 01:15:39 +05:30
kshitijk4poor
db3f9482e6 fix(cli): validate keep once, and reject negative /snapshot prune counts
`/snapshot prune -1` passed keep=-1 to prune_quick_snapshots, whose slice
then deletes the newest snapshots instead of the oldest - the same bug
class #128886 fixed for `hermes backup --keep`.

Use isdecimal() for both entry points. For `backup --keep` the argparse
type is now the single validation (cmd_backup is run_backup's only
caller), so the duplicate run_backup guard is removed; non-numeric input
still exits 2 with a clear message.

The test is folded into tests/hermes_cli/test_backup.py, replacing
test_backup_retention_validation.py.
2026-10-02 01:15:39 +05:30
kshitijk4poor
8b31a4f493 fix(file-ops): refuse JSON NaN/Infinity only when the write introduces it
Moving the strict parse_constant check into the shared _lint_json_inproc
made the fail-closed write gate reject EVERY write to a .json file that
already holds NaN/Infinity, so an unrelated patch to such a file became
impossible (a regression), and the lint-delta baseline flipped too.

Keep the lint lenient and do the strict check in write_file after the
pre-write probe: refuse a candidate containing NaN/Infinity/-Infinity
unless the pre-edit content already used one. Unknown pre-content
(missing file, unreadable) refuses, keeping new files strict.

The parametrized test replaces tests/tools/test_json_write_constants.py.
2026-10-02 01:15:39 +05:30
Rudy Celekli
1c78e721bc fix(file-ops): refuse nonstandard JSON constants before writes
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
(cherry picked from commit 14190a14ac234fb5ead801871386b6428f172269)
2026-10-02 01:15:39 +05:30
Rudy Celekli
13f25e5fd9 fix(tools): preserve each normalized replacement’s Unicode
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
(cherry picked from commit 8e18884e6b4638c8a91ba4fd678821e6812f6130)
2026-10-02 01:15:39 +05:30
Rudy Celekli
447b9ee7d6 fix(cli): reject negative full-backup retention before pruning
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
(cherry picked from commit c9113abe8d65dbb50b0c4aefaade1b9e1ac7b87e)
2026-10-02 01:15:39 +05:30
kshitijk4poor
319269a7fa chore: map rudycelekli for salvage of #128886
Rudy Celekli authors the three salvaged commits in this stack (#128886,
#128888, #128894); map the noreply email so release notes credit them.
2026-10-02 01:15:39 +05:30
kshitijk4poor
dd821c2230 test(gateway): rely on the hermetic HERMES_HOME fixture in the paused-loop test 2026-10-02 01:13:21 +05:30
kshitijk4poor
8c955c2801 test(gateway): pin that a paused hermes holds /loop wakeups until resume
b533839532 gates _loop_wakeup_fire_one on the emergency stop because loop
wakeups are internal events that skip the inbound estop gate, but nothing
exercised that leg. Fold a pause leg into the existing direct-harness loop
wakeup test: while engaged, fire_tick is never called and no turn reaches the
adapter; after disengage the same due tick fires exactly once. The estop
sentinel is isolated under the test's own HERMES_HOME.

Reverting run_goals.py to b533839532^ turns this red
(fire_tick claimed + "tick" injected while paused).
2026-10-02 01:13:21 +05:30
kshitijk4poor
804239bc27 fix(gateway): hold /loop wakeups while hermes is paused
/loop wakeups are injected with internal=True, and _hm_estop_gate returns early
for internal events, so `hermes pause` did not stop loop ticks from starting
agent turns. This is the same problem the heartbeat poller fix handles.

Check the emergency stop before LoopManager.fire_tick. is_due() is read-only and
fire_tick() is what moves next_due_at forward, so the tick stays due and fires on
the first scan after `hermes resume`. The watcher sleeps for its interval (15s)
after every scan, so being paused does not make it busy-spin. check_paused logs
once per engagement.
2026-10-02 01:13:21 +05:30
kshitijk4poor
ff244960a9 refactor(gateway): clarify why the estop gate drops stray heartbeat turns
The poller now checks `hermes pause` before claiming a heartbeat tick, so the
inbound gate only sees heartbeats that slipped past that check. Say so, and
point at settle_heartbeat_attempt as what keeps the tick due for after resume.
2026-10-02 01:13:21 +05:30
Dzianis Vashchuk
faeb7eecc9 fix(gateway): heartbeats respect hermes pause instead of spamming the paused notice
While ESTOP was engaged the heartbeat poller kept injecting the due tick every
5s; the estop gate answered each one with the user-facing paused notice and the
refunded tick stayed due, so the notice was re-sent every poll (~10k Telegram
messages in 5h, twice in prod).

- _heartbeat_poll_watch: skip before claiming the tick while paused (logged once
  per engagement via check_paused); it fires normally after hermes resume.
- _hm_estop_gate: a synthetic heartbeat event is dropped silently; real user
  messages still get the paused notice.

(cherry picked from commit 74c04121e35e4479f59dd522281298a7142ea951)
2026-10-02 01:13:21 +05:30
kshitijk4poor
81e3d81f6c chore: map dzianisv for salvage of #126905 2026-10-02 01:13:21 +05:30
teknium1
aa859fd4c8 feat(plugin-catalog): refuse plugins that override Hermes core or Desktop at runtime
Catalog policy (Teknium): a listed plugin extends Hermes only through public
surfaces. A plugin that replaces core functions, methods or Desktop UI in place
collides with every other plugin patching the same seam and breaks on any core
release.

- New `no core override` admission check (hermes_cli/plugin_validate_core_override.py):
  static taint pass over the import closure of __init__.py and dashboard/*.py.
  Core values are Hermes modules (plus hermes_plugins.*) and attribute chains off
  them, reached by import, importlib/sys.modules lookups, getattr, iteration, or a
  plugin helper that returns one. Rebinds are attribute assignment/deletion,
  setattr/delattr, sys.modules writes, writes into a core module's tables,
  mock.patch, and passing a core value to a helper that setattr's its parameter.
  Tests, benchmarks and CI/smoke scripts outside the import closure are not judged.
- Desktop lint: new `app DOM reach` rule (document-level queries of the app's own
  data-slot/data-tour/data-sidebar/data-testid markup, and document.body
  childList/subtree MutationObservers).
- Desktop lint bug: the comment stripper did not skip string literals, so a string
  holding `/*` (a glob like '**/*.md') opened a fake comment and hid every line up
  to the next `*/` from all rules. Strings are now matched first and kept.
- README rule 9, the plugin-catalog user guide and the plugin developer guide's
  compatibility contract state the rule.

A/B over 466 pinned trees (121 open catalog PR entries + 349 listed entries +
humalike): the new checks flag exactly the hand-verified overrides; zero crashes;
no previously reported Desktop finding disappears.
2026-10-01 12:41:57 -07:00
kshitijk4poor
5a8dea85fb docs(webhook): header-less deliveries get a random per-request id, not a timestamp 2026-10-02 01:07:31 +05:30
kshitijk4poor
4b6fef0def test(gateway): assert only the routed destination in the identity test
The session-identity test compared full private _delivery_info records,
pinning fields (mirror, route, profile) this fix does not change, so any
new record field would break it without a behaviour change. Assert only
each identity's own deliver_extra.chat_id, which still collides on the
old webhook:{route}:{id} shape, and drop the unused payload.
2026-10-02 01:07:31 +05:30
kshitijk4poor
d3a5d51556 fix(gateway): use a random fallback id for header-less webhook deliveries
Header-less POSTs fell back to a millisecond timestamp as their delivery
id. Now that the id is the dedup key within a route, two distinct
header-less deliveries to the same route in the same millisecond were
treated as a retry and the second was dropped as "duplicate". A uuid4
keeps each header-less delivery unique; providers that send a delivery
header still dedup on it.
2026-10-02 01:07:31 +05:30
FroRaut
30f556d094 fix(gateway): scope webhook delivery identities
(cherry picked from commit 798dc009df19d5fa5090cb497d6f62590ab2a2d7)
2026-10-02 01:07:31 +05:30
kshitijk4poor
725c3ec4bc test(auth): fold backslash-userinfo redirect case into the boundary table
The standalone parser-differential test duplicated the authorize-rejection
assertions of test_native_redirect_uri_boundary_matches_browser_authority;
moving its URI into the rejected tuple keeps the coverage (400, no cookie)
with one test instead of two.
2026-10-02 01:05:58 +05:30
kshitijk4poor
1223ffbe38 refactor(auth): table-drive loopback redirect host canonicalisation
The host check was a three-branch if/elif mapping each accepted loopback
literal to its URL authority spelling; a lookup table states the same
allow-list in one place. The final urlunparse also re-set scheme, netloc
and fragment even though every earlier check already guarantees they are
canonical (http, netloc == canonical_netloc, '#' rejected), so only the
empty-path -> "/" normalisation is applied. Behaviour and 400 messages are
unchanged.
2026-10-02 01:05:58 +05:30
FroRaut
37fea25c2b fix(auth): validate native loopback redirect URIs
(cherry picked from commit c719988403d173782f17bb95f5b01e0a8afb2260)
2026-10-02 01:05:58 +05:30