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:
Neerav Makwana
2026-04-26 00:30:47 -04:00
committed by GitHub
parent 1252da325f
commit dc9ce2a1bf
6 changed files with 323 additions and 36 deletions

View File

@@ -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.

View File

@@ -154,6 +154,9 @@ provider you choose.
`models auth login` runs a provider plugins 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:

View File

@@ -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();

View File

@@ -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,
);

View File

@@ -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",
});
});
});

View File

@@ -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);