Files
pi-agent-integrated/server/src/lib/project-trust.test.mjs
T

144 lines
5.8 KiB
JavaScript

import assert from "node:assert/strict";
import { existsSync } from "node:fs";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
import { createJiti } from "jiti";
const jiti = createJiti(import.meta.url);
const {
getProjectTrustStatus,
projectTrustReloadOptions,
trustProject,
} = await jiti.import("./project-trust.ts");
async function createProjectFixture(t) {
const root = await mkdtemp(join(tmpdir(), "pi-web-project-trust-"));
const cwd = join(root, "project");
const agentDir = join(root, "agent");
await mkdir(cwd, { recursive: true });
await mkdir(agentDir, { recursive: true });
t.after(() => rm(root, { recursive: true, force: true }));
return { root, cwd, agentDir };
}
test("clean projects stay on the normal trusted load path", async (t) => {
const { cwd, agentDir } = await createProjectFixture(t);
assert.deepEqual(getProjectTrustStatus(cwd, agentDir), {
requiresTrust: false,
trusted: true,
});
assert.equal(projectTrustReloadOptions(cwd, agentDir), undefined);
});
test("managed runtime never enables project-local resources", async (t) => {
const { cwd, agentDir } = await createProjectFixture(t);
await mkdir(join(cwd, ".pi", "extensions"), { recursive: true });
const previous = process.env.PI_AGENT_MANAGED_RUNTIME;
process.env.PI_AGENT_MANAGED_RUNTIME = "1";
t.after(() => {
if (previous === undefined) delete process.env.PI_AGENT_MANAGED_RUNTIME;
else process.env.PI_AGENT_MANAGED_RUNTIME = previous;
});
assert.deepEqual(getProjectTrustStatus(cwd, agentDir), {
requiresTrust: false,
trusted: false,
});
assert.deepEqual(trustProject(cwd, agentDir), {
requiresTrust: false,
trusted: false,
});
const reloadOptions = projectTrustReloadOptions(cwd, agentDir);
assert.ok(reloadOptions);
assert.equal(await reloadOptions.resolveProjectTrust(), false);
});
test("project extensions execute only after the project is trusted", async (t) => {
const { root, cwd, agentDir } = await createProjectFixture(t);
const extensionDir = join(cwd, ".pi", "extensions");
const marker = join(root, "extension-executed");
await mkdir(extensionDir, { recursive: true });
await writeFile(
join(extensionDir, "probe.js"),
`import { writeFileSync } from "node:fs";\nexport default () => { writeFileSync(${JSON.stringify(marker)}, "executed"); };\n`,
);
assert.deepEqual(getProjectTrustStatus(cwd, agentDir), {
requiresTrust: true,
trusted: false,
});
const restrictedLoader = new DefaultResourceLoader({ cwd, agentDir });
await restrictedLoader.reload(projectTrustReloadOptions(cwd, agentDir));
assert.equal(existsSync(marker), false);
assert.equal(restrictedLoader.getExtensions().extensions.length, 0);
assert.deepEqual(trustProject(cwd, agentDir), {
requiresTrust: true,
trusted: true,
});
const trustedLoader = new DefaultResourceLoader({ cwd, agentDir });
await trustedLoader.reload(projectTrustReloadOptions(cwd, agentDir));
assert.equal(existsSync(marker), true);
assert.equal(trustedLoader.getExtensions().extensions.length, 1);
});
test("the reload resolver reads the latest persisted trust decision", async (t) => {
const { cwd, agentDir } = await createProjectFixture(t);
await mkdir(join(cwd, ".pi", "extensions"), { recursive: true });
const reloadOptions = projectTrustReloadOptions(cwd, agentDir);
assert.ok(reloadOptions);
assert.equal(await reloadOptions.resolveProjectTrust(), false);
trustProject(cwd, agentDir);
assert.equal(await reloadOptions.resolveProjectTrust(), true);
});
test("all project resource loaders and reloads enforce project trust", async () => {
const rpcSource = await readFile(new URL("./rpc-manager.ts", import.meta.url), "utf8");
const modelsSource = await readFile(new URL("../routes/models.ts", import.meta.url), "utf8");
const skillsSource = await readFile(new URL("./skills-service.ts", import.meta.url), "utf8");
const skillsInstallSource = await readFile(new URL("../routes/skills.ts", import.meta.url), "utf8");
const pluginsSource = await readFile(new URL("../routes/config.ts", import.meta.url), "utf8");
assert.match(rpcSource, /projectTrustReloadOptions\(cwd, agentDir\)/);
assert.match(rpcSource, /resourceLoaderReloadOptions: trustReloadOptions/);
assert.equal(
Array.from(rpcSource.matchAll(/this\.syncProjectTrust\(\);\s*await this\.inner\.reload/g)).length,
2,
);
assert.match(modelsSource, /projectTrustReloadOptions\(cwd, agentDir\)/);
assert.match(modelsSource, /resourceLoaderReloadOptions: trustReloadOptions/);
assert.match(skillsSource, /loader\.reload\(projectTrustReloadOptions\(cwd, agentDir\)\)/);
assert.match(pluginsSource, /createAppSettingsManager\(cwd, agentDir, projectTrust\.trusted\)/);
assert.match(
skillsInstallSource,
/getProjectTrustStatus\(cwd, getAgentDir\(\)\)\.trusted/,
);
assert.equal(
Array.from(pluginsSource.matchAll(/createAppSettingsManager\([^\n]+projectTrust\.trusted\)/g)).length,
2,
);
assert.match(pluginsSource, /scope === "project" && !projectTrust\.trusted/);
});
test("the trust API invalidates cached models and restricted runtimes", async () => {
const source = await readFile(new URL("../routes/misc.ts", import.meta.url), "utf8");
const rpcSource = await readFile(new URL("./rpc-manager.ts", import.meta.url), "utf8");
assert.match(source, /trustProject\(result\.cwd, agentDir\)/);
assert.match(source, /invalidateModelsCache\(\)/);
assert.match(source, /destroyRpcSessionsForCwd\(result\.cwd\)/);
assert.match(source, /hasBusyRpcSessionForCwd\(result\.cwd\)/);
assert.match(rpcSource, /trackStartingSession\(cwd\)/);
assert.match(rpcSource, /realpathSync\(resolvedCwd\)/);
});