Skip to content

Commit c7fe9e8

Browse files
committed
fix(kiro): discover the Windows kiro-cli token DB on login import (#710)
`ocx login kiro` could not import an existing Windows Kiro CLI session: it reported no kiro-cli token and fell back to manual access-token paste even while `kiro-cli whoami` showed an active login. The reader and token selector were fine — only discovery was short. `nativeKiroCliSessionEntries()` listed just the macOS and Linux stores, so `%LOCALAPPDATA%\Kiro-Cli\data.sqlite3` was never a candidate. The reporter confirmed that pointing KIROCLI_DB_PATH at it makes the import succeed with no manual paste. Extract the candidate list into a pure `resolveKiroCliNativeSessionEntries( env, platform, home)` and add the Windows store. Pure and parameterized following src/claude/desktop-3p-paths.ts: `process.platform` is stubbable here, but `os.platform()` does not follow it under Bun, so a resolver taking its inputs is the reliable way to exercise the win32 branch and each of its env fallback rungs from a macOS or Linux host. Windows resolves LOCALAPPDATA -> %USERPROFILE%\AppData\Local -> injected platform-native home. Deliberately not `userHome()`, which is `HOME || homedir()`: Windows shells (Git Bash / MSYS / CI) routinely export a POSIX-style HOME, which would aim the fallback at a non-native path. POSIX branches keep `userHome()` so existing HOME-based fixtures still resolve. The list returns one store per platform now instead of macOS+Linux on every host. It also feeds forced-login snapshot/rollback, so naming the database the local kiro-cli actually mutates is the correct contract; every consumer already guards with existsSync. Because the old cross-platform return value was load-bearing for fixtures, four suites seeded the macOS layout unconditionally and would stop finding their fixtures on the Linux and Windows CI legs. They now seed the host-resolved layout and isolate LOCALAPPDATA/USERPROFILE, without which a Windows runner could read the real user profile. Also name the Windows store in the snapshot-failure repair message, which Windows users can now reach. Verification: bun run test 5987 pass / 1 skip / 0 fail (mandated by src/AGENTS.md for OAuth changes); bun x tsc --noEmit clean; new suite 6 pass covering all three win32 rungs by name; four amended suites 129 pass; privacy scan passed. Closes #710
1 parent 47cc9a5 commit c7fe9e8

7 files changed

Lines changed: 221 additions & 18 deletions

src/oauth/kiro-credentials.ts

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { randomUUID } from "node:crypto";
22
import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
33
import { homedir } from "node:os";
4-
import { isAbsolute, join } from "node:path";
4+
import { isAbsolute, join, win32 } from "node:path";
55
import { Database } from "bun:sqlite";
66

77
const DEFAULT_EXPIRES_MS = 3600_000;
@@ -39,7 +39,14 @@ export type KiroDiagnosticStatus =
3939
| "registration_found";
4040

4141
export interface KiroImportDiagnostic {
42-
location: "kiro-creds-file" | "kiro-cli-db-env" | "kiro-cli-data" | "kiro-cli-linux-data" | "amazon-q-data" | "kiro-sso-cache";
42+
location:
43+
| "kiro-creds-file"
44+
| "kiro-cli-db-env"
45+
| "kiro-cli-data"
46+
| "kiro-cli-linux-data"
47+
| "kiro-cli-windows-data"
48+
| "amazon-q-data"
49+
| "kiro-sso-cache";
4350
status: KiroDiagnosticStatus;
4451
}
4552

@@ -122,14 +129,56 @@ function jsonCredentialPaths(): string[] {
122129
.map(expandPath);
123130
}
124131

125-
function nativeKiroCliSessionEntries(): Array<{ location: "kiro-cli-data" | "kiro-cli-linux-data"; path: string }> {
126-
const home = userHome();
132+
export type KiroCliNativeLocation = "kiro-cli-data" | "kiro-cli-linux-data" | "kiro-cli-windows-data";
133+
134+
export interface KiroCliNativeInputs {
135+
env: Record<string, string | undefined>;
136+
platform: NodeJS.Platform;
137+
home: string;
138+
}
139+
140+
/**
141+
* Native kiro-cli session stores, as a pure function of (env, platform, home).
142+
*
143+
* Pure + parameterized following `src/claude/desktop-3p-paths.ts`: `process.platform` is stubbable
144+
* in this repo, but `os.platform()` does NOT follow it under Bun, so a pure resolver is the reliable
145+
* way to exercise the win32 branch (and its env fallbacks) on a macOS/Linux host.
146+
*
147+
* Windows (issue #710): the official installer stores the auth DB at
148+
* `%LOCALAPPDATA%\Kiro-Cli\data.sqlite3`. When LOCALAPPDATA is unset or blank, fall back to
149+
* `%USERPROFILE%\AppData\Local`, then to the injected platform-native home. Deliberately NOT
150+
* `userHome()`: that is `HOME || homedir()` and Windows shells (Git Bash / MSYS / CI) routinely
151+
* export a POSIX-style `HOME`, which would point this at a non-native path.
152+
*
153+
* One entry per platform on purpose: this list also drives forced-login snapshot/rollback, so it
154+
* must name the database the LOCAL kiro-cli actually mutates, never a foreign platform's path.
155+
*/
156+
export function resolveKiroCliNativeSessionEntries(
157+
inputs: KiroCliNativeInputs,
158+
): Array<{ location: KiroCliNativeLocation; path: string }> {
159+
const { env, platform, home } = inputs;
160+
if (platform === "win32") {
161+
const base = env.LOCALAPPDATA?.trim()
162+
|| (env.USERPROFILE?.trim() ? win32.join(env.USERPROFILE.trim(), "AppData", "Local") : "")
163+
|| win32.join(home, "AppData", "Local");
164+
return [{ location: "kiro-cli-windows-data", path: win32.join(base, "Kiro-Cli", "data.sqlite3") }];
165+
}
166+
if (platform === "darwin") {
167+
return [{ location: "kiro-cli-data", path: join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3") }];
168+
}
169+
return [{ location: "kiro-cli-linux-data", path: join(home, ".local", "share", "kiro-cli", "data.sqlite3") }];
170+
}
171+
172+
function nativeKiroCliSessionEntries(): Array<{ location: KiroCliNativeLocation; path: string }> {
127173
// Only the stores that `kiro-cli logout` / `kiro-cli login` themselves mutate. Import fallbacks
128174
// (Amazon Q / SSO cache) and KIROCLI_DB_PATH selectors must not be snapshotted for rollback.
129-
return [
130-
{ location: "kiro-cli-data", path: join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3") },
131-
{ location: "kiro-cli-linux-data", path: join(home, ".local", "share", "kiro-cli", "data.sqlite3") },
132-
];
175+
return resolveKiroCliNativeSessionEntries({
176+
env: process.env,
177+
platform: process.platform,
178+
// POSIX keeps HOME-first userHome() so existing HOME-based fixtures still resolve; win32 prefers
179+
// LOCALAPPDATA/USERPROFILE and only falls back to this platform-native home.
180+
home: process.platform === "win32" ? homedir() : userHome(),
181+
});
133182
}
134183

135184
function sqliteEntries(): Array<{ location: KiroImportDiagnostic["location"]; path: string }> {

src/oauth/kiro.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,8 @@ export async function loginKiro(ctrl: OAuthController, options: KiroLoginOptions
289289
"Kiro CLI session could not be backed up, so OCX will not sign it out. " +
290290
"Repair or remove the unreadable kiro-cli credential database " +
291291
"(usually `~/.local/share/kiro-cli/data.sqlite3` or " +
292-
"`~/Library/Application Support/kiro-cli/data.sqlite3`), " +
292+
"`~/Library/Application Support/kiro-cli/data.sqlite3`, or " +
293+
"`%LOCALAPPDATA%\\Kiro-Cli\\data.sqlite3` on Windows), " +
293294
"unset KIROCLI_DB_PATH / KIRO_CLI_DB_FILE if set for import-only overrides, then retry.",
294295
);
295296
}

tests/kiro-adapter.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { PROVIDER_REGISTRY } from "../src/providers/registry";
1414
import type { OcxParsedRequest, OcxProviderConfig } from "../src/types";
1515

1616
const origHome = process.env.HOME;
17+
const origLocalAppData = process.env.LOCALAPPDATA;
18+
const origUserProfile = process.env.USERPROFILE;
1719
const origRegion = process.env.KIRO_REGION;
1820
const origApiRegion = process.env.KIRO_API_REGION;
1921
const origArn = process.env.KIRO_PROFILE_ARN;
@@ -24,8 +26,12 @@ let tmp: string;
2426

2527
beforeEach(() => {
2628
// isolate: empty HOME so no kiro-cli SQLite is read; deterministic region.
29+
// The native store resolves per-platform (issue #710) and win32 prefers LOCALAPPDATA/USERPROFILE
30+
// over HOME, so an empty HOME alone would no longer keep a Windows runner off its real profile.
2731
tmp = mkdtempSync(join(tmpdir(), "kiro-adapter-"));
2832
process.env.HOME = tmp;
33+
process.env.LOCALAPPDATA = join(tmp, "AppData", "Local");
34+
process.env.USERPROFILE = tmp;
2935
process.env.OPENCODEX_HOME = tmp;
3036
process.env.KIRO_REGION = "us-east-1";
3137
delete process.env.KIRO_API_REGION;
@@ -35,6 +41,8 @@ beforeEach(() => {
3541
});
3642
afterEach(() => {
3743
if (origHome === undefined) delete process.env.HOME; else process.env.HOME = origHome;
44+
if (origLocalAppData === undefined) delete process.env.LOCALAPPDATA; else process.env.LOCALAPPDATA = origLocalAppData;
45+
if (origUserProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = origUserProfile;
3846
if (origRegion === undefined) delete process.env.KIRO_REGION; else process.env.KIRO_REGION = origRegion;
3947
if (origApiRegion === undefined) delete process.env.KIRO_API_REGION; else process.env.KIRO_API_REGION = origApiRegion;
4048
if (origArn === undefined) delete process.env.KIRO_PROFILE_ARN; else process.env.KIRO_PROFILE_ARN = origArn;
@@ -52,7 +60,12 @@ function parsedWith(messages: unknown[], tools?: unknown[], modelId = "claude-so
5260
}
5361

5462
function seedKiroCliMetadata(profileArn: string, region: string): void {
55-
const dir = join(tmp, "Library", "Application Support", "kiro-cli");
63+
// Host-resolved layout (issue #710): mirrors resolveKiroCliNativeSessionEntries.
64+
const dir = process.platform === "win32"
65+
? join(tmp, "AppData", "Local", "Kiro-Cli")
66+
: process.platform === "darwin"
67+
? join(tmp, "Library", "Application Support", "kiro-cli")
68+
: join(tmp, ".local", "share", "kiro-cli");
5669
mkdirSync(dir, { recursive: true });
5770
const db = new Database(join(dir, "data.sqlite3"));
5871
db.run("CREATE TABLE auth_kv (key TEXT PRIMARY KEY, value TEXT)");

tests/kiro-oauth.test.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { inspectKiroCliSqlite, loginKiro, readKiroCliSqlite, refreshKiroToken, r
1111
setDefaultTimeout(30_000);
1212

1313
const origHome = process.env.HOME;
14+
const origLocalAppData = process.env.LOCALAPPDATA;
15+
const origUserProfile = process.env.USERPROFILE;
1416
const origEnvTok = process.env.KIRO_ACCESS_TOKEN;
1517
const origArn = process.env.KIRO_PROFILE_ARN;
1618
const origRegion = process.env.KIRO_REGION;
@@ -33,6 +35,11 @@ function silenceWarn(): ReturnType<typeof spyOn> {
3335
beforeEach(() => {
3436
tmp = mkdtempSync(join(tmpdir(), "kiro-oauth-"));
3537
process.env.HOME = tmp;
38+
// The native-store resolver is per-platform (issue #710), and on win32 it prefers
39+
// LOCALAPPDATA/USERPROFILE over HOME — so HOME alone no longer isolates a Windows runner from its
40+
// real profile. Point both at the temp dir for the duration of each test.
41+
process.env.LOCALAPPDATA = join(tmp, "AppData", "Local");
42+
process.env.USERPROFILE = tmp;
3643
delete process.env.KIRO_ACCESS_TOKEN;
3744
delete process.env.KIRO_PROFILE_ARN;
3845
delete process.env.KIRO_REGION;
@@ -47,6 +54,10 @@ afterEach(() => {
4754
for (const warning of warnSpies.splice(0)) warning.mockRestore();
4855
if (origHome === undefined) delete process.env.HOME;
4956
else process.env.HOME = origHome;
57+
if (origLocalAppData === undefined) delete process.env.LOCALAPPDATA;
58+
else process.env.LOCALAPPDATA = origLocalAppData;
59+
if (origUserProfile === undefined) delete process.env.USERPROFILE;
60+
else process.env.USERPROFILE = origUserProfile;
5061
if (origEnvTok === undefined) delete process.env.KIRO_ACCESS_TOKEN;
5162
else process.env.KIRO_ACCESS_TOKEN = origEnvTok;
5263
if (origArn === undefined) delete process.env.KIRO_PROFILE_ARN;
@@ -69,11 +80,21 @@ afterEach(() => {
6980
rmSync(tmp, { recursive: true, force: true });
7081
});
7182

83+
/**
84+
* The native kiro-cli store is resolved per-platform (issue #710), so fixtures must seed the layout
85+
* the HOST resolves. Mirrors `resolveKiroCliNativeSessionEntries` in src/oauth/kiro-credentials.ts.
86+
*/
87+
function kiroCliDbDir(): string {
88+
if (process.platform === "win32") return join(tmp, "AppData", "Local", "Kiro-Cli");
89+
if (process.platform === "darwin") return join(tmp, "Library", "Application Support", "kiro-cli");
90+
return join(tmp, ".local", "share", "kiro-cli");
91+
}
92+
7293
function seedKiroCliDb(
7394
token: { access_token: string; refresh_token?: string; expires_at?: string; profile_arn?: string; region?: string },
7495
opts: { registration?: Record<string, unknown>; stateArn?: string } = {},
7596
) {
76-
const dir = join(tmp, "Library", "Application Support", "kiro-cli");
97+
const dir = kiroCliDbDir();
7798
mkdirSync(dir, { recursive: true });
7899
const db = new Database(join(dir, "data.sqlite3"));
79100
db.run("CREATE TABLE auth_kv (key TEXT PRIMARY KEY, value TEXT)");
@@ -89,7 +110,7 @@ function seedKiroCliDb(
89110
}
90111

91112
function seedKiroCliRawValue(value: string) {
92-
const dir = join(tmp, "Library", "Application Support", "kiro-cli");
113+
const dir = kiroCliDbDir();
93114
mkdirSync(dir, { recursive: true });
94115
const db = new Database(join(dir, "data.sqlite3"));
95116
db.run("CREATE TABLE auth_kv (key TEXT PRIMARY KEY, value TEXT)");
@@ -103,7 +124,7 @@ function removeKiroCliDb(): void {
103124
}
104125

105126
function kiroCliDbPath(): string {
106-
return join(tmp, "Library", "Application Support", "kiro-cli", "data.sqlite3");
127+
return join(kiroCliDbDir(), "data.sqlite3");
107128
}
108129

109130
function kiroCliRecoveryPath(): string {
@@ -284,7 +305,7 @@ describe("kiro oauth — import-first", () => {
284305
});
285306

286307
test("force login refuses to log out when a present CLI session cannot be snapshotted", async () => {
287-
const dir = join(tmp, "Library", "Application Support", "kiro-cli");
308+
const dir = kiroCliDbDir();
288309
mkdirSync(dir, { recursive: true });
289310
const db = new Database(join(dir, "data.sqlite3"));
290311
db.run("CREATE TABLE other_table (key TEXT PRIMARY KEY, value TEXT)");
@@ -603,7 +624,7 @@ describe("kiro oauth — import-first", () => {
603624
});
604625

605626
test("inspectKiroCliSqlite distinguishes schema mismatch from no token", () => {
606-
const dir = join(tmp, "Library", "Application Support", "kiro-cli");
627+
const dir = kiroCliDbDir();
607628
mkdirSync(dir, { recursive: true });
608629
const db = new Database(join(dir, "data.sqlite3"));
609630
db.run("CREATE TABLE other_table (key TEXT PRIMARY KEY, value TEXT)");
@@ -634,7 +655,7 @@ describe("kiro oauth — import-first", () => {
634655
});
635656

636657
test("inspectKiroCliSqlite distinguishes unreadable database path", () => {
637-
const dir = join(tmp, "Library", "Application Support", "kiro-cli");
658+
const dir = kiroCliDbDir();
638659
mkdirSync(join(dir, "data.sqlite3"), { recursive: true });
639660

640661
const result = inspectKiroCliSqlite();

tests/kiro-review-regressions.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import type { OcxConfig } from "../src/types";
1717

1818
const ENV_KEYS = [
1919
"HOME",
20+
// The native kiro-cli store resolves per-platform (issue #710) and win32 prefers these over HOME,
21+
// so both must be isolated or a Windows runner would read the real user profile.
22+
"LOCALAPPDATA",
23+
"USERPROFILE",
2024
"OPENCODEX_HOME",
2125
"KIRO_ACCESS_TOKEN",
2226
"KIRO_REFRESH_TOKEN",
@@ -41,8 +45,18 @@ function config(): OcxConfig {
4145
};
4246
}
4347

48+
/**
49+
* Seeds/reads the layout the HOST resolver picks (issue #710). Mirrors
50+
* `resolveKiroCliNativeSessionEntries` in src/oauth/kiro-credentials.ts.
51+
*/
52+
function kiroCliDbDir(): string {
53+
if (process.platform === "win32") return join(tmp, "AppData", "Local", "Kiro-Cli");
54+
if (process.platform === "darwin") return join(tmp, "Library", "Application Support", "kiro-cli");
55+
return join(tmp, ".local", "share", "kiro-cli");
56+
}
57+
4458
function kiroCliDbPath(): string {
45-
return join(tmp, "Library", "Application Support", "kiro-cli", "data.sqlite3");
59+
return join(kiroCliDbDir(), "data.sqlite3");
4660
}
4761

4862
function kiroCliRecoveryPath(): string {
@@ -107,6 +121,8 @@ beforeEach(() => {
107121
tmp = mkdtempSync(join(tmpdir(), "kiro-review-regressions-"));
108122
for (const key of ENV_KEYS) delete process.env[key];
109123
process.env.HOME = tmp;
124+
process.env.LOCALAPPDATA = join(tmp, "AppData", "Local");
125+
process.env.USERPROFILE = tmp;
110126
process.env.OPENCODEX_HOME = join(tmp, "opencodex");
111127
});
112128

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, expect, test } from "bun:test";
2+
3+
import { resolveKiroCliNativeSessionEntries } from "../src/oauth/kiro-credentials";
4+
5+
/**
6+
* Issue #710: `ocx login kiro` could not import an existing Windows Kiro CLI session because the
7+
* native-store candidate list only covered macOS and Linux. The reporter verified the real path is
8+
* `%LOCALAPPDATA%\Kiro-Cli\data.sqlite3` (and that pointing KIROCLI_DB_PATH there made the import
9+
* succeed), so the reader and token selector already worked — only discovery was short.
10+
*
11+
* These drive the pure resolver directly, so the win32 branch and each of its env fallback rungs are
12+
* exercised on any host: `process.platform` is stubbable in this repo, but `os.platform()` does not
13+
* follow it under Bun, so a platform-sniffing implementation could not be covered from macOS/Linux.
14+
*/
15+
describe("kiro-cli native session store resolution", () => {
16+
const WIN_HOME = "C:\\Users\\u";
17+
18+
test("win32 uses %LOCALAPPDATA%\\Kiro-Cli\\data.sqlite3 (the path verified on issue #710)", () => {
19+
const entries = resolveKiroCliNativeSessionEntries({
20+
env: { LOCALAPPDATA: "C:\\Users\\u\\AppData\\Local" },
21+
platform: "win32",
22+
home: WIN_HOME,
23+
});
24+
25+
expect(entries).toEqual([
26+
{ location: "kiro-cli-windows-data", path: "C:\\Users\\u\\AppData\\Local\\Kiro-Cli\\data.sqlite3" },
27+
]);
28+
});
29+
30+
// Middle rung of the win32 chain (LOCALAPPDATA -> USERPROFILE -> injected home). A blank
31+
// LOCALAPPDATA must fall THROUGH rather than produce a rootless `\AppData\Local\...` path.
32+
test("win32 falls back to %USERPROFILE%\\AppData\\Local when LOCALAPPDATA is blank", () => {
33+
const entries = resolveKiroCliNativeSessionEntries({
34+
env: { LOCALAPPDATA: " ", USERPROFILE: "C:\\Users\\u" },
35+
platform: "win32",
36+
home: "D:\\injected-home",
37+
});
38+
39+
expect(entries).toEqual([
40+
{ location: "kiro-cli-windows-data", path: "C:\\Users\\u\\AppData\\Local\\Kiro-Cli\\data.sqlite3" },
41+
]);
42+
});
43+
44+
test("win32 falls back to the injected home when no Windows env var is usable", () => {
45+
for (const env of [{}, { LOCALAPPDATA: "" }, { LOCALAPPDATA: "", USERPROFILE: "" }]) {
46+
const entries = resolveKiroCliNativeSessionEntries({ env, platform: "win32", home: WIN_HOME });
47+
48+
expect(entries).toEqual([
49+
{ location: "kiro-cli-windows-data", path: "C:\\Users\\u\\AppData\\Local\\Kiro-Cli\\data.sqlite3" },
50+
]);
51+
}
52+
});
53+
54+
test("darwin keeps the Application Support store", () => {
55+
const entries = resolveKiroCliNativeSessionEntries({
56+
env: {},
57+
platform: "darwin",
58+
home: "/Users/u",
59+
});
60+
61+
expect(entries).toEqual([
62+
{ location: "kiro-cli-data", path: "/Users/u/Library/Application Support/kiro-cli/data.sqlite3" },
63+
]);
64+
});
65+
66+
test("linux keeps the XDG-style share store", () => {
67+
const entries = resolveKiroCliNativeSessionEntries({
68+
env: {},
69+
platform: "linux",
70+
home: "/home/u",
71+
});
72+
73+
expect(entries).toEqual([
74+
{ location: "kiro-cli-linux-data", path: "/home/u/.local/share/kiro-cli/data.sqlite3" },
75+
]);
76+
});
77+
78+
// The list also feeds forced-login snapshot/rollback, which must never touch a foreign platform's
79+
// database: exactly one entry per platform, and Windows env vars never leak into POSIX resolution.
80+
test("resolution is one native store per platform and ignores foreign env vars", () => {
81+
const windowsEnv = { LOCALAPPDATA: "C:\\Users\\u\\AppData\\Local", USERPROFILE: "C:\\Users\\u" };
82+
83+
for (const platform of ["win32", "darwin", "linux"] as const) {
84+
expect(resolveKiroCliNativeSessionEntries({ env: windowsEnv, platform, home: "/home/u" })).toHaveLength(1);
85+
}
86+
87+
expect(resolveKiroCliNativeSessionEntries({ env: windowsEnv, platform: "linux", home: "/home/u" })).toEqual([
88+
{ location: "kiro-cli-linux-data", path: "/home/u/.local/share/kiro-cli/data.sqlite3" },
89+
]);
90+
});
91+
});

0 commit comments

Comments
 (0)