fix(hub): GCP SA verification can silently fail to persist, and start/restart never checked Verified - #1328
Open
ptone wants to merge 2 commits into
Open
fix(hub): GCP SA verification can silently fail to persist, and start/restart never checked Verified#1328ptone wants to merge 2 commits into
ptone wants to merge 2 commits into
Conversation
added 2 commits
August 28, 2026 21:14
Defect 1 — handlers_gcp_identity.go: when runGCPServiceAccountVerification detects an impersonation failure and the subsequent store.UpdateGCPServiceAccount also fails, the error was silently discarded with `_ =`. This left the SA marked Verified=true in the database while the API returned a 502 "verification failed" — keeping the agent-creation gate open for an SA that cannot actually mint tokens. Now the handler returns HTTP 500 with a distinct error code (gcp_verification_persist_failed) and logs both the verification and persistence errors. Defect 2 — handlers_agent_lifecycle.go: createAgentInProject (:770) and updateAgent (:2245) both guard against unverified SAs, but handleAgentLifecycle (start/restart) did not. An SA that was marked unverified after agent creation could still be started. Now start and restart look up the assigned SA and reject the request if it is not verified. Stop and suspend are exempt. Tests: - TestVerification_PersistFailure_DoesNotReportCleanFailure (Defect 1) - TestAgentLifecycle_Start_UnverifiedSA (Defect 2) - TestAgentLifecycle_Start_VerifiedSA (no false positive) - TestAgentLifecycle_Start_NoGCPIdentity (nil safety)
…rs actionable Two follow-ups to the verification gap fixes, prompted by architect review: 1. Success-path persist failure (handlers_gcp_identity.go): when the verification probe succeeds but store.UpdateGCPServiceAccount fails, the handler previously called writeErrorFromErr which returned a generic 500 "Internal server error". The operator had no idea the probe succeeded or that the SA remained unverified in the DB. Now returns HTTP 500 with error code gcp_verification_persist_failed and a message telling the operator to retry verification. This matters because the Defect 2 lifecycle check (previous commit) means an SA stuck at Verified=false will now block start/restart. The operator must know the verify result was not persisted so they can retry, rather than discovering it via an opaque "not verified" error at start time. 2. Lifecycle error messages (handlers_agent_lifecycle.go): the "not verified" and "not available" validation errors now include the SA email (or SA ID when the lookup fails), making them actionable from the error alone. Test: - TestVerification_SuccessPersistFailure_DoesNotReportCleanSuccess: probe succeeds, store update fails → asserts HTTP 500 (not 200), error code gcp_verification_persist_failed, SA still Verified=false in real store. Mutation: removing the error handling makes the test fail (200 instead of 500).
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.
Behaviour change for existing agents
This changes behaviour on live systems. Any already-created agent whose assigned GCP service account has
Verified=falsein the database will now receive a 400 validation error on start or restart, where previously it would start and fail silently with exit code 1 within seconds. Stop and suspend are not affected.Remedy: re-run verification on the service account (
POST .../gcp-service-accounts/<id>/verify). The 400 error message names the SA email and states it is unverified, so the remedy is discoverable from the error alone.This is the right trade — a loud 400 with an actionable message beats a silent exit 1, which is the recurring theme on this tier — but it must not arrive as a surprise on an instance the operator is actively testing on.
What changed
Defect 1 — Verification persist failure (both directions)
handlers_gcp_identity.go,runGCPServiceAccountVerification:When
VerifyImpersonationreturns a result (pass or fail) and the subsequentstore.UpdateGCPServiceAccountalso fails, the database state does not match reality. Both directions now return HTTP 500 with a shared error codegcp_verification_persist_failedand an actionable message telling the operator to retry verification. The log lines distinguish the two directions ("verification failed AND the failure could not be persisted" vs. "verification succeeded but the success could not be persisted") so ops can tell which case occurred.One error code is correct because the remedy is identical in both directions: retry verification. The reviewer independently agreed with this. The distinction (which direction failed) is in the log line and the response message, not the error code, because the caller's action is the same either way.
Failure-path persist failure: The
_ = s.store.UpdateGCPServiceAccount(...)silently discarded the error. The SA remainedVerified=truein the database while the API returned 502 "verification failed" — keeping the agent-creation gate open for an SA that could not mint tokens.Success-path persist failure: The handler called
writeErrorFromErrwhich returned a generic 500 "Internal server error". The operator had no idea the probe succeeded, that the SA was still unverified in the DB, or that retrying would fix it. With the lifecycle check (Defect 2) now in place, this would surface later as an opaque "not verified" error at start time with nothing connecting it to the earlier 500.Defect 2 — Lifecycle handler missing Verified check
handlers_agent_lifecycle.go,handleAgentLifecycle:createAgentInProject(:770) andupdateAgent(:2245) both guard against unverified SAs, buthandleAgentLifecycle(start/restart) did not. An SA marked unverified after agent creation could still be started. Now start and restart look up the assigned SA (whenMetadataMode=assignandServiceAccountIDis set) and reject the request with a 400validation_errorif the SA is not verified. Stop and suspend are exempt — you must always be able to shut down an agent. The error message includes the SA email so the operator knows which SA to re-verify.No evidence of deliberate omission was found. Searched:
git log --all --oneline --grep='Verified' -- pkg/hub/handlers_agent_lifecycle.go(zero results),grep -n -i 'verified\|intentional\|deliberat\|skip.*check'in the handler (zero results near the lifecycle dispatch).Why the
_ =at lines 361/373 is left unchangedThe
_ =sites athandlers_gcp_identity.go:361and:373(auto-verify during SA registration) are not changed by this PR. Those sites run during SA registration when the SA defaults toVerified=false. If auto-verify succeeds but the store update fails, the SA stays unverified — the operator must explicitly verify it, which is the normal flow. If auto-verify fails and the store update also fails, the SA stays unverified — same outcome. Both directions fail closed because a new SA starts unverified.This is the distinction from the explicit re-verification path (line 607), where a previously-verified SA could remain marked as verified after a failed re-verification. At registration, there is no prior verified state to corrupt. The next reader should not have to re-derive this.
Full package suite result
go vet ./pkg/hub/— clean (exit 0).go test ./pkg/hub/— 2 pre-existing failures, 0 failures introduced by this PR:TestTemplateResource_UATConfinementauthz_agent_baseline_test.go:568— expects nil Decision for global template confinement, getsAllowed:falsemainwith no changes applied. Authz confinement logic, unrelated to GCP SA verification.TestScopedAdmin_ProjectAdminDeniedUnboundProjectscoped_admin_test.go:387— expects 403, gets 405mainwith no changes applied. Scoped admin routing, unrelated to GCP SA verification.Both failures reproduced on a clean checkout of
mainwithgit stash && go test -tags sqlite -run '...'. Neither test involves GCP service accounts, verification, or agent lifecycle. No existing test broke as a result of the new lifecycle guard — no fixture that previously scaffolded an agent withMetadataMode=assignand an unverified SA was affected.Test results and mutation analysis
All five new tests pass. Mutation results:
TestVerification_PersistFailure_DoesNotReportCleanFailuregcp_verification_persist_failed, not 502gcp_verification_failed_ =: handler falls through to 502 path → test fails (502 ≠ 500, wrong code)TestVerification_SuccessPersistFailure_DoesNotReportCleanSuccessgcp_verification_persist_failed, not 200writeErrorFromErr: status stays 500 but code becomesinternal_errorand message becomes "Internal server error" → test fails (wrong code and message)TestAgentLifecycle_Start_UnverifiedSATestAgentLifecycle_Start_VerifiedSATestAgentLifecycle_Start_NoGCPIdentity