mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(setup): evaluate Gemini workspace trust before settings content
Treat only ENOENT as absence. Preserve unreadable metadata errors and ignore untrusted workspace content before parsing it. Refs: #3206, #3319 Co-authored-by: haoran20100322 <haoran20100322@outlook.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { realpath } from "node:fs/promises";
|
||||
import { realpath, stat } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { stripComments } from "jsonc-parser";
|
||||
import { z } from "zod";
|
||||
@@ -149,12 +149,27 @@ export const readClientPolicyBlock = async (
|
||||
".gemini",
|
||||
"settings.json",
|
||||
);
|
||||
const document = await readConfigurationFile(path, "json", resources);
|
||||
if (!document.ok) return document;
|
||||
if (document.value !== undefined) {
|
||||
const trusted = await workspaceTrusted(settings, sources, resources);
|
||||
if (!trusted.ok) return trusted;
|
||||
if (trusted.value) {
|
||||
// Only ENOENT proves absence; preserve other metadata failures as unreadable.
|
||||
const present = await stat(path).then(
|
||||
() => ok(true),
|
||||
(cause: unknown) =>
|
||||
cause instanceof Error && "code" in cause && cause.code === "ENOENT"
|
||||
? ok(false)
|
||||
: err({
|
||||
kind: "unreadable" as const,
|
||||
path,
|
||||
detail: `Configuration ${path} could not be read: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
}),
|
||||
);
|
||||
if (!present.ok) return present;
|
||||
const includeWorkspace = present.value
|
||||
? await workspaceTrusted(settings, sources, resources)
|
||||
: ok(false);
|
||||
if (!includeWorkspace.ok) return includeWorkspace;
|
||||
if (includeWorkspace.value) {
|
||||
const document = await readConfigurationFile(path, "json", resources);
|
||||
if (!document.ok) return document;
|
||||
if (document.value !== undefined) {
|
||||
const source = { path, document: document.value };
|
||||
const systemIndex = sources.findIndex(
|
||||
(item) => item.path === settings.systemPath,
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const setupArguments = ["setup", "--client", "gemini_cli", "--yes", "--json"];
|
||||
const doctorArguments = ["doctor", "--client", "gemini_cli", "--json"];
|
||||
|
||||
const geminiWorkspace = async (cli: TestCli) => {
|
||||
const home = await createTestTempDirectory("rea-gemini-workspace-trust-");
|
||||
const project = join(home, "project");
|
||||
const workspace = join(project, ".gemini/settings.json");
|
||||
const user = join(home, ".gemini/settings.json");
|
||||
await mkdir(dirname(workspace), { recursive: true });
|
||||
const environment: NodeJS.ProcessEnv = {
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
GEMINI_CLI_HOME: home,
|
||||
GEMINI_CLI_SYSTEM_SETTINGS_PATH: join(home, "system.json"),
|
||||
GEMINI_CLI_SYSTEM_DEFAULTS_PATH: join(home, "defaults.json"),
|
||||
GEMINI_CLI_TRUSTED_FOLDERS_PATH: join(home, "trust.json"),
|
||||
};
|
||||
const initial = await cli.run({
|
||||
arguments: setupArguments,
|
||||
cwd: home,
|
||||
environment,
|
||||
});
|
||||
expect(initial.exitCode, initial.stdout).toBe(0);
|
||||
const originalUser = await readFile(user);
|
||||
const run = (arguments_: readonly string[]) =>
|
||||
cli.run({ arguments: arguments_, cwd: project, environment });
|
||||
return { workspace, user, originalUser, environment, run };
|
||||
};
|
||||
|
||||
cliTest(
|
||||
"Gemini does not require workspace trust until workspace settings exist",
|
||||
async ({ cli }) => {
|
||||
const fixture = await geminiWorkspace(cli);
|
||||
const trust = join(dirname(fixture.user), "trustedFolders.json");
|
||||
fixture.environment.GEMINI_CLI_TRUSTED_FOLDERS_PATH = trust;
|
||||
for (const text of ["{}", "{ not json"]) {
|
||||
await writeFile(trust, text);
|
||||
const setup = await fixture.run(setupArguments);
|
||||
expect(setup.exitCode, setup.stdout).toBe(0);
|
||||
expect(setup.json).toMatchObject({ status: "ready", appliedActions: [] });
|
||||
const doctor = await fixture.run(doctorArguments);
|
||||
expect(doctor.exitCode, doctor.stdout).toBe(0);
|
||||
expect(doctor.json).toMatchObject({
|
||||
healthy: true,
|
||||
identity: {
|
||||
registrations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
client: "gemini_cli",
|
||||
state: "aligned",
|
||||
remediation: null,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
await expect(access(fixture.workspace)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
expect(await readFile(trust, "utf8")).toBe(text);
|
||||
expect(await readFile(fixture.user)).toEqual(fixture.originalUser);
|
||||
}
|
||||
|
||||
const workspaceText = "{ not json";
|
||||
await writeFile(fixture.workspace, workspaceText);
|
||||
const setup = await fixture.run(setupArguments);
|
||||
expect(setup.exitCode, setup.stdout).toBe(1);
|
||||
expect(setup.json).toMatchObject({
|
||||
status: "needs_human",
|
||||
plannedActions: [],
|
||||
appliedActions: [],
|
||||
remediation: expect.stringContaining(trust),
|
||||
});
|
||||
const doctor = await fixture.run(doctorArguments);
|
||||
expect(doctor.exitCode, doctor.stdout).toBe(1);
|
||||
expect(doctor.json).toMatchObject({
|
||||
healthy: false,
|
||||
identity: {
|
||||
registrations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
client: "gemini_cli",
|
||||
state: "invalid",
|
||||
remediation: expect.stringContaining(trust),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(await readFile(fixture.workspace, "utf8")).toBe(workspaceText);
|
||||
expect(await readFile(trust, "utf8")).toBe("{ not json");
|
||||
expect(await readFile(fixture.user)).toEqual(fixture.originalUser);
|
||||
},
|
||||
);
|
||||
|
||||
for (const scenario of [
|
||||
{ name: "an unlisted workspace", environment: {} },
|
||||
{
|
||||
name: "an explicitly untrusted workspace",
|
||||
environment: { GEMINI_CLI_TRUST_WORKSPACE: "false" },
|
||||
},
|
||||
{
|
||||
name: "a restricted workspace with an explicit trust opt-in",
|
||||
environment: {
|
||||
GEMINI_CLI_TRUST_WORKSPACE: "true",
|
||||
GEMINI_RESTRICTED_MODE: "true",
|
||||
},
|
||||
},
|
||||
])
|
||||
cliTest(
|
||||
`Gemini setup and doctor ignore malformed settings in ${scenario.name}`,
|
||||
async ({ cli }) => {
|
||||
const fixture = await geminiWorkspace(cli);
|
||||
Object.assign(fixture.environment, scenario.environment);
|
||||
const malformed = Buffer.from("{ not json");
|
||||
await writeFile(fixture.workspace, malformed);
|
||||
|
||||
const setup = await fixture.run(setupArguments);
|
||||
expect(setup.exitCode, setup.stdout).toBe(0);
|
||||
expect(setup.json).toMatchObject({ status: "ready", appliedActions: [] });
|
||||
const doctor = await fixture.run(doctorArguments);
|
||||
expect(doctor.exitCode, doctor.stdout).toBe(0);
|
||||
expect(doctor.json).toMatchObject({
|
||||
healthy: true,
|
||||
identity: {
|
||||
registrations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
client: "gemini_cli",
|
||||
state: "aligned",
|
||||
remediation: null,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(await readFile(fixture.workspace)).toEqual(malformed);
|
||||
expect(await readFile(fixture.user)).toEqual(fixture.originalUser);
|
||||
await expect(
|
||||
access(`${fixture.workspace}.rea.backup`),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
|
||||
if (scenario.name === "an explicitly untrusted workspace") {
|
||||
const excluded = Buffer.from(
|
||||
JSON.stringify({ mcp: { excluded: ["rea"] } }),
|
||||
);
|
||||
await writeFile(fixture.workspace, excluded);
|
||||
const excludedSetup = await fixture.run(setupArguments);
|
||||
expect(excludedSetup.exitCode, excludedSetup.stdout).toBe(0);
|
||||
const excludedDoctor = await fixture.run(doctorArguments);
|
||||
expect(excludedDoctor.exitCode, excludedDoctor.stdout).toBe(0);
|
||||
expect(excludedDoctor.json).toMatchObject({
|
||||
identity: {
|
||||
registrations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
client: "gemini_cli",
|
||||
state: "aligned",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(await readFile(fixture.workspace)).toEqual(excluded);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"Gemini still rejects malformed settings and exclusions in a trusted workspace",
|
||||
async ({ cli }) => {
|
||||
const fixture = await geminiWorkspace(cli);
|
||||
fixture.environment.GEMINI_CLI_TRUST_WORKSPACE = "true";
|
||||
for (const scenario of [
|
||||
{
|
||||
text: "{ not json",
|
||||
state: "invalid",
|
||||
diagnostic: "Invalid JSON/JSONC",
|
||||
},
|
||||
{
|
||||
text: JSON.stringify({ mcp: { excluded: ["rea"] } }),
|
||||
state: "stale",
|
||||
diagnostic: "mcp.excluded",
|
||||
},
|
||||
]) {
|
||||
const original = Buffer.from(scenario.text);
|
||||
await writeFile(fixture.workspace, original);
|
||||
const setup = await fixture.run(setupArguments);
|
||||
expect(setup.exitCode, setup.stdout).toBe(1);
|
||||
expect(setup.json).toMatchObject({
|
||||
status: "needs_human",
|
||||
plannedActions: [],
|
||||
appliedActions: [],
|
||||
remediation: expect.stringContaining(scenario.diagnostic),
|
||||
});
|
||||
const doctor = await fixture.run(doctorArguments);
|
||||
expect(doctor.exitCode, doctor.stdout).toBe(1);
|
||||
expect(doctor.json).toMatchObject({
|
||||
healthy: false,
|
||||
identity: {
|
||||
registrations: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
client: "gemini_cli",
|
||||
state: scenario.state,
|
||||
remediation: expect.stringContaining(scenario.diagnostic),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(await readFile(fixture.workspace)).toEqual(original);
|
||||
expect(await readFile(fixture.user)).toEqual(fixture.originalUser);
|
||||
await expect(
|
||||
access(`${fixture.workspace}.rea.backup`),
|
||||
).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user