Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 47 additions & 9 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,14 @@ export class CodexSecurity {
) {
await this.#refreshPersistentRuntime(runtime, scanEnvironment, signal);
}
const effectiveConfig =
runtime.effectiveConfig ?? (await mergedCodexConfig(this.config));
if (runtime.configPath !== undefined) {
await writeCodexConfig(
runtime.configPath,
scanPreflightCodexConfig(effectiveConfig, repo),
);
}
const runtimeHome = await realpath(runtime.codexHome);
requireOutputOutsideRepository(protectedRoot, runtimeHome, "runtime");
if (
Expand Down Expand Up @@ -533,8 +541,6 @@ export class CodexSecurity {
mode,
pluginVersion: runtime.plugin.version,
};
const effectiveConfig =
runtime.effectiveConfig ?? (await mergedCodexConfig(this.config));
const { model } = scanModelConfiguration(effectiveConfig);
validateScanCostLimit(options.maxCostUsd, model);
const tracker = new ScanCostTracker({
Expand Down Expand Up @@ -1578,7 +1584,7 @@ function scanRecipe(
mode,
...(repositoryRevision === null ? {} : { repositoryRevision }),
pluginVersion,
config: scanPreflightCodexConfig(effectiveConfig),
config: scanPreflightCodexConfig(effectiveConfig, repository),
...(failOnSeverity === undefined ? {} : { failOnSeverity }),
...(knowledgeBasePaths === undefined ? {} : { knowledgeBasePaths }),
...(maxCostUsd === undefined ? {} : { maxCostUsd }),
Expand Down Expand Up @@ -1833,7 +1839,10 @@ export function scanRuntimeCodexConfig(
};
}

export function scanPreflightCodexConfig(config: JsonObject): JsonObject {
export function scanPreflightCodexConfig(
config: JsonObject,
activeProjectPath?: string,
): JsonObject {
const safeString = (value: unknown, maxLength: number): value is string =>
typeof value === "string" &&
value.length > 0 &&
Expand Down Expand Up @@ -1903,18 +1912,41 @@ export function scanPreflightCodexConfig(config: JsonObject): JsonObject {
}
return result;
};
const prioritizedEntries = (
value: Record<string, unknown>,
priority: string | undefined,
): [string, unknown][] => {
const entries = Object.entries(value);
if (priority === undefined || !Object.hasOwn(value, priority)) {
return entries;
}
return [
[priority, value[priority]],
...entries.filter(([key]) => key !== priority),
];
};

const result = executionConfig(config);
if (safeProfileName(config["profile"])) {
result["profile"] = config["profile"];
const selectedProfile = safeProfileName(config["profile"])
? config["profile"]
: undefined;
if (selectedProfile !== undefined) {
result["profile"] = selectedProfile;
}
const profiles = config["profiles"];
if (isRecord(profiles)) {
const sanitized: JsonObject = {};
for (const [name, profile] of Object.entries(profiles).slice(0, 256)) {
let accepted = 0;
for (const [name, profile] of prioritizedEntries(
profiles,
selectedProfile,
)) {
if (!safeProfileName(name) || !isRecord(profile)) continue;
const projected = executionConfig(profile as JsonObject);
if (Object.keys(projected).length > 0) sanitized[name] = projected;
if (Object.keys(projected).length === 0) continue;
sanitized[name] = projected;
accepted += 1;
if (accepted === 256) break;
}
if (Object.keys(sanitized).length > 0) result["profiles"] = sanitized;
}
Expand All @@ -1927,13 +1959,19 @@ export function scanPreflightCodexConfig(config: JsonObject): JsonObject {
const projects = config["projects"];
if (isRecord(projects)) {
const sanitized: JsonObject = {};
for (const [path, project] of Object.entries(projects).slice(0, 256)) {
let accepted = 0;
for (const [path, project] of prioritizedEntries(
projects,
activeProjectPath,
)) {
if (!safeString(path, 4096) || !isAbsolute(path) || !isRecord(project)) {
continue;
}
const trust = project["trust_level"];
if (trust !== "trusted" && trust !== "untrusted") continue;
sanitized[path] = { trust_level: trust };
accepted += 1;
if (accepted === 256) break;
}
if (Object.keys(sanitized).length > 0) result["projects"] = sanitized;
}
Expand Down
102 changes: 102 additions & 0 deletions sdk/typescript/tests-ts/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,93 @@ describe("CodexSecurity orchestration", () => {
).resolves.toBeUndefined();
});

test("prioritizes the selected profile and active project before projection limits", () => {
const activeProject = "/workspace/active";
const profiles = Object.fromEntries([
...Array.from({ length: 256 }, (_, index) => [
`profile_${index}`,
{ features: { goals: index % 2 === 0 } },
]),
["selected", { agents: { max_threads: 17 } }],
]);
const projects = Object.fromEntries([
...Array.from({ length: 256 }, (_, index) => [
`/workspace/project-${index}`,
{ trust_level: "untrusted" },
]),
[activeProject, { trust_level: "trusted" }],
]);

const prioritized = scanPreflightCodexConfig(
{
profile: "selected",
profiles,
projects,
},
activeProject,
);

expect(prioritized["profile"]).toBe("selected");
expect(Object.keys(prioritized["profiles"] as JsonObject)).toHaveLength(
256,
);
expect(prioritized["profiles"]).toMatchObject({
selected: { agents: { max_threads: 17 } },
});
expect(Object.keys(prioritized["projects"] as JsonObject)).toHaveLength(
256,
);
expect(prioritized["projects"]).toMatchObject({
[activeProject]: { trust_level: "trusted" },
});

const validProfiles = Object.fromEntries(
Array.from({ length: 256 }, (_, index) => [
`valid_${index}`,
{ features: { goals: true } },
]),
);
const validProjects = Object.fromEntries(
Array.from({ length: 256 }, (_, index) => [
`/valid/project-${index}`,
{ trust_level: "trusted" },
]),
);
const afterInvalid = scanPreflightCodexConfig({
profiles: {
...Object.fromEntries(
Array.from({ length: 256 }, (_, index) => [
`invalid.profile.${index}`,
{ features: { goals: false } },
]),
),
...validProfiles,
},
projects: {
...Object.fromEntries(
Array.from({ length: 256 }, (_, index) => [
`relative-${index}`,
{ trust_level: "trusted" },
]),
),
...validProjects,
},
});

expect(Object.keys(afterInvalid["profiles"] as JsonObject)).toHaveLength(
256,
);
expect(afterInvalid["profiles"]).toMatchObject({
valid_255: { features: { goals: true } },
});
expect(Object.keys(afterInvalid["projects"] as JsonObject)).toHaveLength(
256,
);
expect(afterInvalid["projects"]).toMatchObject({
"/valid/project-255": { trust_level: "trusted" },
});
});

test("selects a real-scan target in the active repository layout", async () => {
await expect(
stat(join(REPOSITORY_ROOT, INTEGRATION_TARGET)),
Expand Down Expand Up @@ -2268,11 +2355,21 @@ describe("CodexSecurity orchestration", () => {
expect(interpreter).not.toBeNull();
let capturedConfigPath: string | undefined;
let capturedCodexHome: string | undefined;
const unrelatedProjects = Object.fromEntries(
Array.from({ length: 256 }, (_, index) => [
join(root, `unrelated-project-${index}`),
{ trust_level: "untrusted" },
]),
);
const client = new TestClient(
{
pluginPath: PLUGIN_ROOT,
codexOverrides: {
features: { goals: true },
projects: {
...unrelatedProjects,
[repository]: { trust_level: "trusted" },
},
mcp_servers: {
private: {
command: "echo",
Expand Down Expand Up @@ -2331,6 +2428,11 @@ describe("CodexSecurity orchestration", () => {
expect(serialized).not.toContain("RUNTIME_SHELL_SECRET");
expect(serialized).not.toContain("mcp_servers");
expect(serialized).not.toContain("shell_environment_policy");
expect(parseToml(serialized)).toMatchObject({
projects: {
[repository]: { trust_level: "trusted" },
},
});
expect(input).toContain('--config "$CODEX_SECURITY_CONFIG_PATH"');
expect(input).toContain("--effective-config");
const shellEnvironment = options.env as Record<string, string>;
Expand Down