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
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.serverDisableAcpRegistryProvider]: AuthOrchestrationOperateScope,
[WS_METHODS.serverLogoutAcpRegistry]: AuthOrchestrationOperateScope,
[WS_METHODS.serverDiscoverSourceControl]: AuthOrchestrationReadScope,
[WS_METHODS.serverListOnePasswordAccounts]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetTraceDiagnostics]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetProcessDiagnostics]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetHostResources]: AuthOrchestrationReadScope,
Expand Down
9 changes: 3 additions & 6 deletions apps/server/src/orchestration-v2/ProviderAdapterDriver.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import {
ProviderDriverKind,
ProviderInstanceId,
type ProviderInstanceEnvironment,
} from "@t3tools/contracts";
import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";
import * as Schema from "effect/Schema";
import type * as Effect from "effect/Effect";
import type * as Scope from "effect/Scope";

import type { ResolvedProviderEnvironment } from "../provider/ProviderInstanceEnvironment.ts";
import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";

export class ProviderAdapterDriverCreateError extends Schema.TaggedError<ProviderAdapterDriverCreateError>()(
Expand All @@ -27,7 +24,7 @@ export interface ProviderAdapterDriverCreateInput<Config> {
readonly instanceId: ProviderInstanceId;
readonly displayName: string | undefined;
readonly accentColor?: string | undefined;
readonly environment: ProviderInstanceEnvironment;
readonly environment: ResolvedProviderEnvironment;
readonly enabled: boolean;
readonly config: Config;
}
Expand Down
29 changes: 19 additions & 10 deletions apps/server/src/orchestration-v2/ProviderAdapterRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import {
ProviderDriverKind,
ProviderInstanceEnvironment,
ProviderInstanceId,
ProviderSessionId,
ThreadId,
Expand Down Expand Up @@ -38,6 +39,13 @@ const driver = ProviderDriverKind.make("codex");
const personalId = ProviderInstanceId.make("codex_personal");
const workId = ProviderInstanceId.make("codex_work");

const HOME_ACCOUNT = "my.1password.com";
const decodeEnvironment = Schema.decodeSync(ProviderInstanceEnvironment);
const onePasswordVariable = (name: string, reference: string) => ({
name,
value: { kind: "1password" as const, reference, account: HOME_ACCOUNT },
});

const makeAdapter = (instanceId: ProviderInstanceId): ProviderAdapterV2Shape =>
({
instanceId,
Expand Down Expand Up @@ -347,19 +355,20 @@ it.effect("opens v2 sessions with resolved secrets and rebuilds them when a secr
Effect.gen(function* () {
const variables = [];
const unresolved = [];
for (const variable of environment ?? []) {
if (variable.value === apiKeyReference) {
variables.push({ ...variable, value: yield* Ref.get(apiKey) });
} else if (variable.value.startsWith("op://")) {
unresolved.push(variable.name);
for (const { name, value, sensitive } of environment ?? []) {
if (typeof value !== "string" && value.reference === apiKeyReference) {
variables.push({ name, value: yield* Ref.get(apiKey), sensitive: true });
} else if (typeof value !== "string") {
unresolved.push(name);
} else {
variables.push(variable);
variables.push({ name, value, sensitive });
}
}
return { variables, unresolved };
}),
prime: () => Effect.void,
invalidate: Effect.void,
listOnePasswordAccounts: Effect.succeed([]),
};
const secretDriver: ProviderDriver<Record<string, never>> = {
driverKind: driver,
Expand Down Expand Up @@ -408,11 +417,11 @@ it.effect("opens v2 sessions with resolved secrets and rebuilds them when a secr
configMap: {
[secretInstanceId]: {
driver,
environment: [
{ name: "OPENAI_API_KEY", value: apiKeyReference, sensitive: true },
{ name: "ANTHROPIC_API_KEY", value: "op://Vault/Locked/api-key", sensitive: true },
environment: decodeEnvironment([
onePasswordVariable("OPENAI_API_KEY", apiKeyReference),
onePasswordVariable("ANTHROPIC_API_KEY", "op://Vault/Locked/api-key"),
{ name: "CODEX_PROFILE", value: "work", sensitive: false },
],
]),
config: {},
},
},
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/orchestration-v2/ProviderAdapterRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as Scope from "effect/Scope";

import { literalProviderInstanceEnvironment } from "../provider/ProviderInstanceEnvironment.ts";
import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts";
import {
ProviderAdapterDriverCreateError,
Expand Down Expand Up @@ -271,7 +272,7 @@ const createAdapterEntryFromConfigEntry = Effect.fn(
instanceId: input.instanceId,
displayName: input.entry.displayName,
accentColor: input.entry.accentColor,
environment: input.entry.environment ?? [],
environment: literalProviderInstanceEnvironment(input.entry.environment),
enabled: input.entry.enabled ?? decodedConfigEnabled(typedConfig) ?? true,
config: typedConfig,
})
Expand Down
6 changes: 4 additions & 2 deletions apps/server/src/project/AgentSessionScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { normalizeProjectPathForComparison } from "@t3tools/shared/path";
import * as ServerConfig from "../config.ts";
import * as ProjectStore from "../orchestration-v2/ProjectStore.ts";
import { resolveCodexHomeLayout } from "../provider/Drivers/CodexHomeLayout.ts";
import { literalProviderInstanceEnvironment } from "../provider/ProviderInstanceEnvironment.ts";
import { expandHomePath } from "../pathExpansion.ts";
import * as ServerSettings from "../serverSettings.ts";
import {
Expand Down Expand Up @@ -1127,8 +1128,9 @@ export const make = Effect.gen(function* () {
for (const { instanceId, config: instance } of instances) {
const homeVariable = source === "claudeAgent" ? "CLAUDE_CONFIG_DIR" : "CODEX_HOME";
const environmentHome =
instance.environment?.findLast((variable) => variable.name === homeVariable)?.value ??
hostEnvironment[homeVariable];
literalProviderInstanceEnvironment(instance.environment).findLast(
(variable) => variable.name === homeVariable,
)?.value ?? hostEnvironment[homeVariable];

let homePath: string;
if (source === "claudeAgent") {
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/provider/Drivers/ClaudeCredential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ const VERIFY_TIMEOUT = Duration.seconds(10);
* The OAuth token a Claude instance was configured with, if any.
*
* Reads the same variable the CLI itself reads, so an instance whose
* environment carries an `op://` reference is checked with whatever that
* reference resolved to.
* environment reads the token from a secret store is checked with whatever
* that secret resolved to.
*/
export function claudeOAuthTokenFromEnvironment(
environment: NodeJS.ProcessEnv,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
ProviderDriverKind,
type ProviderInstanceConfig,
type ProviderInstanceConfigMap,
ProviderInstanceEnvironment,
ProviderInstanceId,
} from "@t3tools/contracts";
import { HostProcessPlatform, isHostWindows } from "@t3tools/shared/hostProcess";
Expand All @@ -48,6 +49,7 @@ import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as Ref from "effect/Ref";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import { HttpClient, HttpClientResponse } from "effect/unstable/http";

Expand Down Expand Up @@ -871,14 +873,25 @@ describe("ProviderInstanceRegistryLive: rebuildInstanceWhen", () => {
),
);

const decodeEnvironment = Schema.decodeSync(ProviderInstanceEnvironment);

const codexDriverKind = ProviderDriverKind.make("codex");
const firstId = ProviderInstanceId.make("codex_first");
const secondId = ProviderInstanceId.make("codex_second");
const firstEntry: ProviderInstanceConfig = {
driver: codexDriverKind,
displayName: "Codex (first)",
enabled: false,
environment: [{ name: "OP_TOKEN", value: "op://Vault/Item/token", sensitive: true }],
environment: decodeEnvironment([
{
name: "OP_TOKEN",
value: {
kind: "1password",
reference: "op://Vault/Item/token",
account: "my.1password.com",
},
},
]),
config: makeCodexConfig({ homePath: "/home/julius/.codex_first" }),
};
const secondEntry: ProviderInstanceConfig = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ const buildEntry = <R>(input: {
instanceId,
displayName: entry.displayName,
accentColor: entry.accentColor,
environment: resolvedEnvironment.variables ?? [],
environment: resolvedEnvironment.variables,
enabled: resolveEntryEnabled(entry, typedConfig),
config: typedConfig,
})
Expand Down
80 changes: 63 additions & 17 deletions apps/server/src/provider/Layers/ProviderRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
CodexSettings,
DEFAULT_SERVER_SETTINGS,
ProviderDriverKind,
ProviderInstanceEnvironment,
ProviderInstanceId,
ServerSettings,
type ServerProvider,
Expand Down Expand Up @@ -65,12 +66,20 @@ import {
} from "../providerStatusCache.ts";
import { COMPACT_SLASH_COMMAND } from "../providerSnapshot.ts";
import type { ProviderInstance } from "../ProviderDriver.ts";
import { literalProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";
import * as ProviderRegistry from "../Services/ProviderRegistry.ts";
import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
const decodeServerSettings = Schema.decodeSync(ServerSettings);
const encodeServerSettings = Schema.encodeSync(ServerSettings);
const encodedDefaultServerSettings = encodeServerSettings(DEFAULT_SERVER_SETTINGS);
const decodeEnvironment = Schema.decodeSync(ProviderInstanceEnvironment);
const HOME_ACCOUNT = "my.1password.com";
const onePasswordVariable = (name: string, reference: string, sensitive = true) => ({
name,
value: { kind: "1password" as const, reference, account: HOME_ACCOUNT },
sensitive,
});

const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({});
const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({});
Expand Down Expand Up @@ -1609,9 +1618,14 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
const invalidations = yield* Ref.make(0);
const rebuiltIds = yield* Ref.make<ReadonlyArray<ProviderInstanceId>>([]);
const secretResolverLayer = Layer.succeed(ProviderSecretResolver, {
resolve: (environment) => Effect.succeed({ variables: environment, unresolved: [] }),
resolve: (environment) =>
Effect.succeed({
variables: literalProviderInstanceEnvironment(environment),
unresolved: [],
}),
prime: () => Effect.void,
invalidate: Ref.update(invalidations, (count) => count + 1),
listOnePasswordAccounts: Effect.succeed([]),
});
const instanceRegistryLayer = Layer.succeed(
ProviderInstanceRegistry.ProviderInstanceRegistry,
Expand All @@ -1623,9 +1637,9 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
rebuildInstanceWhen: (instanceId, shouldRebuild) =>
shouldRebuild({
driver: codexDriver,
environment: [
{ name: "CODEX_TOKEN", value: "op://Vault/Item/token", sensitive: true },
],
environment: decodeEnvironment([
onePasswordVariable("CODEX_TOKEN", "op://Vault/Item/token"),
]),
})
? Ref.update(rebuiltIds, (previous) => [...previous, instanceId]).pipe(
Effect.as(true),
Expand Down Expand Up @@ -1687,9 +1701,14 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test

const rebuiltIds = yield* Ref.make<ReadonlyArray<ProviderInstanceId>>([]);
const secretResolverLayer = Layer.succeed(ProviderSecretResolver, {
resolve: (environment) => Effect.succeed({ variables: environment, unresolved: [] }),
resolve: (environment) =>
Effect.succeed({
variables: literalProviderInstanceEnvironment(environment),
unresolved: [],
}),
prime: () => Effect.void,
invalidate: Effect.void,
listOnePasswordAccounts: Effect.succeed([]),
});
const instanceRegistryLayer = Layer.succeed(
ProviderInstanceRegistry.ProviderInstanceRegistry,
Expand All @@ -1700,9 +1719,9 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
rebuildInstanceWhen: (instanceId, shouldRebuild) =>
shouldRebuild({
driver: codexDriver,
environment: [
{ name: "CODEX_TOKEN", value: "op://Vault/Item/token", sensitive: true },
],
environment: decodeEnvironment([
onePasswordVariable("CODEX_TOKEN", "op://Vault/Item/token"),
]),
})
? Ref.update(rebuiltIds, (previous) => [...previous, instanceId]).pipe(
Effect.as(true),
Expand Down Expand Up @@ -1764,14 +1783,21 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
const primed = yield* Ref.make<ReadonlyArray<ReadonlyArray<string>>>([]);
const rebuiltIds = yield* Ref.make<ReadonlyArray<ProviderInstanceId>>([]);
const secretResolverLayer = Layer.succeed(ProviderSecretResolver, {
resolve: (environment) => Effect.succeed({ variables: environment, unresolved: [] }),
resolve: (environment) =>
Effect.succeed({
variables: literalProviderInstanceEnvironment(environment),
unresolved: [],
}),
prime: (references) =>
Ref.update(primed, (previous) => [...previous, references]).pipe(Effect.asVoid),
Ref.update(primed, (previous) => [
...previous,
references.map((secret) => secret.reference),
]).pipe(Effect.asVoid),
invalidate: Effect.void,
listOnePasswordAccounts: Effect.succeed([]),
});
const environmentFor = (reference: string) => [
{ name: "TOKEN", value: reference, sensitive: true },
];
const environmentFor = (reference: string) =>
decodeEnvironment([onePasswordVariable("TOKEN", reference)]);
const instanceRegistryLayer = Layer.succeed(
ProviderInstanceRegistry.ProviderInstanceRegistry,
{
Expand Down Expand Up @@ -2926,14 +2952,30 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
displayName: "Claude Secret",
enabled: false,
environment: [
{ name: "CLAUDE_CODE_OAUTH_TOKEN", value: claudeReference, sensitive: true },
{
name: "CLAUDE_CODE_OAUTH_TOKEN",
value: {
kind: "1password",
reference: claudeReference,
account: HOME_ACCOUNT,
},
},
],
},
codex_secret: {
driver: "codex",
displayName: "Codex Secret",
enabled: false,
environment: [{ name: "TOKEN", value: codexReference, sensitive: true }],
environment: [
{
name: "TOKEN",
value: {
kind: "1password",
reference: codexReference,
account: HOME_ACCOUNT,
},
},
],
},
} as unknown as ContractServerSettings["providerInstances"],
}),
Expand All @@ -2944,14 +2986,18 @@ it.layer(Layer.mergeAll(TestNodeServices, ServerSettingsModule.layerTest(), Test
const recordingSecretResolverLayer = Layer.succeed(ProviderSecretResolver, {
resolve: (environment) =>
Ref.update(calls, (previous) => [...previous, "resolve"]).pipe(
Effect.as({ variables: environment, unresolved: [] }),
Effect.as({
variables: literalProviderInstanceEnvironment(environment),
unresolved: [],
}),
),
prime: (references) =>
Ref.update(calls, (previous) => [
...previous,
`prime:${Array.from(references).join(",")}`,
`prime:${references.map((secret) => secret.reference).join(",")}`,
]).pipe(Effect.asVoid),
invalidate: Effect.void,
listOnePasswordAccounts: Effect.succeed([]),
});

const scope = yield* Scope.make();
Expand Down
Loading
Loading