Skip to content

Commit 2cba644

Browse files
committed
merge: sync main into gitlab review decision fix
2 parents fe5ac2b + c6ef3b4 commit 2cba644

575 files changed

Lines changed: 27193 additions & 2557 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ The most common defect in this repo is a change that works on the path you teste
9191
An empty database is a bad test. Seed your worktree's `.t3` with a copy of real data instead of pointing at live state:
9292

9393
- The first `vp run dev` in a worktree does this for you when its database is missing and the source is at most 500 MiB. Larger sources warn and dev starts with an empty database. `--no-seed` or `T3CODE_DEV_SEED=0` skips seeding. Manual migration still accepts larger sources and may use substantial disk space before pruning.
94-
- Run `vp run migrate-dev-db` with your dev server stopped to refresh. It rebuilds `<worktree>/.t3/userdata/statev2.sqlite` from a read-only snapshot of `~/.t3/userdata/statev2.sqlite`, the developer's real data. It keeps recent projects and their stopped threads, and drops scheduled tasks, pending work, and auth sessions, so your dev server never runs the developer's agents. Raise `--projects` and `--threads-per-project` for more data.
94+
- Run `vp run migrate-dev-db` with your dev server stopped to refresh. It rebuilds `<worktree>/.t3/userdata/statev2.sqlite` from a read-only snapshot of `~/.t3/userdata/statev2.sqlite`, the developer's real data. It keeps recent projects and their stopped threads, and drops scheduled tasks, pending work, auth sessions, and links to other environments, so your dev server never runs the developer's agents or reaches their other machines. Raise `--projects` and `--threads-per-project` for more data.
9595
- Refresh `statev2.sqlite`, not `state.sqlite`. The server copies the V1 `state.sqlite` only when `statev2.sqlite` is missing.
9696
- Bring `secrets` and `settings.json` only if the flow under test needs them.
9797
- Copy in, never symlink. Data flows one way: into your sandbox, never back out.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// @effect-diagnostics nodeBuiltinImport:off -- These tests assert real POSIX modes on a temporary directory.
2+
import * as NodeFS from "node:fs";
3+
import * as NodeOS from "node:os";
4+
import * as NodePath from "node:path";
5+
import { assert, describe, it } from "@effect/vitest";
6+
import { vi } from "vite-plus/test";
7+
8+
vi.mock("node:fs", { spy: true });
9+
10+
import { openClerkTokenStorage, type ClerkTokenStorage } from "./ClerkTokenStore.ts";
11+
12+
// oxlint-disable-next-line t3code/no-global-process-runtime -- Unix mode bits are the assertion; Windows reports them through ACLs.
13+
const describePosix = describe.skipIf(process.platform === "win32");
14+
15+
const modeOf = (path: string): number => NodeFS.statSync(path).mode & 0o777;
16+
17+
function writeLoose(path: string, contents: string): void {
18+
NodeFS.writeFileSync(path, contents, { mode: 0o666 });
19+
NodeFS.chmodSync(path, 0o666);
20+
}
21+
22+
async function withStateDir(run: (stateDir: string) => Promise<void> | void): Promise<void> {
23+
const stateDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-clerk-tokens-"));
24+
try {
25+
await run(stateDir);
26+
} finally {
27+
NodeFS.rmSync(stateDir, { recursive: true, force: true });
28+
}
29+
}
30+
31+
const idleStorage = (): ClerkTokenStorage => ({
32+
getItem: async () => null,
33+
setItem: async () => undefined,
34+
removeItem: () => undefined,
35+
});
36+
37+
describePosix("ClerkTokenStore", () => {
38+
it("keeps clerk tokens at 0600 inside a 0700 directory", async () => {
39+
await withStateDir(async (stateDir) => {
40+
NodeFS.chmodSync(stateDir, 0o755);
41+
const directory = NodePath.join(stateDir, "clerk");
42+
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
43+
const tokenPath = NodePath.join(directory, "clerk-tokens.json");
44+
NodeFS.mkdirSync(directory, { mode: 0o755 });
45+
NodeFS.chmodSync(directory, 0o755);
46+
writeLoose(legacyPath, '{"__clerk_client_jwt":"enc:existing"}\n');
47+
48+
const opened: string[] = [];
49+
const storage = openClerkTokenStorage(stateDir, (cwd) => {
50+
opened.push(cwd);
51+
return {
52+
getItem: async () => {
53+
writeLoose(tokenPath, "from-get");
54+
return "from-get";
55+
},
56+
setItem: (_key, value) => {
57+
writeLoose(tokenPath, value);
58+
},
59+
removeItem: () => {
60+
NodeFS.rmSync(tokenPath, { force: true });
61+
},
62+
};
63+
});
64+
65+
assert.deepEqual(opened, [directory]);
66+
assert.equal(modeOf(stateDir), 0o755);
67+
assert.equal(modeOf(directory), 0o700);
68+
assert.equal(modeOf(tokenPath), 0o600);
69+
assert.equal(NodeFS.existsSync(legacyPath), false);
70+
assert.equal(
71+
NodeFS.readFileSync(tokenPath, "utf8"),
72+
'{"__clerk_client_jwt":"enc:existing"}\n',
73+
);
74+
75+
assert.equal(await storage.getItem("client"), "from-get");
76+
assert.equal(modeOf(tokenPath), 0o600);
77+
await storage.setItem("client", "from-set");
78+
assert.equal(modeOf(tokenPath), 0o600);
79+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "from-set");
80+
storage.removeItem("client");
81+
assert.equal(NodeFS.existsSync(tokenPath), false);
82+
83+
writeLoose(tokenPath, "kept");
84+
writeLoose(legacyPath, "stale");
85+
NodeFS.chmodSync(directory, 0o755);
86+
openClerkTokenStorage(stateDir, () => idleStorage());
87+
assert.equal(modeOf(directory), 0o700);
88+
assert.equal(modeOf(tokenPath), 0o600);
89+
assert.equal(NodeFS.existsSync(legacyPath), false);
90+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "kept");
91+
});
92+
});
93+
94+
it("removes the legacy token file when the private copy already exists", async () => {
95+
await withStateDir((stateDir) => {
96+
const directory = NodePath.join(stateDir, "clerk");
97+
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
98+
const tokenPath = NodePath.join(directory, "clerk-tokens.json");
99+
NodeFS.mkdirSync(directory, { mode: 0o755 });
100+
writeLoose(legacyPath, "stale-credential");
101+
writeLoose(tokenPath, "kept-credential");
102+
103+
openClerkTokenStorage(stateDir, () => idleStorage());
104+
105+
assert.equal(NodeFS.existsSync(legacyPath), false);
106+
assert.equal(modeOf(directory), 0o700);
107+
assert.equal(modeOf(tokenPath), 0o600);
108+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "kept-credential");
109+
});
110+
});
111+
112+
it("resolves a relative state directory before opening storage", async () => {
113+
await withStateDir(async (stateDir) => {
114+
const relativeStateDir = NodePath.relative(process.cwd(), stateDir);
115+
const opened: string[] = [];
116+
openClerkTokenStorage(relativeStateDir, (directory) => {
117+
opened.push(directory);
118+
return idleStorage();
119+
});
120+
121+
const directory = NodePath.join(stateDir, "clerk");
122+
assert.equal(NodePath.isAbsolute(relativeStateDir), false);
123+
assert.deepEqual(opened, [directory]);
124+
assert.equal(modeOf(directory), 0o700);
125+
});
126+
});
127+
128+
it("migrates relative state paths from Electron userData, not the working directory", async () => {
129+
await withStateDir((stateDir) => {
130+
const relativeStateDir = NodePath.relative(process.cwd(), stateDir);
131+
const userData = NodePath.join(
132+
stateDir,
133+
"electron",
134+
...relativeStateDir.split(NodePath.sep).map(() => "profile"),
135+
);
136+
const legacyStateDir = NodePath.join(userData, relativeStateDir);
137+
const legacyPath = NodePath.join(legacyStateDir, "clerk-tokens.json");
138+
NodeFS.mkdirSync(legacyStateDir, { recursive: true });
139+
writeLoose(legacyPath, "existing-session");
140+
const unrelatedPath = NodePath.join(stateDir, "clerk-tokens.json");
141+
writeLoose(unrelatedPath, "not-the-electron-session");
142+
143+
openClerkTokenStorage(relativeStateDir, idleStorage, userData);
144+
145+
const tokenPath = NodePath.join(stateDir, "clerk", "clerk-tokens.json");
146+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "existing-session");
147+
assert.equal(modeOf(tokenPath), 0o600);
148+
assert.equal(NodeFS.existsSync(legacyPath), false);
149+
assert.equal(NodeFS.readFileSync(unrelatedPath, "utf8"), "not-the-electron-session");
150+
});
151+
});
152+
153+
it("migrates tokens when the legacy directory is on another filesystem", async () => {
154+
await withStateDir((stateDir) => {
155+
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
156+
writeLoose(legacyPath, "existing-session");
157+
using _rename = vi.spyOn(NodeFS, "renameSync").mockImplementation(() => {
158+
throw Object.assign(new Error("cross-device link"), { code: "EXDEV" });
159+
});
160+
161+
openClerkTokenStorage(stateDir, idleStorage);
162+
163+
const tokenPath = NodePath.join(stateDir, "clerk", "clerk-tokens.json");
164+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "existing-session");
165+
assert.equal(modeOf(tokenPath), 0o600);
166+
assert.equal(NodeFS.existsSync(legacyPath), false);
167+
});
168+
});
169+
170+
it("does not follow a symlinked clerk directory or token file", async () => {
171+
await withStateDir(async (stateDir) => {
172+
const outsideDirectory = NodePath.join(stateDir, "outside-dir");
173+
NodeFS.mkdirSync(outsideDirectory, { mode: 0o755 });
174+
NodeFS.chmodSync(outsideDirectory, 0o755);
175+
NodeFS.symlinkSync(outsideDirectory, NodePath.join(stateDir, "clerk"));
176+
177+
assert.throws(() => openClerkTokenStorage(stateDir, idleStorage), /must be a real directory/);
178+
assert.equal(modeOf(outsideDirectory), 0o755);
179+
NodeFS.unlinkSync(NodePath.join(stateDir, "clerk"));
180+
181+
const outsideFile = NodePath.join(stateDir, "outside.json");
182+
NodeFS.writeFileSync(outsideFile, "secret", { mode: 0o644 });
183+
NodeFS.chmodSync(outsideFile, 0o644);
184+
NodeFS.symlinkSync(outsideFile, NodePath.join(stateDir, "clerk-tokens.json"));
185+
assert.throws(() => openClerkTokenStorage(stateDir, idleStorage), /must be a regular file/);
186+
assert.equal(modeOf(outsideFile), 0o644);
187+
});
188+
});
189+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// @effect-diagnostics nodeBuiltinImport:off -- The Clerk bridge must lock the token file down before startup can yield to Electron's ready event.
2+
import * as NodeFS from "node:fs";
3+
import * as NodePath from "node:path";
4+
5+
// electron-store chmods every write to 0666; the private directory protects tokens during writes.
6+
const CLERK_TOKEN_DIRECTORY_MODE = 0o700;
7+
const CLERK_TOKEN_FILE_MODE = 0o600;
8+
const CLERK_TOKEN_DIRECTORY_NAME = "clerk";
9+
const CLERK_TOKEN_FILE_NAME = "clerk-tokens.json";
10+
11+
type Awaitable<T> = T | Promise<T>;
12+
13+
export interface ClerkTokenStorage {
14+
readonly getItem: (key: string) => Awaitable<string | null>;
15+
readonly setItem: (key: string, value: string) => Awaitable<void>;
16+
readonly removeItem: (key: string) => Awaitable<void>;
17+
}
18+
19+
function isErrno(error: unknown, code: string): boolean {
20+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
21+
}
22+
23+
function lstatIfExists(path: string): NodeFS.Stats | undefined {
24+
try {
25+
return NodeFS.lstatSync(path);
26+
} catch (error) {
27+
if (isErrno(error, "ENOENT")) return undefined;
28+
throw error;
29+
}
30+
}
31+
32+
function assertRegularFile(path: string, stat: NodeFS.Stats): void {
33+
if (stat.isSymbolicLink() || !stat.isFile()) {
34+
throw new Error(`Clerk token file "${path}" must be a regular file.`);
35+
}
36+
}
37+
38+
function restrictClerkTokenFile(path: string): void {
39+
const stat = lstatIfExists(path);
40+
if (stat === undefined) return;
41+
assertRegularFile(path, stat);
42+
if ((stat.mode & 0o777) === CLERK_TOKEN_FILE_MODE) return;
43+
NodeFS.chmodSync(path, CLERK_TOKEN_FILE_MODE);
44+
}
45+
46+
function migrateLegacyClerkTokenFile(stateDir: string, directory: string): void {
47+
const legacyPath = NodePath.join(stateDir, CLERK_TOKEN_FILE_NAME);
48+
const tokenPath = NodePath.join(directory, CLERK_TOKEN_FILE_NAME);
49+
const legacy = lstatIfExists(legacyPath);
50+
if (legacy !== undefined) {
51+
assertRegularFile(legacyPath, legacy);
52+
NodeFS.chmodSync(legacyPath, CLERK_TOKEN_FILE_MODE);
53+
}
54+
55+
const destination = lstatIfExists(tokenPath);
56+
if (destination === undefined) {
57+
if (legacy === undefined) return;
58+
try {
59+
NodeFS.copyFileSync(legacyPath, tokenPath, NodeFS.constants.COPYFILE_EXCL);
60+
NodeFS.chmodSync(tokenPath, CLERK_TOKEN_FILE_MODE);
61+
NodeFS.unlinkSync(legacyPath);
62+
} catch (error) {
63+
if (!isErrno(error, "ENOENT")) throw error;
64+
}
65+
return;
66+
}
67+
68+
assertRegularFile(tokenPath, destination);
69+
NodeFS.chmodSync(tokenPath, CLERK_TOKEN_FILE_MODE);
70+
if (legacy === undefined) return;
71+
try {
72+
NodeFS.unlinkSync(legacyPath);
73+
} catch (error) {
74+
if (!isErrno(error, "ENOENT")) throw error;
75+
}
76+
}
77+
78+
function restrictAfter<T>(run: () => Awaitable<T>, tighten: () => void): Awaitable<T> {
79+
let result: Awaitable<T>;
80+
try {
81+
result = run();
82+
} catch (error) {
83+
tighten();
84+
throw error;
85+
}
86+
if (result instanceof Promise) return result.finally(tighten);
87+
tighten();
88+
return result;
89+
}
90+
91+
/** Opens Clerk token storage in a private directory and keeps the file at `0600`. */
92+
export function openClerkTokenStorage(
93+
stateDir: string,
94+
createStorage: (directory: string) => ClerkTokenStorage,
95+
userDataPath = ".",
96+
): ClerkTokenStorage {
97+
const absoluteStateDir = NodePath.resolve(stateDir);
98+
const directory = NodePath.join(absoluteStateDir, CLERK_TOKEN_DIRECTORY_NAME);
99+
NodeFS.mkdirSync(directory, { recursive: true, mode: CLERK_TOKEN_DIRECTORY_MODE });
100+
const stat = NodeFS.lstatSync(directory);
101+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
102+
throw new Error(`Clerk token directory "${directory}" must be a real directory.`);
103+
}
104+
NodeFS.chmodSync(directory, CLERK_TOKEN_DIRECTORY_MODE);
105+
// electron-store rebases relative paths under Electron userData, not the working directory.
106+
migrateLegacyClerkTokenFile(NodePath.resolve(userDataPath, stateDir), directory);
107+
const tokenPath = NodePath.join(directory, CLERK_TOKEN_FILE_NAME);
108+
restrictClerkTokenFile(tokenPath);
109+
110+
const storage = createStorage(directory);
111+
const tighten = () => restrictClerkTokenFile(tokenPath);
112+
return {
113+
getItem: (key) => restrictAfter(() => storage.getItem(key), tighten),
114+
setItem: (key, value) => restrictAfter(() => storage.setItem(key, value), tighten),
115+
removeItem: (key) => restrictAfter(() => storage.removeItem(key), tighten),
116+
};
117+
}

