fix(runner): harden dormant provider boundaries (#12654)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Paperclip Runner currently enables only the Codex production path. > - The package also contains dormant OpenCode and ACPX provider boundaries. > - Dormant boundaries must still fail safe before later activation work. > - Provider children must not inherit unrelated server secrets or host homes. > - Permission defaults must require interaction instead of broad automatic approval. > - This pull request hardens those boundaries without activating them. > - The benefit is a safer base for later provider-specific runnerd work. ## Linked Issues or Issue Description **What existing behavior does this improve?** This improves the inactive OpenCode and ACPX provider boundary in Paperclip Runner. **Subsystem affected** The adapter permission contract, Runner provider environment, and native execution input builder. **Current behavior** Dormant OpenCode code can inherit the full server environment. Its default permission mode allows operations. ACPX also defaults to broad approval. The provider guard can accept inherited object property names. **Proposed behavior** Use exact provider identifiers. Use interactive defaults. Allow only required OpenCode environment keys. Reject invalid proxy permission modes. **Reason and benefit** This reduces accidental authority and secret exposure before future provider activation. **Breaking changes** No production provider is activated. Codex runtime selection and Codex credential-home discovery do not change. Dormant OpenCode and ACPX callers that omit permission modes now receive safer defaults. ## What Changed - Change dormant OpenCode and ACPX permission defaults to interactive modes. - Reject prototype property names as provider identifiers. - Default dormant ACPX input to the qualified Codex agent profile. - Add an explicit OpenCode runner environment allowlist. - Exclude host homes, server credentials, database values, and Node injection options. - Add a fail-closed OpenCode proxy permission parser. - Add focused tests for defaults, filtering, and invalid values. ## Verification GitHub Actions must run: - Adapter utility tests. - Paperclip Runner tests, type checks, and build. - Server native runtime tests. - Repository test, type-check, build, policy, and security gates. No local test command was run. The repository owner requested GitHub-only verification. ## Risks Future OpenCode credential providers must add required variables to the allowlist through review. The safer defaults can pause dormant internal scenarios that relied on implicit broad approval. Production Codex behavior is unchanged. ## Model Used OpenAI Codex with the GPT-5 agent model. The work used high reasoning, repository inspection, tool use, and parallel security review. ## 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 - [ ] 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
bfb98aff5d
commit
1ed29abaa6
|
|
@ -0,0 +1,25 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
isPaperclipRunnerProvider,
|
||||
resolvePaperclipRunnerPermissionMode,
|
||||
} from "./paperclip-runner-permissions.js";
|
||||
|
||||
describe("Paperclip Runner permission defaults", () => {
|
||||
it("uses interactive defaults for dormant non-Codex providers", () => {
|
||||
expect(resolvePaperclipRunnerPermissionMode("opencode", undefined)).toBe(
|
||||
"ask",
|
||||
);
|
||||
expect(resolvePaperclipRunnerPermissionMode("acpx", undefined)).toBe(
|
||||
"approve-reads",
|
||||
);
|
||||
});
|
||||
|
||||
it("recognizes only exact provider identifiers", () => {
|
||||
expect(isPaperclipRunnerProvider("codex")).toBe(true);
|
||||
expect(isPaperclipRunnerProvider("opencode")).toBe(true);
|
||||
expect(isPaperclipRunnerProvider("acpx")).toBe(true);
|
||||
expect(isPaperclipRunnerProvider("toString")).toBe(false);
|
||||
expect(isPaperclipRunnerProvider("__proto__")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -49,7 +49,7 @@ export const PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES = {
|
|||
opencode: {
|
||||
configurable: true,
|
||||
configKey: "opencodePermissionMode",
|
||||
defaultMode: "allow",
|
||||
defaultMode: "ask",
|
||||
description: "Controls OpenCode tool permissions inside the assigned Paperclip environment.",
|
||||
options: [
|
||||
{ value: "allow", label: "Full auto (allow)", description: "Allow OpenCode operations without approval pauses." },
|
||||
|
|
@ -60,7 +60,7 @@ export const PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES = {
|
|||
acpx: {
|
||||
configurable: true,
|
||||
configKey: "acpxPermissionMode",
|
||||
defaultMode: "approve-all",
|
||||
defaultMode: "approve-reads",
|
||||
description: "Controls ACPX agent operations inside the assigned Paperclip environment.",
|
||||
options: [
|
||||
{ value: "approve-all", label: "Full auto (approve all)", description: "Approve ACPX operations without approval pauses." },
|
||||
|
|
@ -71,7 +71,7 @@ export const PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES = {
|
|||
} as const satisfies Record<PaperclipRunnerProvider, PaperclipRunnerPermissionCapability>;
|
||||
|
||||
export function isPaperclipRunnerProvider(value: unknown): value is PaperclipRunnerProvider {
|
||||
return typeof value === "string" && value in PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES;
|
||||
return value === "codex" || value === "opencode" || value === "acpx";
|
||||
}
|
||||
|
||||
export function resolvePaperclipRunnerPermissionMode(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
assertOpenCodeProxyCollaborationMode,
|
||||
openCodeProxyCollaborationModes,
|
||||
} from "./opencode-proxy-collaboration-mode.js";
|
||||
import { parseOpenCodeProxyPermissionMode } from "./opencode-proxy-permission-mode.js";
|
||||
|
||||
type RpcMessage = { id?: string | number; method?: string; params?: unknown; result?: unknown; error?: unknown };
|
||||
|
||||
|
|
@ -78,6 +79,9 @@ async function open(params: Record<string, unknown>, resume: boolean): Promise<R
|
|||
: null;
|
||||
driver = new OpenCodeServerDriver({
|
||||
model,
|
||||
permissionMode: parseOpenCodeProxyPermissionMode(
|
||||
process.env.PAPERCLIP_OPENCODE_PERMISSION_MODE,
|
||||
),
|
||||
command: openCodeCommand(),
|
||||
runtimeDirectory: runtimeDirectory(),
|
||||
environment: process.env,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseOpenCodeProxyPermissionMode } from "./opencode-proxy-permission-mode.js";
|
||||
|
||||
describe("OpenCode runnerd proxy permission mode", () => {
|
||||
it.each(["allow", "ask", "deny"] as const)(
|
||||
"admits the exact %s mode",
|
||||
(mode) => {
|
||||
expect(parseOpenCodeProxyPermissionMode(mode)).toBe(mode);
|
||||
},
|
||||
);
|
||||
|
||||
it("defaults an unset or empty mode to ask", () => {
|
||||
expect(parseOpenCodeProxyPermissionMode(undefined)).toBe("ask");
|
||||
expect(parseOpenCodeProxyPermissionMode(" ")).toBe("ask");
|
||||
});
|
||||
|
||||
it("rejects an unknown mode", () => {
|
||||
expect(() => parseOpenCodeProxyPermissionMode("approve-all")).toThrow(
|
||||
"PAPERCLIP_OPENCODE_PERMISSION_MODE is invalid",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
export type OpenCodeProxyPermissionMode = "allow" | "ask" | "deny";
|
||||
|
||||
export function parseOpenCodeProxyPermissionMode(
|
||||
value: string | undefined,
|
||||
): OpenCodeProxyPermissionMode {
|
||||
const configured = value?.trim();
|
||||
if (!configured) return "ask";
|
||||
if (
|
||||
configured === "allow"
|
||||
|| configured === "ask"
|
||||
|| configured === "deny"
|
||||
) {
|
||||
return configured;
|
||||
}
|
||||
throw new Error("PAPERCLIP_OPENCODE_PERMISSION_MODE is invalid");
|
||||
}
|
||||
|
|
@ -13,7 +13,11 @@ import { describe, expect, it } from "vitest";
|
|||
|
||||
import { validatePrpEvent } from "../protocol/replay-contract.js";
|
||||
import { digestPaperclipSemanticContent } from "../semantic-tools/receipts.js";
|
||||
import { DurablePrpControlPlane } from "./durable-prp-control-plane.js";
|
||||
import {
|
||||
DurablePrpControlPlane,
|
||||
spawnRunner,
|
||||
type RunnerProcessLaunchSpec,
|
||||
} from "./durable-prp-control-plane.js";
|
||||
import type { DurableRecoveryIdentity } from "./prp-transport-types.js";
|
||||
|
||||
const identity: DurableRecoveryIdentity = {
|
||||
|
|
@ -27,6 +31,59 @@ const identity: DurableRecoveryIdentity = {
|
|||
const expectedRunnerVersion = "0.3.0";
|
||||
const expectedRunnerDigest = `sha256:${"a".repeat(64)}`;
|
||||
|
||||
it("preserves an explicit OpenCode permission mode at the runner spawn boundary", () => {
|
||||
const launches: RunnerProcessLaunchSpec[] = [];
|
||||
spawnRunner({
|
||||
connection: { mode: "connect", connectUrl: "ws://127.0.0.1:43127" },
|
||||
stateDirectory: "/tmp/paperclip-runner-test",
|
||||
identity,
|
||||
ticket: "bootstrap-ticket",
|
||||
maxOutboxBytes: 256 * 1024,
|
||||
p0ReserveBytes: 64 * 1024,
|
||||
runnerVersion: expectedRunnerVersion,
|
||||
runnerDigest: expectedRunnerDigest,
|
||||
environment: {
|
||||
PATH: "/bin",
|
||||
OPENROUTER_API_KEY: "provider-key",
|
||||
PAPERCLIP_OPENCODE_COMMAND: "/provider-pack/opencode",
|
||||
PAPERCLIP_OPENCODE_PERMISSION_MODE: "deny",
|
||||
PAPERCLIP_OPENCODE_RUNTIME_DIR: "/runner/opencode",
|
||||
DATABASE_URL: "must-not-reach-runnerd",
|
||||
PAPERCLIP_API_KEY: "must-not-reach-runnerd",
|
||||
NODE_OPTIONS: "--require=/untrusted/bootstrap.cjs",
|
||||
},
|
||||
processLauncher: (spec) => {
|
||||
launches.push(spec);
|
||||
return {
|
||||
child: {
|
||||
pid: 42,
|
||||
exitCode: null,
|
||||
signalCode: null,
|
||||
kill: () => true,
|
||||
},
|
||||
completion: Promise.resolve({
|
||||
code: 0,
|
||||
signal: null,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(launches).toHaveLength(1);
|
||||
expect(launches[0]!.environment).toMatchObject({
|
||||
PATH: "/bin",
|
||||
OPENROUTER_API_KEY: "provider-key",
|
||||
PAPERCLIP_OPENCODE_COMMAND: "/provider-pack/opencode",
|
||||
PAPERCLIP_OPENCODE_PERMISSION_MODE: "deny",
|
||||
PAPERCLIP_OPENCODE_RUNTIME_DIR: "/runner/opencode",
|
||||
});
|
||||
expect(launches[0]!.environment.DATABASE_URL).toBeUndefined();
|
||||
expect(launches[0]!.environment.PAPERCLIP_API_KEY).toBeUndefined();
|
||||
expect(launches[0]!.environment.NODE_OPTIONS).toBeUndefined();
|
||||
});
|
||||
|
||||
function domainDigest(domain: string, parts: readonly Buffer[]): Buffer {
|
||||
const digest = createHash("sha256")
|
||||
.update(domain)
|
||||
|
|
|
|||
|
|
@ -1876,6 +1876,7 @@ const runnerExplicitProviderEnvironmentKeys = [
|
|||
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
|
||||
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
|
||||
"PAPERCLIP_OPENCODE_COMMAND",
|
||||
"PAPERCLIP_OPENCODE_PERMISSION_MODE",
|
||||
"PAPERCLIP_OPENCODE_RUNTIME_DIR",
|
||||
"PAPERCLIP_RUNNER_INSTANCE_ID",
|
||||
"PAPERCLIP_RUN_ID",
|
||||
|
|
|
|||
|
|
@ -62,7 +62,16 @@ it("preserves OpenCode runtime bindings when a durable runner is respawned", ()
|
|||
options: {
|
||||
provider: "opencode",
|
||||
stateDirectory: "/isolated/session",
|
||||
environment: { PATH: "/bin", OPENROUTER_API_KEY: "test-provider-key" },
|
||||
opencodePermissionMode: "deny",
|
||||
environment: {
|
||||
PATH: "/bin",
|
||||
OPENROUTER_API_KEY: "test-provider-key",
|
||||
HOME: "/host/home",
|
||||
CODEX_HOME: "/host/codex-home",
|
||||
DATABASE_URL: "must-not-reach-runnerd",
|
||||
PAPERCLIP_API_KEY: "must-not-reach-runnerd",
|
||||
NODE_OPTIONS: "--require=/untrusted/bootstrap.cjs",
|
||||
},
|
||||
opencodeCommand: "/provider-pack/opencode",
|
||||
opencodeRuntimeDirectory: "/isolated/session/opencode",
|
||||
},
|
||||
|
|
@ -80,6 +89,7 @@ it("preserves OpenCode runtime bindings when a durable runner is respawned", ()
|
|||
});
|
||||
expect(environment).toMatchObject({
|
||||
PAPERCLIP_OPENCODE_COMMAND: "/provider-pack/opencode",
|
||||
PAPERCLIP_OPENCODE_PERMISSION_MODE: "deny",
|
||||
PAPERCLIP_OPENCODE_RUNTIME_DIR: "/isolated/session/opencode",
|
||||
PAPERCLIP_RUNNER_INSTANCE_ID: "runner-1",
|
||||
PAPERCLIP_RUN_ID: "run-1",
|
||||
|
|
@ -87,6 +97,35 @@ it("preserves OpenCode runtime bindings when a durable runner is respawned", ()
|
|||
PAPERCLIP_NATIVE_RUNTIME_CONTEXT_PATH: "/isolated/runtime-context.json",
|
||||
OPENROUTER_API_KEY: "test-provider-key",
|
||||
});
|
||||
expect(environment.HOME).toBeUndefined();
|
||||
expect(environment.CODEX_HOME).toBeUndefined();
|
||||
expect(environment.DATABASE_URL).toBeUndefined();
|
||||
expect(environment.PAPERCLIP_API_KEY).toBeUndefined();
|
||||
expect(environment.NODE_OPTIONS).toBeUndefined();
|
||||
|
||||
const defaultPermissionEnvironment =
|
||||
createCapabilityRunnerdProviderEnvironment({
|
||||
provider: "opencode",
|
||||
options: {
|
||||
provider: "opencode",
|
||||
stateDirectory: "/isolated/session",
|
||||
environment: { PATH: "/bin" },
|
||||
},
|
||||
identity: {
|
||||
runnerInstanceId: "runner-1",
|
||||
environmentLeaseId: "lease-1",
|
||||
runId: "run-1",
|
||||
normalizedSessionId: "session-1",
|
||||
turnId: "turn-1",
|
||||
itemId: "item-1",
|
||||
},
|
||||
codexHome: "/isolated/codex-home",
|
||||
runtimeContextPath: "/isolated/runtime-context.json",
|
||||
hasRuntimeContext: false,
|
||||
});
|
||||
expect(defaultPermissionEnvironment.PAPERCLIP_OPENCODE_PERMISSION_MODE).toBe(
|
||||
"ask",
|
||||
);
|
||||
});
|
||||
|
||||
it("passes the configured Codex API key only through the provider process environment", () => {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@ import {
|
|||
} from "../drivers/acpx/qualified-profiles.js";
|
||||
import { createSanitizedAcpxSpawnInput } from "../drivers/acpx/environment.js";
|
||||
import type { NativeRuntimeContextSnapshot } from "../contracts/runtime-context.js";
|
||||
import type { NativeAcpxPermissionMode } from "../contracts/native-execution.js";
|
||||
import type {
|
||||
NativeAcpxPermissionMode,
|
||||
NativeOpenCodePermissionMode,
|
||||
} from "../contracts/native-execution.js";
|
||||
import { nativeMcpLaunchBinding } from "../drivers/native-mcp.js";
|
||||
import {
|
||||
prepareIsolatedCodexHome,
|
||||
|
|
@ -235,6 +238,7 @@ export interface CapabilityRunnerdProcessEvidence {
|
|||
|
||||
export interface CapabilityRunnerdCodexTransportOptions {
|
||||
provider?: "codex" | "opencode" | "acpx";
|
||||
opencodePermissionMode?: NativeOpenCodePermissionMode;
|
||||
acpxAgent?: QualifiedAcpxAgent;
|
||||
acpxPermissionMode?: NativeAcpxPermissionMode;
|
||||
acpxPermissionModePinned?: boolean;
|
||||
|
|
@ -828,9 +832,10 @@ export function createCapabilityRunnerdProviderEnvironment(input: {
|
|||
};
|
||||
if (input.provider === "opencode") {
|
||||
return {
|
||||
...process.env,
|
||||
...input.options.environment,
|
||||
...createSanitizedOpenCodeRunnerEnvironment(input.options.environment),
|
||||
PAPERCLIP_OPENCODE_COMMAND: input.options.opencodeCommand ?? "opencode",
|
||||
PAPERCLIP_OPENCODE_PERMISSION_MODE:
|
||||
input.options.opencodePermissionMode ?? "ask",
|
||||
PAPERCLIP_OPENCODE_RUNTIME_DIR:
|
||||
input.options.opencodeRuntimeDirectory ??
|
||||
resolve(input.options.stateDirectory ?? tmpdir(), "opencode"),
|
||||
|
|
@ -865,6 +870,51 @@ export function createCapabilityRunnerdProviderEnvironment(input: {
|
|||
return environment;
|
||||
}
|
||||
|
||||
const OPEN_CODE_RUNNER_ENVIRONMENT_KEYS = new Set([
|
||||
"PATH",
|
||||
"LANG",
|
||||
"LANGUAGE",
|
||||
"LC_ALL",
|
||||
"LC_CTYPE",
|
||||
"TZ",
|
||||
"TMPDIR",
|
||||
"TEMP",
|
||||
"TMP",
|
||||
"SSL_CERT_FILE",
|
||||
"SSL_CERT_DIR",
|
||||
"NODE_EXTRA_CA_CERTS",
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"NO_PROXY",
|
||||
"ALL_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"no_proxy",
|
||||
"all_proxy",
|
||||
"SystemRoot",
|
||||
"PATHEXT",
|
||||
"WINDIR",
|
||||
"RUST_BACKTRACE",
|
||||
"OPENROUTER_API_KEY",
|
||||
"PAPERCLIP_NATIVE_MCP_NAME",
|
||||
"PAPERCLIP_NATIVE_MCP_URL",
|
||||
"PAPERCLIP_NATIVE_MCP_TOKEN",
|
||||
]);
|
||||
|
||||
function createSanitizedOpenCodeRunnerEnvironment(
|
||||
source: NodeJS.ProcessEnv | undefined,
|
||||
): NodeJS.ProcessEnv {
|
||||
const candidate = { ...process.env, ...source };
|
||||
return Object.fromEntries(
|
||||
Object.entries(candidate).filter(
|
||||
([key, value]) =>
|
||||
typeof value === "string"
|
||||
&& (OPEN_CODE_RUNNER_ENVIRONMENT_KEYS.has(key)
|
||||
|| /^LC_[A-Z0-9_]{1,32}$/.test(key)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveSourceCodexHome(
|
||||
environment: NodeJS.ProcessEnv | undefined,
|
||||
): string | null {
|
||||
|
|
@ -1003,7 +1053,9 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
|||
options.environment,
|
||||
options.acpxAgent ?? "codex",
|
||||
).env
|
||||
: createSanitizedCodexEnvironment(options.environment),
|
||||
: options.provider === "opencode"
|
||||
? createSanitizedOpenCodeRunnerEnvironment(options.environment)
|
||||
: createSanitizedCodexEnvironment(options.environment),
|
||||
).sort(),
|
||||
diagnostics: ["lab transport selected authenticated durable PRP"],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export function buildNativeExecutionInput(input: {
|
|||
?? (input.issue.workMode === "planning" ? "plan" : "default");
|
||||
const acpxProfile = input.provider === "acpx"
|
||||
? resolveQualifiedAcpxProfile(
|
||||
input.acpxAgent ?? "pi",
|
||||
input.acpxAgent ?? "codex",
|
||||
input.model ?? "",
|
||||
)
|
||||
: null;
|
||||
|
|
@ -119,7 +119,7 @@ export function buildNativeExecutionInput(input: {
|
|||
kind: "acpx",
|
||||
agent: acpxProfile!.agent,
|
||||
model: input.model,
|
||||
permissionMode: input.acpxPermissionMode ?? "approve-all",
|
||||
permissionMode: input.acpxPermissionMode ?? "approve-reads",
|
||||
profile: {
|
||||
driverKind: acpxProfile!.driverKind,
|
||||
protocolVersion: acpxProfile!.protocolVersion,
|
||||
|
|
@ -137,7 +137,7 @@ export function buildNativeExecutionInput(input: {
|
|||
? {
|
||||
kind: "opencode",
|
||||
model: input.model,
|
||||
permissionMode: input.opencodePermissionMode ?? "allow",
|
||||
permissionMode: input.opencodePermissionMode ?? "ask",
|
||||
}
|
||||
: {
|
||||
kind: "codex",
|
||||
|
|
|
|||
|
|
@ -221,6 +221,16 @@ describe("buildNativeExecutionInput wake projection", () => {
|
|||
model: "claude-sonnet-5",
|
||||
acpxPermissionMode: "deny-all",
|
||||
});
|
||||
const defaultOpenCode = buildNativeExecutionInput({
|
||||
...common,
|
||||
provider: "opencode",
|
||||
model: "openrouter/z-ai/glm-5.2",
|
||||
});
|
||||
const defaultAcpx = buildNativeExecutionInput({
|
||||
...common,
|
||||
provider: "acpx",
|
||||
model: "gpt-5.6-sol",
|
||||
});
|
||||
|
||||
expect(codex).toMatchObject({
|
||||
schema: "paperclip.native-execution-input.v4",
|
||||
|
|
@ -234,6 +244,16 @@ describe("buildNativeExecutionInput wake projection", () => {
|
|||
schema: "paperclip.native-execution-input.v4",
|
||||
provider: { kind: "acpx", permissionMode: "deny-all" },
|
||||
});
|
||||
expect(defaultOpenCode).toMatchObject({
|
||||
provider: { kind: "opencode", permissionMode: "ask" },
|
||||
});
|
||||
expect(defaultAcpx).toMatchObject({
|
||||
provider: {
|
||||
kind: "acpx",
|
||||
agent: "codex",
|
||||
permissionMode: "approve-reads",
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify([codex, opencode, acpx]))
|
||||
.not.toMatch(/OPENAI_API_KEY|ANTHROPIC_API_KEY|AWS_SECRET_ACCESS_KEY|PAPERCLIP_API_KEY/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue