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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
CommandId,
MessageId,
ProjectId,
ProviderDriverKind,
ProviderInstanceId,
ThreadId,
type OrchestrationV2ThreadProjection,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -60,8 +62,11 @@ const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
const layerCheckpointStore = CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry));

const layerServerSettings = ServerSettings.layerTest({
providers: {
cursor: { enabled: true },
providerInstances: {
[ProviderInstanceId.make("cursor")]: {
driver: ProviderDriverKind.make("cursor"),
enabled: true,
},
},
});
const layerBackgroundPolicy = BackgroundPolicy.layer.pipe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
MessageId,
type OrchestrationV2ThreadProjection,
ProjectId,
ProviderDriverKind,
ProviderInstanceId,
ThreadId,
} from "@t3tools/contracts";
import * as Console from "effect/Console";
Expand Down Expand Up @@ -59,8 +61,11 @@ const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
const layerCheckpointStore = CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry));

const layerServerSettings = ServerSettings.layerTest({
providers: {
grok: { enabled: true },
providerInstances: {
[ProviderInstanceId.make("grok")]: {
driver: ProviderDriverKind.make("grok"),
enabled: true,
},
},
});
const layerBackgroundPolicy = BackgroundPolicy.layer.pipe(
Expand Down
20 changes: 13 additions & 7 deletions apps/server/src/project/AgentSessionScanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,19 @@ const layerScannerTest = (input: ScannerTestInput) =>
Layer.provide(
Layer.mergeAll(
ServerSettings.layerTest({
providers: {
claudeAgent: { homePath: input.claudeHomePath },
codex: { homePath: input.codexHomePath },
// Default slots point at the fixture homes; explicit test instances
// replace a slot with the same id.
providerInstances: {
[ProviderInstanceId.make("claudeAgent")]: {
driver: ProviderDriverKind.make("claudeAgent"),
config: { homePath: input.claudeHomePath },
},
[ProviderInstanceId.make("codex")]: {
driver: ProviderDriverKind.make("codex"),
config: { homePath: input.codexHomePath },
},
...input.providerInstances,
},
...(input.providerInstances === undefined
? {}
: { providerInstances: input.providerInstances }),
}),
ServerConfig.layerTest(
input.claudeHomePath,
Expand Down Expand Up @@ -533,7 +539,7 @@ it.layer(NodeServices.layer)("AgentSessionScanner", (it) => {
}),
);

it.effect("uses explicit provider instance homes instead of overridden legacy homes", () =>
it.effect("uses explicit default-slot homes instead of the fixture homes", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const claudeHomePath = yield* makeTempDir("t3code-claude-legacy-");
Expand Down
13 changes: 6 additions & 7 deletions apps/server/src/project/AgentSessionScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1102,16 +1102,15 @@ export const make = Effect.gen(function* () {
instanceId: ProviderInstanceId.make(instanceId),
config,
}));
// The built-in default instance runs with default config when settings
// have no entry for it.
if (!Object.hasOwn(settings.providerInstances, source)) {
const legacyInstance = {
const defaultInstance = {
instanceId: ProviderInstanceId.make(source),
config: {
driver: ProviderDriverKind.make(source),
config: settings.providers[source],
},
config: { driver: ProviderDriverKind.make(source) },
};
if (resolveProviderInstanceEnabled(legacyInstance.config)) {
instances.push(legacyInstance);
if (resolveProviderInstanceEnabled(defaultInstance.config)) {
instances.push(defaultInstance);
}
}

Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/Drivers/AcpRegistryDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ export const AcpRegistryDriver: ProviderDriver<AcpRegistrySettings, AcpRegistryD
metadata: {
displayName: "ACP Registry",
supportsMultipleInstances: true,
hasDefaultInstance: false,
},
configSchema: AcpRegistrySettings,
defaultConfig: () => decodeSettings({}),
Expand Down
64 changes: 10 additions & 54 deletions apps/server/src/provider/ProviderInstanceRegistryHydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,10 @@
* ProviderInstanceRegistryHydration — derive a `ProviderInstanceConfigMap`
* from `ServerSettings` and keep `ProviderInstanceRegistry` in sync with it.
*
* The server still reads two shapes:
*
* 1. `settings.providerInstances` — the new driver-agnostic map the
* registry expects. Keyed by `ProviderInstanceId`, values are
* `ProviderInstanceConfig` envelopes.
* 2. `settings.providers.<kind>` — the legacy single-instance-per-driver
* fields (`providers.codex`, `providers.claudeAgent`, …). These are
* the source of truth for every deployment that hasn't been migrated
* yet to an explicit `providerInstances` entry.
*
* This module bridges (2) into (1) and wires the resulting map into a
* mutable registry. For every built-in driver whose id is not already
* present in `providerInstances` (keyed on
* `defaultInstanceIdForDriver(driverKind)` — literally the driver kind as a
* routing slug), we synthesize an envelope from the legacy field. The
* registry decodes both flavours through the same `configSchema` and ends
* up with one uniform `ProviderInstance` per entry.
*
* Explicit `providerInstances` entries always win — users can already
* override the legacy `providers.<kind>` blob by authoring a
* `providerInstances.codex` entry with a matching driver, and we don't
* want the synthesized envelope to silently stomp their config.
* `settings.providerInstances` is the source of truth. Every built-in driver
* with a default instance also runs at `defaultInstanceIdForDriver(kind)`
* when that slot has no entry, using the driver's default config, so a fresh
* install shows its built-in providers without writing settings first.
*
* Hot-reload
* ----------
Expand Down Expand Up @@ -76,46 +58,20 @@ type ProviderInstanceRegistryHydrationEnv =
| ServerConfig.ServerConfig;

/**
* Synthesize a `ProviderInstanceConfigMap` from a `ServerSettings` snapshot.
*
* Strategy:
* 1. Copy all explicit `settings.providerInstances` entries verbatim.
* 2. For each built-in driver whose `defaultInstanceIdForDriver(id)` key
* is *not* already in the explicit map, synthesize an entry from the
* matching legacy `settings.providers.<kind>` blob.
*
* The returned map is the input the registry consumes; pure & exported
* separately so the hydration logic can be exercised by unit tests
* without layering.
* Explicit `providerInstances` entries plus an implicit default instance for
* each built-in driver whose default slot is empty. Pure so the hydration
* rule can be tested without layers.
*/
export const deriveProviderInstanceConfigMap = (
settings: ServerSettings,
): ProviderInstanceConfigMap => {
const merged: Record<string, ProviderInstanceConfig> = { ...settings.providerInstances };

for (const driver of BUILT_IN_DRIVERS) {
if (driver.metadata.hasDefaultInstance === false) continue;
const instanceId = defaultInstanceIdForDriver(driver.driverKind);
if (instanceId in merged) {
// Explicit `providerInstances` entry for this slot — user-authored
// config always wins over the legacy mirror.
continue;
}

// Only built-in drivers have a legacy mirror; the registry's
// `providers` struct is keyed on the same literal slug as
// `driverKind`. Access is dynamic (the driver kind is a branded string),
// but it's constrained to `keyof settings.providers` by the union of
// built-in driver kinds.
const legacyKey = driver.driverKind as keyof ServerSettings["providers"];
const legacyConfig = settings.providers[legacyKey];
if (legacyConfig === undefined) {
continue;
}

merged[instanceId] = {
driver: driver.driverKind,
config: legacyConfig,
};
if (instanceId in merged) continue;
merged[instanceId] = { driver: driver.driverKind };
}

return merged as ProviderInstanceConfigMap;
Expand Down
68 changes: 33 additions & 35 deletions apps/server/src/provider/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ const decodeServerSettings = Schema.decodeSync(ServerSettings);
const encodeServerSettings = Schema.encodeSync(ServerSettings);
const encodedDefaultServerSettings = encodeServerSettings(DEFAULT_SERVER_SETTINGS);

/** Default slots that settings mark disabled, so their probes never spawn. */
const disabledDefaultSlots = (
...drivers: ReadonlyArray<string>
): ContractServerSettings["providerInstances"] =>
Object.fromEntries(
drivers.map((driver) => [
ProviderInstanceId.make(driver),
{ driver: ProviderDriverKind.make(driver), enabled: false },
]),
);

const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({});
const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({});
const decodeCodexSettings = Schema.decodeSync(CodexSettings);
Expand Down Expand Up @@ -2663,24 +2674,18 @@ it.layer(
const serverSettings = yield* makeMutableServerSettingsService(
decodeServerSettings(
deepMerge(encodedDefaultServerSettings, {
providers: {
// Disable every built-in probe that would otherwise spawn
// on the CI host. `enabled: false` short-circuits each
// driver's probe *before* it touches the spawner, so the
// test environment stays isolated from the dev
// machine's PATH.
codex: { enabled: false },
claudeAgent: { enabled: false },
cursor: { enabled: false },
grok: { enabled: false },
opencode: { enabled: false },
},
// `providerInstances` keys are branded `ProviderInstanceId`;
// the branded index signature rejects plain string literals
// at the TS level even though the runtime schema happily
// accepts + decodes them. Cast the patch to `unknown` so
// the `Schema.decodeSync` below does the real validation.
providerInstances: {
// Disable every built-in probe that would otherwise spawn
// on the CI host. `enabled: false` short-circuits each
// driver's probe *before* it touches the spawner, so the
// test environment stays isolated from the dev
// machine's PATH.
...disabledDefaultSlots("codex", "claudeAgent", "cursor", "grok", "opencode"),
// Matches the shape the user had in `.t3/dev/settings.json`
// when the bug was reported: a custom enabled Codex instance
// pointing at a binary the server has to actually spawn.
Expand Down Expand Up @@ -2781,12 +2786,13 @@ it.layer(
const mutableServerSettings = yield* makeMutableServerSettingsService(
decodeServerSettings(
deepMerge(encodedDefaultServerSettings, {
providers: {
codex: { enabled: true, binaryPath: firstMissing },
claudeAgent: { enabled: false },
cursor: { enabled: false },
grok: { enabled: false },
opencode: { enabled: false },
providerInstances: {
...disabledDefaultSlots("claudeAgent", "cursor", "grok", "opencode"),
[ProviderInstanceId.make("codex")]: {
driver: ProviderDriverKind.make("codex"),
enabled: true,
config: { binaryPath: firstMissing },
},
},
}),
),
Expand Down Expand Up @@ -2871,8 +2877,13 @@ it.layer(
),
);
yield* serverSettings.updateSettings({
providers: {
codex: { enabled: true, binaryPath: secondMissing },
providerInstances: {
...disabledDefaultSlots("claudeAgent", "cursor", "grok", "opencode"),
[ProviderInstanceId.make("codex")]: {
driver: ProviderDriverKind.make("codex"),
enabled: true,
config: { binaryPath: secondMissing },
},
},
});
// Start the lazy stream only after publishing. A watcher that did
Expand Down Expand Up @@ -2900,14 +2911,8 @@ it.layer(
const serverSettings = yield* makeMutableServerSettingsService(
decodeServerSettings(
deepMerge(encodedDefaultServerSettings, {
providers: {
codex: { enabled: false },
claudeAgent: { enabled: false },
cursor: { enabled: false },
grok: { enabled: false },
opencode: { enabled: false },
},
providerInstances: {
...disabledDefaultSlots("codex", "claudeAgent", "cursor", "grok", "opencode"),
ghost_main: {
driver: "ghostDriver",
displayName: "A fork-only driver we don't ship",
Expand Down Expand Up @@ -2971,14 +2976,7 @@ it.layer(
const serverSettings = yield* makeMutableServerSettingsService(
decodeServerSettings(
deepMerge(encodedDefaultServerSettings, {
providers: {
codex: {
enabled: false,
},
grok: {
enabled: false,
},
},
providerInstances: disabledDefaultSlots("codex", "grok"),
}),
),
);
Expand Down
27 changes: 24 additions & 3 deletions apps/server/src/provider/providerInstallation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,14 @@ describe("provider installation routing", () => {
it.effect("keeps external installs manual without hiding shared install status", () =>
Effect.gen(function* () {
const harness = yield* makeHarness({
settings: { providers: { antigravity: { binaryPath: "/external/agy" } } },
settings: {
providerInstances: {
[ProviderInstanceId.make("antigravity")]: {
driver: ProviderDriverKind.make("antigravity"),
config: { binaryPath: "/external/agy" },
},
},
},
});
const start = yield* Effect.flip(harness.router.start({ instanceId }));
const remove = yield* Effect.flip(harness.router.remove({ instanceId }));
Expand All @@ -164,7 +171,14 @@ describe("provider installation routing", () => {
const codexId = ProviderInstanceId.make("codex");
const harness = yield* makeHarness({
instance: instance(ProviderDriverKind.make("codex"), codexId),
settings: { providers: { codex: { setupMode: "managed" } } },
settings: {
providerInstances: {
[ProviderInstanceId.make("codex")]: {
driver: ProviderDriverKind.make("codex"),
config: { setupMode: "managed" },
},
},
},
});
assert.equal((yield* harness.router.start({ instanceId: codexId })).driver, "codex");
yield* harness.router.cancel({ instanceId: codexId, operationId: "operation" });
Expand All @@ -180,7 +194,14 @@ describe("provider installation routing", () => {
const codexId = ProviderInstanceId.make("codex");
const harness = yield* makeHarness({
instance: instance(ProviderDriverKind.make("codex"), codexId),
settings: { providers: { codex: { setupMode: "existing" } } },
settings: {
providerInstances: {
[ProviderInstanceId.make("codex")]: {
driver: ProviderDriverKind.make("codex"),
config: { setupMode: "existing" },
},
},
},
});
assert.include(
(yield* Effect.flip(harness.router.start({ instanceId: codexId }))).detail,
Expand Down
3 changes: 1 addition & 2 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,8 +583,7 @@ const layerRuntimeCoreDependenciesBase = Layer.mergeAll(
// The instance registry is the new routing keystone — text generation,
// adapter lookup, and runtime ingestion all resolve `ProviderInstanceId`
// through this layer. Built-in drivers come from `BUILT_IN_DRIVERS`;
// `providerInstances` hydration merges `settings.providers.<kind>`
// with explicit `providerInstances` entries on boot.
// hydration adds their default instances to `providerInstances` on boot.
Layer.provideMerge(ProviderInstanceRegistryHydration.layer),
Layer.provideMerge(
Layer.mergeAll(
Expand Down
Loading
Loading