‎apps/desktop/src/app/DesktopClerk.test.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
// @effect-diagnostics nodeBuiltinImport:off globalFetchInEffect:off - Hosted handoff test uses a real localhost listener without an OpenAI account.
2+
import * as NodeFS from "node:fs";
23
import * as NodeHttp from "node:http";
3-
import * as NodePath from "@effect/platform-node/NodePath";
4+
import * as NodeOS from "node:os";
5+
import * as NodePath from "node:path";
6+
import * as NodePathLayer from "@effect/platform-node/NodePath";
47
import { codexAuthHandoffUrl, readCodexAuthDelivery } from "@t3tools/shared/codexAuthHandoff";
58
import { EnvironmentId, ProviderInstanceId } from "@t3tools/contracts";
69
import * as HostProcess from "@t3tools/shared/HostProcess";
710
import { assert, describe, it } from "@effect/vitest";
811
import * as Effect from "effect/Effect";
912
import * as Layer from "effect/Layer";
10-
import { beforeEach, vi } from "vite-plus/test";
13+
import { afterAll, beforeEach, vi } from "vite-plus/test";
14+
15+
const clerkStateDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-desktop-clerk-"));
16+
afterAll(() => {
17+
NodeFS.rmSync(clerkStateDir, { recursive: true, force: true });
18+
});
1119

