feat(runner): isolate Codex security configuration (#12364)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Codex executes commands inside the runner workspace > - The provider process must not inherit host credentials or broad file access > - Planning mode also needs a read-only workspace boundary > - This pull request defines the isolated launch and thread configuration > - A later pull request will use it from the Codex driver > - The benefit is an independently reviewed security boundary ## Linked Issues or Issue Description **Subsystem affected** `packages/paperclip-runner` Codex launch security. **Problem or motivation** A provider process can inherit host paths, environment secrets, network access, or write permissions unless the runner constructs a closed configuration. **Proposed solution** Build explicit app-server arguments and thread parameters. Deny host roots and network access, allow only a small environment list, and use a read-only profile for plan mode. **Alternatives considered** Relying on ambient Codex defaults would make runner safety depend on each host installation. **Roadmap alignment** This supports the Codex-first experimental runner. It does not enable the runner adapter. ## What Changed - Added deny-by-default filesystem rules. - Added separate execution and planning permission profiles. - Added network denial. - Added an explicit environment allowlist. - Disabled host apps, plugins, memories, multi-agent behavior, and image generation. - Added security configuration tests. ## Verification - `pnpm --filter @paperclipai/paperclip-runner test:typescript` - `pnpm -r typecheck` - `pnpm build` - The focused security configuration test has 3 passing cases. ## Risks The risk is an overly broad provider launch. Tests verify denied host roots, read-only context roots, workspace permissions, network denial, and secret exclusion. ## Model Used OpenAI Codex with GPT-5.6 and repository tool use. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
parent
3942739906
commit
9d7d9ea724
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
createIsolatedCodexAppServerArgs,
|
||||
createSecuredCodexThreadParams,
|
||||
createSkilllessCodexThreadConfig,
|
||||
} from "./codex-security-config.js";
|
||||
|
||||
describe("Codex security configuration", () => {
|
||||
it("disables host extensions and makes collaboration instructions explicit", () => {
|
||||
expect(createSkilllessCodexThreadConfig("/workspace", {}, false)).toEqual({
|
||||
"skills.include_instructions": false,
|
||||
include_apps_instructions: false,
|
||||
include_collaboration_mode_instructions: false,
|
||||
"features.apps": false,
|
||||
"features.plugins": false,
|
||||
"features.multi_agent": false,
|
||||
"features.memories": false,
|
||||
"features.image_generation": false,
|
||||
});
|
||||
});
|
||||
|
||||
it("denies host roots, network access, and unlisted environment variables", () => {
|
||||
const args = createIsolatedCodexAppServerArgs({
|
||||
HOME: "/host/home",
|
||||
CODEX_HOME: "/host/codex",
|
||||
PATH: "/safe/bin",
|
||||
LANG: "C.UTF-8",
|
||||
OPENAI_API_KEY: "must-not-cross",
|
||||
}, ["/runner/context"]);
|
||||
const serialized = args.join("\n");
|
||||
|
||||
expect(serialized).toContain('"/host/home"="none"');
|
||||
expect(serialized).toContain('"/host/codex"="none"');
|
||||
expect(serialized).toContain('"/runner/context"="read"');
|
||||
expect(serialized).toContain('":workspace_roots"={"."="write"}');
|
||||
expect(serialized).toContain('":workspace_roots"={"."="read"}');
|
||||
expect(serialized).toContain("network.enabled=false");
|
||||
expect(serialized).toContain('PATH="/safe/bin"');
|
||||
expect(serialized).toContain('LANG="C.UTF-8"');
|
||||
expect(serialized).not.toContain("OPENAI_API_KEY");
|
||||
expect(serialized).not.toContain("must-not-cross");
|
||||
});
|
||||
|
||||
it("uses a read-only permission profile for plan mode", () => {
|
||||
expect(createSecuredCodexThreadParams("/workspace", "plan")).toMatchObject({
|
||||
cwd: "/workspace",
|
||||
permissions: "paperclip-runner-workspace-read-only",
|
||||
runtimeWorkspaceRoots: ["/workspace"],
|
||||
config: {
|
||||
"skills.include_instructions": false,
|
||||
include_collaboration_mode_instructions: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
import { resolve } from "node:path";
|
||||
|
||||
const SKILLLESS_PERMISSION_PROFILE = "paperclip-runner-workspace-only";
|
||||
const PLANNING_PERMISSION_PROFILE = "paperclip-runner-workspace-read-only";
|
||||
|
||||
const SKILLLESS_BASE_CONFIG = {
|
||||
"skills.include_instructions": false,
|
||||
include_apps_instructions: false,
|
||||
include_collaboration_mode_instructions: true,
|
||||
"features.apps": false,
|
||||
"features.plugins": false,
|
||||
"features.multi_agent": false,
|
||||
"features.memories": false,
|
||||
"features.image_generation": false,
|
||||
} as const;
|
||||
|
||||
function commandEnvironment(
|
||||
source: NodeJS.ProcessEnv = process.env,
|
||||
): Record<string, string> {
|
||||
const environment: Record<string, string> = {};
|
||||
for (const key of [
|
||||
"PATH",
|
||||
"PATHEXT",
|
||||
"SystemRoot",
|
||||
"WINDIR",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
] as const) {
|
||||
const value = source[key];
|
||||
if (value !== undefined) environment[key] = value;
|
||||
}
|
||||
return environment;
|
||||
}
|
||||
|
||||
export function createSkilllessCodexThreadConfig(
|
||||
_workingDirectory: string,
|
||||
_source: NodeJS.ProcessEnv = process.env,
|
||||
includeCollaborationModeInstructions = true,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
...SKILLLESS_BASE_CONFIG,
|
||||
include_collaboration_mode_instructions:
|
||||
includeCollaborationModeInstructions,
|
||||
};
|
||||
}
|
||||
|
||||
function collaborationThreadConfig(
|
||||
includeCollaborationModeInstructions = true,
|
||||
includeSkillInstructions = false,
|
||||
) {
|
||||
return {
|
||||
...SKILLLESS_BASE_CONFIG,
|
||||
"skills.include_instructions": includeSkillInstructions,
|
||||
include_collaboration_mode_instructions:
|
||||
includeCollaborationModeInstructions,
|
||||
};
|
||||
}
|
||||
|
||||
function tomlString(value: string): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
export function createIsolatedCodexAppServerArgs(
|
||||
source: NodeJS.ProcessEnv = process.env,
|
||||
readOnlyRoots: string[] = [],
|
||||
): string[] {
|
||||
const deniedHostRoots = [
|
||||
...new Set(
|
||||
[source.HOME, source.CODEX_HOME]
|
||||
.filter(
|
||||
(value): value is string =>
|
||||
typeof value === "string" && value.trim().length > 0,
|
||||
)
|
||||
.map((value) => resolve(value)),
|
||||
),
|
||||
];
|
||||
const filesystemRules = [
|
||||
`":root"="none"`,
|
||||
`":minimal"="read"`,
|
||||
`":tmpdir"="none"`,
|
||||
...deniedHostRoots.map((path) => `${tomlString(path)}="none"`),
|
||||
...readOnlyRoots.map((path) => `${tomlString(resolve(path))}="read"`),
|
||||
`":workspace_roots"={"."="write"}`,
|
||||
].join(",");
|
||||
const planningFilesystemRules = [
|
||||
`":root"="none"`,
|
||||
`":minimal"="read"`,
|
||||
`":tmpdir"="none"`,
|
||||
...deniedHostRoots.map((path) => `${tomlString(path)}="none"`),
|
||||
...readOnlyRoots.map((path) => `${tomlString(resolve(path))}="read"`),
|
||||
`":workspace_roots"={"."="read"}`,
|
||||
].join(",");
|
||||
const commandEnv = Object.entries(commandEnvironment(source))
|
||||
.map(([key, value]) => `${key}=${tomlString(value)}`)
|
||||
.join(",");
|
||||
return [
|
||||
"-c",
|
||||
`default_permissions=${tomlString(SKILLLESS_PERMISSION_PROFILE)}`,
|
||||
"-c",
|
||||
`permissions.${SKILLLESS_PERMISSION_PROFILE}.filesystem={${filesystemRules}}`,
|
||||
"-c",
|
||||
`permissions.${SKILLLESS_PERMISSION_PROFILE}.network.enabled=false`,
|
||||
"-c",
|
||||
`permissions.${PLANNING_PERMISSION_PROFILE}.filesystem={${planningFilesystemRules}}`,
|
||||
"-c",
|
||||
`permissions.${PLANNING_PERMISSION_PROFILE}.network.enabled=false`,
|
||||
"-c",
|
||||
`shell_environment_policy.inherit="none"`,
|
||||
"-c",
|
||||
"shell_environment_policy.ignore_default_excludes=false",
|
||||
...(commandEnv.length > 0
|
||||
? ["-c", `shell_environment_policy.set={${commandEnv}}`]
|
||||
: []),
|
||||
"--disable",
|
||||
"image_generation",
|
||||
"app-server",
|
||||
];
|
||||
}
|
||||
|
||||
export function createSecuredCodexThreadParams(
|
||||
workingDirectory: string,
|
||||
mode: "default" | "plan" = "default",
|
||||
includeCollaborationModeInstructions = true,
|
||||
includeSkillInstructions = false,
|
||||
): Record<string, unknown> {
|
||||
const permissionProfile =
|
||||
mode === "plan"
|
||||
? PLANNING_PERMISSION_PROFILE
|
||||
: SKILLLESS_PERMISSION_PROFILE;
|
||||
return {
|
||||
cwd: workingDirectory,
|
||||
config: collaborationThreadConfig(
|
||||
includeCollaborationModeInstructions,
|
||||
includeSkillInstructions,
|
||||
),
|
||||
permissions: permissionProfile,
|
||||
runtimeWorkspaceRoots: [workingDirectory],
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue