From ad2e047ced6f007948e9098d1eb503e2a3932032 Mon Sep 17 00:00:00 2001 From: nullStack65 Date: Thu, 1 Oct 2026 23:03:12 -0400 Subject: [PATCH 1/5] feat(contracts): add managed launch identity envelope --- packages/contracts/src/index.ts | 1 + .../contracts/src/providerIdentity.test.ts | 71 +++++++++++ packages/contracts/src/providerIdentity.ts | 119 ++++++++++++++++++ packages/contracts/src/server.ts | 5 + 4 files changed, 196 insertions(+) create mode 100644 packages/contracts/src/providerIdentity.test.ts create mode 100644 packages/contracts/src/providerIdentity.ts diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 978a0459e69b..89a01fe02f2a 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -14,6 +14,7 @@ export * from "./ipc.ts"; export * from "./terminal.ts"; export * from "./provider.ts"; export * from "./providerInstance.ts"; +export * from "./providerIdentity.ts"; export * from "./providerSetup.ts"; export * from "./providerRuntime.ts"; export * from "./providerUsageLimits.ts"; diff --git a/packages/contracts/src/providerIdentity.test.ts b/packages/contracts/src/providerIdentity.test.ts new file mode 100644 index 000000000000..7c20022e8ae3 --- /dev/null +++ b/packages/contracts/src/providerIdentity.test.ts @@ -0,0 +1,71 @@ +import * as Schema from "effect/Schema"; +import { describe, expect, it } from "vite-plus/test"; +import { + compareManagedProviderLaunchIdentity, + ManagedProviderLaunchIdentity, + qualifyManagedProviderLaunch, +} from "./providerIdentity.ts"; +import { ServerProvider } from "./server.ts"; + +const host = { environmentId: "env-a", providerInstanceId: "codex" } as const; +const keyIds = [{ namespace: "provider.example", id: "key-a" }] as const; +const identity = { + subject: "subject-a", + keyIds: [...keyIds], + providerHostInstance: host, +} satisfies typeof ManagedProviderLaunchIdentity.Type; + +describe("managed provider launch identity", () => { + it("keeps old provider snapshots compatible when optional identity fields are absent", () => { + const provider = Schema.decodeUnknownSync(ServerProvider)({ + instanceId: "codex", + driver: "codex", + enabled: true, + installed: true, + version: "1.0.0", + status: "ready", + auth: { status: "authenticated" }, + checkedAt: "2026-01-01T00:00:00.000Z", + models: [], + }); + expect(provider.auth.keyIds).toBeUndefined(); + expect(provider.providerHostInstance).toBeUndefined(); + }); + + it("fails closed when any managed-launch component is unavailable", () => { + expect(qualifyManagedProviderLaunch({ keyIds, providerHostInstance: host })).toEqual({ + qualified: false, + reason: "missing-subject", + }); + expect( + qualifyManagedProviderLaunch({ subject: "subject-a", providerHostInstance: host }), + ).toEqual({ + qualified: false, + reason: "missing-key-id", + }); + expect(qualifyManagedProviderLaunch({ subject: "subject-a", keyIds })).toEqual({ + qualified: false, + reason: "missing-host-identity", + }); + }); + + it("compares namespace-qualified key sets and reports exact drift", () => { + expect(compareManagedProviderLaunchIdentity(identity, identity)).toBe("match"); + expect( + compareManagedProviderLaunchIdentity(identity, { ...identity, subject: "subject-b" }), + ).toBe("subject-drift"); + expect( + compareManagedProviderLaunchIdentity(identity, { + ...identity, + keyIds: [{ namespace: "provider.example", id: "key-b" }], + }), + ).toBe("key-id-drift"); + expect( + compareManagedProviderLaunchIdentity(identity, { + ...identity, + providerHostInstance: { ...host, environmentId: "env-b" }, + }), + ).toBe("host-drift"); + expect(compareManagedProviderLaunchIdentity(identity, undefined)).toBe("unknown-identity"); + }); +}); diff --git a/packages/contracts/src/providerIdentity.ts b/packages/contracts/src/providerIdentity.ts new file mode 100644 index 000000000000..0ad2b9fc2086 --- /dev/null +++ b/packages/contracts/src/providerIdentity.ts @@ -0,0 +1,119 @@ +/** + * Managed-launch identity is deliberately separate from ordinary provider + * auth metadata. Host identity is derived from the persisted environment + * and configured instance; key identifiers are accepted only when a provider + * supplies a real public identifier. No local value is a substitute. + */ +import * as Schema from "effect/Schema"; +import { EnvironmentId } from "./environment.ts"; +import { TrimmedNonEmptyString } from "./baseSchemas.ts"; +import { ProviderInstanceId } from "./providerInstance.ts"; + +export const ProviderKeyIdentifier = Schema.Struct({ + /** Provider or issuer namespace; IDs from different namespaces never match. */ + namespace: TrimmedNonEmptyString, + /** Opaque provider-issued public identifier. Never a token, digest, or UUID fallback. */ + id: TrimmedNonEmptyString, +}); +export type ProviderKeyIdentifier = typeof ProviderKeyIdentifier.Type; + +export const ProviderHostInstanceIdentity = Schema.Struct({ + /** Persisted T3 environment identity. A new state directory is a new host. */ + environmentId: EnvironmentId, + /** Configured provider routing identity within that environment. */ + providerInstanceId: ProviderInstanceId, +}); +export type ProviderHostInstanceIdentity = typeof ProviderHostInstanceIdentity.Type; + +/** Derive the structured host identity from the two existing persisted values. */ +export const makeProviderHostInstanceIdentity = (input: { + readonly environmentId: EnvironmentId; + readonly providerInstanceId: ProviderInstanceId; +}): ProviderHostInstanceIdentity => ({ + environmentId: input.environmentId, + providerInstanceId: input.providerInstanceId, +}); + +export const ManagedProviderLaunchIdentity = Schema.Struct({ + /** Non-secret authenticated principal from the verified session. */ + subject: TrimmedNonEmptyString, + /** One or more provider-issued public IDs; omission is not qualification. */ + keyIds: Schema.Array(ProviderKeyIdentifier), + providerHostInstance: ProviderHostInstanceIdentity, +}); +export type ManagedProviderLaunchIdentity = typeof ManagedProviderLaunchIdentity.Type; + +export const ManagedProviderLaunchNotQualifiedReason = Schema.Literals([ + "missing-subject", + "missing-key-id", + "missing-host-identity", +]); +export type ManagedProviderLaunchNotQualifiedReason = + typeof ManagedProviderLaunchNotQualifiedReason.Type; + +export type ManagedProviderLaunchQualification = + | { readonly qualified: true; readonly identity: ManagedProviderLaunchIdentity } + | { + readonly qualified: false; + readonly reason: ManagedProviderLaunchNotQualifiedReason; + }; + +/** + * Project the three independent launch inputs without manufacturing any + * identity. This is intentionally fail-closed when the provider has no real + * key ID or when the authenticated session is anonymous. Session/access-token + * and DPoP-key rotation are not drift when the subject/key ID is unchanged; + * provider key replacement, subject replacement, or host changes are drift. + */ +export const qualifyManagedProviderLaunch = (input: { + readonly subject?: string | undefined; + readonly keyIds?: ReadonlyArray | undefined; + readonly providerHostInstance?: ProviderHostInstanceIdentity | undefined; +}): ManagedProviderLaunchQualification => { + const subject = input.subject?.trim(); + if (!subject) return { qualified: false, reason: "missing-subject" }; + if (!input.keyIds || input.keyIds.length === 0) { + return { qualified: false, reason: "missing-key-id" }; + } + if (!input.providerHostInstance) { + return { qualified: false, reason: "missing-host-identity" }; + } + return { + qualified: true, + identity: { + subject, + keyIds: [...input.keyIds], + providerHostInstance: input.providerHostInstance, + }, + }; +}; + +export type ManagedProviderLaunchDrift = + | "subject-drift" + | "key-id-drift" + | "host-drift" + | "unknown-identity"; + +const keyIdSet = (keyIds: ReadonlyArray): Set => + new Set(keyIds.map((keyId) => `${keyId.namespace}\u0000${keyId.id}`)); + +const equalSets = (left: Set, right: Set): boolean => + left.size === right.size && [...left].every((value) => right.has(value)); + +/** Exact comparison used by re-authorization/rotation gates. */ +export const compareManagedProviderLaunchIdentity = ( + expected: ManagedProviderLaunchIdentity | undefined, + actual: ManagedProviderLaunchIdentity | undefined, +): "match" | ManagedProviderLaunchDrift => { + if (!expected || !actual) return "unknown-identity"; + if (expected.subject !== actual.subject) return "subject-drift"; + if (!equalSets(keyIdSet(expected.keyIds), keyIdSet(actual.keyIds))) return "key-id-drift"; + if ( + expected.providerHostInstance.environmentId !== actual.providerHostInstance.environmentId || + expected.providerHostInstance.providerInstanceId !== + actual.providerHostInstance.providerInstanceId + ) { + return "host-drift"; + } + return "match"; +}; diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index 49a377208799..954441c58982 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -24,6 +24,7 @@ import { import { EditorId, FileManagerRevealKind, RemoteOpenTarget } from "./editor.ts"; import { ModelCapabilities } from "./model.ts"; import { ProviderDriverKind, ProviderInstanceId } from "./providerInstance.ts"; +import { ProviderHostInstanceIdentity, ProviderKeyIdentifier } from "./providerIdentity.ts"; import { ServerProviderUsageLimits, UsageLimitSourceSnapshots } from "./providerUsageLimits.ts"; import { ServerSettings } from "./settings.ts"; @@ -63,6 +64,8 @@ export const ServerProviderAuth = Schema.Struct({ type: Schema.optional(TrimmedNonEmptyString), label: Schema.optional(TrimmedNonEmptyString), email: Schema.optional(TrimmedNonEmptyString), + /** Present only when the provider supplies a real stable public key ID. */ + keyIds: Schema.optional(Schema.Array(ProviderKeyIdentifier)), }); export type ServerProviderAuth = typeof ServerProviderAuth.Type; @@ -214,6 +217,8 @@ export const ServerProvider = Schema.Struct({ version: Schema.NullOr(TrimmedNonEmptyString), status: ServerProviderState, auth: ServerProviderAuth, + /** Optional additive envelope; older clients omit and continue to decode. */ + providerHostInstance: Schema.optional(ProviderHostInstanceIdentity), checkedAt: IsoDateTime, message: Schema.optional(TrimmedNonEmptyString), // Optional for back-compat: every legacy producer omits this field and From 02a358a2ee7972867797e81ab032923c37e519ec Mon Sep 17 00:00:00 2001 From: nullStack65 Date: Thu, 1 Oct 2026 23:09:00 -0400 Subject: [PATCH 2/5] fix(contracts): harden managed identity comparison --- .../contracts/src/providerIdentity.test.ts | 22 +++++++- packages/contracts/src/providerIdentity.ts | 52 ++++++++++++++++--- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/packages/contracts/src/providerIdentity.test.ts b/packages/contracts/src/providerIdentity.test.ts index 7c20022e8ae3..767d9ef006e5 100644 --- a/packages/contracts/src/providerIdentity.test.ts +++ b/packages/contracts/src/providerIdentity.test.ts @@ -7,6 +7,7 @@ import { } from "./providerIdentity.ts"; import { ServerProvider } from "./server.ts"; +const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider); const host = { environmentId: "env-a", providerInstanceId: "codex" } as const; const keyIds = [{ namespace: "provider.example", id: "key-a" }] as const; const identity = { @@ -17,7 +18,7 @@ const identity = { describe("managed provider launch identity", () => { it("keeps old provider snapshots compatible when optional identity fields are absent", () => { - const provider = Schema.decodeUnknownSync(ServerProvider)({ + const provider = decodeServerProvider({ instanceId: "codex", driver: "codex", enabled: true, @@ -68,4 +69,23 @@ describe("managed provider launch identity", () => { ).toBe("host-drift"); expect(compareManagedProviderLaunchIdentity(identity, undefined)).toBe("unknown-identity"); }); + + it("does not collide on embedded NULs and normalizes duplicate IDs", () => { + const first = { + ...identity, + keyIds: [ + { namespace: "a\u0000b", id: "c" }, + { namespace: "a\u0000b", id: "c" }, + ], + }; + const second = { + ...identity, + keyIds: [{ namespace: "a", id: "b\u0000c" }], + }; + expect(compareManagedProviderLaunchIdentity(first, second)).toBe("key-id-drift"); + expect(qualifyManagedProviderLaunch(first).qualified).toBe(true); + if (qualifyManagedProviderLaunch(first).qualified) { + expect(qualifyManagedProviderLaunch(first).identity.keyIds).toHaveLength(1); + } + }); }); diff --git a/packages/contracts/src/providerIdentity.ts b/packages/contracts/src/providerIdentity.ts index 0ad2b9fc2086..6647c04b0913 100644 --- a/packages/contracts/src/providerIdentity.ts +++ b/packages/contracts/src/providerIdentity.ts @@ -5,8 +5,7 @@ * supplies a real public identifier. No local value is a substitute. */ import * as Schema from "effect/Schema"; -import { EnvironmentId } from "./environment.ts"; -import { TrimmedNonEmptyString } from "./baseSchemas.ts"; +import { EnvironmentId, TrimmedNonEmptyString } from "./baseSchemas.ts"; import { ProviderInstanceId } from "./providerInstance.ts"; export const ProviderKeyIdentifier = Schema.Struct({ @@ -82,7 +81,9 @@ export const qualifyManagedProviderLaunch = (input: { qualified: true, identity: { subject, - keyIds: [...input.keyIds], + // Key IDs are a set by contract. Sorting and deduplicating makes the + // projection deterministic without treating duplicates as new keys. + keyIds: normalizeKeyIds(input.keyIds), providerHostInstance: input.providerHostInstance, }, }; @@ -94,11 +95,46 @@ export type ManagedProviderLaunchDrift = | "host-drift" | "unknown-identity"; -const keyIdSet = (keyIds: ReadonlyArray): Set => - new Set(keyIds.map((keyId) => `${keyId.namespace}\u0000${keyId.id}`)); +type KeyIdSet = ReadonlyMap>; -const equalSets = (left: Set, right: Set): boolean => - left.size === right.size && [...left].every((value) => right.has(value)); +const normalizeKeyIds = ( + keyIds: ReadonlyArray, +): ReadonlyArray => { + const sorted = [...keyIds].sort((left, right) => + left.namespace === right.namespace + ? left.id.localeCompare(right.id) + : left.namespace.localeCompare(right.namespace), + ); + return sorted.filter( + (keyId, index) => + index === 0 || + keyId.namespace !== sorted[index - 1]?.namespace || + keyId.id !== sorted[index - 1]?.id, + ); +}; + +// Keep namespace and ID as separate map keys. A delimiter-joined string would +// make tuples containing that delimiter collide and could hide key rotation. +const keyIdSet = (keyIds: ReadonlyArray): KeyIdSet => { + const byNamespace = new Map>(); + for (const keyId of keyIds) { + const ids = byNamespace.get(keyId.namespace) ?? new Set(); + ids.add(keyId.id); + byNamespace.set(keyId.namespace, ids); + } + return byNamespace; +}; + +const equalKeyIdSets = (left: KeyIdSet, right: KeyIdSet): boolean => { + if (left.size !== right.size) return false; + for (const [namespace, ids] of left) { + const otherIds = right.get(namespace); + if (!otherIds || ids.size !== otherIds.size || [...ids].some((id) => !otherIds.has(id))) { + return false; + } + } + return true; +}; /** Exact comparison used by re-authorization/rotation gates. */ export const compareManagedProviderLaunchIdentity = ( @@ -107,7 +143,7 @@ export const compareManagedProviderLaunchIdentity = ( ): "match" | ManagedProviderLaunchDrift => { if (!expected || !actual) return "unknown-identity"; if (expected.subject !== actual.subject) return "subject-drift"; - if (!equalSets(keyIdSet(expected.keyIds), keyIdSet(actual.keyIds))) return "key-id-drift"; + if (!equalKeyIdSets(keyIdSet(expected.keyIds), keyIdSet(actual.keyIds))) return "key-id-drift"; if ( expected.providerHostInstance.environmentId !== actual.providerHostInstance.environmentId || expected.providerHostInstance.providerInstanceId !== From 321db04f88bc4c96e4141a47e96546f3a9d0f66c Mon Sep 17 00:00:00 2001 From: nullStack65 Date: Thu, 1 Oct 2026 23:17:08 -0400 Subject: [PATCH 3/5] feat(server): stamp managed provider host identity --- .../provider/Layers/ProviderRegistry.test.ts | 54 +++++++++++++++---- .../src/provider/Layers/ProviderRegistry.ts | 52 ++++++++++++++---- .../contracts/src/providerIdentity.test.ts | 8 +++ packages/contracts/src/providerIdentity.ts | 12 +++-- 4 files changed, 104 insertions(+), 22 deletions(-) diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index caee1981d79f..95637cfd034b 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -19,6 +19,7 @@ import { ClaudeSettings, CodexSettings, DEFAULT_SERVER_SETTINGS, + EnvironmentId, ProviderDriverKind, ProviderInstanceId, ServerSettings, @@ -45,9 +46,11 @@ import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistr import { mergeProviderSnapshot, upsertProviderWorkspaceSnapshot, + stampProviderHostInstance, ProviderRegistryLive, } from "./ProviderRegistry.ts"; import * as ServerConfig from "../../config.ts"; +import * as ServerEnvironment from "../../environment/ServerEnvironment.ts"; import * as ServerSettingsModule from "../../serverSettings.ts"; import { readProviderStatusCache, @@ -63,6 +66,30 @@ const decodeServerSettings = Schema.decodeSync(ServerSettings); const encodeServerSettings = Schema.encodeSync(ServerSettings); const encodedDefaultServerSettings = encodeServerSettings(DEFAULT_SERVER_SETTINGS); +const withProviderHost = (provider: Provider) => ({ + ...provider, + providerHostInstance: { + environmentId: "provider-registry-test", + providerInstanceId: provider.instanceId, + }, +}); + +describe("provider host identity stamping", () => { + it("uses the persisted environment plus instance and never continuation metadata", () => { + const provider = { + instanceId: ProviderInstanceId.make("codex_work"), + continuation: { groupKey: "codex:home:/shared" }, + } as ServerProvider; + assert.deepStrictEqual(stampProviderHostInstance(provider, EnvironmentId.make("env-a")), { + ...provider, + providerHostInstance: { + environmentId: "env-a", + providerInstanceId: "codex_work", + }, + }); + }); +}); + const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({}); const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({}); const decodeCodexSettings = Schema.decodeSync(CodexSettings); @@ -361,7 +388,16 @@ const awaitPersistedProvider = ( Effect.forkScoped, ); -it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))( +it.layer( + Layer.mergeAll( + NodeServices.layer, + ServerSettingsModule.layerTest(), + TestHttpClientLive, + Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, { + getEnvironmentId: Effect.succeed(EnvironmentId.make("provider-registry-test")), + }), + ), +)( "ProviderRegistry", (it) => { describe("checkCodexProviderStatus", () => { @@ -1422,7 +1458,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te ).pipe(Scope.provide(scope)); yield* Effect.gen(function* () { const registry = yield* ProviderRegistry.ProviderRegistry; - assert.deepStrictEqual(yield* registry.getProviders, [initialProvider]); + assert.deepStrictEqual(yield* registry.getProviders, [withProviderHost(initialProvider)]); assert.strictEqual(yield* Ref.get(refreshCalls), 0); }).pipe(Effect.provide(runtimeServices)); }), @@ -1753,7 +1789,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te ); assert.deepStrictEqual( recoveredProviders.find((provider) => provider.instanceId === codexInstanceId), - codexProvider, + withProviderHost(codexProvider), ); yield* Ref.set(catalogSnapshot, changedCatalogProvider); @@ -1765,7 +1801,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te ); assert.deepStrictEqual( changedProviders.find((provider) => provider.instanceId === codexInstanceId), - codexProvider, + withProviderHost(codexProvider), ); }).pipe(Effect.provide(runtimeServices)); @@ -1880,7 +1916,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te const cachedProvider = yield* readProviderStatusCache(filePath); assert.deepStrictEqual(cachedProvider, { - ...refreshedProvider, + ...withProviderHost(refreshedProvider), models: [...initialProvider.models], }); }).pipe(Effect.provide(runtimeServices)); @@ -2094,10 +2130,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te yield* Effect.gen(function* () { const registry = yield* ProviderRegistry.ProviderRegistry; - assert.deepStrictEqual(yield* registry.getProviders, [cachedProvider]); - assert.deepStrictEqual(yield* registry.refresh(codexDriver), [cachedProvider]); + assert.deepStrictEqual(yield* registry.getProviders, [withProviderHost(cachedProvider)]); + assert.deepStrictEqual(yield* registry.refresh(codexDriver), [withProviderHost(cachedProvider)]); assert.deepStrictEqual(yield* registry.refreshInstance(codexInstanceId), [ - cachedProvider, + withProviderHost(cachedProvider), ]); }).pipe(Effect.provide(runtimeServices)); }), @@ -2205,7 +2241,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te yield* Effect.gen(function* () { const registry = yield* ProviderRegistry.ProviderRegistry; - assert.deepStrictEqual(yield* registry.getProviders, [codexProvider]); + assert.deepStrictEqual(yield* registry.getProviders, [withProviderHost(codexProvider)]); yield* Ref.set(failNextList, true); yield* PubSub.publish(changes, undefined); diff --git a/apps/server/src/provider/Layers/ProviderRegistry.ts b/apps/server/src/provider/Layers/ProviderRegistry.ts index a8e6caf95aa7..42e296afe618 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.ts @@ -24,6 +24,8 @@ */ import { defaultInstanceIdForDriver, + type EnvironmentId, + makeProviderHostInstanceIdentity, ProviderDriverKind, type ProviderInstanceId, type ServerProvider, @@ -41,6 +43,7 @@ import * as Stream from "effect/Stream"; import * as Semaphore from "effect/Semaphore"; import { ServerConfig } from "../../config.ts"; +import * as ServerEnvironment from "../../environment/ServerEnvironment.ts"; import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts"; import { ProviderRegistry, type ProviderRegistryShape } from "../Services/ProviderRegistry.ts"; import { @@ -57,12 +60,15 @@ import type { ProviderSnapshotSource } from "../builtInProviderCatalog.ts"; const loadProviders = ( providerSources: ReadonlyArray, + environmentId: EnvironmentId, ): Effect.Effect> => Effect.forEach( providerSources, (providerSource) => providerSource.getSnapshot.pipe( - Effect.flatMap((snapshot) => correlateSnapshotWithSource(providerSource, snapshot)), + Effect.flatMap((snapshot) => + correlateSnapshotWithSource(providerSource, snapshot, environmentId), + ), ), { concurrency: "unbounded", @@ -231,9 +237,22 @@ const haveProvidersChanged = ( nextProviders: ReadonlyArray, ): boolean => !Equal.equals(previousProviders, nextProviders); +/** Stamp the persisted environment and configured instance at the registry boundary. */ +export const stampProviderHostInstance = ( + provider: ServerProvider, + environmentId: EnvironmentId, +): ServerProvider => ({ + ...provider, + providerHostInstance: makeProviderHostInstanceIdentity({ + environmentId, + providerInstanceId: provider.instanceId, + }), +}); + const correlateSnapshotWithSource = ( source: ProviderSnapshotSource, snapshot: ServerProvider, + environmentId: EnvironmentId, ): Effect.Effect => { if (snapshot.instanceId !== source.instanceId) { return Effect.die( @@ -249,7 +268,10 @@ const correlateSnapshotWithSource = ( ), ); } - return Effect.succeed(snapshot); + // Provider drivers only know their configured instance. The registry owns + // the persisted environment identity and stamps the pair centrally so + // unavailable, cached, and live snapshots use the same host contract. + return Effect.succeed(stampProviderHostInstance(snapshot, environmentId)); }; /** @@ -278,6 +300,9 @@ export const ProviderRegistryLive = Layer.effect( ProviderRegistry, Effect.gen(function* () { const instanceRegistry = yield* ProviderInstanceRegistry; + const environmentId = yield* ServerEnvironment.ServerEnvironmentIdentity.pipe( + Effect.flatMap((identity) => identity.getEnvironmentId), + ); const config = yield* ServerConfig; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -296,7 +321,7 @@ export const ProviderRegistryLive = Layer.effect( // below. const bootInstances = yield* instanceRegistry.listInstances; const bootSources = bootInstances.map(buildSnapshotSource); - const fallbackProviders = yield* loadProviders(bootSources); + const fallbackProviders = yield* loadProviders(bootSources, environmentId); const fallbackByInstance = new Map(); for (let index = 0; index < fallbackProviders.length; index++) { const provider = fallbackProviders[index]; @@ -526,7 +551,7 @@ export const ProviderRegistryLive = Layer.effect( ) { return yield* providerSource.refresh.pipe( Effect.flatMap((nextProvider) => - correlateSnapshotWithSource(providerSource, nextProvider).pipe( + correlateSnapshotWithSource(providerSource, nextProvider, environmentId).pipe( Effect.flatMap(syncProvider), ), ), @@ -669,7 +694,9 @@ export const ProviderRegistryLive = Layer.effect( for (const [, instance] of newlyAdded) { const source = buildSnapshotSource(instance); yield* Stream.runForEach(source.streamChanges, (provider) => - correlateSnapshotWithSource(source, provider).pipe(Effect.flatMap(syncProvider)), + correlateSnapshotWithSource(source, provider, environmentId).pipe( + Effect.flatMap(syncProvider), + ), ).pipe(Effect.forkScoped); } yield* Effect.yieldNow; @@ -684,16 +711,21 @@ export const ProviderRegistryLive = Layer.effect( Effect.gen(function* () { const source = buildSnapshotSource(instance); const provider = yield* source.getSnapshot; - yield* correlateSnapshotWithSource(source, provider).pipe( + yield* correlateSnapshotWithSource(source, provider, environmentId).pipe( Effect.flatMap(syncProvider), ); }).pipe(Effect.ignoreCause({ log: true })), { concurrency: "unbounded", discard: true }, ); - yield* upsertProviders(unavailableProviders, { - persist: false, - replace: true, - }); + yield* upsertProviders( + unavailableProviders.map((provider) => + stampProviderHostInstance(provider, environmentId), + ), + { + persist: false, + replace: true, + }, + ); const nextSubs = new Map(carriedOver); for (const [instanceId, instance] of newlyAdded) { diff --git a/packages/contracts/src/providerIdentity.test.ts b/packages/contracts/src/providerIdentity.test.ts index 767d9ef006e5..e59323489baf 100644 --- a/packages/contracts/src/providerIdentity.test.ts +++ b/packages/contracts/src/providerIdentity.test.ts @@ -8,6 +8,7 @@ import { import { ServerProvider } from "./server.ts"; const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider); +const decodeManagedProviderLaunchIdentity = Schema.decodeUnknownSync(ManagedProviderLaunchIdentity); const host = { environmentId: "env-a", providerInstanceId: "codex" } as const; const keyIds = [{ namespace: "provider.example", id: "key-a" }] as const; const identity = { @@ -48,6 +49,13 @@ describe("managed provider launch identity", () => { qualified: false, reason: "missing-host-identity", }); + expect(() => + decodeManagedProviderLaunchIdentity({ + subject: "subject-a", + keyIds: [], + providerHostInstance: host, + }), + ).toThrow(); }); it("compares namespace-qualified key sets and reports exact drift", () => { diff --git a/packages/contracts/src/providerIdentity.ts b/packages/contracts/src/providerIdentity.ts index 6647c04b0913..38d1befc7e6b 100644 --- a/packages/contracts/src/providerIdentity.ts +++ b/packages/contracts/src/providerIdentity.ts @@ -37,7 +37,7 @@ export const ManagedProviderLaunchIdentity = Schema.Struct({ /** Non-secret authenticated principal from the verified session. */ subject: TrimmedNonEmptyString, /** One or more provider-issued public IDs; omission is not qualification. */ - keyIds: Schema.Array(ProviderKeyIdentifier), + keyIds: Schema.Array(ProviderKeyIdentifier).check(Schema.isNonEmpty()), providerHostInstance: ProviderHostInstanceIdentity, }); export type ManagedProviderLaunchIdentity = typeof ManagedProviderLaunchIdentity.Type; @@ -102,8 +102,14 @@ const normalizeKeyIds = ( ): ReadonlyArray => { const sorted = [...keyIds].sort((left, right) => left.namespace === right.namespace - ? left.id.localeCompare(right.id) - : left.namespace.localeCompare(right.namespace), + ? left.id < right.id + ? -1 + : left.id > right.id + ? 1 + : 0 + : left.namespace < right.namespace + ? -1 + : 1, ); return sorted.filter( (keyId, index) => From fba4bb5ca0b38b7b7d45614d5eb080f1dbeb9dcc Mon Sep 17 00:00:00 2001 From: nullStack65 Date: Thu, 1 Oct 2026 23:42:17 -0400 Subject: [PATCH 4/5] test(contracts): use branded managed identity fixtures --- packages/contracts/src/providerIdentity.test.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/contracts/src/providerIdentity.test.ts b/packages/contracts/src/providerIdentity.test.ts index e59323489baf..9ddafcea5183 100644 --- a/packages/contracts/src/providerIdentity.test.ts +++ b/packages/contracts/src/providerIdentity.test.ts @@ -6,10 +6,15 @@ import { qualifyManagedProviderLaunch, } from "./providerIdentity.ts"; import { ServerProvider } from "./server.ts"; +import { EnvironmentId } from "./baseSchemas.ts"; +import { ProviderInstanceId } from "./providerInstance.ts"; const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider); const decodeManagedProviderLaunchIdentity = Schema.decodeUnknownSync(ManagedProviderLaunchIdentity); -const host = { environmentId: "env-a", providerInstanceId: "codex" } as const; +const host = { + environmentId: EnvironmentId.make("env-a"), + providerInstanceId: ProviderInstanceId.make("codex"), +}; const keyIds = [{ namespace: "provider.example", id: "key-a" }] as const; const identity = { subject: "subject-a", @@ -72,7 +77,7 @@ describe("managed provider launch identity", () => { expect( compareManagedProviderLaunchIdentity(identity, { ...identity, - providerHostInstance: { ...host, environmentId: "env-b" }, + providerHostInstance: { ...host, environmentId: EnvironmentId.make("env-b") }, }), ).toBe("host-drift"); expect(compareManagedProviderLaunchIdentity(identity, undefined)).toBe("unknown-identity"); @@ -91,9 +96,10 @@ describe("managed provider launch identity", () => { keyIds: [{ namespace: "a", id: "b\u0000c" }], }; expect(compareManagedProviderLaunchIdentity(first, second)).toBe("key-id-drift"); - expect(qualifyManagedProviderLaunch(first).qualified).toBe(true); - if (qualifyManagedProviderLaunch(first).qualified) { - expect(qualifyManagedProviderLaunch(first).identity.keyIds).toHaveLength(1); + const qualification = qualifyManagedProviderLaunch(first); + expect(qualification.qualified).toBe(true); + if (qualification.qualified) { + expect(qualification.identity.keyIds).toHaveLength(1); } }); }); From 74d6facafd91621616a63402e3947a39f0243c53 Mon Sep 17 00:00:00 2001 From: nullStack65 Date: Fri, 2 Oct 2026 00:28:51 -0400 Subject: [PATCH 5/5] test(server): isolate provider identity fixture --- .../provider/Layers/ProviderRegistry.test.ts | 63 ++++++++----------- .../Layers/ProviderRegistryIdentity.test.ts | 20 ++++++ 2 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 apps/server/src/provider/Layers/ProviderRegistryIdentity.test.ts diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index 95637cfd034b..b5cabeeeb1dc 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -46,7 +46,6 @@ import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistr import { mergeProviderSnapshot, upsertProviderWorkspaceSnapshot, - stampProviderHostInstance, ProviderRegistryLive, } from "./ProviderRegistry.ts"; import * as ServerConfig from "../../config.ts"; @@ -66,30 +65,16 @@ const decodeServerSettings = Schema.decodeSync(ServerSettings); const encodeServerSettings = Schema.encodeSync(ServerSettings); const encodedDefaultServerSettings = encodeServerSettings(DEFAULT_SERVER_SETTINGS); -const withProviderHost = (provider: Provider) => ({ +const withProviderHost = ( + provider: Provider, +) => ({ ...provider, providerHostInstance: { - environmentId: "provider-registry-test", - providerInstanceId: provider.instanceId, + environmentId: EnvironmentId.make("provider-registry-test"), + providerInstanceId: ProviderInstanceId.make(provider.instanceId), }, }); -describe("provider host identity stamping", () => { - it("uses the persisted environment plus instance and never continuation metadata", () => { - const provider = { - instanceId: ProviderInstanceId.make("codex_work"), - continuation: { groupKey: "codex:home:/shared" }, - } as ServerProvider; - assert.deepStrictEqual(stampProviderHostInstance(provider, EnvironmentId.make("env-a")), { - ...provider, - providerHostInstance: { - environmentId: "env-a", - providerInstanceId: "codex_work", - }, - }); - }); -}); - const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({}); const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({}); const decodeCodexSettings = Schema.decodeSync(CodexSettings); @@ -104,11 +89,16 @@ process.env.T3CODE_CURSOR_ENABLED = "1"; const encoder = new TextEncoder(); const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z"); -const TestHttpClientLive = Layer.succeed( - HttpClient.HttpClient, - HttpClient.make((request) => - Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }))), +const TestHttpClientLive = Layer.mergeAll( + Layer.succeed( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }))), + ), ), + Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, { + getEnvironmentId: Effect.succeed(EnvironmentId.make("provider-registry-test")), + }), ); const BackgroundPolicyAlwaysRunLayer = Layer.mock(BackgroundPolicy.BackgroundPolicy)({ @@ -388,16 +378,7 @@ const awaitPersistedProvider = ( Effect.forkScoped, ); -it.layer( - Layer.mergeAll( - NodeServices.layer, - ServerSettingsModule.layerTest(), - TestHttpClientLive, - Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, { - getEnvironmentId: Effect.succeed(EnvironmentId.make("provider-registry-test")), - }), - ), -)( +it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))( "ProviderRegistry", (it) => { describe("checkCodexProviderStatus", () => { @@ -1458,7 +1439,9 @@ it.layer( ).pipe(Scope.provide(scope)); yield* Effect.gen(function* () { const registry = yield* ProviderRegistry.ProviderRegistry; - assert.deepStrictEqual(yield* registry.getProviders, [withProviderHost(initialProvider)]); + assert.deepStrictEqual(yield* registry.getProviders, [ + withProviderHost(initialProvider), + ]); assert.strictEqual(yield* Ref.get(refreshCalls), 0); }).pipe(Effect.provide(runtimeServices)); }), @@ -1801,7 +1784,7 @@ it.layer( ); assert.deepStrictEqual( changedProviders.find((provider) => provider.instanceId === codexInstanceId), - withProviderHost(codexProvider), + withProviderHost(codexProvider), ); }).pipe(Effect.provide(runtimeServices)); @@ -2130,8 +2113,12 @@ it.layer( yield* Effect.gen(function* () { const registry = yield* ProviderRegistry.ProviderRegistry; - assert.deepStrictEqual(yield* registry.getProviders, [withProviderHost(cachedProvider)]); - assert.deepStrictEqual(yield* registry.refresh(codexDriver), [withProviderHost(cachedProvider)]); + assert.deepStrictEqual(yield* registry.getProviders, [ + withProviderHost(cachedProvider), + ]); + assert.deepStrictEqual(yield* registry.refresh(codexDriver), [ + withProviderHost(cachedProvider), + ]); assert.deepStrictEqual(yield* registry.refreshInstance(codexInstanceId), [ withProviderHost(cachedProvider), ]); diff --git a/apps/server/src/provider/Layers/ProviderRegistryIdentity.test.ts b/apps/server/src/provider/Layers/ProviderRegistryIdentity.test.ts new file mode 100644 index 000000000000..259825d7eece --- /dev/null +++ b/apps/server/src/provider/Layers/ProviderRegistryIdentity.test.ts @@ -0,0 +1,20 @@ +import { assert, describe, it } from "@effect/vitest"; +import { EnvironmentId, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts"; + +import { stampProviderHostInstance } from "./ProviderRegistry.ts"; + +describe("provider host identity stamping", () => { + it("uses the persisted environment plus instance and never continuation metadata", () => { + const provider = { + instanceId: ProviderInstanceId.make("codex_work"), + continuation: { groupKey: "codex:home:/shared" }, + } as ServerProvider; + assert.deepStrictEqual(stampProviderHostInstance(provider, EnvironmentId.make("env-a")), { + ...provider, + providerHostInstance: { + environmentId: EnvironmentId.make("env-a"), + providerInstanceId: ProviderInstanceId.make("codex_work"), + }, + }); + }); +});