1220
const { createClerkBridgeMock, requestSingleInstanceLockMock, storageAdapter, storageMock } =
1321
vi.hoisted(() => ({
@@ -58,7 +66,7 @@ const layerDesktopClerk = (
5866
listeners = new Map<string, (...args: unknown[]) => void>(),
5967
) => {
6068
const environment = DesktopEnvironment.DesktopEnvironment.of({
61-
stateDir: "/tmp/t3-state",
69+
stateDir: clerkStateDir,
6270
isDevelopment,
6371
appDataDirectory: "/tmp/app-data",
6472
platform,
@@ -92,7 +100,7 @@ const layerDesktopClerk = (
92100
return DesktopClerk.layer.pipe(
93101
Layer.provide(
94102
Layer.mergeAll(
95-
NodePath.layerPosix,
103+
NodePathLayer.layerPosix,
96104
Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment),
97105
Layer.succeed(ElectronApp.ElectronApp, electronApp),
98106
Layer.succeed(ElectronShell.ElectronShell, shell),
@@ -159,6 +167,10 @@ describe("DesktopClerk", () => {
159167
"on:open-url",
160168
"createClerkBridge",
161169
]);
170+
assert.deepEqual(storageMock.mock.calls, [
171+
[{ path: NodePath.join(NodePath.resolve(clerkStateDir), "clerk") }],
172+
]);
173+
assert.notStrictEqual(createClerkBridgeMock.mock.calls[0]?.[0]?.storage, storageAdapter);
162174
},
163175
);
164176

0 commit comments

Comments
 (0)