Skip to content

fix(gateway): gate chat-triggered restart through operator policy - #7

Closed
Gabriel300p wants to merge 15 commits into
mainfrom
fix/restart-command-policy-plugin-20260810
Closed

fix(gateway): gate chat-triggered restart through operator policy#7
Gabriel300p wants to merge 15 commits into
mainfrom
fix/restart-command-policy-plugin-20260810

Conversation

@Gabriel300p

@Gabriel300p Gabriel300p commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Context

This replaces the restart-policy portion of stale PR #4 with a minimal core boundary on current main. PR #4 is closed as a contaminated recovery PR: stale base and more than 40 unrelated files.

ClickUp source: DTC-03 86e2k3fph.

Why this is core, not a plugin

A control gated by plugins.enabled disappears if config.yaml becomes malformed and normal config loading falls back to defaults. The final design adds hermes_cli.restart_policy and invokes it directly from hermes_cli.lifecycle for pre_gateway_dispatch.

It is always shipped with the runtime, independent of plugin discovery, and remains inert when the policy setting is absent. A core skip is terminal: compatibility plugins are not invoked afterward, so a later rewrite cannot revive the request.

Canonical command boundary

The policy mirrors the gateway's active dispatch order:

  1. parse the incoming command with MessageEvent.get_command() semantics, including arguments and every @suffix form;
  2. resolve built-in/registry aliases through hermes_cli.commands.resolve_command();
  3. when the typed name is not built-in, traverse the exact GatewayRunner.config.quick_commands map active in the live process;
  4. only type: alias entries are followed; every hop re-checks built-in precedence.

Using the live GatewayRunner.config object is deliberate: it is the same mapping the later dispatch sink will expand, including managed-overlay results and the last active config if the YAML becomes malformed after startup.

Covered bypass forms include:

  • /restart
  • /restart <args>
  • /restart@HermesBot
  • /restart@ops-bot
  • /restart@
  • built-in registry aliases resolving to restart
  • quick-command aliases directly or transitively targeting restart
  • quick alias → registry alias → restart

Built-in commands retain precedence over same-name quick commands. type: exec, malformed aliases, cycles, lookalikes and unrelated commands do not invent a restart capability.

Blocking behavior

When gateway.restart_command_enabled: false, the boundary schedules a policy notice on the current event loop and returns:

{"action":"skip","reason":"restart_disabled_by_operator_policy"}

The request stops before auth/pairing, active-session busy handling, restart markers, drain or process restart. Notice delivery is best effort; failure to reply never re-enables restart.

Strict policy semantics

  • missing setting: enabled for backward compatibility;
  • explicit boolean: respected;
  • explicit non-boolean: blocked, fail closed;
  • malformed/unreadable user config: blocked, fail closed;
  • malformed/unreadable managed config: blocked, fail closed;
  • managed policy overrides user policy;
  • legacy top-level key is accepted and wins over the nested key within one document.

The policy reader parses the raw active-profile and managed YAML documents directly. It never uses the normal fail-open/default-merging loader for this security decision.

Configuration

gateway:
  restart_command_enabled: false

No plugin enablement is required. A supervised deployment/restart of the merged code is still required before the boundary exists in the live process.

Verification

The focused suite covers parser/suffix variants, registry aliases, active quick-command mappings in object and dict config shapes, alias chains, built-in precedence, cycles, exec/malformed entries, active-session independence, detached notice delivery, missing/invalid values, strict user and managed YAML parsing, managed precedence, core availability without plugin discovery and terminal core skip before compatibility plugins.

Repository CI for head 336bb3a0d5da2aa6eed5c6a6adb2e711ba7d9364 is pending. A fresh independent review must evaluate the final core + quick-command design; old plugin reviews are retained as historical findings, not treated as approval.

Live gates after merge

  • deploy the exact merged commit through the supervised host path;
  • set gateway.restart_command_enabled: false;
  • send canonical, suffix, registry-alias and quick-alias restart forms;
  • prove PID unchanged and no new restart marker files;
  • prove an active session is neither interrupted nor steered;
  • prove /status and unrelated quick commands remain functional;
  • temporarily corrupt a disposable config copy and prove restart remains blocked;
  • exercise rollback through the supervised config/deploy path.

