feat(server): DELETE /v1/hosts/{host_id} to deregister a retired host (#2038) - #4360
Open
abhay-codes07 wants to merge 4 commits into
Open
feat(server): DELETE /v1/hosts/{host_id} to deregister a retired host (#2038)#4360abhay-codes07 wants to merge 4 commits into
abhay-codes07 wants to merge 4 commits into
Conversation
…host (omnigent-ai#2038) External hosts self-register via the daemon and become rows in the host store, but `server/routes/hosts.py` exposed only GET/POST, so there was no supported way to remove one. A permanently decommissioned machine lingered as an offline entry in every session-creation host picker, and the only workaround was hand-deleting the postgres row. The persistent teardown already exists: `HostStore.delete_host` unbinds any sessions still pointing at the host (nulls their `host_id`) and deletes the row (which also revokes its launch token). This wires it to a route so retiring a host has a first-class API and a clean IaC story. `DELETE /v1/hosts/{host_id}` mirrors `get_host`'s auth: 404 for an unknown host, 403 when the caller is not the owner. It refuses an online host (409) so an active machine is not pulled out from under running work, and refuses a server-managed sandbox host (409) whose lifecycle belongs to the server that created it. On success it returns 204 and the host is gone from the store and the pickers. Tests: removes an offline host (row and picker), 404 unknown, 409 online, 409 managed sandbox, 403 wrong owner, and that a bound session is unbound rather than left dangling at a deleted row. Each DELETE-path test fails against `main` with 405 (no route) and passes with it. Signed-off-by: abhay-codes07 <abhaysingh0293@gmail.com>
Signed-off-by: abhay-codes07 <abhaysingh0293@gmail.com>
Deleting a host nulled only omnigent_conversation_metadata.host_id and left runner_id, workspace, and git_branch pointing at a machine that no longer exists. A session in that state is wedged in every direction: it cannot auto-relaunch (that path is gated on host_id), cannot rebind to a new host (the atomic bind matches only runner_id IS NULL, so it returns "session already has a runner bound" forever), and cannot be stopped (Stop requires both host_id and runner_id). There is no API that repairs it, and no foreign key fails loudly to signal the inconsistency. Null all four columns in the same statement instead, matching the full unbind ConversationStore.clear_host_binding already performs for a failed per-session bind. The change lands in the shared primitive rather than behind a flag for the new deregistration route, because it is correct for the only other caller too: managed-sandbox teardown runs after the sandbox has been terminated, so a surviving runner_id there is just as stale. Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
13 tasks
A scheduled task pinned to a connected host stores that host_id as a soft reference with no foreign key. Once the host row is gone the task fails on every single fire with host_not_found, and the user cannot repair it in place: the PATCH validator rejects a null host_id, so the only way out is to delete the task and build it again from scratch. Managed hosts were never pinnable, so deregistering a connected host is the first path that can create this orphan. Refuse the delete with 409 while any task still pins the host, naming the tasks so the caller knows what to fix. Silently unpinning would be worse than the 409: an unpinned task resolves the owner's first online host at fire time, so clearing the pin would quietly relocate the work to a different machine. Re-pointing a task at another host is already a single PATCH, which makes the refusal recoverable with the surface that exists. Paused tasks count too — they break identically once resumed. Reading the tasks needs a host-scoped query rather than a filter over the owner's tasks, because single-user deployments store tasks with a null user_id while their hosts are owned by the reserved "local" user, so an owner filter would miss every pinned task there. Also corrects the route docstring, which is published to the public API reference through openapi.json: it promised the host would stay out of the pickers "forever" and claimed the delete revokes a launch token. Neither holds for the hosts this route can delete. Only managed hosts ever carry a token and those are refused, and an external host keeps its host_id locally, so its daemon recreates the row on the next reconnect. Signed-off-by: SabhyaC26 <sabhyachhabria@gmail.com>
abhay-codes07
force-pushed
the
feat/deregister-host-route
branch
from
August 7, 2026 14:43
1525ae8 to
ed750dc
Compare
Contributor
|
@abhay-codes07 This PR is a Bug fix, Feature, or UI / frontend change but the Demo section is missing or only contains a placeholder. These change types require a screenshot or screen recording so reviewers can see the new behaviour without checking out the branch. Please update the Demo section with:
Use |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
Closes #2038
Summary
Continues #3238, which auto-closed after 7 days of inactivity (my fault — I did not reply in time) and could not be reopened (GitHub 422 on a closed PR whose branch was later rebased). Same branch, rebased onto current
main, with @SabhyaC26's follow-up commits preserved.External hosts self-register via the daemon and become rows in the host store, but
server/routes/hosts.pyexposed only GET/POST, so a permanently decommissioned machine lingered as an offline entry in every session-creation host picker forever, with no supported way to remove it.This adds
DELETE /v1/hosts/{host_id}and hardens the teardown:get_host's auth (404 unknown → 403 non-owner), refuses an online host (409) and a server-managed sandbox host (409), else deletes and returns 204.runner_id/workspace/git_branchalongsidehost_id, not justhost_id. A survivingrunner_idwedged the session (rebind is gated onrunner_id IS NULL); clearinghost_id+workspacetogether respectsck_conversation_metadata_workspace_required_for_host.host_idas a soft reference (no FK). Deleting a pinned host would leave each task failing at fire time withhost_not_found, which the user cannot recover from. The route now returns 409 and names the pinning tasks (paused tasks count — they break the same way once resumed), via a newScheduledTaskStore.list_by_host_id.Who calls this (per the earlier review question)
Host-lifecycle / IaC tooling and self-hosters who rotate runner machines (the #2038 use case: register is automatic, deregister was impossible). The scheduled-task-pin guard above is the concrete safety case that motivated finishing it: without a supported delete, a retired-then-recreated host leaves pinned tasks silently broken. A future web-UI "remove host" affordance can build on the same route.
Test Plan
Covers: offline delete (row + picker gone), 404 unknown, 409 online, 409 managed sandbox, 403 wrong owner, full session unbind (row reusable afterward), 409 when a scheduled task pins the host (incl. paused), tasks pinned elsewhere ignored, delete succeeds after a task is repointed, and
list_by_host_idat the store level.ruff check+ruff format --checkclean;openapi.jsonregenerated (drift test green).Demo
N/A. Backend REST route + store changes with no visual surface. Before: no way to remove a retired host (
DELETE405); a pinned task on a deleted host fails forever withhost_not_found. After:DELETE /v1/hosts/{id}returns 204 (409 if online / server-managed / pinned by tasks, 403/404 for auth), and a deleted host fully unbinds its sessions.Type of change
Test coverage
Changelog
Added
DELETE /v1/hosts/{host_id}to deregister a retired host: it fully unbinds any sessions still pointing at it and refuses (409) to delete a host that is online, server-managed, or still pinned by scheduled tasks.