From 5485b8bc66b2be6285d711cfba06855fa49a853e Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Mon, 12 Oct 2026 05:33:33 +0000 Subject: [PATCH 1/6] chore(specs): gardener checkbox sync --- specs/113-upstream-resilience/tasks.md | 80 +++++++++++++------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/specs/113-upstream-resilience/tasks.md b/specs/113-upstream-resilience/tasks.md index 4eec10b78..ab269a07c 100644 --- a/specs/113-upstream-resilience/tasks.md +++ b/specs/113-upstream-resilience/tasks.md @@ -10,25 +10,25 @@ Format: `[ID] [P?] [Story] Description (files) — FR`. `[P]` = can run in paral ## Slice 113-a — oauth-refresh-robustness (US1, G1+G2) — T100–T129 -- [ ] T100 [P] [US1] Failing test: `ClassifyRefreshError` table — `OAuthError{invalid_grant}` wrapped by mcp-go (`%w`), 200-with-error-body form, `invalid_client`, `unauthorized_client`, `unsupported_grant_type`, `invalid_scope`, mcp-go `"refresh token request failed with status 503: "`, 429, `*net.OpError` dial/reset, `context.DeadlineExceeded`, `server not found`, `server does not use OAuth`, unknown; plus the manual-path typed error (`internal/oauth/refresh_errors_test.go`) — FR-007, FR-008 -- [ ] T101 [US1] Implement `ClassifyRefreshError` + `RefreshHTTPError`; derive the existing metric labels from it; keep the substring list only as the last fallback (`internal/oauth/refresh_errors.go`, `refresh_manager.go`) — FR-007, FR-008 -- [ ] T102 [P] [US1] Failing test: storage `UpdateOAuthToken(key, mutate)` preserves `ClientID/ClientSecret/CallbackPort/RedirectURI/Created/DisplayName` when `UpdateOAuthClientCredentials` runs concurrently (100 interleaved goroutines, `-race`) (`internal/storage/bbolt_oauth_update_test.go`) — FR-006 -- [ ] T103 [US1] Implement `BoltDB.UpdateOAuthToken`; switch `PersistentTokenStore.SaveToken` and the manual refresh path to it (`internal/storage/bbolt.go`, `internal/oauth/persistent_token_store.go`, `internal/upstream/core/client.go`) — FR-006 -- [ ] T104 [P] [US1] Failing test: coordinator — N=50 barrier-started `Do` calls on one key run `fn` once and all get the same token; a caller whose observed RT is stale gets the persisted token with `skipped=true` and no `fn` call; a failing `fn` returns the same error to all waiters; a cancelled initiator does not cancel the flight; different keys run in parallel (`internal/oauth/refresh_coordinator_test.go`) — FR-001, FR-003, FR-004 -- [ ] T105 [US1] Implement `RefreshCoordinator` (`internal/oauth/refresh_coordinator.go`) — FR-001, FR-003, FR-004, FR-011 -- [ ] T106 [P] [US1] Failing test: `PersistentTokenStore.GetToken` with a bound refresher and an expired token refreshes through the coordinator and returns the new token; without a bound refresher it never calls the network (assert on an `httptest` AS request counter); terminal error wraps `transport.ErrOAuthAuthorizationRequired`; transient returns `ErrTokenRefreshTransient` (`internal/oauth/persistent_token_store_test.go`) — FR-002, FR-005 -- [ ] T107 [US1] Implement refresher binding: `SetRefresher`/`BindRefreshHandler` with an `atomic.Pointer` handler slot; `GetToken` pre-emption; `SaveToken` never enters the coordinator (`internal/oauth/persistent_token_store.go`, `internal/oauth/config.go` attach, `internal/upstream/core/connection_oauth.go` bind call) — FR-002, FR-005 -- [ ] T108 [US1] Route `RefreshOAuthTokenDirect` (both sub-paths) through the coordinator with trigger `proactive`; manual path returns `RefreshHTTPError` (`internal/upstream/core/client.go`, `internal/upstream/manager.go`) — FR-001, FR-007 -- [ ] T109 [P] [US1] Failing test (SC-001): rotating `httptest` AS that revokes the grant on refresh-token reuse; expired token; 50 goroutines through a real mcp-go `StreamableHTTP` client with OAuth against an `httptest` MCP server + 1 concurrent `RefreshOAuthTokenDirect`; assert exactly 1 token request, all calls succeed; `-race -count=20` (`internal/upstream/core/oauth_refresh_race_test.go`) — FR-012, SC-001 -- [ ] T110 [P] [US1] Failing test: `invalid_client` with a DCR record → `ClearOAuthClientCredentials` called once, schedule `RefreshStateFailed` with the re-register message, zero further retries; second `invalid_client` after a re-registration and before any success → terminal "rejects new registrations"; static client → terminal, credentials untouched; a registration or grant saved by a concurrent login is not cleared (compare-and-clear) (`internal/oauth/refresh_manager_invalid_client_test.go`) — FR-009, SC-002 -- [ ] T111 [US1] Implement `invalid_client` handling and per-key re-registration state (`internal/oauth/refresh_manager.go`, `refresh_coordinator.go`) — FR-009 -- [ ] T112 [P] [US1] Failing test: `RefreshManager` reads records by server key (token-age log and `EmitOAuthTokenRefreshed` fire for a keyed record) (`internal/oauth/refresh_manager_test.go`) — FR-010 -- [ ] T113 [US1] Fix the two `GetOAuthToken(serverName)` lookups to use the server key (needs the server URL: obtain via the runtime interface or store the key on the schedule) (`internal/oauth/refresh_manager.go`) — FR-010 -- [ ] T114 [US1] CLI in-memory store path uses the same coordinator (`internal/oauth/config.go` `globalTokenStoreManager`) + test — FR-014 -- [ ] T119 [P] [US1] Failing test: bound store returns `ExpiresAt` zero to mcp-go and mcp-go never issues a refresh even when the token expires between `GetToken` and `IsExpired`; reactive refresh with a DCR record and a handler lacking a client id uses stored credentials; terminal latch (no network after `invalid_grant` until a new token is saved) and transient cooldown; compare-and-clear does not clear a registration saved by a concurrent login; login-flow `invalid_client` after re-registration is terminal (`internal/oauth/refresh_coordinator_test.go`, `internal/upstream/core/oauth_refresh_race_test.go`) — FR-002, FR-003, FR-004, FR-009 -- [ ] T120 [P] [US1] Failing test: login saves a new token and DCR client while an old refresh flight is in flight; the old flight's success is discarded (login token kept, client/token pair consistent) and its `invalid_grant` failure neither latches nor fails the schedule (`internal/oauth/refresh_coordinator_test.go`) — FR-006a +- [x] T100 [P] [US1] Failing test: `ClassifyRefreshError` table — `OAuthError{invalid_grant}` wrapped by mcp-go (`%w`), 200-with-error-body form, `invalid_client`, `unauthorized_client`, `unsupported_grant_type`, `invalid_scope`, mcp-go `"refresh token request failed with status 503: "`, 429, `*net.OpError` dial/reset, `context.DeadlineExceeded`, `server not found`, `server does not use OAuth`, unknown; plus the manual-path typed error (`internal/oauth/refresh_errors_test.go`) — FR-007, FR-008 +- [x] T101 [US1] Implement `ClassifyRefreshError` + `RefreshHTTPError`; derive the existing metric labels from it; keep the substring list only as the last fallback (`internal/oauth/refresh_errors.go`, `refresh_manager.go`) — FR-007, FR-008 +- [x] T102 [P] [US1] Failing test: storage `UpdateOAuthToken(key, mutate)` preserves `ClientID/ClientSecret/CallbackPort/RedirectURI/Created/DisplayName` when `UpdateOAuthClientCredentials` runs concurrently (100 interleaved goroutines, `-race`) (`internal/storage/bbolt_oauth_update_test.go`) — FR-006 +- [x] T103 [US1] Implement `BoltDB.UpdateOAuthToken`; switch `PersistentTokenStore.SaveToken` and the manual refresh path to it (`internal/storage/bbolt.go`, `internal/oauth/persistent_token_store.go`, `internal/upstream/core/client.go`) — FR-006 +- [x] T104 [P] [US1] Failing test: coordinator — N=50 barrier-started `Do` calls on one key run `fn` once and all get the same token; a caller whose observed RT is stale gets the persisted token with `skipped=true` and no `fn` call; a failing `fn` returns the same error to all waiters; a cancelled initiator does not cancel the flight; different keys run in parallel (`internal/oauth/refresh_coordinator_test.go`) — FR-001, FR-003, FR-004 +- [x] T105 [US1] Implement `RefreshCoordinator` (`internal/oauth/refresh_coordinator.go`) — FR-001, FR-003, FR-004, FR-011 +- [x] T106 [P] [US1] Failing test: `PersistentTokenStore.GetToken` with a bound refresher and an expired token refreshes through the coordinator and returns the new token; without a bound refresher it never calls the network (assert on an `httptest` AS request counter); terminal error wraps `transport.ErrOAuthAuthorizationRequired`; transient returns `ErrTokenRefreshTransient` (`internal/oauth/persistent_token_store_test.go`) — FR-002, FR-005 +- [x] T107 [US1] Implement refresher binding: `SetRefresher`/`BindRefreshHandler` with an `atomic.Pointer` handler slot; `GetToken` pre-emption; `SaveToken` never enters the coordinator (`internal/oauth/persistent_token_store.go`, `internal/oauth/config.go` attach, `internal/upstream/core/connection_oauth.go` bind call) — FR-002, FR-005 +- [x] T108 [US1] Route `RefreshOAuthTokenDirect` (both sub-paths) through the coordinator with trigger `proactive`; manual path returns `RefreshHTTPError` (`internal/upstream/core/client.go`, `internal/upstream/manager.go`) — FR-001, FR-007 +- [x] T109 [P] [US1] Failing test (SC-001): rotating `httptest` AS that revokes the grant on refresh-token reuse; expired token; 50 goroutines through a real mcp-go `StreamableHTTP` client with OAuth against an `httptest` MCP server + 1 concurrent `RefreshOAuthTokenDirect`; assert exactly 1 token request, all calls succeed; `-race -count=20` (`internal/upstream/core/oauth_refresh_race_test.go`) — FR-012, SC-001 +- [x] T110 [P] [US1] Failing test: `invalid_client` with a DCR record → `ClearOAuthClientCredentials` called once, schedule `RefreshStateFailed` with the re-register message, zero further retries; second `invalid_client` after a re-registration and before any success → terminal "rejects new registrations"; static client → terminal, credentials untouched; a registration or grant saved by a concurrent login is not cleared (compare-and-clear) (`internal/oauth/refresh_manager_invalid_client_test.go`) — FR-009, SC-002 +- [x] T111 [US1] Implement `invalid_client` handling and per-key re-registration state (`internal/oauth/refresh_manager.go`, `refresh_coordinator.go`) — FR-009 +- [x] T112 [P] [US1] Failing test: `RefreshManager` reads records by server key (token-age log and `EmitOAuthTokenRefreshed` fire for a keyed record) (`internal/oauth/refresh_manager_test.go`) — FR-010 +- [x] T113 [US1] Fix the two `GetOAuthToken(serverName)` lookups to use the server key (needs the server URL: obtain via the runtime interface or store the key on the schedule) (`internal/oauth/refresh_manager.go`) — FR-010 +- [x] T114 [US1] CLI in-memory store path uses the same coordinator (`internal/oauth/config.go` `globalTokenStoreManager`) + test — FR-014 +- [x] T119 [P] [US1] Failing test: bound store returns `ExpiresAt` zero to mcp-go and mcp-go never issues a refresh even when the token expires between `GetToken` and `IsExpired`; reactive refresh with a DCR record and a handler lacking a client id uses stored credentials; terminal latch (no network after `invalid_grant` until a new token is saved) and transient cooldown; compare-and-clear does not clear a registration saved by a concurrent login; login-flow `invalid_client` after re-registration is terminal (`internal/oauth/refresh_coordinator_test.go`, `internal/upstream/core/oauth_refresh_race_test.go`) — FR-002, FR-003, FR-004, FR-009 +- [x] T120 [P] [US1] Failing test: login saves a new token and DCR client while an old refresh flight is in flight; the old flight's success is discarded (login token kept, client/token pair consistent) and its `invalid_grant` failure neither latches nor fails the schedule (`internal/oauth/refresh_coordinator_test.go`) — FR-006a - [ ] T115 [US1] 5xx/429/network → existing backoff with `failed_server_error`/`failed_network` labels; test asserts backoff sequence unchanged — FR-008, FR-013 -- [ ] T116 Docs: `docs/oauth-resource-autodetect.md` or the OAuth troubleshooting page — refresh serialization, `invalid_client` behaviour, new metric labels (published-site rules apply) +- [x] T116 Docs: `docs/oauth-resource-autodetect.md` or the OAuth troubleshooting page — refresh serialization, `invalid_client` behaviour, new metric labels (published-site rules apply) - [ ] T117 Gates: race tests, server-tag tests (CI skip regex), golangci-lint v2 ×2, `./scripts/test-api-e2e.sh` (isolated instance) - [ ] T118 Cross-model review (opencode harness, gpt-6.1-sol), ≤10 rounds - T121–T129 reserved for 113-a follow-ups @@ -36,40 +36,40 @@ Format: `[ID] [P?] [Story] Description (files) — FR`. `[P]` = can run in paral ## Slice 113-b — oauth-discovery-overrides (US2, G3+G5) — T130–T159 - [ ] T130 [P] [US2] Failing test: `OAuthConfig` new fields JSON/mapstructure round-trip, `OAuthConfigChanged` detects each, `MergeOAuthConfig` merges each, storage `UpstreamRecord` round-trip (`internal/config/*_test.go`, `internal/storage/*_test.go`) — FR-022, FR-028, FR-029 -- [ ] T131 [US2] Add the four fields; update `OAuthConfigChanged`, `MergeOAuthConfig`, `copyOAuthConfig` (`internal/config/config.go`, `merge.go`) — FR-022, FR-028 -- [ ] T132 [P] [US2] Failing test: validation table — https ok; http loopback (`localhost`, `127.0.0.5`, `[::1]`) ok; http public host rejected; relative, fragment, userinfo, empty host rejected; load path drops invalid with Warn and boot succeeds (`internal/config/oauth_validation_test.go`) — FR-023 -- [ ] T133 [US2] Implement write-time validation (`ValidateDetailed`, REST, MCP `upstream_servers`, CLI) and load-time sanitize (`internal/config/oauth_validation.go`, write paths) — FR-023 -- [ ] T134 [P] [US2] Failing test: `OAuthTransportWrapper` rewrites AS metadata fields from overrides, output passes mcp-go URL validation; synthesizes a document when metadata is unreachable and authz+token overrides exist; leaves non-metadata responses untouched (`internal/oauth/transport_wrapper_test.go`) — FR-024 -- [ ] T135 [US2] Implement metadata rewrite/synthesis in the wrapper; pass `auth_server_metadata_url` to `OAuthConfig.AuthServerMetadataURL` (`internal/oauth/transport_wrapper.go`, `internal/oauth/config.go`) — FR-024 +- [x] T131 [US2] Add the four fields; update `OAuthConfigChanged`, `MergeOAuthConfig`, `copyOAuthConfig` (`internal/config/config.go`, `merge.go`) — FR-022, FR-028 +- [x] T132 [P] [US2] Failing test: validation table — https ok; http loopback (`localhost`, `127.0.0.5`, `[::1]`) ok; http public host rejected; relative, fragment, userinfo, empty host rejected; load path drops invalid with Warn and boot succeeds (`internal/config/oauth_validation_test.go`) — FR-023 +- [x] T133 [US2] Implement write-time validation (`ValidateDetailed`, REST, MCP `upstream_servers`, CLI) and load-time sanitize (`internal/config/oauth_validation.go`, write paths) — FR-023 +- [x] T134 [P] [US2] Failing test: `OAuthTransportWrapper` rewrites AS metadata fields from overrides, output passes mcp-go URL validation; synthesizes a document when metadata is unreachable and authz+token overrides exist; leaves non-metadata responses untouched (`internal/oauth/transport_wrapper_test.go`) — FR-024 +- [x] T135 [US2] Implement metadata rewrite/synthesis in the wrapper; pass `auth_server_metadata_url` to `OAuthConfig.AuthServerMetadataURL` (`internal/oauth/transport_wrapper.go`, `internal/oauth/config.go`) — FR-024 - [ ] T136 [P] [US2] Failing test: `extra_params` injected on a token endpoint `/oauth2/v1/exchange` learned from metadata; not on `/api/tokens/list`; heuristic still used when no endpoint known; 201→200 normalization follows the same match (`transport_wrapper_test.go`) — FR-025 -- [ ] T137 [US2] Implement endpoint-URL matching (`internal/oauth/transport_wrapper.go`) — FR-025 +- [x] T137 [US2] Implement endpoint-URL matching (`internal/oauth/transport_wrapper.go`) — FR-025 - [ ] T138 [P] [US2] Failing test: end-to-end with `httptest` AS on a non-standard metadata path + override; mcp-go authorization URL host equals override; token exchange and refresh hit the override (`internal/upstream/core/oauth_overrides_test.go`) — FR-024, FR-031 -- [ ] T139 [P] [US2] Failing test: discovery cache — 5 `createOAuthConfigInternal` calls → 1 request per discovery URL; different overrides → separate entries; failure cached 30 s then retried; LRU bound; invalidation on a differing `resource_metadata` (`internal/oauth/discovery_cache_test.go`) — FR-026, FR-027, SC-003 -- [ ] T140 [US2] Implement `discovery_cache.go` and route preflight discovery through it; invalidation hook from the 401 PRM-URL handling (`internal/oauth/discovery.go`, `discovery_cache.go`, `config.go`, `internal/upstream/core/connection.go` one call) — FR-026, FR-027 -- [ ] T141 [P] [US2] Failing test: scope waterfall — PRM scopes + AS advertises `offline_access` → appended once; explicit `oauth.scopes` → unchanged; AS-derived scopes → unchanged; empty → unchanged (`internal/oauth/config_test.go`) — FR-020 -- [ ] T142 [US2] Implement `offline_access` append (`internal/oauth/config.go`) — FR-020 -- [ ] T143 [P] [US2] Failing test: `grant_types_supported` without `refresh_token` → one Warn per server per process (zap observer); absent field → none (`internal/oauth/discovery_test.go`) — FR-021 -- [ ] T144 [US2] Implement the warning (`internal/oauth/discovery.go` or `config.go`) — FR-021 +- [x] T139 [P] [US2] Failing test: discovery cache — 5 `createOAuthConfigInternal` calls → 1 request per discovery URL; different overrides → separate entries; failure cached 30 s then retried; LRU bound; invalidation on a differing `resource_metadata` (`internal/oauth/discovery_cache_test.go`) — FR-026, FR-027, SC-003 +- [x] T140 [US2] Implement `discovery_cache.go` and route preflight discovery through it; invalidation hook from the 401 PRM-URL handling (`internal/oauth/discovery.go`, `discovery_cache.go`, `config.go`, `internal/upstream/core/connection.go` one call) — FR-026, FR-027 +- [x] T141 [P] [US2] Failing test: scope waterfall — PRM scopes + AS advertises `offline_access` → appended once; explicit `oauth.scopes` → unchanged; AS-derived scopes → unchanged; empty → unchanged (`internal/oauth/config_test.go`) — FR-020 +- [x] T142 [US2] Implement `offline_access` append (`internal/oauth/config.go`) — FR-020 +- [x] T143 [P] [US2] Failing test: `grant_types_supported` without `refresh_token` → one Warn per server per process (zap observer); absent field → none (`internal/oauth/discovery_test.go`) — FR-021 +- [x] T144 [US2] Implement the warning (`internal/oauth/discovery.go` or `config.go`) — FR-021 - [ ] T145 [US2] `ServerFieldMaskDecisions` rows `NotSecret` for the four fields; contracts OAuth projection; `cmd/generate-types` + regenerate `frontend/src/types/contracts.ts`; `make swagger`, commit `oas/` — FR-028 -- [ ] T150 [P] [US2] Failing test: OAuth client refuses https→http redirect to a non-loopback host and cross-origin redirect of a token/registration POST; same-origin redirect still works; with endpoint overrides and unreachable standard metadata mcp-go never hits `/authorize`, `/token`, `/register` defaults; override with a query string rejected at write time (`internal/oauth/transport_wrapper_test.go`, `internal/config/oauth_validation_test.go`) — FR-023, FR-024, FR-027a +- [x] T150 [P] [US2] Failing test: OAuth client refuses https→http redirect to a non-loopback host and cross-origin redirect of a token/registration POST; same-origin redirect still works; with endpoint overrides and unreachable standard metadata mcp-go never hits `/authorize`, `/token`, `/register` defaults; override with a query string rejected at write time (`internal/oauth/transport_wrapper_test.go`, `internal/config/oauth_validation_test.go`) — FR-023, FR-024, FR-027a - [ ] T146 [US2] `logSafeURL` everywhere a new URL is logged; test asserts no query string in logs (`internal/oauth`) — FR-030 -- [ ] T147 Docs: `docs/configuration.md` OAuth section (four fields, validation, precedence over discovery, `offline_access` rule, cache TTLs) +- [x] T147 Docs: `docs/configuration.md` OAuth section (four fields, validation, precedence over discovery, `offline_access` rule, cache TTLs) - [ ] T148 Gates (as T117) incl. server-tag build - [ ] T149 Cross-model review (opencode harness, gpt-6.1-sol), ≤10 rounds - T151–T159 reserved for 113-b follow-ups ## Slice 113-c — call-error-taxonomy (US3, G6) — T160–T189 -- [ ] T160 [P] [US3] Failing test: `callerr.Classify` table covering every FR-041 rule incl. `isError`, `ErrSessionTerminated`, `OAuthAuthorizationRequiredError`, recorded 401/403, `limiter.LimitError`, `managed.ErrConnectionGenerationChanged`, profile refusal, `jsonschema.ValidationError`, caller `context.Canceled`, `DeadlineExceeded` dispatched vs not, `*net.OpError`, mcp-go `*transport.Error`, recorded 502 with untyped mcp-go error, mcp-go `ErrInvalidParams`/`ErrInternalError`, `errors.New("custom -32001 message")` after a recorded 200, unknown (`internal/callerr/classify_test.go`) — FR-040, FR-041, FR-049 +- [x] T160 [P] [US3] Failing test: `callerr.Classify` table covering every FR-041 rule incl. `isError`, `ErrSessionTerminated`, `OAuthAuthorizationRequiredError`, recorded 401/403, `limiter.LimitError`, `managed.ErrConnectionGenerationChanged`, profile refusal, `jsonschema.ValidationError`, caller `context.Canceled`, `DeadlineExceeded` dispatched vs not, `*net.OpError`, mcp-go `*transport.Error`, recorded 502 with untyped mcp-go error, mcp-go `ErrInvalidParams`/`ErrInternalError`, `errors.New("custom -32001 message")` after a recorded 200, unknown (`internal/callerr/classify_test.go`) — FR-040, FR-041, FR-049 - [ ] T161 [US3] Implement `internal/callerr` — FR-040, FR-041 -- [ ] T162 [P] [US3] Failing test: `Outcome.AuditClass()` total mapping table (`internal/callerr/audit_test.go`) — FR-047 -- [ ] T163 [US3] Implement mapping; `audit_funnel.go` uses it so audit and activity agree (`internal/callerr`, `internal/server/audit_funnel.go`) — FR-047 -- [ ] T164 [P] [US3] Failing test: transport call recorder — status recorded for the request carrying the recorder ctx only; installed in every `CreateHTTPClient` branch (plain, static headers, trace, Retry-After, OAuth); concurrent calls do not cross-contaminate (`internal/transport/call_recorder_test.go`) — FR-042 +- [x] T162 [P] [US3] Failing test: `Outcome.AuditClass()` total mapping table (`internal/callerr/audit_test.go`) — FR-047 +- [x] T163 [US3] Implement mapping; `audit_funnel.go` uses it so audit and activity agree (`internal/callerr`, `internal/server/audit_funnel.go`) — FR-047 +- [x] T164 [P] [US3] Failing test: transport call recorder — status recorded for the request carrying the recorder ctx only; installed in every `CreateHTTPClient` branch (plain, static headers, trace, Retry-After, OAuth); concurrent calls do not cross-contaminate (`internal/transport/call_recorder_test.go`) — FR-042 - [ ] T165 [US3] Implement `WithCallRecorder` + round-tripper; install in `CreateHTTPClient` (`internal/transport/`) — FR-042 -- [ ] T166 [P] [US3] Failing test: `ActivityRecord` new fields omitempty; a pre-Spec-113 JSON fixture decodes byte-identically on re-encode (`internal/storage/activity_models_test.go`) — FR-043 -- [ ] T167 [US3] Add fields (`internal/storage/activity_models.go`) — FR-043 -- [ ] T168 [US3] Stamp fields at every tool-call activity write path (direct, `call_tool_*`, `code_execution` sub-calls, REST `/api/v1/tools/call`); recorder ctx installed at those dispatch sites (`internal/server/mcp.go`, `mcp_routing.go`, `mcp_code_execution.go`, `server.go`) — FR-044, FR-048 -- [ ] T169 [P] [US3] Failing integration test (SC-004): `httptest` Streamable HTTP upstream producing isError, 502 HTML, JSON-RPC -32001, reset, timeout, 404 session, 401; assert persisted record and `GET /api/v1/activity` JSON per class (`internal/server/call_error_taxonomy_test.go`) — SC-004 +- [x] T166 [P] [US3] Failing test: `ActivityRecord` new fields omitempty; a pre-Spec-113 JSON fixture decodes byte-identically on re-encode (`internal/storage/activity_models_test.go`) — FR-043 +- [x] T167 [US3] Add fields (`internal/storage/activity_models.go`) — FR-043 +- [x] T168 [US3] Stamp fields at every tool-call activity write path (direct, `call_tool_*`, `code_execution` sub-calls, REST `/api/v1/tools/call`); recorder ctx installed at those dispatch sites (`internal/server/mcp.go`, `mcp_routing.go`, `mcp_code_execution.go`, `server.go`) — FR-044, FR-048 +- [x] T169 [P] [US3] Failing integration test (SC-004): `httptest` Streamable HTTP upstream producing isError, 502 HTML, JSON-RPC -32001, reset, timeout, 404 session, 401; assert persisted record and `GET /api/v1/activity` JSON per class (`internal/server/call_error_taxonomy_test.go`) — SC-004 - [ ] T170 [US3] REST + SSE + contracts + `cmd/generate-types`; `ActivityFilter` `error_class`/`fault_domain`; `make swagger`, commit `oas/` (`internal/contracts/activity.go`, `internal/httpapi/activity.go`, `internal/runtime/activity_service.go`, `event_bus.go`) — FR-045, FR-046 - [ ] T171 [US3] CLI: `activity list|show -o json|yaml` fields; `--error-class`, `--fault-domain`; `activity show` table line (`cmd/mcpproxy/activity_cmd.go` + tests) — FR-045, FR-046 - [ ] T172 Docs: `docs/features/activity-log.md` + `docs/cli/activity-commands.md` (class table, fault domains, filters) From fd6790671797cff24b87388e72ce13ed313ff0a4 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Mon, 12 Oct 2026 05:33:45 +0000 Subject: [PATCH 2/6] chore(roadmap): regenerate ROADMAP.md after gardener checkbox sync --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index b13819da2..71f97a851 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1040,5 +1040,5 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—` | [110-catalog-popularity](./specs/110-catalog-popularity/) | `in-flight` | 19/23 (83%) | | [111-personal-docker-image](./specs/111-personal-docker-image/) | — | — | | [112-client-header-forwarding](./specs/112-client-header-forwarding/) | `shipped` | 38/40 (95%) | -| [113-upstream-resilience](./specs/113-upstream-resilience/) | `drafted` | 0/100 (0%) | +| [113-upstream-resilience](./specs/113-upstream-resilience/) | `in-flight` | 40/100 (40%) | | [115-mcp-credential-lifecycle](./specs/115-mcp-credential-lifecycle/) | `shipped` | 86/87 (99%) | From 5d56924982ac4346c273829c4bc5ad1ffa9d7d5c Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Mon, 19 Oct 2026 05:31:02 +0000 Subject: [PATCH 3/6] chore(specs): gardener checkbox sync --- specs/113-upstream-resilience/tasks.md | 40 ++++++++++---------- specs/116-config-commit-consistency/tasks.md | 16 ++++---- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/specs/113-upstream-resilience/tasks.md b/specs/113-upstream-resilience/tasks.md index ab269a07c..fde4a6a92 100644 --- a/specs/113-upstream-resilience/tasks.md +++ b/specs/113-upstream-resilience/tasks.md @@ -70,24 +70,24 @@ Format: `[ID] [P?] [Story] Description (files) — FR`. `[P]` = can run in paral - [x] T167 [US3] Add fields (`internal/storage/activity_models.go`) — FR-043 - [x] T168 [US3] Stamp fields at every tool-call activity write path (direct, `call_tool_*`, `code_execution` sub-calls, REST `/api/v1/tools/call`); recorder ctx installed at those dispatch sites (`internal/server/mcp.go`, `mcp_routing.go`, `mcp_code_execution.go`, `server.go`) — FR-044, FR-048 - [x] T169 [P] [US3] Failing integration test (SC-004): `httptest` Streamable HTTP upstream producing isError, 502 HTML, JSON-RPC -32001, reset, timeout, 404 session, 401; assert persisted record and `GET /api/v1/activity` JSON per class (`internal/server/call_error_taxonomy_test.go`) — SC-004 -- [ ] T170 [US3] REST + SSE + contracts + `cmd/generate-types`; `ActivityFilter` `error_class`/`fault_domain`; `make swagger`, commit `oas/` (`internal/contracts/activity.go`, `internal/httpapi/activity.go`, `internal/runtime/activity_service.go`, `event_bus.go`) — FR-045, FR-046 -- [ ] T171 [US3] CLI: `activity list|show -o json|yaml` fields; `--error-class`, `--fault-domain`; `activity show` table line (`cmd/mcpproxy/activity_cmd.go` + tests) — FR-045, FR-046 -- [ ] T172 Docs: `docs/features/activity-log.md` + `docs/cli/activity-commands.md` (class table, fault domains, filters) +- [x] T170 [US3] REST + SSE + contracts + `cmd/generate-types`; `ActivityFilter` `error_class`/`fault_domain`; `make swagger`, commit `oas/` (`internal/contracts/activity.go`, `internal/httpapi/activity.go`, `internal/runtime/activity_service.go`, `event_bus.go`) — FR-045, FR-046 +- [x] T171 [US3] CLI: `activity list|show -o json|yaml` fields; `--error-class`, `--fault-domain`; `activity show` table line (`cmd/mcpproxy/activity_cmd.go` + tests) — FR-045, FR-046 +- [x] T172 Docs: `docs/features/activity-log.md` + `docs/cli/activity-commands.md` (class table, fault domains, filters) - [ ] T173 Gates (as T117) - [ ] T174 Cross-model review (opencode harness, gpt-6.1-sol), ≤10 rounds - T175–T189 reserved for 113-c follow-ups ## Slice 113-d — health-call-failure-rate (US4, G7) — T190–T219 -- [ ] T190 [P] [US4] Failing test: `callstats.Window` with injected clock — bucket rotation, 5 min expiry, counted vs uncounted, dominant kind, concurrent `Record` under `-race` (`internal/upstream/callstats/window_test.go`) — FR-060, FR-066 +- [x] T190 [P] [US4] Failing test: `callstats.Window` with injected clock — bucket rotation, 5 min expiry, counted vs uncounted, dominant kind, concurrent `Record` under `-race` (`internal/upstream/callstats/window_test.go`) — FR-060, FR-066 - [ ] T191 [US4] Implement `Window`, `Registry` (drop on remove/rename) (`internal/upstream/callstats/`) — FR-060, FR-066 -- [ ] T192 [P] [US4] Failing test: local predicate table (or 113-c classifier if merged) — isError, policy refusal, limiter, generation refusal, validation, cancel, auth → uncounted; network, post-dispatch timeout, 5xx, JSON-RPC error → failure; success → counted ok (`internal/upstream/callstats/classify_test.go`) — FR-061 -- [ ] T193 [US4] Implement predicate (`internal/upstream/callstats/classify.go`) — FR-061 -- [ ] T194 [P] [US4] Failing test: both choke points record — `Manager.CallTool`/`CallToolOnEpoch` and `code_execution` dispatch (`internal/upstream/manager_callstats_test.go`, `internal/server/mcp_code_execution_callstats_test.go`) — FR-062 -- [ ] T195 [US4] Record in `Manager.callTool` and after `CallToolOnEpoch` in `mcp_code_execution.go`; expose `Manager.CallStats(server)` and `RecordCallOutcome` (`internal/upstream/manager.go`, `internal/server/mcp_code_execution.go`) — FR-062 -- [ ] T196 [P] [US4] Failing test: calculator precedence and boundaries table (quarantined, disabled, disconnected, error, OAuth expired, refresh failed, missing secret, retry stopped each win; 4/4 → healthy; 5 @ 50% → healthy; 5 @ 60% → degraded; summary text) (`internal/health/calculator_test.go`) — FR-063, FR-064, FR-067 -- [ ] T197 [US4] Add `CallsInWindow`, `CallFailuresInWindow` and the degraded branch with constants (`internal/health/calculator.go`) — FR-063, FR-064, FR-065 -- [ ] T198 [US4] Fill the inputs at all three construction sites from `Manager.CallStats` (`internal/runtime/runtime.go`, `internal/server/server.go`, `internal/server/mcp.go`) + a test that the three agree — FR-063 +- [x] T192 [P] [US4] Failing test: local predicate table (or 113-c classifier if merged) — isError, policy refusal, limiter, generation refusal, validation, cancel, auth → uncounted; network, post-dispatch timeout, 5xx, JSON-RPC error → failure; success → counted ok (`internal/upstream/callstats/classify_test.go`) — FR-061 +- [x] T193 [US4] Implement predicate (`internal/upstream/callstats/classify.go`) — FR-061 +- [x] T194 [P] [US4] Failing test: both choke points record — `Manager.CallTool`/`CallToolOnEpoch` and `code_execution` dispatch (`internal/upstream/manager_callstats_test.go`, `internal/server/mcp_code_execution_callstats_test.go`) — FR-062 +- [x] T195 [US4] Record in `Manager.callTool` and after `CallToolOnEpoch` in `mcp_code_execution.go`; expose `Manager.CallStats(server)` and `RecordCallOutcome` (`internal/upstream/manager.go`, `internal/server/mcp_code_execution.go`) — FR-062 +- [x] T196 [P] [US4] Failing test: calculator precedence and boundaries table (quarantined, disabled, disconnected, error, OAuth expired, refresh failed, missing secret, retry stopped each win; 4/4 → healthy; 5 @ 50% → healthy; 5 @ 60% → degraded; summary text) (`internal/health/calculator_test.go`) — FR-063, FR-064, FR-067 +- [x] T197 [US4] Add `CallsInWindow`, `CallFailuresInWindow` and the degraded branch with constants (`internal/health/calculator.go`) — FR-063, FR-064, FR-065 +- [x] T198 [US4] Fill the inputs at all three construction sites from `Manager.CallStats` (`internal/runtime/runtime.go`, `internal/server/server.go`, `internal/server/mcp.go`) + a test that the three agree — FR-063 - [ ] T199 [US4] Health-change SSE: verify the existing poll/diff emits on a failure-rate change; add debounce only if a push path is needed (record which in the PR) — FR-068 - [ ] T200 [P] [US4] Integration test (SC-005): `httptest` upstream failing 6/10 → `/api/v1/servers` health degraded; advance clock past window → healthy — SC-005 - [ ] T201 Docs: health status page (`docs/` health section) — new degraded reason, thresholds @@ -97,14 +97,14 @@ Format: `[ID] [P?] [Story] Description (files) — FR`. `[P]` = can run in paral ## Slice 113-e — session-terminated-reinit (US5, G8) — T220–T239 -- [ ] T220 [P] [US5] Failing test pinning mcp-go v1.0.0 behaviour: 404 on a non-initialize POST → `ErrSessionTerminated` and session id cleared; 404 with no session id → also `ErrSessionTerminated`; a second `Initialize` on the same client/transport stores the new session id (or document that it cannot) (`internal/upstream/core/session_reinit_mcpgo_test.go`) — FR-088 -- [ ] T221 [US5] Implement `core.Client.SessionSnapshot()` and `ReinitializeSession(ctx)` without touching `connectionEpoch` (`internal/upstream/core/session_reinit.go`) — FR-080, FR-081, FR-083 -- [ ] T222 [P] [US5] Failing test (SC-006): `httptest` Streamable HTTP server that forgets the session once; single call → 1 initialize, call succeeds, state stays Ready, counter = 1 (`internal/upstream/managed/session_reinit_test.go`) — FR-080, FR-084, FR-085 +- [x] T220 [P] [US5] Failing test pinning mcp-go v1.0.0 behaviour: 404 on a non-initialize POST → `ErrSessionTerminated` and session id cleared; 404 with no session id → also `ErrSessionTerminated`; a second `Initialize` on the same client/transport stores the new session id (or document that it cannot) (`internal/upstream/core/session_reinit_mcpgo_test.go`) — FR-088 +- [x] T221 [US5] Implement `core.Client.SessionSnapshot()` and `ReinitializeSession(ctx)` without touching `connectionEpoch` (`internal/upstream/core/session_reinit.go`) — FR-080, FR-081, FR-083 +- [x] T222 [P] [US5] Failing test (SC-006): `httptest` Streamable HTTP server that forgets the session once; single call → 1 initialize, call succeeds, state stays Ready, counter = 1 (`internal/upstream/managed/session_reinit_test.go`) — FR-080, FR-084, FR-085 - [ ] T223 [P] [US5] Failing test: 10 concurrent calls on the same terminated session → exactly 1 initialize, 10 retries, all succeed; callers seeing a newer session id retry without re-init (`session_reinit_test.go`) — FR-082 -- [ ] T231 [P] [US5] Failing test: write/destructive tool after 404 → re-init, no repeat, `session_terminated` error, next call succeeds; changed tool hash after re-init → no retry, differential update runs; health `Ping` 404 first → following call joins the re-init (no request sent without a session id); cached-tool-count `ListTools` 404 → re-init, no `Error` state (`internal/upstream/managed/session_reinit_test.go`) — FR-080–FR-083 -- [ ] T232 [P] [US5] Failing test: a pinned call waiting on the re-init gate is refused with `ErrConnectionGenerationChanged` when the re-list shows its tool changed; after a re-init the upstream's `notifications/tools/list_changed` on the GET stream is still received (or, if the listener cannot be restarted, the full reconnect path runs and no retry happens) (`internal/upstream/managed/session_reinit_test.go`) — FR-083a +- [x] T231 [P] [US5] Failing test: write/destructive tool after 404 → re-init, no repeat, `session_terminated` error, next call succeeds; changed tool hash after re-init → no retry, differential update runs; health `Ping` 404 first → following call joins the re-init (no request sent without a session id); cached-tool-count `ListTools` 404 → re-init, no `Error` state (`internal/upstream/managed/session_reinit_test.go`) — FR-080–FR-083 +- [x] T232 [P] [US5] Failing test: a pinned call waiting on the re-init gate is refused with `ErrConnectionGenerationChanged` when the re-list shows its tool changed; after a re-init the upstream's `notifications/tools/list_changed` on the GET stream is still received (or, if the listener cannot be restarted, the full reconnect path runs and no retry happens) (`internal/upstream/managed/session_reinit_test.go`) — FR-083a - [ ] T224 [P] [US5] Failing test: retry also 404 → call fails and existing error path runs; re-initialize fails → original path; modern protocol or empty pre-call session id → no retry; non-404 errors → no retry (`session_reinit_test.go`) — FR-080, FR-084, FR-086 -- [ ] T225 [US5] Implement single-flight re-init + retry-once in `managed.Client` call, `ListTools` leader and prompt paths; only the final error reaches `isConnectionError`; schedule async tool refresh after re-init (`internal/upstream/managed/client.go`, `prompts.go`) — FR-080–FR-084 +- [x] T225 [US5] Implement single-flight re-init + retry-once in `managed.Client` call, `ListTools` leader and prompt paths; only the final error reaches `isConnectionError`; schedule async tool refresh after re-init (`internal/upstream/managed/client.go`, `prompts.go`) — FR-080–FR-084 - [ ] T226 [US5] Info log + per-client counter in diagnostics JSON (Prometheus counter if a seam exists) — FR-085 - [ ] T227 [P] [US5] Test: `CallToolOnEpoch` pinned call succeeds after re-init (epoch unchanged) — FR-083 - [ ] T228 Docs: troubleshooting note on session loss behaviour @@ -114,10 +114,10 @@ Format: `[ID] [P?] [Story] Description (files) — FR`. `[P]` = can run in paral ## Slice 113-f — mcp-cache-hints (US6, G9) — T240–T249 -- [ ] T240 [P] [US6] Failing test: modern (2026-07-28) `tools/list` and `prompts/list` on `/mcp`, `/mcp/all`, `/mcp/code`, `/mcp/call`, `/mcp/p/` carry `ttlMs:0`, `cacheScope:"private"`; legacy responses carry neither (`internal/server/cache_hints_test.go`) — FR-090, FR-092, SC-007 -- [ ] T241 [US6] Add `mcpserver.WithCacheHints(0, mcp.CacheScopePrivate)` to the four `NewMCPServer` calls (`internal/server/mcp.go`, `mcp_routing.go`) — FR-090, FR-091 +- [x] T240 [P] [US6] Failing test: modern (2026-07-28) `tools/list` and `prompts/list` on `/mcp`, `/mcp/all`, `/mcp/code`, `/mcp/call`, `/mcp/p/` carry `ttlMs:0`, `cacheScope:"private"`; legacy responses carry neither (`internal/server/cache_hints_test.go`) — FR-090, FR-092, SC-007 +- [x] T241 [US6] Add `mcpserver.WithCacheHints(0, mcp.CacheScopePrivate)` to the four `NewMCPServer` calls (`internal/server/mcp.go`, `mcp_routing.go`) — FR-090, FR-091 - [ ] T242 [US6] Re-run the three frozen tool-surface golden tests; confirm unchanged — FR-094 -- [ ] T243 [US6] Tick T063/T064 in `specs/058-mcp-2026-upgrade/tasks.md` — FR-093 +- [x] T243 [US6] Tick T063/T064 in `specs/058-mcp-2026-upgrade/tasks.md` — FR-093 - [ ] T244 Gates (as T117) - [ ] T245 Cross-model review (opencode harness, gpt-6.1-sol), ≤10 rounds - T246–T249 reserved diff --git a/specs/116-config-commit-consistency/tasks.md b/specs/116-config-commit-consistency/tasks.md index bb5e4c86f..d8229165a 100644 --- a/specs/116-config-commit-consistency/tasks.md +++ b/specs/116-config-commit-consistency/tasks.md @@ -25,7 +25,7 @@ Branch `116-pr1-config-commit` from `main`. ### Setup - [ ] T001 [US1] Add test seams (nil in production): `commitHooks`, `reconcileEntry` (at the `LoadConfiguredServers` entry on `main`), `beforeCommit`, `beforeApprovalLock`, `discoveryBeforeIndex`, `restPatchAfterParse`, `createAfterConfigRead` (`config_cow.go:93`) -- [ ] T002 [P] [US1] Add `scripts/check-upstream-write-sites.sh` (allowlist: `internal/storage/**`, `internal/runtime/commit*.go`, `*_test.go`) and wire it into `.github/workflows/unit-tests.yml`. It is expected to FAIL until T030 +- [x] T002 [P] [US1] Add `scripts/check-upstream-write-sites.sh` (allowlist: `internal/storage/**`, `internal/runtime/commit*.go`, `*_test.go`) and wire it into `.github/workflows/unit-tests.yml`. It is expected to FAIL until T030 ### Tests first (must fail on `main`) @@ -36,8 +36,8 @@ Branch `116-pr1-config-commit` from `main`. - [ ] T014 [P] [US1] `internal/server/create_commit_race_test.go`: T1.5 (parked add vs ApplyConfig profiles edit) - [ ] T015 [P] [US1] `internal/server/mcp_quarantine_index_test.go`: T1.6 (B1: MCP `quarantine_server` purges the index before returning) - [ ] T016 [P] [US1] `internal/server/shared_authbroker_patch_test.go`: T1.7 (REST and MCP patch keep `Shared` and `AuthBroker`) -- [ ] T017 [P] [US1] `internal/runtime/configsvc/generations_test.go`: generation rule table (data-model §2), incarnation kept on update and reset on recreate, `SeedVersion` monotonic (T1.9) -- [ ] T018 [P] [US1] `internal/storage/server_commit_test.go`: `ApplyServerCommit` is all-or-nothing (inject an error on the 2nd row → no row changed), writes the meta generation, stamps Generation/Incarnation +- [x] T017 [P] [US1] `internal/runtime/configsvc/generations_test.go`: generation rule table (data-model §2), incarnation kept on update and reset on recreate, `SeedVersion` monotonic (T1.9) +- [x] T018 [P] [US1] `internal/storage/server_commit_test.go`: `ApplyServerCommit` is all-or-nothing (inject an error on the 2nd row → no row changed), writes the meta generation, stamps Generation/Incarnation - [ ] T018a [P] [US1] `internal/runtime/commit_bootstrap_test.go`: T1.10 (boot with an empty storage and manager repairs the projection and schedules every enabled server; an unchanged `SaveConfiguration` still writes the file; restart keeps generations of unchanged rows) - [ ] T018b [P] [US1] `internal/runtime/commit_removal_exclusion_test.go`: T1.11 (formerly T2.3; regression coverage that a parked removal blocks a B toggle, REST update and MCP patch, which then get 404) - [ ] T018c [P] [US1] `internal/runtime/discovery_purge_race_test.go`: T1.12 (a pass parked after its approval check vs `QuarantineServer(true)` → no re-index) @@ -52,11 +52,11 @@ Branch `116-pr1-config-commit` from `main`. ### Implementation -- [ ] T020 [US1] `internal/config/server_entry_equal.go`: `ServerEntryEqual` (ignores Created/Updated; includes the explicit bit, Shared, AuthBroker) with a table test +- [x] T020 [US1] `internal/config/server_entry_equal.go`: `ServerEntryEqual` (ignores Created/Updated; includes the explicit bit, Shared, AuthBroker) with a table test - [ ] T020a [US1] `internal/commitview` leaf package: `ServerGen{Gen, Incarnation}` (PR2 adds `OAuthEpoch`, `CredentialBinding`, `Binding()`), `ErrStaleGeneration`; `TestCommitviewIsLeaf` (`go list -deps`, no module-internal import). `configsvc.ServerGen` and `upstream.ErrStaleGeneration` are aliases (review R7.2) - [ ] T021 [US1] `configsvc`: remove `SetPrePublishHook` (admission moves to commit step 2, R1.7); `ServerGen` (alias of `commitview.ServerGen`), `Snapshot.ServerGenerations`, `Prepare(PrepareInput{Config, Source, Replaced})`/`Publish`/`SeedVersion`; the replaced-name row of the generation table checked first, and `Changes` computed in `Prepare` (review R8.2). Re-express `Update`/`UpdateIfCurrent` on top. Remove `ReloadFromFile`'s raw publish (makes T017 pass) - [ ] T022 [US1] storage: `UpstreamRecord.Generation/Incarnation`, `ApplyServerCommit` (one tx; `Saves []ServerSave{Config, Stamp}` written with each row's prepared stamp verbatim, zero or above-`G` stamps rejected, review R9.3; saves through the quarantine-keeping invariant; deletes as today: row + call history; `Replaces` deleted before `Saves`, any other delete/save overlap an invariant error, review R8.2), meta key (makes T018 pass) -- [ ] T023 [US1] `internal/runtime/commit.go`: `Commit`, `CommitTx` ops (with the `replaced` bookkeeping and the `Replaced` change kind, makes T018f pass; review R8.2), pipeline steps 1-6 and 8 plus after-unlock (plan §Commit pipeline). Approval locks taken in sorted order in step 4 for every name step 6 writes (changed names, hook names, removes, replacements, OAuth clears and projection-repair saves; every configured server on a global approval-policy change; plan.md step 4, PR1 review rounds 4-5), with `UnderApprovalLock` hooks. Compensating disk restore on a storage failure: capture the raw file state `{exists, bytes, mode}` inside L0 before step 5 and restore **that** (same bytes and mode, or remove the file when it was absent), never a config rebuilt from memory, with the self-write marker bookkeeping (forget the failed write's marker, record one for the restored bytes) and `desiredCfg` untouched (makes T018e and T018g pass; reviews R5.5, R9.2). The batch's per-save stamps are copied from `PreparedSnapshot.ServerGenerations` and asserted equal before `ApplyServerCommit` (makes T018i pass; review R9.3). Re-entry guard +- [x] T023 [US1] `internal/runtime/commit.go`: `Commit`, `CommitTx` ops (with the `replaced` bookkeeping and the `Replaced` change kind, makes T018f pass; review R8.2), pipeline steps 1-6 and 8 plus after-unlock (plan §Commit pipeline). Approval locks taken in sorted order in step 4 for every name step 6 writes (changed names, hook names, removes, replacements, OAuth clears and projection-repair saves; every configured server on a global approval-policy change; plan.md step 4, PR1 review rounds 4-5), with `UnderApprovalLock` hooks. Compensating disk restore on a storage failure: capture the raw file state `{exists, bytes, mode}` inside L0 before step 5 and restore **that** (same bytes and mode, or remove the file when it was absent), never a config rebuilt from memory, with the self-write marker bookkeeping (forget the failed write's marker, record one for the restored bytes) and `desiredCfg` untouched (makes T018e and T018g pass; reviews R5.5, R9.2). The batch's per-save stamps are copied from `PreparedSnapshot.ServerGenerations` and asserted equal before `ApplyServerCommit` (makes T018i pass; review R9.3). Re-entry guard - [ ] T024 [US1] `internal/runtime/commit_reconcile.go`: move `LoadConfiguredServers`' storage phase into step 6 (diff against the stored view; OAuth-config-change clear; newly-quarantined index purge, FR-008; orphan GC on the working copy, with the empty guard at boot only). Replace the boot call (`lifecycle.go:206`) with `Commit("boot", ReconcileLive + Bootstrap)`; `Bootstrap()` anywhere else is an invariant error, and every other reconcile is `ReconcileLive()` alone (makes T018j pass; review R9.6). `LoadConfiguredServers` is deleted once T026/T027 have converted its other callers. Delete the removal goroutines (removal is synchronous in step 6) - [ ] T024a [US1] D15: move a discovery pass's final eligibility/generation check and all its index, StateView and last-good writes into one hold of L1 (coordinate with Spec 117 PR-3 G8; skip if already landed) (makes T018c pass). `ReconcileLive()`; delete `LoadConfiguredServers(cfg)`'s captured-argument form (D14) - [ ] T024b [US1] `checkToolApprovalsHeld` / `deleteToolApprovalHeld` caller-held variants and the inventory of helpers inside the D15 hold; `resolveToolQuarantineGateHeld` reads the row + `meta.quarantine_enabled` (written by the commit tx) (D16) (makes T018d pass) @@ -65,12 +65,12 @@ Branch `116-pr1-config-commit` from `main`. - [ ] T026 [US1] `ApplyConfig`/`applyConfigLocked`, `ReloadConfiguration`, `UpdateConfig`, `UpdateListenAddress`, `SaveConfiguration` (persist the live config, no storage read), `gateInitialConfig`, `SetServerShared` → `Commit` (makes T011 pass) - [ ] T027 [US1] `EnableServer`, `BulkEnableServers`, `QuarantineServer` (index purge in the commit; baseline approval after the commit, which keeps the `CommitResult` and passes its incarnation on rather than recapturing by name; PR2 T053 adds the check, review R9.5), `RestartServer`'s disk refresh → `Commit` (makes T013 pass) - [ ] T028 [US1] `internal/server`: `createServer` → `Commit(CreateServer)` and delete `createMu` (the mutex only: the #1570 create-only behaviour and test assertions stay, plan.md `createMu` note); `removeServerStorage` → `Commit(RemoveServer)` (PR1 deletes as today); `UpdateServer` → `Commit(UpdateServer)` with the in-place mutation removed; registry source add/edit/remove → `Commit(MutateGlobal)` (makes T014 and T016 pass) -- [ ] T029 [US1] `internal/server/mcp.go`: update/patch merge inside `UpdateServer`; the result's connection status comes from the generation's worker result (bounded wait, as today): the update returns the server's committed generation and the monitor skips any registered instance stamped below it, so the replaced instance's ready/error state is never reported (PR1 review round 6); quarantine → `Commit(SetQuarantined)`; remove → `Commit(RemoveServer)` (makes T015 and the MCP half of T012 pass) +- [x] T029 [US1] `internal/server/mcp.go`: update/patch merge inside `UpdateServer`; the result's connection status comes from the generation's worker result (bounded wait, as today): the update returns the server's committed generation and the monitor skips any registered instance stamped below it, so the replaced instance's ready/error state is never reported (PR1 review round 6); quarantine → `Commit(SetQuarantined)`; remove → `Commit(RemoveServer)` (makes T015 and the MCP half of T012 pass) - [ ] T029a [US1] If Spec 117 PR-2 has merged: convert `CommitServerApproval`/`FinishServerApproval` to `Commit(ExpectIncarnation(PR2: generation until then), SetQuarantined(false), UnderApprovalLock(...))` with its tests unchanged. Otherwise: add `UnderApprovalLock` with a unit test (hook runs under the lock; a hook error aborts with nothing written) -- [ ] T030 [US1] `internal/httpapi/server.go` PATCH: move omitted-field, map-merge, isolation and forward-header resolution into a `func(cur *config.ServerConfig) error` passed down to `UpdateServer` (makes T012 pass; T002 now passes) +- [x] T030 [US1] `internal/httpapi/server.go` PATCH: move omitted-field, map-merge, isolation and forward-header resolution into a `func(cur *config.ServerConfig) error` passed down to `UpdateServer` (makes T012 pass; T002 now passes) - [ ] T031 [US1] `TestCommit_*` + `TestLockOrder_*` at `-race -count=20` with `GOMAXPROCS=1` and `4`; the CI skip lane under `-tags server`; both lint passes; `./scripts/test-api-e2e.sh` - [ ] T032 [US1] Live L1 on the qa070 harness ([quickstart.md](quickstart.md)); evidence in `evidence/s116-pr1/EVIDENCE.md` -- [ ] T033 [US1] Docs: `docs/architecture.md` "Config commit" section (pipeline + lock order); CLAUDE.md Recent Changes line +- [x] T033 [US1] Docs: `docs/architecture.md` "Config commit" section (pipeline + lock order); CLAUDE.md Recent Changes line - [ ] T034 [US1] opencode Sol review in chunks (commit/storage; writers; httpapi/mcp). Fix in-scope findings only **Checkpoint**: every server writer goes through `Commit`, and storage no From 7b43ddec6c63ca90cf77727c8a27dd334784425d Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Mon, 19 Oct 2026 05:31:05 +0000 Subject: [PATCH 4/6] chore(roadmap): regenerate ROADMAP.md after gardener checkbox sync --- ROADMAP.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 11aea3f5a..05583172c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1027,7 +1027,7 @@ graph LR | SDK v1 migration | Todo (parked) | P3 | — | | | | Upgrade awareness & guided update | Done | P0 | — | [079-upgrade-nudge](./specs/079-upgrade-nudge/) | | | Connect step trust: preview, visible backup, one-click undo | Done | P0 | — | [078-connect-trust-preview](./specs/078-connect-trust-preview/) | | -| Spec 116 config commit consistency: one commit section, atomic removal, upstream retirement | Done | P0 | 81/124 (65%) | [116-config-commit-consistency](./specs/116-config-commit-consistency/) | #1590 #1600 #1608 #1612 #1621 | +| Spec 116 config commit consistency: one commit section, atomic removal, upstream retirement | Done | P0 | 89/124 (72%) | [116-config-commit-consistency](./specs/116-config-commit-consistency/) | #1590 #1600 #1608 #1612 #1621 | | Spec 117 review-bound approvals: durable baselines, hash-bound approvals, ordered tool inventories | Done | P0 | 46/50 (92%) | [117-review-bound-approvals](./specs/117-review-bound-approvals/) | #1604 #1605 #1609 | | Non-Docker sandbox isolation (Landlock) `MCP-34` | Done | P1 | — | | | | Spec 076 deterministic offline tool-scanner `MCP-3574` | Done | P1 | 22/24 (92%) | [076-deterministic-tool-scanner](./specs/076-deterministic-tool-scanner/) | | @@ -1036,7 +1036,7 @@ graph LR | Scanner simplification (deterministic default, opt-in deep scan) | Done | P1 | 38/42 (90%) | [077-scanner-simplification](./specs/077-scanner-simplification/) | | | Deferred-schema serialization for the direct tools/list surface (spec 102) | Done | P1 | 89/89 (100%) | [102-schema-deferred](./specs/102-schema-deferred/) | #1063 | | Agent-token scope hardening: every MCP request authorized by its own scope (spec 105) | Done | P1 | 111/113 (98%) | [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | | -| Spec 113 upstream resilience: OAuth refresh, discovery, call-error taxonomy, session re-init | Done | P1 | 0/100 (0%) | [113-upstream-resilience](./specs/113-upstream-resilience/) | #1495 #1498 #1499 #1501 #1502 #1504 #1510 #1650 | +| Spec 113 upstream resilience: OAuth refresh, discovery, call-error taxonomy, session re-init | Done | P1 | 60/100 (60%) | [113-upstream-resilience](./specs/113-upstream-resilience/) | #1495 #1498 #1499 #1501 #1502 #1504 #1510 #1650 | | Spec 115 admin MCP credentials tool: client credentials + agent token lifecycle | Done | P1 | 86/87 (99%) | [115-mcp-credential-lifecycle](./specs/115-mcp-credential-lifecycle/) | #1557 | | Quarantine/approval testing pilot: independent policy model, scenario inventory, mutation gate | Done | P1 | — | | #1619 #1631 #1634 | | Tray↔core decoupling: socket/REST API only, no config-file reads | Done | P2 | — | | | @@ -1168,7 +1168,7 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—` | [110-catalog-popularity](./specs/110-catalog-popularity/) | `in-flight` | 19/23 (83%) | | [111-personal-docker-image](./specs/111-personal-docker-image/) | — | — | | [112-client-header-forwarding](./specs/112-client-header-forwarding/) | `shipped` | 38/40 (95%) | -| [113-upstream-resilience](./specs/113-upstream-resilience/) | `in-flight` | 40/100 (40%) | +| [113-upstream-resilience](./specs/113-upstream-resilience/) | `in-flight` | 60/100 (60%) | | [115-mcp-credential-lifecycle](./specs/115-mcp-credential-lifecycle/) | `shipped` | 86/87 (99%) | -| [116-config-commit-consistency](./specs/116-config-commit-consistency/) | `in-flight` | 81/124 (65%) | +| [116-config-commit-consistency](./specs/116-config-commit-consistency/) | `in-flight` | 89/124 (72%) | | [117-review-bound-approvals](./specs/117-review-bound-approvals/) | `in-flight` | 46/50 (92%) | From ac22202319bd34464bd1faac2ebc11f41d576809 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Mon, 2 Nov 2026 05:23:13 +0000 Subject: [PATCH 5/6] chore(specs): gardener checkbox sync --- specs/058-mcp-2026-upgrade/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/058-mcp-2026-upgrade/tasks.md b/specs/058-mcp-2026-upgrade/tasks.md index b98680c56..27200a864 100644 --- a/specs/058-mcp-2026-upgrade/tasks.md +++ b/specs/058-mcp-2026-upgrade/tasks.md @@ -155,7 +155,7 @@ Single Go project. Backend paths are repo-relative: `internal/`, `cmd/`, `bench/ - [x] T063 [P] [US5] Write failing tests that list/read responses carry `ttlMs` and an identity-appropriate `cacheScope`, in `internal/server/cache_hints_test.go` - [x] T064 [US5] Configure cache hints — private for identity-scoped surfaces, public for shared ones — in `internal/server/mcp_routing.go` and `internal/server/mcp.go` -- [ ] T065 [P] [US5] Write a failing test that repeated `tools/list` calls are byte-identical under unchanged state, in `internal/server/stateless_list_test.go` +- [x] T065 [P] [US5] Write a failing test that repeated `tools/list` calls are byte-identical under unchanged state, in `internal/server/stateless_list_test.go` - [ ] T066 [US5] Make tool ordering deterministic wherever map iteration currently leaks in, in `internal/server/mcp_routing.go` - [ ] T067 [P] [US5] Write a failing test that external `$ref`s are not dereferenced and that schemas validate as JSON Schema 2020-12, in `internal/server/mcp_input_validation_test.go` - [ ] T068 [US5] Install a rejecting loader so no external `$ref` is fetched, and confirm the dialect default, in `internal/server/mcp_input_validation.go` From f3a63ca7699cf82296ab3ccdd4ef9216201ce0ce Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Mon, 2 Nov 2026 05:23:21 +0000 Subject: [PATCH 6/6] chore(roadmap): regenerate ROADMAP.md after gardener checkbox sync --- ROADMAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 575c8e46b..3d91f4871 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1012,7 +1012,7 @@ graph LR | Token-efficiency benchmark: measured savings, published results | In progress | P1 | 62/64 (97%) | [103-token-bench](./specs/103-token-bench/) | | | Telemetry identity & data quality (machine_id + CI-filter hardening) | In progress | P1 | — | | | | Telemetry v7: honest funnel + churn instrumentation | In progress | P1 | — | [080-telemetry-v7-churn](./specs/080-telemetry-v7-churn/) | | -| MCP protocol upgrade to 2026-07-28 revision | In progress | P1 | 40/81 (49%) | [058-mcp-2026-upgrade](./specs/058-mcp-2026-upgrade/) | | +| MCP protocol upgrade to 2026-07-28 revision | In progress | P1 | 41/81 (51%) | [058-mcp-2026-upgrade](./specs/058-mcp-2026-upgrade/) | | | Spec 108 profiles v3: scoped profiles, client binding, scope-aware observability | In progress | P1 | 194/195 (99%) | [108-profiles-v3](./specs/108-profiles-v3/) | | | Spec 109 navigation, scope filters and cross-surface UX consistency | In progress | P1 | 257/258 (100%) | [109-ux-navigation-consistency](./specs/109-ux-navigation-consistency/) | | | Planning/docs truth automation | In progress | P2 | — | | | @@ -1122,7 +1122,7 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—` | [055-frontend-major-upgrades](./specs/055-frontend-major-upgrades/) | `shipped` | 23/24 (96%) | | [056-output-schema-validation](./specs/056-output-schema-validation/) | `in-flight` | 22/24 (92%) | | [057-in-proxy-profiles](./specs/057-in-proxy-profiles/) | `in-flight` | 20/25 (80%) | -| [058-mcp-2026-upgrade](./specs/058-mcp-2026-upgrade/) | `in-flight` | 40/81 (49%) | +| [058-mcp-2026-upgrade](./specs/058-mcp-2026-upgrade/) | `in-flight` | 41/81 (51%) | | [059-output-sanitisation](./specs/059-output-sanitisation/) | `shipped` | 24/25 (96%) | | [060-settings-page](./specs/060-settings-page/) | `shipped` | 16/16 (100%) | | [064-glass-cockpit](./specs/064-glass-cockpit/) | — | — |