No runtime activation is included in this PR.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29e77b27c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +24 to +27
_RESTART_COMMAND = re.compile(
r"^\s*/restart(?:@[A-Za-z0-9_]+)?(?:\s+.*)?\s*$",
re.IGNORECASE | re.DOTALL,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Match every command form accepted by the gateway parser

When an enabled policy receives text such as /restart@ops-bot or /restart@, this regex rejects it because the suffix is not limited to [A-Za-z0-9_]+, but MessageEvent.get_command() in gateway/platforms/base.py unconditionally discards everything after @ and returns restart. The request therefore reaches _handle_restart_command even when gateway.restart_command_enabled is false, providing a trivial bypass on platforms that accept free-form command text; identify restart requests using the same parser semantics as dispatch.

AGENTS.md reference: AGENTS.md:L54-L57

Useful? React with 👍 / 👎.

Comment on lines +104 to +111
def _pre_gateway_dispatch(*, event: Any = None, **_kwargs: Any) -> Optional[dict[str, str]]:
"""Rewrite disabled restart commands before auth and normal dispatch."""
text = _extract_event_text(event)
if not _is_restart_command(text):
return None
if _restart_command_enabled():
return None
return {"action": "rewrite", "text": _BLOCKED_COMMAND}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Dispatch the blocked command on the active-session path

When /restart is sent while an agent is running, rewriting it to this plugin command prevents the active-session fast path from recognizing it because that path uses resolve_command(), which only resolves built-ins. With the default display.busy_input_mode: interrupt, the rewritten text consequently falls into the ordinary follow-up path and interrupts or redirects the active turn instead of invoking _handle_blocked; other busy modes may queue it. Thus a supposedly side-effect-free denial can disrupt an in-progress experiment and may not return the policy explanation.

AGENTS.md reference: AGENTS.md:L84-L87

Useful? React with 👍 / 👎.

Comment on lines +31 to +38
def _load_runtime_config() -> Mapping[str, Any]:
"""Load the current profile config lazily so policy changes apply live."""
from hermes_cli.config import load_config

config = load_config()
if not isinstance(config, Mapping):
raise TypeError("Hermes config must be a mapping")
return config

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Detect parse failures instead of trusting load_config fallback

If the plugin is already enabled and the last valid config allowed restart, malformed YAML does not raise here: hermes_cli.config.load_config() deliberately returns the last-known-good config (or defaults on a fresh load). _restart_command_enabled() therefore sees the old true/absent value and permits /restart, contrary to the documented and tested fail-closed behavior for a parse failure. This matters during manual or non-atomic config edits intended to disable restarts, so the policy must use a read path that exposes parse/read failure rather than silently serving fallback state.

AGENTS.md reference: AGENTS.md:L84-L87

Useful? React with 👍 / 👎.

@Gabriel300p
Gabriel300p force-pushed the fix/restart-command-policy-plugin-20260810 branch from 29e77b2 to 1b606ee Compare August 10, 2026 13:52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b606ee379

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +107 to +108
if not _is_restart_command(text):
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Block aliases that resolve to restart

When an idle gateway has a quick_commands alias whose target is /restart, sending that alias bypasses this check because the hook only sees the original text and returns here. The cold dispatch path later expands alias targets in gateway/run.py and dispatches the resulting canonical restart command to _handle_restart_command, despite gateway.restart_command_enabled: false; the policy must also gate commands after alias resolution or resolve aliases before deciding.

AGENTS.md reference: AGENTS.md:L54-L57

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 976683b62b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +147 to +150
try:
from hermes_cli.commands import resolve_command

definition = resolve_command(command)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve quick-command aliases before applying restart policy

When an idle gateway defines a quick_commands alias targeting /restart, this resolves only central registry aliases and returns the typed alias unchanged, so the policy allows it; gateway/run.py then expands the quick-command target later at lines 11858-11878 and reaches _handle_restart_command despite gateway.restart_command_enabled: false. Fresh evidence in this revision is that moving the gate into restart_policy.py still calls only commands.resolve_command and the new alias test mocks that resolver rather than exercising a real quick command.

AGENTS.md reference: AGENTS.md:L54-L57

Useful? React with 👍 / 👎.

Comment thread hermes_cli/lifecycle.py
Comment on lines 41 to +43
from hermes_cli import plugins

return plugins.invoke_hook(hook_name, **kwargs)
return core_results + plugins.invoke_hook(hook_name, **kwargs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reapply restart policy after plugin rewrites

With the policy disabled, a non-restart message passes the core check, but this still returns any plugin result such as {"action": "rewrite", "text": "/restart"}; the gateway applies that rewrite at gateway/run.py lines 11101-11105 and later invokes the restart handler at line 11998 without another policy check. Thus an enabled pre-dispatch plugin can accidentally or deliberately revive a blocked chat restart, contrary to this function's stated terminal security boundary; validate the rewritten command before returning it or gate at the final canonical dispatch point.

AGENTS.md reference: AGENTS.md:L54-L57

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex review

Please review the current head 336bb3a0d5da2aa6eed5c6a6adb2e711ba7d9364, not the superseded plugin commits. Focus on: core lifecycle ordering, strict user/managed config failure semantics, active GatewayRunner.config.quick_commands expansion parity, built-in precedence, alias chains/cycles, active-session behavior, notice scheduling, and whether any path can still reach restart markers/drain while the policy is false.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 336bb3a0d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +275 to +278
if canonical_gateway_command(event, gateway) != "restart":
return None
if restart_command_enabled():
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recheck policy after command-hook rewrites

When the policy is false, a harmless built-in such as /status returns here without reading the policy, but gateway/run.py lines 11909-11944 then lets a command:status or command:* hook rewrite it to restart; the recomputed canonical name reaches the restart handler at lines 11998-11999, which writes restart markers and requests the drain. Fresh evidence in the current head is this separate decision-hook rewrite path, rather than the previously reported pre_gateway_dispatch plugin rewrite. Reapply the policy after command-hook rewrites or enforce it at the final canonical restart dispatch.

AGENTS.md reference: AGENTS.md:L54-L57

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

@codex address that feedback

Fix the two remaining rewrite bypasses on current head 336bb3a0d5da2aa6eed5c6a6adb2e711ba7d9364 by enforcing restart policy at the final canonical dispatch sinks, not with another partial regex/hook layer.

Required behavior:

  1. Add an async helper in hermes_cli.restart_policy that, given the final canonical command, gateway and event, returns/produces a blocked result only when the final command is restart and policy is disabled. It must send the same notice directly (or safely schedule it), fail closed if policy/config cannot be read, and make no marker/drain/restart side effects.
  2. Invoke that helper immediately before BOTH calls to _handle_restart_command(event) in gateway/run.py:
    • the active-session fast path;
    • the cold final dispatch after quick-command expansion and all command:<canonical> / wildcard hook rewrites.
  3. Keep the early pre-dispatch check as defense-in-depth if useful, but the final sink is authoritative.
  4. A pre-dispatch compatibility plugin rewriting /status/restart must be blocked.
  5. A later command:status or command:* hook rewriting to restart must be blocked.
  6. Direct, registry-alias, quick-alias and chained-alias restarts must remain blocked.
  7. With policy enabled/absent, restart behavior remains backward compatible.
  8. Add focused tests that stub _handle_restart_command/marker/drain paths and prove they are never called for every disabled-policy route, including active session and both rewrite stages. Also prove unrelated command rewrites still work.
  9. Run focused tests plus full CI, and update the PR description to document the final-sink invariant.

Do not merge, deploy, edit live config or activate runtime.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

Copy link
Copy Markdown
Owner Author

Superseded by #9. O baseline que bloqueava a CI foi corrigido e mergeado em #8; a implementação foi reconstruída sobre o main atual em rebase/restart-command-policy-20260810, sem reutilizar a branch antiga. Não mergear este PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant