From 186c11f5408ef84773ac322e71c55cf0eb4f2b78 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Wed, 28 Oct 2026 17:24:19 -0400 Subject: [PATCH] fix(server): checking a rotated secret signs its provider back in Signed-off-by: Yordis Prieto --- .../src/provider/ProviderRegistry.test.ts | 6 ++++-- apps/server/src/provider/ProviderRegistry.ts | 21 ++++++++++--------- docs/user/provider-secrets.md | 4 +++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts index 209f683c4fd2e..c5a8ac9105413 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -1495,11 +1495,12 @@ it.layer( }), ); - it.effect("recovers the unavailable instances a successful secret check unlocks", () => + it.effect("rebuilds every instance that reads a secret a check succeeds on", () => Effect.gen(function* () { const codexDriver = ProviderDriverKind.make("codex"); const readableId = ProviderInstanceId.make("codex"); const otherId = ProviderInstanceId.make("codex_other"); + const liveId = ProviderInstanceId.make("codex_live"); const reference = "op://Vault/Item/token"; const unavailableProvider = (instanceId: ProviderInstanceId) => ({ @@ -1550,6 +1551,7 @@ it.layer( otherId, decodeEnvironment([onePasswordVariable("CODEX_TOKEN", "op://Vault/Other/token")]), ], + [liveId, decodeEnvironment([onePasswordVariable("CODEX_TOKEN", reference)])], ]), ), subscribeChanges: Effect.flatMap(PubSub.unbounded(), PubSub.subscribe), @@ -1581,7 +1583,7 @@ it.layer( const result = yield* registry.checkSecretSource(source); assert.deepStrictEqual(result, { _tag: "Resolved" }); - assert.deepStrictEqual(yield* Ref.get(rebuiltIds), [readableId]); + assert.deepStrictEqual(yield* Ref.get(rebuiltIds), [readableId, liveId]); assert.strictEqual(yield* Ref.get(invalidations), 0); }).pipe(Effect.provide(runtimeServices)); }), diff --git a/apps/server/src/provider/ProviderRegistry.ts b/apps/server/src/provider/ProviderRegistry.ts index a655dcc8831d3..8975a43aa2e8b 100644 --- a/apps/server/src/provider/ProviderRegistry.ts +++ b/apps/server/src/provider/ProviderRegistry.ts @@ -95,8 +95,8 @@ export class ProviderRegistry extends Context.Service< }) => Effect.Effect>; /** - * Settings "check" for one secret source. A readable source also rebuilds the unavailable - * instances that were waiting on it, from the value the check just cached. + * Settings "check" for one secret source. A readable source also rebuilds every instance + * that reads it, from the value the check just cached. */ readonly checkSecretSource: ( source: ProviderSecretSource, @@ -894,17 +894,18 @@ export const layer = Layer.effect( if (result._tag !== "Resolved" || reference === undefined) { return result; } - // The check left the value in the resolver's cache, so these rebuilds - // read it from memory and never reach the store again. + // Live instances are targets too: they read the secret at create time, so a + // rotated value only reaches them through a rebuild. The check left the + // value in the resolver's cache, so these rebuilds never reach the store again. const environments = yield* instanceRegistry.listEnvironments; - const waiting = (yield* instanceRegistry.listUnavailable) - .map(snapshotInstanceKey) - .filter((instanceId) => - collectProviderSecretReferences([environments.get(instanceId)]).some((candidate) => + const consumers = Array.from(environments) + .filter(([, environment]) => + collectProviderSecretReferences([environment]).some((candidate) => Equal.equals(candidate, reference), ), - ); - yield* rebuildSecretBackedInstances(waiting); + ) + .map(([instanceId]) => instanceId); + yield* rebuildSecretBackedInstances(consumers); return result; }); diff --git a/docs/user/provider-secrets.md b/docs/user/provider-secrets.md index 964dd1784ecd3..f6ce5dfdddd33 100644 --- a/docs/user/provider-secrets.md +++ b/docs/user/provider-secrets.md @@ -104,7 +104,7 @@ does. ## I Rotated The Secret, How Do I Pick Up The New One -Refresh provider status in Settings. +Refresh provider status in Settings, or use the check button on the variable that reads it. Refresh drops everything that was held in memory and rebuilds the providers that use references, so the next read goes back to the secret manager. Providers with only literal variables are left alone. @@ -113,6 +113,8 @@ The 1Password CLI's own cache is never used, so a refresh cannot be answered wit it also means T3 Code never starts the CLI's background `op daemon`, which can hang behind a macOS permission prompt when the server runs in the background. +The check button reads only that one reference and rebuilds only the providers that use it. + Changing a provider under Settings, Secrets also makes the variables that use it read again. Threads that are already running keep the process they were given. Work started after the refresh