From 2304ed4ef3a54dda8a3c99396fed4dbced5af2b3 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Mon, 14 Sep 2026 11:44:43 +0900 Subject: [PATCH 1/3] fix(config): track recovery backups without suppressing recovery --- src/config.ts | 6 +++ src/oauth/store.ts | 6 +++ structure/config.md | 6 +++ structure/overview.md | 3 +- structure/providers/xai-grok.md | 2 + .../config/config-ownership-uninstall.test.ts | 51 ++++++++++++++++++- tests/oauth/oauth-store-multi.test.ts | 36 +++++++++++++ 7 files changed, 107 insertions(+), 3 deletions(-) diff --git a/src/config.ts b/src/config.ts index 4f851e4ffb..a2e19d090d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4700,6 +4700,12 @@ export function backupInvalidConfig(configPath: string): string | null { try { copyFileSync(configPath, backupPath); try { chmodSync(backupPath, 0o600); } catch { /* best-effort */ } + try { + // Legacy/shared homes may intentionally refuse ownership. Preserve recovery anyway. + recordOwnedConfigPath(getConfigDir(), backupPath); + } catch { + console.warn("[config] Recovery backup created, but uninstall ownership registration failed."); + } return backupPath; } catch { return null; diff --git a/src/oauth/store.ts b/src/oauth/store.ts index 04ac676c9a..25eb6cd42c 100644 --- a/src/oauth/store.ts +++ b/src/oauth/store.ts @@ -437,6 +437,12 @@ function backupLegacyOnce(): void { try { copyFileSync(path, backup); try { chmodSync(backup, 0o600); } catch { /* best-effort */ } + try { + // Register only the copy we just created. An unowned home still needs downgrade recovery. + recordOwnedConfigPath(getConfigDir(), backup); + } catch { + console.warn("[oauth] Recovery backup created, but uninstall ownership registration failed."); + } } catch { /* best-effort */ } } diff --git a/structure/config.md b/structure/config.md index a4c0b97ead..128d86c276 100644 --- a/structure/config.md +++ b/structure/config.md @@ -242,6 +242,12 @@ and removes only normalized manifest entries. Manifest-owned directory links are traversing their targets. Unknown files remain in place and make the command report a partial uninstall with their exact paths. +New invalid-config recovery copies and the newly created OAuth downgrade copy are registered +after copying, so owned uninstall includes them. Registration is best-effort: an intentionally +unowned legacy home or a metadata-write failure must not suppress the recovery copy. Existing +OAuth downgrade copies are neither rewritten nor retroactively claimed. Unregistered copies +remain subject to the existing partial/refused uninstall result. + Legacy nonempty config directories are deliberately not retroactively claimed. If either ownership file is missing, malformed, or bound to another root, uninstall refuses config deletion and reports the residual directory for manual review; there is no recursive-delete fallback. diff --git a/structure/overview.md b/structure/overview.md index 0151115adc..b81b030f0c 100644 --- a/structure/overview.md +++ b/structure/overview.md @@ -47,7 +47,8 @@ preserves saved user model selections and historical usage. See the bounded installed service resolve it the same way (`src/config.ts`). Ownership inside that root is tracked by the uninstall manifest in `src/lib/config-ownership.ts`, which starts from a declared path list and grows as opencodex claims further paths at runtime — so the manifest, not this table, is what -bounds uninstall. This table groups the state by purpose; it is not an exhaustive file list, and +bounds uninstall. Newly generated recovery backups follow the [backup ownership contract](config.md#restore) +without suppressing recovery when registration is unavailable. This table groups state by purpose; it is not an exhaustive file list, and derived files such as `auth.json.pre-multiauth` are covered by the group they belong to. `$CODEX_HOME` is a separate root with a separate owner, and opencodex writes there too: removing the diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 0ca554f4e0..9cfa4afd06 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -35,6 +35,8 @@ The shared Responses path follows the [bounded multipart recovery contract](../s `auth.json` load-merge-persist (`src/oauth/store.ts`); generation-guarded persist (`expectedGeneration` → superseded adoption), conditional `needsReauth`, bounded jittered retry for transient token-endpoint failures. + Newly created legacy-store recovery copies follow the [backup ownership contract](../config.md#restore); + an ownership-registration failure does not discard downgrade recovery. - **Reactive 401 replay:** both the adapter recovery loop and native Responses passthrough branch force-refresh once (singleflight, generation-checked) and replay OAuth-backed xAI requests exactly once with a re-resolved transport; API-key/BYOK paths are excluded diff --git a/tests/config/config-ownership-uninstall.test.ts b/tests/config/config-ownership-uninstall.test.ts index 79c8888f90..c8a2830644 100644 --- a/tests/config/config-ownership-uninstall.test.ts +++ b/tests/config/config-ownership-uninstall.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, test } from "bun:test"; +import { describe, expect, test, spyOn } from "bun:test"; +import * as ownership from "../../src/lib/config-ownership"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -8,10 +9,56 @@ import { recordOwnedConfigPath, removeOwnedConfigState, } from "../../src/lib/config-ownership"; -import { getDefaultConfig, saveConfig } from "../../src/config"; +import { backupInvalidConfig, getDefaultConfig, saveConfig } from "../../src/config"; import { removeTreeWithRetry } from "../helpers/remove-tree"; describe("owned config uninstall", () => { + test("ownership registration exceptions do not invalidate a completed recovery backup", () => { + const dir = mkdtempSync(join(tmpdir(), "ocx-backup-register-failure-")); + const path = join(dir, "config.json"); + const previous = process.env.OPENCODEX_HOME; + process.env.OPENCODEX_HOME = dir; + writeFileSync(path, "recover-this-fixture"); + const registration = spyOn(ownership, "recordOwnedConfigPath").mockImplementation(() => { throw new Error("fixture failure"); }); + const warning = spyOn(console, "warn").mockImplementation(() => {}); + try { + const backup = backupInvalidConfig(path); + expect(backup).not.toBeNull(); + expect(readFileSync(backup!, "utf8")).toBe("recover-this-fixture"); + expect(warning).toHaveBeenCalledTimes(1); + } finally { + registration.mockRestore(); + warning.mockRestore(); + if (previous === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previous; + removeTreeWithRetry(dir); + } + }); + + for (const owned of [true, false]) { + test(`invalid-config backup preserves recovery and respects ownership (${owned})`, () => { + const dir = mkdtempSync(join(tmpdir(), "ocx-backup-owned-")); + const path = join(dir, "config.json"); + const previous = process.env.OPENCODEX_HOME; + process.env.OPENCODEX_HOME = dir; + try { + if (owned) expect(recordOwnedConfigPath(dir, path)).toBe(true); + writeFileSync(path, '{"recovery-fixture":'); + if (!owned) expect(recordOwnedConfigPath(dir, path)).toBe(false); + const backup = backupInvalidConfig(path); + expect(backup).not.toBeNull(); + expect(readFileSync(backup!, "utf8")).toBe(readFileSync(path, "utf8")); + const removal = removeOwnedConfigState(dir); + expect(removal.status).toBe(owned ? "removed" : "refused"); + expect(existsSync(backup!)).toBe(!owned); + } finally { + if (previous === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previous; + removeTreeWithRetry(dir); + } + }); + } + test("first owned write creates a missing config root and its metadata", () => { const parent = mkdtempSync(join(tmpdir(), "ocx-config-first-owned-path-")); const dir = join(parent, "config"); diff --git a/tests/oauth/oauth-store-multi.test.ts b/tests/oauth/oauth-store-multi.test.ts index 02d8f8024f..ba4f49f6d5 100644 --- a/tests/oauth/oauth-store-multi.test.ts +++ b/tests/oauth/oauth-store-multi.test.ts @@ -36,6 +36,7 @@ import { } from "../../src/oauth/store"; import type { OAuthCredentials } from "../../src/oauth/types"; import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { recordOwnedConfigPath, removeOwnedConfigState } from "../../src/lib/config-ownership"; const TEST_DIR = join(import.meta.dir, ".tmp-oauth-store-multi-test"); let previousOpencodexHome: string | undefined; @@ -152,6 +153,41 @@ describe("multi-account auth store", () => { expect(existsSync(`${authPath}.pre-multiauth`)).toBe(true); }); + test("uninstall removes a legacy recovery backup from an owned home", async () => { + const dir = join(TEST_DIR, "owned"); + const path = join(dir, "auth.json"); + process.env.OPENCODEX_HOME = dir; + try { + expect(recordOwnedConfigPath(dir, path)).toBe(true); + const original = JSON.stringify({ xai: cred({ email: "old@example.test" }) }); + writeFileSync(path, original); + await saveCredential("xai", cred({ email: "old@example.test", access: "new-access" })); + expect(readFileSync(`${path}.pre-multiauth`, "utf8")).toBe(original); + await flushConfigDirHardeningForTests(); + expect(removeOwnedConfigState(dir).status).toBe("removed"); + expect(existsSync(`${path}.pre-multiauth`)).toBe(false); + } finally { + process.env.OPENCODEX_HOME = TEST_DIR; + } + }); + + test("migration leaves a pre-existing unregistered backup unchanged and unclaimed", async () => { + const dir = join(TEST_DIR, "existing-backup"); + const path = join(dir, "auth.json"); + process.env.OPENCODEX_HOME = dir; + try { + expect(recordOwnedConfigPath(dir, path)).toBe(true); + writeFileSync(path, JSON.stringify({ xai: cred({ email: "old@example.test" }) })); + writeFileSync(`${path}.pre-multiauth`, "prior-recovery-fixture"); + await saveCredential("xai", cred({ email: "old@example.test" })); + await flushConfigDirHardeningForTests(); + expect(removeOwnedConfigState(dir).status).toBe("partial"); + expect(readFileSync(`${path}.pre-multiauth`, "utf8")).toBe("prior-recovery-fixture"); + } finally { + process.env.OPENCODEX_HOME = TEST_DIR; + } + }); + test("legacy credential WITHOUT identity gets a deterministic account id across loads", async () => { // Legacy stores are re-normalized on EVERY load without being persisted, so the // derived id must be stable: a time-salted id would make getAccountSet and From 993e5578417a01c798c8bf05557d3acaf4dfcf37 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 15 Sep 2026 22:24:08 +0900 Subject: [PATCH 2/3] fix(config): track the OAuth recovery backup without suppressing recovery Splits out the half of this change that could not ship. The src/oauth/store.ts hunk is unchanged: backupLegacyOnce registers the auth.json.pre-multiauth copy it just created, and a registration failure warns and leaves the copy intact, so recovery wins and registration is best-effort. What is removed is the matching registration in src/config/salvage.ts. backupInvalidConfig mints a timestamp-unique path on every invalid-config load, and recordOwnedConfigPath appends without a bound, deduping only on an identical string that a timestamped name never is. isManifest rejects a manifest whose paths exceed MANIFEST_MAX_PATHS (1024) at read time, so past that threshold loadOwnership returns null permanently: createOwnership will not replace it because the directory is not empty, and nothing self-heals. The consequence inverts this change's own goal. removeOwnedConfigState then takes its refusal branch, "config ownership metadata is missing or invalid", so uninstall stops deleting auth.json and config.json and leaves refresh tokens and API keys on disk. A service restarting against a persistently broken config.json reaches that, one manifest slot and one backup file per load. backupLegacyOnce does not have the problem because it is guarded once-only and registers a fixed name, which is also true of every pre-existing caller; salvage.ts would have been the first unbounded one. The two salvage-specific tests are removed with it, and structure/config.md now records why invalid-config copies are not registered, plus the shape that would work: sweep them by name pattern at removal time, so ownership stays a fixed-size manifest and cleanup stays complete. That is a separate change. bun test tests/config/config-ownership-uninstall.test.ts tests/oauth/oauth-store-multi.test.ts: 51 pass / 0 fail. bun run structure:check: pass. Co-authored-by: luvs01 --- src/config/salvage.ts | 8 --- structure/config.md | 8 ++- .../config/config-ownership-uninstall.test.ts | 51 +------------------ 3 files changed, 8 insertions(+), 59 deletions(-) diff --git a/src/config/salvage.ts b/src/config/salvage.ts index c184cee125..254e4e19f6 100644 --- a/src/config/salvage.ts +++ b/src/config/salvage.ts @@ -3,8 +3,6 @@ import { join } from "node:path"; import * as z from "zod/v4"; import { CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR } from "../codex/account-namespace-match"; import { redactSecretString } from "../lib/redact"; -import { recordOwnedConfigPath } from "../lib/config-ownership"; -import { getConfigDir } from "./paths"; import { hasWarnedConfigFallback, markWarnedConfigFallback } from "./warn-memo"; import { configSchema } from "./schema/config-schema"; import type { OcxConfig } from "../types"; @@ -239,12 +237,6 @@ export function backupInvalidConfig(configPath: string): string | null { try { copyFileSync(configPath, backupPath); try { chmodSync(backupPath, 0o600); } catch { /* best-effort */ } - try { - // Legacy/shared homes may intentionally refuse ownership. Preserve recovery anyway. - recordOwnedConfigPath(getConfigDir(), backupPath); - } catch { - console.warn("[config] Recovery backup created, but uninstall ownership registration failed."); - } return backupPath; } catch { return null; diff --git a/structure/config.md b/structure/config.md index 7e60ded716..3f29d3e6c9 100644 --- a/structure/config.md +++ b/structure/config.md @@ -256,8 +256,12 @@ and removes only normalized manifest entries. Manifest-owned directory links are traversing their targets. Unknown files remain in place and make the command report a partial uninstall with their exact paths. -New invalid-config recovery copies and the newly created OAuth downgrade copy are registered -after copying, so owned uninstall includes them. Registration is best-effort: an intentionally +The newly created OAuth downgrade copy is registered after copying, so owned uninstall +includes it. Invalid-config recovery copies are deliberately NOT registered: their names carry +a timestamp, so one entry per invalid load would grow the uninstall manifest without bound, and +the manifest stops validating past its path ceiling. A manifest that stops validating makes +uninstall refuse outright, which would leave credentials on disk. Sweeping those copies by name +pattern at removal time is the shape that fits; it is not in this change. Registration is best-effort: an intentionally unowned legacy home or a metadata-write failure must not suppress the recovery copy. Existing OAuth downgrade copies are neither rewritten nor retroactively claimed. Unregistered copies remain subject to the existing partial/refused uninstall result. diff --git a/tests/config/config-ownership-uninstall.test.ts b/tests/config/config-ownership-uninstall.test.ts index c8a2830644..79c8888f90 100644 --- a/tests/config/config-ownership-uninstall.test.ts +++ b/tests/config/config-ownership-uninstall.test.ts @@ -1,5 +1,4 @@ -import { describe, expect, test, spyOn } from "bun:test"; -import * as ownership from "../../src/lib/config-ownership"; +import { describe, expect, test } from "bun:test"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -9,56 +8,10 @@ import { recordOwnedConfigPath, removeOwnedConfigState, } from "../../src/lib/config-ownership"; -import { backupInvalidConfig, getDefaultConfig, saveConfig } from "../../src/config"; +import { getDefaultConfig, saveConfig } from "../../src/config"; import { removeTreeWithRetry } from "../helpers/remove-tree"; describe("owned config uninstall", () => { - test("ownership registration exceptions do not invalidate a completed recovery backup", () => { - const dir = mkdtempSync(join(tmpdir(), "ocx-backup-register-failure-")); - const path = join(dir, "config.json"); - const previous = process.env.OPENCODEX_HOME; - process.env.OPENCODEX_HOME = dir; - writeFileSync(path, "recover-this-fixture"); - const registration = spyOn(ownership, "recordOwnedConfigPath").mockImplementation(() => { throw new Error("fixture failure"); }); - const warning = spyOn(console, "warn").mockImplementation(() => {}); - try { - const backup = backupInvalidConfig(path); - expect(backup).not.toBeNull(); - expect(readFileSync(backup!, "utf8")).toBe("recover-this-fixture"); - expect(warning).toHaveBeenCalledTimes(1); - } finally { - registration.mockRestore(); - warning.mockRestore(); - if (previous === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previous; - removeTreeWithRetry(dir); - } - }); - - for (const owned of [true, false]) { - test(`invalid-config backup preserves recovery and respects ownership (${owned})`, () => { - const dir = mkdtempSync(join(tmpdir(), "ocx-backup-owned-")); - const path = join(dir, "config.json"); - const previous = process.env.OPENCODEX_HOME; - process.env.OPENCODEX_HOME = dir; - try { - if (owned) expect(recordOwnedConfigPath(dir, path)).toBe(true); - writeFileSync(path, '{"recovery-fixture":'); - if (!owned) expect(recordOwnedConfigPath(dir, path)).toBe(false); - const backup = backupInvalidConfig(path); - expect(backup).not.toBeNull(); - expect(readFileSync(backup!, "utf8")).toBe(readFileSync(path, "utf8")); - const removal = removeOwnedConfigState(dir); - expect(removal.status).toBe(owned ? "removed" : "refused"); - expect(existsSync(backup!)).toBe(!owned); - } finally { - if (previous === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = previous; - removeTreeWithRetry(dir); - } - }); - } - test("first owned write creates a missing config root and its metadata", () => { const parent = mkdtempSync(join(tmpdir(), "ocx-config-first-owned-path-")); const dir = join(parent, "config"); From 12545992954c0e1819f5ff74870133f3775404bb Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Wed, 16 Sep 2026 09:07:42 +0900 Subject: [PATCH 3/3] fix(oauth): warn when recovery backup registration is refused Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- src/oauth/store.ts | 4 +++- structure/config.md | 3 ++- structure/providers/xai-grok.md | 2 +- structure/runtime.md | 3 +++ structure/transports/inventory.md | 3 +++ tests/oauth/oauth-store-multi.test.ts | 25 +++++++++++++++++++++++++ 6 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/oauth/store.ts b/src/oauth/store.ts index 25eb6cd42c..a7ea6eaa2d 100644 --- a/src/oauth/store.ts +++ b/src/oauth/store.ts @@ -439,7 +439,9 @@ function backupLegacyOnce(): void { try { chmodSync(backup, 0o600); } catch { /* best-effort */ } try { // Register only the copy we just created. An unowned home still needs downgrade recovery. - recordOwnedConfigPath(getConfigDir(), backup); + if (!recordOwnedConfigPath(getConfigDir(), backup)) { + console.warn("[oauth] Recovery backup created, but uninstall ownership registration failed."); + } } catch { console.warn("[oauth] Recovery backup created, but uninstall ownership registration failed."); } diff --git a/structure/config.md b/structure/config.md index 3f29d3e6c9..540acf4240 100644 --- a/structure/config.md +++ b/structure/config.md @@ -263,7 +263,8 @@ the manifest stops validating past its path ceiling. A manifest that stops valid uninstall refuse outright, which would leave credentials on disk. Sweeping those copies by name pattern at removal time is the shape that fits; it is not in this change. Registration is best-effort: an intentionally unowned legacy home or a metadata-write failure must not suppress the recovery copy. Existing -OAuth downgrade copies are neither rewritten nor retroactively claimed. Unregistered copies +OAuth downgrade copies are neither rewritten nor retroactively claimed. Both a `false` registration +result and a thrown registration error emit the same fixed warning without error details. Unregistered copies remain subject to the existing partial/refused uninstall result. Legacy nonempty config directories are deliberately not retroactively claimed. If either ownership diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 2e682b1843..393343340b 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -39,7 +39,7 @@ The shared Responses path follows the [bounded multipart recovery contract](../s (`expectedGeneration` → superseded adoption), conditional `needsReauth`, bounded jittered retry for transient token-endpoint failures. Newly created legacy-store recovery copies follow the [backup ownership contract](../config.md#restore); - an ownership-registration failure does not discard downgrade recovery. + an ownership-registration failure (a `false` return or thrown error) warns without discarding downgrade recovery. - **Reactive 401 replay:** both the adapter recovery loop and native Responses passthrough branch force-refresh once (singleflight, generation-checked) and replay OAuth-backed xAI requests exactly once with a re-resolved transport; API-key/BYOK paths are excluded diff --git a/structure/runtime.md b/structure/runtime.md index 37cee672c1..d7c0434f2f 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -1,5 +1,8 @@ # Runtime +OAuth recovery-copy registration warns on both refusal and exceptions while preserving the copy; +see the [backup ownership contract](config.md#restore). + Responses admission and finalization are composed through the [core module ownership](transports/responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 91d527750a..3806cfe9f3 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -1,5 +1,8 @@ # Transport Inventory +The shared OAuth store warns on recovery-copy ownership refusal or exceptions without losing +the copy; see [backup ownership](../config.md#restore). + The existing Responses transport is divided by responsibility in the [core module ownership](responses.md#core-module-ownership). This surface retains its existing behavior. diff --git a/tests/oauth/oauth-store-multi.test.ts b/tests/oauth/oauth-store-multi.test.ts index ba4f49f6d5..32fcf293f8 100644 --- a/tests/oauth/oauth-store-multi.test.ts +++ b/tests/oauth/oauth-store-multi.test.ts @@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import * as atomicWrite from "../../src/config/atomic-write"; import * as oauthStore from "../../src/oauth/store"; +import * as configOwnership from "../../src/lib/config-ownership"; import { flushConfigDirHardeningForTests } from "../../src/config/paths"; import { resetHardenedStateForTests, @@ -171,6 +172,30 @@ describe("multi-account auth store", () => { } }); + test.each(["false", "throw"] as const)("recovery survives registration %s and warns without exposing credentials", async (failure) => { + const path = join(TEST_DIR, "auth.json"); + const backup = `${path}.pre-multiauth`; + const original = JSON.stringify({ xai: cred({ email: "old@example.test" }) }); + writeFileSync(path, original); + const register = configOwnership.recordOwnedConfigPath; + const registration = spyOn(configOwnership, "recordOwnedConfigPath").mockImplementation((dir, candidate) => { + if (candidate !== backup) return register(dir, candidate); + if (failure === "throw") throw new Error("private ownership failure fixture"); + return false; + }); + const warning = spyOn(console, "warn").mockImplementation(() => {}); + try { + await saveCredential("xai", cred({ email: "old@example.test", access: "new-access" })); + expect(registration).toHaveBeenCalledWith(TEST_DIR, backup); + expect(readFileSync(backup, "utf8")).toBe(original); + expect(getCredential("xai")?.access).toBe("new-access"); + expect(warning.mock.calls).toEqual([["[oauth] Recovery backup created, but uninstall ownership registration failed."]]); + } finally { + warning.mockRestore(); + registration.mockRestore(); + } + }); + test("migration leaves a pre-existing unregistered backup unchanged and unclaimed", async () => { const dir = join(TEST_DIR, "existing-backup"); const path = join(dir, "auth.json");