Skip to content

fix(gateway): close restart admission bypasses - #4

Closed
Gabriel300p wants to merge 12 commits into
recovery/local-hermes-changes-20260724from
fix/gateway-restart-admission-boundary-20260729
Closed

fix(gateway): close restart admission bypasses#4
Gabriel300p wants to merge 12 commits into
recovery/local-hermes-changes-20260724from
fix/gateway-restart-admission-boundary-20260729

Conversation

@Gabriel300p

Copy link
Copy Markdown
Owner

Summary

  • adds gateway.restart_command_enabled with strict fail-closed parsing and operator policy disable;
  • rejects SIGUSR1 restart admission atomically while active work exists;
  • preserves the persistent gateway.lock inode during stale PID cleanup, preventing split singleton locks.

Incident evidence

  • a Telegram /restart interrupted 8 active agents because the running PID predated the busy gate;
  • systemctl reload remained an external bypass;
  • gateway.lock was unlinked while the gateway still held its old fd.

Verification

@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: 1ec7a19fcd

ℹ️ 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 thread gateway/config.py Outdated
Comment on lines +929 to +930
if "restart_command_enabled" not in data:
restart_command_enabled = True

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 Load the restart policy from config.yaml

When an operator sets the documented gateway.restart_command_enabled: false in ~/.hermes/config.yaml, load_gateway_config() never copies that nested key into gw_data (unlike write_sessions_json at lines 1106-1112), so this branch treats it as absent and enables /restart. The new control therefore does not work through the production configuration path; the tests call GatewayConfig.from_dict() directly and miss this propagation failure. Copy the nested setting before constructing GatewayConfig and cover the real loader with a temporary HERMES_HOME.

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

Useful? React with 👍 / 👎.

Comment thread gateway/run.py Outdated
Comment on lines +20330 to +20334
if active_work != 0:
logger.warning(
"SIGUSR1 restart refused: %d active agent/cron/API run(s)", active_work
)
return False

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 Keep SIGUSR1 compatible with graceful restart callers

When hermes gateway restart or the update flow sends SIGUSR1 while any work is active, _graceful_restart_via_sigusr1() sends the signal only once and then waits for the process to exit; this refusal provides no acknowledgement, so the caller waits for the full drain timeout and falls through to systemctl restart or SIGTERM—the hard path that can interrupt the active work. Preserve a drain-aware operator path, or make these callers preflight and report a busy refusal instead of timing out and forcing the restart.

AGENTS.md reference: AGENTS.md:L114-L117

Useful? React with 👍 / 👎.

Comment thread gateway/run.py Outdated
"SIGUSR1 restart refused: %d active agent/cron/API run(s)", active_work
)
return False
return bool(runner.request_restart(detached=False, via_service=True))

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 Close admission before accepting the restart

When an API request or cron tick is admitted immediately after this callback returns, request_restart() has set _restart_requested but _draining is not set until the scheduled restart task reaches stop() after its 50 ms sleep. The API and cron admission gates inspect only _draining/_external_drain_active, so new work can register after the zero-work check and still be interrupted by this supposedly idle-only restart. Make the common admission gates honor the restart flag synchronously, or coordinate admission and counting with a shared lock.

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: 9206bcc8e1

ℹ️ 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 thread gateway/run.py Outdated
# Close chat/API/in-process-cron admission before reading counters. The
# synchronous gate is visible to the cron scheduler thread's can_dispatch
# callback and is rolled back on every refusal.
runner._draining = True

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 Recheck cron admission when claiming work

When SIGUSR1 arrives after cron.scheduler.tick() passes its can_dispatch check at line 3726 but before _submit_with_guard() adds the due job to _running_job_ids at line 3816, setting _draining here cannot stop that already-admitted tick: the strict snapshot sees zero work, accepts the restart, and the cron thread then launches work during teardown. Fresh evidence beyond the earlier admission comment is the unchanged gap between the cron gate and claim, with no second gate or shared lock; coordinate the restart snapshot with the cron claim or recheck admission while claiming.

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: 8b40e72fc9

