mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix: honor agent for models auth writes (#71933)
Honor the parent `models auth --agent <id>` flag across auth write commands: `add`, `login`, `setup-token`, `paste-token`, and `login-github-copilot`. The auth helpers now resolve the requested configured agent before choosing the auth-profile store and provider workspace, while preserving default-agent behavior when `--agent` is omitted. Validation: - `pnpm test src/cli/models-cli.test.ts src/commands/models/auth.test.ts` - `pnpm test src/commands/models/auth.test.ts` - `pnpm docs:check-mdx` - `pnpm check:changed` - `pnpm check` - `pnpm build` - `pnpm test src/cli/run-main.test.ts` Full `pnpm test` was also run; it failed in unrelated `src/cli/run-main.test.ts` assertions during the full-suite order, while the exact file passes on both latest main and this branch. The PR diff only touches models auth CLI/auth files, docs, and changelog. Fixes #71864. Thanks @neeravmakwana.
This commit is contained in:
@@ -80,6 +80,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/registry: resolve web provider ownership from the installed plugin index instead of broad manifest scans on secret, tool, and pricing paths. Thanks @shakkernerd.
|
||||
- Config/providers: accept `video` and `audio` in configured model `input` values and
|
||||
preserve them in provider catalog entries. Fixes #20721. Thanks @alvinttang.
|
||||
- Models/auth: honor the parent `--agent` flag for auth write commands (`add`, `login`, `setup-token`, `paste-token`, and the GitHub Copilot shortcut) so OAuth/API-key/token results are written to the requested agent store instead of the default agent. Fixes #71864. (#71933) Thanks @balric-seo.
|
||||
- TTS: strip model-emitted TTS directives from streamed block text before channel
|
||||
delivery, including directives split across adjacent blocks, while preserving
|
||||
the accumulated raw reply for final-mode synthesis. Fixes #38937.
|
||||
|
||||
@@ -154,6 +154,9 @@ provider you choose.
|
||||
|
||||
`models auth login` runs a provider plugin’s auth flow (OAuth/API key). Use
|
||||
`openclaw plugins list` to see which providers are installed.
|
||||
Use `openclaw models auth --agent <id> <subcommand>` to write auth results to a
|
||||
specific configured agent store. The parent `--agent` flag is honored by
|
||||
`add`, `login`, `setup-token`, `paste-token`, and `login-github-copilot`.
|
||||
|
||||
Examples:
|
||||
|
||||
|
||||
@@ -6,10 +6,19 @@ import { registerModelsCli } from "./models-cli.js";
|
||||
const mocks = vi.hoisted(() => ({
|
||||
modelsStatusCommand: vi.fn().mockResolvedValue(undefined),
|
||||
noopAsync: vi.fn(async () => undefined),
|
||||
modelsAuthAddCommand: vi.fn().mockResolvedValue(undefined),
|
||||
modelsAuthLoginCommand: vi.fn().mockResolvedValue(undefined),
|
||||
modelsAuthPasteTokenCommand: vi.fn().mockResolvedValue(undefined),
|
||||
modelsAuthSetupTokenCommand: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { modelsStatusCommand, modelsAuthLoginCommand } = mocks;
|
||||
const {
|
||||
modelsAuthAddCommand,
|
||||
modelsAuthLoginCommand,
|
||||
modelsAuthPasteTokenCommand,
|
||||
modelsAuthSetupTokenCommand,
|
||||
modelsStatusCommand,
|
||||
} = mocks;
|
||||
|
||||
vi.mock("../commands/models.js", () => ({
|
||||
modelsStatusCommand: mocks.modelsStatusCommand,
|
||||
@@ -41,10 +50,10 @@ vi.mock("../commands/models/list.js", () => ({
|
||||
modelsStatusCommand: mocks.modelsStatusCommand,
|
||||
}));
|
||||
vi.mock("../commands/models/auth.js", () => ({
|
||||
modelsAuthAddCommand: mocks.noopAsync,
|
||||
modelsAuthAddCommand: mocks.modelsAuthAddCommand,
|
||||
modelsAuthLoginCommand: mocks.modelsAuthLoginCommand,
|
||||
modelsAuthPasteTokenCommand: mocks.noopAsync,
|
||||
modelsAuthSetupTokenCommand: mocks.noopAsync,
|
||||
modelsAuthPasteTokenCommand: mocks.modelsAuthPasteTokenCommand,
|
||||
modelsAuthSetupTokenCommand: mocks.modelsAuthSetupTokenCommand,
|
||||
}));
|
||||
vi.mock("../commands/models/auth-order.js", () => ({
|
||||
modelsAuthOrderClearCommand: mocks.noopAsync,
|
||||
@@ -80,7 +89,10 @@ vi.mock("../commands/models/set-image.js", () => ({
|
||||
|
||||
describe("models cli", () => {
|
||||
beforeEach(() => {
|
||||
modelsAuthAddCommand.mockClear();
|
||||
modelsAuthLoginCommand.mockClear();
|
||||
modelsAuthPasteTokenCommand.mockClear();
|
||||
modelsAuthSetupTokenCommand.mockClear();
|
||||
modelsStatusCommand.mockClear();
|
||||
});
|
||||
|
||||
@@ -108,9 +120,10 @@ describe("models cli", () => {
|
||||
const login = auth?.commands.find((cmd) => cmd.name() === "login-github-copilot");
|
||||
expect(login).toBeTruthy();
|
||||
|
||||
await program.parseAsync(["models", "auth", "login-github-copilot", "--yes"], {
|
||||
from: "user",
|
||||
});
|
||||
await program.parseAsync(
|
||||
["models", "auth", "--agent", "poe", "login-github-copilot", "--yes"],
|
||||
{ from: "user" },
|
||||
);
|
||||
|
||||
expect(modelsAuthLoginCommand).toHaveBeenCalledTimes(1);
|
||||
expect(modelsAuthLoginCommand).toHaveBeenCalledWith(
|
||||
@@ -118,6 +131,7 @@ describe("models cli", () => {
|
||||
provider: "github-copilot",
|
||||
method: "device",
|
||||
yes: true,
|
||||
agent: "poe",
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
@@ -134,6 +148,43 @@ describe("models cli", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "add",
|
||||
args: ["models", "auth", "--agent", "poe", "add"],
|
||||
command: modelsAuthAddCommand,
|
||||
expected: { agent: "poe" },
|
||||
},
|
||||
{
|
||||
label: "login",
|
||||
args: ["models", "auth", "--agent", "poe", "login", "--provider", "openai-codex"],
|
||||
command: modelsAuthLoginCommand,
|
||||
expected: { agent: "poe", provider: "openai-codex" },
|
||||
},
|
||||
{
|
||||
label: "setup-token",
|
||||
args: ["models", "auth", "--agent", "poe", "setup-token", "--provider", "anthropic"],
|
||||
command: modelsAuthSetupTokenCommand,
|
||||
expected: { agent: "poe", provider: "anthropic" },
|
||||
},
|
||||
{
|
||||
label: "paste-token",
|
||||
args: ["models", "auth", "--agent", "poe", "paste-token", "--provider", "anthropic"],
|
||||
command: modelsAuthPasteTokenCommand,
|
||||
expected: { agent: "poe", provider: "anthropic" },
|
||||
},
|
||||
{
|
||||
label: "login-github-copilot",
|
||||
args: ["models", "auth", "--agent", "poe", "login-github-copilot", "--yes"],
|
||||
command: modelsAuthLoginCommand,
|
||||
expected: { agent: "poe", provider: "github-copilot", method: "device", yes: true },
|
||||
},
|
||||
])("passes parent --agent to models auth $label", async ({ args, command, expected }) => {
|
||||
await runModelsCommand(args);
|
||||
|
||||
expect(command).toHaveBeenCalledWith(expect.objectContaining(expected), expect.any(Object));
|
||||
});
|
||||
|
||||
it("shows help for models auth without error exit", async () => {
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
|
||||
@@ -282,7 +282,7 @@ export function registerModelsCli(program: Command) {
|
||||
});
|
||||
|
||||
const auth = models.command("auth").description("Manage model auth profiles");
|
||||
auth.option("--agent <id>", "Agent id for auth order get/set/clear");
|
||||
auth.option("--agent <id>", "Agent id for auth commands");
|
||||
auth.action(() => {
|
||||
auth.help();
|
||||
});
|
||||
@@ -290,10 +290,13 @@ export function registerModelsCli(program: Command) {
|
||||
auth
|
||||
.command("add")
|
||||
.description("Interactive auth helper (provider auth or paste token)")
|
||||
.action(async () => {
|
||||
.action(async (command) => {
|
||||
const agent =
|
||||
resolveOptionFromCommand<string>(command, "agent") ??
|
||||
resolveOptionFromCommand<string>(auth, "agent");
|
||||
await runModelsCommand(async () => {
|
||||
const { modelsAuthAddCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthAddCommand({}, defaultRuntime);
|
||||
await modelsAuthAddCommand({ agent }, defaultRuntime);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -303,7 +306,8 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--provider <id>", "Provider id registered by a plugin")
|
||||
.option("--method <id>", "Provider auth method id")
|
||||
.option("--set-default", "Apply the provider's default model recommendation", false)
|
||||
.action(async (opts) => {
|
||||
.action(async (opts, command) => {
|
||||
const agent = resolveOptionFromCommand<string>(command, "agent");
|
||||
await runModelsCommand(async () => {
|
||||
const { modelsAuthLoginCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthLoginCommand(
|
||||
@@ -311,6 +315,7 @@ export function registerModelsCli(program: Command) {
|
||||
provider: opts.provider as string | undefined,
|
||||
method: opts.method as string | undefined,
|
||||
setDefault: Boolean(opts.setDefault),
|
||||
agent,
|
||||
},
|
||||
defaultRuntime,
|
||||
);
|
||||
@@ -322,13 +327,15 @@ export function registerModelsCli(program: Command) {
|
||||
.description("Run a provider CLI to create/sync a token (TTY required)")
|
||||
.option("--provider <name>", "Provider id")
|
||||
.option("--yes", "Skip confirmation", false)
|
||||
.action(async (opts) => {
|
||||
.action(async (opts, command) => {
|
||||
const agent = resolveOptionFromCommand<string>(command, "agent");
|
||||
await runModelsCommand(async () => {
|
||||
const { modelsAuthSetupTokenCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthSetupTokenCommand(
|
||||
{
|
||||
provider: opts.provider as string | undefined,
|
||||
yes: Boolean(opts.yes),
|
||||
agent,
|
||||
},
|
||||
defaultRuntime,
|
||||
);
|
||||
@@ -344,7 +351,8 @@ export function registerModelsCli(program: Command) {
|
||||
"--expires-in <duration>",
|
||||
"Optional expiry duration (e.g. 365d, 12h). Stored as absolute expiresAt.",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
.action(async (opts, command) => {
|
||||
const agent = resolveOptionFromCommand<string>(command, "agent");
|
||||
await runModelsCommand(async () => {
|
||||
const { modelsAuthPasteTokenCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthPasteTokenCommand(
|
||||
@@ -352,6 +360,7 @@ export function registerModelsCli(program: Command) {
|
||||
provider: opts.provider as string | undefined,
|
||||
profileId: opts.profileId as string | undefined,
|
||||
expiresIn: opts.expiresIn as string | undefined,
|
||||
agent,
|
||||
},
|
||||
defaultRuntime,
|
||||
);
|
||||
@@ -362,7 +371,8 @@ export function registerModelsCli(program: Command) {
|
||||
.command("login-github-copilot")
|
||||
.description("Login to GitHub Copilot via GitHub device flow (TTY required)")
|
||||
.option("--yes", "Overwrite existing profile without prompting", false)
|
||||
.action(async (opts) => {
|
||||
.action(async (opts, command) => {
|
||||
const agent = resolveOptionFromCommand<string>(command, "agent");
|
||||
await runModelsCommand(async () => {
|
||||
const { modelsAuthLoginCommand } = await import("../commands/models/auth.js");
|
||||
await modelsAuthLoginCommand(
|
||||
@@ -370,6 +380,7 @@ export function registerModelsCli(program: Command) {
|
||||
provider: "github-copilot",
|
||||
method: "device",
|
||||
yes: Boolean(opts.yes),
|
||||
agent,
|
||||
},
|
||||
defaultRuntime,
|
||||
);
|
||||
|
||||
@@ -74,11 +74,15 @@ vi.mock("@clack/prompts", () => ({
|
||||
text: mocks.clackText,
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/agent-scope.js", () => ({
|
||||
resolveDefaultAgentId: mocks.resolveDefaultAgentId,
|
||||
resolveAgentDir: mocks.resolveAgentDir,
|
||||
resolveAgentWorkspaceDir: mocks.resolveAgentWorkspaceDir,
|
||||
}));
|
||||
vi.mock("../../agents/agent-scope.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../agents/agent-scope.js")>();
|
||||
return {
|
||||
...actual,
|
||||
resolveDefaultAgentId: mocks.resolveDefaultAgentId,
|
||||
resolveAgentDir: mocks.resolveAgentDir,
|
||||
resolveAgentWorkspaceDir: mocks.resolveAgentWorkspaceDir,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../agents/workspace.js", () => ({
|
||||
resolveDefaultAgentWorkspaceDir: mocks.resolveDefaultAgentWorkspaceDir,
|
||||
@@ -92,10 +96,14 @@ vi.mock("../../wizard/clack-prompter.js", () => ({
|
||||
createClackPrompter: mocks.createClackPrompter,
|
||||
}));
|
||||
|
||||
vi.mock("./shared.js", () => ({
|
||||
loadValidConfigOrThrow: mocks.loadValidConfigOrThrow,
|
||||
updateConfig: mocks.updateConfig,
|
||||
}));
|
||||
vi.mock("./shared.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./shared.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadValidConfigOrThrow: mocks.loadValidConfigOrThrow,
|
||||
updateConfig: mocks.updateConfig,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../config/logging.js", () => ({
|
||||
logConfigUpdated: mocks.logConfigUpdated,
|
||||
@@ -199,8 +207,12 @@ vi.mock("../provider-auth-helpers.js", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const { modelsAuthLoginCommand, modelsAuthPasteTokenCommand, modelsAuthSetupTokenCommand } =
|
||||
await import("./auth.js");
|
||||
const {
|
||||
modelsAuthAddCommand,
|
||||
modelsAuthLoginCommand,
|
||||
modelsAuthPasteTokenCommand,
|
||||
modelsAuthSetupTokenCommand,
|
||||
} = await import("./auth.js");
|
||||
|
||||
function createRuntime(): RuntimeEnv {
|
||||
return {
|
||||
@@ -317,6 +329,22 @@ describe("modelsAuthLoginCommand", () => {
|
||||
restoreStdin = null;
|
||||
});
|
||||
|
||||
function useCoderAgentConfig() {
|
||||
currentConfig = {
|
||||
agents: {
|
||||
list: [{ id: "main" }, { id: "coder", workspace: "/tmp/openclaw/workspaces/coder" }],
|
||||
},
|
||||
};
|
||||
const originalConfig = currentConfig;
|
||||
mocks.resolveAgentDir.mockImplementation((_cfg: OpenClawConfig, agentId: string) =>
|
||||
agentId === "coder" ? "/tmp/openclaw/agents/coder" : "/tmp/openclaw/agents/main",
|
||||
);
|
||||
mocks.resolveAgentWorkspaceDir.mockImplementation((_cfg: OpenClawConfig, agentId: string) =>
|
||||
agentId === "coder" ? "/tmp/openclaw/workspaces/coder" : "/tmp/openclaw/workspace",
|
||||
);
|
||||
return originalConfig;
|
||||
}
|
||||
|
||||
it("runs plugin-owned openai-codex login", async () => {
|
||||
const runtime = createRuntime();
|
||||
const fakeStore = {
|
||||
@@ -372,6 +400,36 @@ describe("modelsAuthLoginCommand", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the requested agent store for provider auth login", async () => {
|
||||
const runtime = createRuntime();
|
||||
const coderStore = {
|
||||
profiles: {
|
||||
"openai-codex:coder@example.com": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
},
|
||||
},
|
||||
usageStats: {},
|
||||
};
|
||||
const originalConfig = useCoderAgentConfig();
|
||||
mocks.loadAuthProfileStoreForRuntime.mockReturnValue(coderStore);
|
||||
|
||||
await modelsAuthLoginCommand({ provider: "openai-codex", agent: "coder" }, runtime);
|
||||
|
||||
expect(mocks.resolveDefaultAgentId).not.toHaveBeenCalled();
|
||||
expect(mocks.resolveAgentDir).toHaveBeenCalledWith(originalConfig, "coder");
|
||||
expect(mocks.loadAuthProfileStoreForRuntime).toHaveBeenCalledWith("/tmp/openclaw/agents/coder");
|
||||
expect(runProviderAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: "/tmp/openclaw/agents/coder",
|
||||
workspaceDir: "/tmp/openclaw/workspaces/coder",
|
||||
}),
|
||||
);
|
||||
expect(mocks.upsertAuthProfile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agentDir: "/tmp/openclaw/agents/coder" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads the owning plugin for an explicit provider even in a clean config", async () => {
|
||||
const runtime = createRuntime();
|
||||
const runClaudeCliMigration = vi.fn().mockResolvedValue({
|
||||
@@ -418,6 +476,7 @@ describe("modelsAuthLoginCommand", () => {
|
||||
workspaceDir: "/tmp/openclaw/workspace",
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
includeUntrustedWorkspacePlugins: false,
|
||||
providerRefs: ["anthropic"],
|
||||
activate: true,
|
||||
}),
|
||||
@@ -706,6 +765,40 @@ describe("modelsAuthLoginCommand", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("writes pasted tokens to the requested agent store", async () => {
|
||||
const runtime = createRuntime();
|
||||
useCoderAgentConfig();
|
||||
mocks.clackText.mockResolvedValue("openai-token");
|
||||
|
||||
await modelsAuthPasteTokenCommand({ provider: "openai", agent: "coder" }, runtime);
|
||||
|
||||
expect(mocks.resolveDefaultAgentId).not.toHaveBeenCalled();
|
||||
expect(mocks.upsertAuthProfile).toHaveBeenCalledWith({
|
||||
profileId: "openai:manual",
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "openai",
|
||||
token: "openai-token",
|
||||
},
|
||||
agentDir: "/tmp/openclaw/agents/coder",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects an unknown agent before prompting for pasted tokens", async () => {
|
||||
const runtime = createRuntime();
|
||||
currentConfig = { agents: { list: [{ id: "main" }] } };
|
||||
|
||||
await expect(
|
||||
modelsAuthPasteTokenCommand({ provider: "openai", agent: "missing" }, runtime),
|
||||
).rejects.toThrow(
|
||||
'Unknown agent id "missing". Use "openclaw agents list" to see configured agents.',
|
||||
);
|
||||
|
||||
expect(mocks.clackText).not.toHaveBeenCalled();
|
||||
expect(mocks.upsertAuthProfile).not.toHaveBeenCalled();
|
||||
expect(mocks.updateConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs token auth for any token-capable provider plugin", async () => {
|
||||
const runtime = createRuntime();
|
||||
const runTokenAuth = vi.fn().mockResolvedValue({
|
||||
@@ -748,4 +841,118 @@ describe("modelsAuthLoginCommand", () => {
|
||||
agentDir: "/tmp/openclaw/agents/main",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the requested agent store for setup-token provider auth", async () => {
|
||||
const runtime = createRuntime();
|
||||
useCoderAgentConfig();
|
||||
const runTokenAuth = vi.fn().mockResolvedValue({
|
||||
profiles: [
|
||||
{
|
||||
profileId: "moonshot:token",
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "moonshot",
|
||||
token: "moonshot-token",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.resolvePluginProviders.mockReturnValue([
|
||||
{
|
||||
id: "moonshot",
|
||||
label: "Moonshot",
|
||||
auth: [
|
||||
{
|
||||
id: "setup-token",
|
||||
label: "setup-token",
|
||||
kind: "token",
|
||||
run: runTokenAuth,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
await modelsAuthSetupTokenCommand({ provider: "moonshot", yes: true, agent: "coder" }, runtime);
|
||||
|
||||
expect(mocks.resolveDefaultAgentId).not.toHaveBeenCalled();
|
||||
expect(runTokenAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: "/tmp/openclaw/agents/coder",
|
||||
workspaceDir: "/tmp/openclaw/workspaces/coder",
|
||||
}),
|
||||
);
|
||||
expect(mocks.upsertAuthProfile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agentDir: "/tmp/openclaw/agents/coder" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the requested agent store for interactive token auth add", async () => {
|
||||
const runtime = createRuntime();
|
||||
useCoderAgentConfig();
|
||||
const runTokenAuth = vi.fn().mockResolvedValue({
|
||||
profiles: [
|
||||
{
|
||||
profileId: "moonshot:token",
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "moonshot",
|
||||
token: "moonshot-token",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.resolvePluginProviders.mockReturnValue([
|
||||
{
|
||||
id: "moonshot",
|
||||
label: "Moonshot",
|
||||
auth: [
|
||||
{
|
||||
id: "setup-token",
|
||||
label: "setup-token",
|
||||
kind: "token",
|
||||
run: runTokenAuth,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
mocks.clackSelect.mockResolvedValueOnce("moonshot").mockResolvedValueOnce("setup-token");
|
||||
|
||||
await modelsAuthAddCommand({ agent: "coder" }, runtime);
|
||||
|
||||
expect(mocks.resolveDefaultAgentId).not.toHaveBeenCalled();
|
||||
expect(runTokenAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentDir: "/tmp/openclaw/agents/coder",
|
||||
workspaceDir: "/tmp/openclaw/workspaces/coder",
|
||||
}),
|
||||
);
|
||||
expect(mocks.upsertAuthProfile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agentDir: "/tmp/openclaw/agents/coder" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the requested agent store when interactive auth add falls back to paste-token", async () => {
|
||||
const runtime = createRuntime();
|
||||
useCoderAgentConfig();
|
||||
mocks.resolvePluginProviders.mockReturnValue([]);
|
||||
mocks.clackSelect.mockResolvedValue("custom");
|
||||
mocks.clackText
|
||||
.mockResolvedValueOnce("openai")
|
||||
.mockResolvedValueOnce("openai:manual")
|
||||
.mockResolvedValueOnce("openai-token");
|
||||
mocks.clackConfirm.mockResolvedValue(false);
|
||||
|
||||
await modelsAuthAddCommand({ agent: "coder" }, runtime);
|
||||
|
||||
expect(mocks.resolveDefaultAgentId).not.toHaveBeenCalled();
|
||||
expect(mocks.upsertAuthProfile).toHaveBeenCalledWith({
|
||||
profileId: "openai:manual",
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "openai",
|
||||
token: "openai-token",
|
||||
},
|
||||
agentDir: "/tmp/openclaw/agents/coder",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
pickAuthMethod,
|
||||
resolveProviderMatch,
|
||||
} from "../provider-auth-helpers.js";
|
||||
import { loadValidConfigOrThrow, updateConfig } from "./shared.js";
|
||||
import { loadValidConfigOrThrow, resolveKnownAgentId, updateConfig } from "./shared.js";
|
||||
|
||||
function guardCancel<T>(value: T | symbol): T {
|
||||
if (typeof value === "symbol" || isCancel(value)) {
|
||||
@@ -103,16 +103,20 @@ function listProvidersWithTokenMethods(providers: ProviderPlugin[]): ProviderPlu
|
||||
|
||||
async function resolveModelsAuthContext(params?: {
|
||||
requestedProvider?: string;
|
||||
rawAgentId?: string | null;
|
||||
}): Promise<ResolvedModelsAuthContext> {
|
||||
const config = await loadValidConfigOrThrow();
|
||||
const defaultAgentId = resolveDefaultAgentId(config);
|
||||
const agentDir = resolveAgentDir(config, defaultAgentId);
|
||||
const agentId =
|
||||
resolveKnownAgentId({ cfg: config, rawAgentId: params?.rawAgentId }) ??
|
||||
resolveDefaultAgentId(config);
|
||||
const agentDir = resolveAgentDir(config, agentId);
|
||||
const workspaceDir =
|
||||
resolveAgentWorkspaceDir(config, defaultAgentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||
resolveAgentWorkspaceDir(config, agentId) ?? resolveDefaultAgentWorkspaceDir();
|
||||
const providers = resolvePluginProviders({
|
||||
config,
|
||||
workspaceDir,
|
||||
mode: "setup",
|
||||
includeUntrustedWorkspacePlugins: false,
|
||||
bundledProviderAllowlistCompat: true,
|
||||
bundledProviderVitestCompat: true,
|
||||
...(params?.requestedProvider?.trim()
|
||||
@@ -127,9 +131,10 @@ async function resolveModelsAuthContext(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveModelsAuthAgentDir(): Promise<string> {
|
||||
async function resolveModelsAuthAgentDir(rawAgentId?: string | null): Promise<string> {
|
||||
const config = await loadValidConfigOrThrow();
|
||||
return resolveAgentDir(config, resolveDefaultAgentId(config));
|
||||
const agentId = resolveKnownAgentId({ cfg: config, rawAgentId }) ?? resolveDefaultAgentId(config);
|
||||
return resolveAgentDir(config, agentId);
|
||||
}
|
||||
|
||||
function resolveRequestedProviderOrThrow(
|
||||
@@ -321,7 +326,7 @@ async function runProviderAuthMethod(params: {
|
||||
}
|
||||
|
||||
export async function modelsAuthSetupTokenCommand(
|
||||
opts: { provider?: string; yes?: boolean },
|
||||
opts: { provider?: string; yes?: boolean; agent?: string },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
if (!process.stdin.isTTY) {
|
||||
@@ -330,6 +335,7 @@ export async function modelsAuthSetupTokenCommand(
|
||||
|
||||
const { config, agentDir, workspaceDir, providers } = await resolveModelsAuthContext({
|
||||
requestedProvider: opts.provider,
|
||||
rawAgentId: opts.agent,
|
||||
});
|
||||
const tokenProviders = listProvidersWithTokenMethods(providers);
|
||||
if (tokenProviders.length === 0) {
|
||||
@@ -376,10 +382,11 @@ export async function modelsAuthPasteTokenCommand(
|
||||
provider?: string;
|
||||
profileId?: string;
|
||||
expiresIn?: string;
|
||||
agent?: string;
|
||||
},
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const agentDir = await resolveModelsAuthAgentDir();
|
||||
const agentDir = await resolveModelsAuthAgentDir(opts.agent);
|
||||
const rawProvider = normalizeOptionalString(opts.provider);
|
||||
if (!rawProvider) {
|
||||
throw new Error("Missing --provider.");
|
||||
@@ -435,8 +442,10 @@ export async function modelsAuthPasteTokenCommand(
|
||||
}
|
||||
}
|
||||
|
||||
export async function modelsAuthAddCommand(_opts: Record<string, never>, runtime: RuntimeEnv) {
|
||||
const { config, agentDir, workspaceDir, providers } = await resolveModelsAuthContext();
|
||||
export async function modelsAuthAddCommand(opts: { agent?: string }, runtime: RuntimeEnv) {
|
||||
const { config, agentDir, workspaceDir, providers } = await resolveModelsAuthContext({
|
||||
rawAgentId: opts.agent,
|
||||
});
|
||||
const tokenProviders = listProvidersWithTokenMethods(providers);
|
||||
|
||||
const provider = await select({
|
||||
@@ -528,7 +537,10 @@ export async function modelsAuthAddCommand(_opts: Record<string, never>, runtime
|
||||
).trim()
|
||||
: undefined;
|
||||
|
||||
await modelsAuthPasteTokenCommand({ provider: providerId, profileId, expiresIn }, runtime);
|
||||
await modelsAuthPasteTokenCommand(
|
||||
{ provider: providerId, profileId, expiresIn, agent: opts.agent },
|
||||
runtime,
|
||||
);
|
||||
}
|
||||
|
||||
type LoginOptions = {
|
||||
@@ -536,6 +548,7 @@ type LoginOptions = {
|
||||
method?: string;
|
||||
setDefault?: boolean;
|
||||
yes?: boolean;
|
||||
agent?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -588,6 +601,7 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
|
||||
|
||||
const { config, agentDir, workspaceDir, providers } = await resolveModelsAuthContext({
|
||||
requestedProvider: opts.provider,
|
||||
rawAgentId: opts.agent,
|
||||
});
|
||||
const prompter = createClackPrompter();
|
||||
const authProviders = listProvidersWithAuthMethods(providers);
|
||||
|
||||
Reference in New Issue
Block a user