Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/server/src/provider/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
({
Expand Down Expand Up @@ -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<void>(), PubSub.subscribe),
Expand Down Expand Up @@ -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));
}),
Expand Down
21 changes: 11 additions & 10 deletions apps/server/src/provider/ProviderRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ export class ProviderRegistry extends Context.Service<
}) => Effect.Effect<ReadonlyArray<ServerProvider>>;

/**
* 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,
Expand Down Expand Up @@ -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;
});

Expand Down
4 changes: 3 additions & 1 deletion docs/user/provider-secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
Loading