ℹ️ 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 thread gateway/run.py Outdated
# Close chat/API/in-process-cron admission before reading counters. The
# synchronous gate is visible to the cron scheduler thread's can_dispatch
# callback and is rolled back on every refusal.
runner._draining = True

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 Recheck draining before claiming a chat turn

When a normal chat handler has passed the _draining check at line 10039 and is suspended in the unconditional topic-root await at line 10272, SIGUSR1 can execute this zero-work admission, set _draining, and schedule shutdown; the handler then resumes, checks only _external_drain_active at line 10289, and installs its running sentinel at line 10321. This starts an agent after the idle-only restart was accepted, potentially force-interrupting it when the drain times out. Fresh evidence beyond the earlier API/cron reports is this unchanged await-to-claim gap; recheck _draining immediately before the final claim or make the claim reject draining atomically.

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: 442c4b014d

ℹ️ 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 thread gateway/run.py
def request_restart(self, *, detached: bool = False, via_service: bool = False) -> bool:
if self._restart_task_started:
return False
self._draining = True

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 Linearize /restart with cron admission

When a messaging /restart races a ticker between the slash handler's _active_work_count() snapshot and _submit_with_guard(), the ticker can claim a cron job before this line finally closes admission; shutdown then proceeds with active work and may interrupt that job after the drain timeout. The new _admit_restart_signal() closes this race only for SIGUSR1, so the sibling chat path should close admission and perform the strict count under the same coordination before committing the restart.

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

Useful? React with 👍 / 👎.

Comment thread cron/scheduler.py
return None
# Advance only after the atomic restart gate. A refused claim
# stays due for the next allowed tick instead of being skipped.
advance_next_run(job_id)

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 Move cron persistence outside the running-set lock

When another process holds .jobs.lock, advance_next_run() can wait for the configured 30-second lock timeout while this code holds _running_lock. A SIGUSR1 callback concurrently calls get_running_job_ids() from the gateway event loop, so it blocks on that lock and freezes all gateway traffic for the same interval. Publish the running claim under _running_lock, then perform the persistent next-run update without holding the running-set lock, rolling back the claim if necessary.

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: 4657ad6666

ℹ️ 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 thread gateway/config.py Outdated
Comment on lines +929 to +930
if "restart_command_enabled" not in data:
restart_command_enabled = True

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 Honor nested restart policy for --config

The ordinary load_gateway_config() path now flattens gateway.restart_command_enabled, but gateway/run.py:21245 still feeds --config YAML directly to GatewayConfig.from_dict(). In that path, a config file containing the documented gateway: {restart_command_enabled: false} falls into this branch because only the top-level key is checked, so /restart remains enabled despite the operator policy; read the same nested key here before defaulting to True.

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

Useful? React with 👍 / 👎.

@Gabriel300p

Copy link
Copy Markdown
Owner Author

@codex review

@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: ebe00f052a

ℹ️ 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 thread gateway/platforms/api_server.py Outdated
Comment on lines +726 to +727
if adapter._draining_response() is not None:
raise RuntimeError("gateway is draining; pending API work refused")

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 Return a drain response from the cron fire guard

When a Chronos fire request passes the earlier _draining_response() check and a restart closes admission before this guard runs, this RuntimeError escapes _handle_cron_fire() because that call site does not catch it. The client then gets an aiohttp 500 instead of the retryable 503/Retry-After response used for all other drain refusals, so a normal maintenance race can look like a server failure to the scheduler; return/catch the draining response here instead of raising a generic exception.

Useful? React with 👍 / 👎.

@Gabriel300p

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ 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".

Copy link
Copy Markdown
Owner Author

Superseded by the current-main restart boundary — 2026-08-10

This recovery-branch PR is permanently unsuitable for integration:

Outcome: SUPERSEDED_CONTAMINATED_RECOVERY_PR / HISTORY_PRESERVED.

Closing #4 does not declare DTC-03 complete and does not activate anything in runtime. The remaining path is #7 → CI/review → merge → supervised deployment and live canary.

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