Skip to content

fix(gateway): apply platform-disabled skill gate to auto-skill channel bindings - #59478

Open
pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/gateway-auto-skill-disabled-gate
Open

pierrenode wants to merge 1 commit into
NousResearch:mainfrom
pierrenode:fix/gateway-auto-skill-disabled-gate

Conversation

@pierrenode

@pierrenode pierrenode commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

_handle_message_with_agent()'s auto-skill block (Telegram DM Topics, Discord channel_skill_bindings) loads bound skills via _load_skill_payload() with a raw identifier, bypassing get_skill_commands()'s scan-time disabled filter. Result: a skill an operator disables for a platform (or globally, via skills.disabled) still gets its full content injected into every new session bound to that channel/topic.

The stacked-skill (#58888) and bundle (#59156) invocation paths already re-check get_disabled_skill_names() for exactly this reason — the auto-skill block was the one path in gateway/run.py still missing it.

Fix: check each resolved skill's name against get_disabled_skill_names(platform=...) before injecting it, skip and log disabled ones. Mirrors the existing stacked/bundle gates exactly (same helper, same platform scoping, same log style). No behavior change for any binding that references an enabled skill.

Related Issue

No filed issue — found via sibling-gap review of #58888 / #59156 (both merged in the last 24h to the same file).

Type of Change

Changes Made

  • gateway/run.py: re-check get_disabled_skill_names() in the auto-skill loading loop, skipping disabled skills (+14 lines)
  • tests/gateway/test_auto_skill_platform_disabled.py: new AST invariant regression test — _handle_message_with_agent requires a large unrelated mocked harness to invoke directly, so this mirrors the existing test_10710_auto_reset_evicts_cached_agent.py approach (verified: fails without the fix, passes with it)

How to Test

python3.11 -m pytest tests/gateway/test_auto_skill_platform_disabled.py -v --override-ini="addopts="

Also re-ran the full sibling suite to confirm no regressions: test_stacked_skill_platform_disabled.py, test_discord_channel_skills.py, test_slack_channel_skills.py, test_dm_topics.py, test_fresh_reset_skill_injection.py, test_skill_bundles.py, test_skill_commands.py — 156 passed.

Checklist

  • Read the Contributing Guide | Conventional Commits | No duplicate PR
  • Single logical fix | Tests added | Platform: macOS
  • Docs — N/A | Cross-platform — N/A

@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/gateway Gateway runner, session dispatch, delivery tool/skills Skills system (list, view, manage) sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data P2 Medium — degraded but workaround exists labels Jul 6, 2026
@egilewski

Copy link
Copy Markdown
Contributor

looks mergeable

I reviewed the gateway auto-skill disabled-gate fix against current GitHub main; the change is limited to gateway/run.py plus a focused regression test, git merge-tree against the selected current main was clean, git diff --check passed, the focused gateway/agent skill suites passed as 107 plus 49 tests, and CodeRabbit completed with no findings.

Security evidence:

  • trust boundary: Platform and global skill-disable configuration controls whether channel/topic auto-skill bindings may inject full skill content into new gateway sessions.
  • source/sink/invariant: event.auto_skill from channel_skill_bindings / Telegram DM Topics reaches _load_skill_payload and _build_skill_message; disabled raw identifiers and loaded display names must be skipped before message construction.
  • current-main reproduction: Current main loads and builds bound auto-skills without a get_disabled_skill_names(platform=...) check in this auto-load block.
  • PR-head or patch-replay validation: PR head obtains the same disabled set for the source platform and skips either the raw binding identifier or loaded skill display name before _build_skill_message.
  • positive/negative cases: The new AST regression pins the disabled-gate call in the auto-skill block, while existing stacked, bundle, Discord/Slack channel-skill, DM-topic, and fresh-reset skill-injection suites still passed.
  • residual bypass search: Sibling slash/stacked/bundle paths already apply matching disabled gates; no additional channel/topic auto-load path or identifier/display-name mismatch was found in this scoped review.
  • reviewer validation: CodeRabbit completed post-clean-pass review with zero findings.

Signed: GPT-5.5-xhigh in Codex

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracing this remaining auto-skill path. The production change addresses a live gap: current gateway/run.py:11157-11185 loads and builds new-session auto-skills without consulting get_disabled_skill_names(), while the established bundle path already applies the same canonical-name/raw-identifier gate in agent/skill_bundles.py:289-320.

Problems

  • tests/gateway/test_auto_skill_platform_disabled.py:56 parses inspect.getsource(gateway_run) into an AST. AGENTS.md:1358-1411 explicitly bans source-reading/source-shape tests; this test can pass without proving disabled content is excluded from the emitted gateway turn.

Suggested changes

  • Replace the AST assertion with a focused behavioral gateway test that drives a new-session event.auto_skill through the handler and asserts disabled content is absent, with an enabled-skill control.

Automated hermes-sweeper review.

every new session bound to it via channel_skill_bindings / DM Topics.
"""
tree = ast.parse(inspect.getsource(gateway_run))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please replace this source/AST assertion with a behavioral gateway-path test. AGENTS.md:1358-1411 explicitly bans tests that read production source because they pin implementation shape rather than proving that disabled auto-skill content is excluded from the emitted turn.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 15, 2026
@pierrenode
pierrenode force-pushed the fix/gateway-auto-skill-disabled-gate branch from 197b818 to 1a78a1b Compare July 26, 2026 23:34

@GottZ GottZ left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Summary

Two PRs address the auto-skill channel-binding path that bypasses platform/global disabled-skill filtering. #59478 gates both raw identifiers and resolved display names and now exercises the behavior directly, while #59511 only gates raw identifiers before loading and includes an unrelated dashboard-auth change.

Related pull requests

  • #59478 related — (+187/-17) — merge: Extracts auto-skill resolution, applies the platform-scoped disabled set to both the raw binding identifier and loaded display name before message construction, and behaviorally verifies that disabled content is excluded while enabled content remains. The diff addresses the keep_open review on #59478 by replacing the prohibited source-shape/AST test with focused behavioral tests, including raw-name, display-name, enabled-control, and platform-forwarding cases.
  • #59511 [closed] duplicate — (+100/-1) — keep closed as duplicate of #59478: It targets the same bypass but checks only the raw identifier before payload loading, retains source-reading tests, and bundles an unrelated hermes_cli/dashboard_auth/prefix.py change. It remains relevant as the superseded duplicate that established the same intended gate but is less complete and branch-contaminated.

Duplicates

#59511 substantially duplicates #59478's auto-skill platform-disabled gate; #59478 is the cleaner and more complete canonical implementation.

Suggested consolidation

Merge #59478: its diff closes the reported bypass for both raw and canonical/display skill names and its revised behavioral tests explicitly resolve the keep_open review's test-design objection. Keep #59511 closed as a duplicate; its unrelated dashboard-auth change should only proceed through a separate clean PR if still needed.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    subgraph Dup59478 ["PRs duplicating each other"]
        P59478["PR #59478 (open)"]
        P59511["PR #59511 (closed)"]
    end
    class P59478 open
    class P59511 closed
    class P59478 target
    click P59478 "https://github.com/NousResearch/hermes-agent/pull/59478"
    click P59511 "https://github.com/NousResearch/hermes-agent/pull/59511"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed or no verify verdict yet (state tag in the node label).

Cross-PR triage: Reviewed 2 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 15 kB of PR diffs, 5 kB of issue/PR text, 3 kB of discussion (5 comments), 1 verify verdict. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

@pierrenode
pierrenode force-pushed the fix/gateway-auto-skill-disabled-gate branch from 1a78a1b to d491740 Compare July 29, 2026 14:03
@egilewski

Copy link
Copy Markdown
Contributor

looks mergeable

Security evidence:

  • trust boundary: An operator's global or platform-specific disabled-skill policy must constrain channel/topic auto-skill bindings before skill instructions enter a new gateway session, including bindings supplied as raw identifiers.
  • source/sink/invariant: MessageEvent.auto_skill reaches skill loading and message construction; both the raw identifier and the loaded canonical/display name must be rejected against the disabled set for the actual source platform before content is injected.
  • current-main reproduction: The real gateway handler on current main injected a disabled skill's sentinel instructions into a new Telegram session even though that skill was disabled for Telegram.
  • PR-head or patch-replay validation: The current-main replay excluded those instructions, preserved the original user text, and forwarded the actual platform to the disabled-skill check.
  • positive/negative cases: Display-name and raw-identifier disables were both blocked, while enabled and ordered multi-skill bindings still loaded normally.
  • residual bypass search: Telegram topic, Discord and Slack binding propagation, global/platform config unioning, identifier normalization, canonical names, stacked invocations, bundles, and preloaded-skill sibling gates showed no remaining auto-binding bypass in scope.
  • reviewer validation: The baseline and replay handler probes passed; 62 focused gateway/skill/config tests, Ruff, and the diff check passed; all required current-head checks are green.

Not checked:

  • Full local test suite
  • CodeRabbit review

Signed: GPT-5.6-sol-xhigh in Codex

@pierrenode
pierrenode force-pushed the fix/gateway-auto-skill-disabled-gate branch from d491740 to 39fc513 Compare August 11, 2026 15:39
@pierrenode

Copy link
Copy Markdown
Contributor Author

Rebased onto current upstream/main. gateway/run.py had drifted enough (a new TurnRunner class, extensive internal restructuring) that the automatic rebase produced an unreviewable multi-thousand-line conflict — reapplied the fix fresh against the current file instead: same _resolve_auto_skill_content() extraction, same call-site replacement, same test file, byte-identical logic. Verified _platform_name is still in scope at the call site. Mutation-verified (removing the disabled-check breaks 2 of 4 tests), ruff clean, python -c "import gateway.run" clean. Fresh competitor search: no overlap. Squashed to a single commit on top of current upstream/main.

…l bindings

_handle_message_with_agent's auto-skill block loads bound skills
(channel_skill_bindings, Telegram DM Topics) via a raw identifier — the
same bypass class the stacked (NousResearch#58888) and bundle (NousResearch#59156) invocation
paths had: it skips get_skill_commands()'s scan-time disabled filter, so
an operator who disables a skill for this platform (or globally) still
had its full content injected into every new session bound to that
channel/topic.

Re-check get_disabled_skill_names() before injecting, mirroring the
stacked/bundle gates.

The loading+gate logic is extracted into a standalone
_resolve_auto_skill_content() so it can be driven directly in tests
instead of asserting on gateway/run.py's source shape.
@pierrenode
pierrenode force-pushed the fix/gateway-auto-skill-disabled-gate branch from 39fc513 to 16d7871 Compare August 13, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data tool/skills Skills system (list, view, manage) type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants