Skip to content
6 changes: 5 additions & 1 deletion apps/server/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* terminal-fallback agents run `t3 acp-mcp-call` per tool call, so their
* startup sits on first-message latency. Both dispatch here before the full
* CLI module graph (seconds of evaluation) loads; everything else defers to
* the real CLI in ./binCli.ts.
* the real CLI in ./binCli.ts. Managed Codex re-runs `t3 codex-managed-token`
* on a short interval to renew its bearer, so it dispatches here as well.
*/
import { isEntrypoint } from "./entrypoint.ts";

Expand All @@ -20,6 +21,9 @@ if (
if (command === "acp-mcp-bridge" || command === "acp-mcp-call") {
const { runAcpMcpCliFastPath } = await import("./mcp/AcpMcpStdioBridge.ts");
await runAcpMcpCliFastPath(command, process.argv.slice(3));
} else if (command === "codex-managed-token") {
const { runCodexManagedTokenCommand } = await import("./provider/codexManagedTokenCommand.ts");
await runCodexManagedTokenCommand();
} else {
const { runCli } = await import("./binCli.ts");
runCli();
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/binCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as CliError from "effect/cli/CliError";
import * as NetService from "@t3tools/shared/Net";
import packageJson from "../package.json" with { type: "json" };
import { acpMcpBridgeCommand, acpMcpCallCommand } from "./cli/acpMcpBridge.ts";
import { codexManagedTokenCommand } from "./cli/codexManagedToken.ts";
import { authCommand } from "./cli/auth.ts";
import { appCommand } from "./cli/app.ts";
import { browserCommand } from "./cli/browser.ts";
Expand Down Expand Up @@ -67,6 +68,7 @@ export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>
),
acpMcpBridgeCommand,
acpMcpCallCommand,
codexManagedTokenCommand,
startCommand,
serveCommand,
appCommand,
Expand Down
21 changes: 21 additions & 0 deletions apps/server/src/cli/codexManagedToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as Effect from "effect/Effect";
import { Command } from "effect/cli";

import {
codexManagedTokenCommandName,
runCodexManagedTokenCommand,
} from "../provider/codexManagedTokenCommand.ts";

/**
* `t3 codex-managed-token` — internal credential command managed Codex runs to
* renew its bearer from the provider's loopback bridge.
*
* Real invocations dispatch through the bin.ts fast path before the CLI graph
* loads; this definition keeps the command wired for anything that drives the
* full CLI programmatically.
*/
export const codexManagedTokenCommand = Command.make(codexManagedTokenCommandName).pipe(
Command.withDescription("Print the current managed Codex credential for Codex."),
Command.unlisted,
Command.withHandler(() => Effect.promise(() => runCodexManagedTokenCommand())),
);
9 changes: 8 additions & 1 deletion apps/server/src/provider/CodexChatGptAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*
readonly defaultReturnUrl?: string;
readonly reconnectProfile?: ChatGptReconnectProfile | null;
readonly telemetryFlow?: "direct" | "primary_handoff";
/** Retire issued runtime credentials on sign-in, logout or credential replacement, not renewal. */
readonly onCredentialsReplaced?: Effect.Effect<void>;
}) {
const analytics = yield* Effect.serviceOption(AnalyticsService.AnalyticsService);
const settings = yield* Effect.serviceOption(ServerSettings.ServerSettingsService);
Expand Down Expand Up @@ -439,6 +441,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*
method: string,
context: ProviderAuthFlowContext,
) {
yield* options.onCredentialsReplaced ?? Effect.void;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const endpoints = yield* discover;
const existing = yield* read;
const previous = Option.getOrUndefined(existing);
Expand Down Expand Up @@ -786,6 +789,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*
),
);
const logout = Effect.gen(function* () {
yield* options.onCredentialsReplaced ?? Effect.void;
const credentials = Option.getOrUndefined(yield* read);
let confirmed = !credentials?.refreshToken;
if (credentials?.refreshToken) {
Expand Down Expand Up @@ -967,6 +971,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*
"import",
"The transferred identity conflicts with the saved ChatGPT connection.",
);
yield* options.onCredentialsReplaced ?? Effect.void;
yield* saveRegistration(registration);
yield* save(profile.credentials, true);
});
Expand All @@ -985,6 +990,8 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function*
read,
access,
exportProfile,
revoke: lock.withPermit(withSessionLock(remove)),
revoke: lock.withPermit(
withSessionLock((options.onCredentialsReplaced ?? Effect.void).pipe(Effect.andThen(remove))),
),
};
});
188 changes: 179 additions & 9 deletions apps/server/src/provider/CodexManagedRuntime.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// @effect-diagnostics nodeBuiltinImport:off - checks the host's default Codex directory without writing to it.
// @effect-diagnostics nodeBuiltinImport:off globalFetchInEffect:off - checks the host's default Codex directory without writing to it.
import * as NodeOS from "node:os";
import * as NodeChildProcess from "node:child_process";
import * as NodeUtil from "node:util";
import * as NodeURL from "node:url";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import { CodexSettings, EnvironmentId, ProviderInstanceId } from "@t3tools/contracts";
Expand All @@ -11,17 +14,36 @@ import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import { HttpClient, HttpClientResponse } from "effect/http";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as ServerConfig from "../config.ts";
import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
import * as CodexInstallation from "./CodexInstallation.ts";
import { makeCodexManagedRuntime } from "./CodexManagedRuntime.ts";
import { makeCodexManagedTokenSource } from "./CodexManagedTokenSource.ts";
import * as ProviderCredentialStore from "./ProviderCredentialStore.ts";
import { codexAppServerArgs } from "./codexLaunchArgs.ts";
import { resolveManagedCodexHomeLayout } from "./CodexManagedHome.ts";

const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
const decodeSettings = Schema.decodeSync(CodexSettings);
const serverEntrypoint = NodeURL.fileURLToPath(new URL("../bin.ts", import.meta.url));
it.effect("closes the managed credential bridge with its provider scope", () =>
Effect.gen(function* () {
const source = yield* makeCodexManagedTokenSource(
ProviderInstanceId.make("closed-codex"),
"dummy-account",
Effect.succeed({ accessToken: "dummy-token", clientId: "dummy-account" }),
).pipe(Effect.scoped);
const reached = yield* Effect.promise(() =>
fetch(source.url).then(
() => true,
() => false,
),
);
assert.isFalse(reached);
}),
);
it.effect("managed home defaults to the global Codex home and honors configured home paths", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
Expand Down Expand Up @@ -79,6 +101,7 @@ it.effect.each(
getOrCreateRandom: () => Effect.die("unused"),
});
let leases = 0;
let renewals = 0;
const executable = {
executablePath:
source === "managed" ? "/isolated/tools/codex/0.156.1/bin/codex" : "/user/bin/codex",
Expand Down Expand Up @@ -149,7 +172,143 @@ it.effect.each(
);
assert.isFalse(yield* fs.exists(path.join(effective.config.homePath, "auth.json")));
}
assert.strictEqual(effective.environment.ACCESS_TOKEN, "dummy-owned-access");
// Emulate the same app-server making later requests, without resolving
// or restarting its runtime. Run its configured credential command.
const authSetting = (name: string) => {
const prefix = `model_providers.openai_token_sharing.auth.${name}=`;
const value = codexAppServerArgs(effective.config.launchArgs).find((arg) =>
arg.startsWith(prefix),
);
assert.isDefined(value);
return JSON.parse(value!.slice(prefix.length));
};
// The command is T3's own hidden subcommand, not a Node `-e` script the
// packaged executable cannot evaluate.
assert.equal(authSetting("command"), process.execPath);
assert.deepEqual(authSetting("args"), [serverEntrypoint, "codex-managed-token"]);
const requestToken = Effect.promise(async () => {
const { stdout } = await NodeUtil.promisify(NodeChildProcess.execFile)(
authSetting("command"),
authSetting("args"),
{ env: effective.environment, timeout: 25_000 },
);
return stdout;
});
assert.equal(yield* requestToken, "dummy-owned-access");
const expired = { ...JSON.parse(json), expiresAt: 0 };
yield* store.set(new TextEncoder().encode(JSON.stringify(expired)));
// The credential commands themselves must renew an expired token,
// even when no provider status read runs first.
assert.deepEqual(
yield* Effect.all([requestToken, requestToken], { concurrency: "unbounded" }),
["dummy-renewed-access", "dummy-renewed-access"],
);
assert.strictEqual(renewals, 1);
assert.isUndefined(effective.environment.ACCESS_TOKEN);
assert.notInclude(JSON.stringify(effective.environment), "dummy-owned-access");
const rejected = yield* Effect.promise(() =>
fetch(effective.environment.T3CODE_MANAGED_CODEX_AUTH_URL!),
);
assert.equal(rejected.status, 401);
yield* store.set(
new TextEncoder().encode(
JSON.stringify({
...expired,
clientId: "another-account",
accessToken: "dummy-other-account-access",
expiresAt: (yield* Clock.currentTimeMillis) + 3_600_000,
}),
),
);
const switched = yield* Effect.promise(() =>
fetch(effective.environment.T3CODE_MANAGED_CODEX_AUTH_URL!, {
headers: {
Authorization: `Bearer ${effective.environment.T3CODE_MANAGED_CODEX_AUTH_SECRET}`,
"X-T3-Codex-Account": effective.environment.T3CODE_MANAGED_CODEX_AUTH_ACCOUNT!,
},
}),
);
assert.equal(switched.status, 409);
assert.equal(yield* Effect.promise(() => switched.text()), "");
// A child holding the old bridge secret cannot expand its authority
// by naming the account that is now selected in T3.
const forged = yield* Effect.promise(() =>
fetch(effective.environment.T3CODE_MANAGED_CODEX_AUTH_URL!, {
headers: {
Authorization: `Bearer ${effective.environment.T3CODE_MANAGED_CODEX_AUTH_SECRET}`,
"X-T3-Codex-Account": "another-account",
},
}),
);
assert.equal(forged.status, 409);
assert.equal(yield* Effect.promise(() => forged.text()), "");
const replacement = yield* runtime.resolve;
assert.notEqual(
replacement.environment.T3CODE_MANAGED_CODEX_AUTH_SECRET,
effective.environment.T3CODE_MANAGED_CODEX_AUTH_SECRET,
);
const replacementToken = yield* Effect.promise(async () => {
const { stdout } = await NodeUtil.promisify(NodeChildProcess.execFile)(
authSetting("command"),
authSetting("args"),
{ env: replacement.environment, timeout: 25_000 },
);
return stdout;
});
assert.equal(replacementToken, "dummy-other-account-access");
const oldListenerReached = yield* Effect.promise(() =>
fetch(effective.environment.T3CODE_MANAGED_CODEX_AUTH_URL!).then(
() => true,
() => false,
),
);
assert.isFalse(oldListenerReached);
yield* runtime.auth.controller.logout(Effect.void);
const signedOutListenerReached = yield* Effect.promise(() =>
fetch(replacement.environment.T3CODE_MANAGED_CODEX_AUTH_URL!).then(
() => true,
() => false,
),
);
assert.isFalse(signedOutListenerReached);
// Model a later sign-in to the same registration without replacing
// the provider. Its old capability must not become usable again.
yield* store.set(
new TextEncoder().encode(
JSON.stringify({
...expired,
clientId: "another-account",
accessToken: "dummy-reconnected-access",
expiresAt: (yield* Clock.currentTimeMillis) + 3_600_000,
}),
),
);
const reconnected = yield* runtime.resolve;
assert.notEqual(
reconnected.environment.T3CODE_MANAGED_CODEX_AUTH_SECRET,
replacement.environment.T3CODE_MANAGED_CODEX_AUTH_SECRET,
);
const oldCapabilityStatus = yield* Effect.promise(() =>
fetch(replacement.environment.T3CODE_MANAGED_CODEX_AUTH_URL!, {
headers: {
Authorization: `Bearer ${replacement.environment.T3CODE_MANAGED_CODEX_AUTH_SECRET}`,
"X-T3-Codex-Account": "another-account",
},
}).then(
(response) => response.status,
() => null,
),
);
assert.notEqual(oldCapabilityStatus, 200);
const reconnectedToken = yield* Effect.promise(async () => {
const { stdout } = await NodeUtil.promisify(NodeChildProcess.execFile)(
authSetting("command"),
authSetting("args"),
{ env: reconnected.environment, timeout: 25_000 },
);
return stdout;
});
assert.equal(reconnectedToken, "dummy-reconnected-access");
assert.isUndefined(effective.environment.OPENAI_API_KEY);
assert.isUndefined(effective.environment.OPENAI_BASE_URL);
assert.isUndefined(effective.environment.T3CODE_CODEX_LAUNCH_ARGS);
Expand Down Expand Up @@ -194,26 +353,37 @@ it.effect.each(
[
"https://auth.openai.com/.well-known/openid-configuration",
"https://auth.openai.com/revoke",
"https://auth.openai.com/api/accounts/oauth/token",
].includes(request.url),
);
if (request.url.endsWith("/token")) renewals++;
return HttpClientResponse.fromWeb(
request,
request.url.endsWith("/revoke")
? new Response(null, { status: 200 })
: Response.json({
issuer: "https://auth.openai.com",
authorization_endpoint: "https://auth.openai.com/api/accounts/authorize",
token_endpoint: "https://auth.openai.com/api/accounts/oauth/token",
jwks_uri: "https://auth.openai.com/jwks",
revocation_endpoint: "https://auth.openai.com/revoke",
}),
: request.url.endsWith("/token")
? Response.json({
access_token: "dummy-renewed-access",
refresh_token: "dummy-rotated-refresh",
expires_in: 3600,
token_type: "Bearer",
scope: "chatgpt.tokens.use.direct",
})
: Response.json({
issuer: "https://auth.openai.com",
authorization_endpoint: "https://auth.openai.com/api/accounts/authorize",
token_endpoint: "https://auth.openai.com/api/accounts/oauth/token",
jwks_uri: "https://auth.openai.com/jwks",
revocation_endpoint: "https://auth.openai.com/revoke",
}),
);
}),
),
),
);
}).pipe(
Effect.scoped,
Effect.provideService(HostProcess.Arguments, [process.execPath, serverEntrypoint]),
Effect.provide(
ServerConfig.layerTest(process.cwd(), {
prefix: "t3-managed-runtime-",
Expand Down
Loading
Loading