Unify Paperclip Runner experimental controls (#12666)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip Runner is an experimental execution adapter.
> - The adapter and its required sandbox ingress had separate settings.
> - A user could enable one setting and still have an unusable runner
configuration.
> - The runtime already makes one durable native or legacy decision for
each run.
> - This pull request uses that runtime decision for ingress
authorization.
> - The benefit is one clear opt-in with safe recovery for existing
native runs.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

This improves the experimental settings and transport authorization for
Paperclip Runner.

**Subsystem affected**

Cross-cutting. This change affects the React settings UI, shared
settings contracts, adapter utilities, and server runtime selection.

**Current behavior**

Settings shows separate Paperclip Runner and Runner Preview Ingress
controls. A user can enable the runner but leave required sandbox
ingress disabled.

**Proposed behavior**

Settings shows only Paperclip Runner. Its native runtime decision also
authorizes provider WebSocket ingress when the execution target requires
it. A persisted native run keeps its recovery transport after the
setting is disabled.

**Reason and benefit**

Paperclip Runner is one experimental capability. One opt-in removes an
invalid partial configuration and makes the rollout boundary easier to
understand.

**Breaking changes**

The Runner Preview Ingress card is removed. The old
`enableRunnerPreviewIngress` key remains accepted in stored settings and
managed configuration, but it has no server runtime effect. The public
adapter-utils input remains compatible through a deprecated alias.

**Additional context**

Refs: #12638, #12641, #12656.

## What Changed

- Removed the separate Runner Preview Ingress card from Experimental
Settings.
- Made resolved native runtime selection authorize required provider
ingress.
- Preserved ingress recovery for persisted native runs after the rollout
flag is disabled.
- Kept the old settings key and adapter-utils input as deprecated
compatibility contracts.
- Added focused UI, runtime policy, transport, stored-settings, and
managed-config regression tests.
- Updated deployment documentation and feature descriptions.

## Verification

- GitHub Actions will run typecheck, tests, build, policy, and browser
shards.
- Focused tests cover the single settings control, runtime
authorization, fail-closed transport selection, the deprecated public
input, and old managed configuration.
- No local tests were run, per the maintainer request to use GitHub
Actions for verification.
- `git diff --check` passes.

## Risks

Low to moderate risk. The effective ingress gate changes from a separate
stored flag to the resolved native run decision. Fresh runs still
require `enableNativeRunner`. Persisted native runs remain recoverable.
Legacy adapters never receive ingress authorization.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex, GPT-5, with reasoning, tool use, and code execution.

## 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
- [x] All Paperclip CI gates are green
- [x] 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:
Dotta 2026-09-01 09:21:23 -05:00 committed by GitHub
parent 1c499b6b4d
commit ee2a190626
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 208 additions and 72 deletions

View File

@ -28,10 +28,14 @@ All environment variables that Paperclip uses for server configuration.
| `PAPERCLIP_HIDDEN_SETTINGS` | (unset) | Comma-separated settings surfaces to hide from the UI and floor at the API, for operators hosting Paperclip for others (managed cloud, internal shared server). See [Hiding settings surfaces](#hiding-settings-surfaces). |
| `PAPERCLIP_SETTING_DEFAULTS` | (unset) | JSON object replacing the schema default of selected instance settings, for hosting operators. See [Operator setting defaults](#operator-setting-defaults). |
Daytona connectivity for `paperclip_runner` uses authenticated preview WSS and
is gated by the instance experimental setting `enableRunnerPreviewIngress`
(default `false`). The setting has no effect on legacy adapters or callback
bridges.
Daytona connectivity for `paperclip_runner` uses authenticated provider
WebSocket ingress and follows the instance experimental setting
`enableNativeRunner` (default `false`). There is no separate ingress opt-in.
Disabling Paperclip Runner blocks fresh native starts while persisted native
runs retain their recovery path. The deprecated `enableRunnerPreviewIngress`
key remains accepted in stored and managed configuration for version-skew
compatibility, but it has no runtime effect. The setting has no effect on
legacy adapters or callback bridges.
### Preinstalled remote runner images

View File

@ -38,7 +38,7 @@ describe("paperclip runner transport routing", () => {
runId: "00000000-0000-4000-8000-000000000001",
localConnectUrl:
"ws://127.0.0.1:3100/api/runner/v1/connect/00000000-0000-4000-8000-000000000001",
enableRunnerPreviewIngress: false,
runnerIngressAuthorized: false,
});
expect(result.mode).toBe("local_loopback");
});
@ -59,13 +59,34 @@ describe("paperclip runner transport routing", () => {
runId: "00000000-0000-4000-8000-000000000001",
localConnectUrl: "ws://127.0.0.1/unused",
runnerPublicUrl: "wss://paperclip.example.test",
enableRunnerPreviewIngress: true,
runnerIngressAuthorized: true,
});
expect(result.mode).toBe("provider_ingress");
expect(getRunnerIngressEndpoint).toHaveBeenCalledOnce();
});
it("does not request preview ingress while the new-runner rollout flag is off", async () => {
it("accepts the deprecated ingress input alias for existing consumers", async () => {
const target: AdapterExecutionTarget = {
kind: "remote",
transport: "sandbox",
providerKey: "daytona",
remoteCwd: "/workspace",
leaseId: "lease-legacy",
effectiveCapabilities: capabilities,
getRunnerIngressEndpoint: vi.fn(async () => ingress()),
};
const result = await resolvePaperclipRunnerTransport({
target,
runId: "00000000-0000-4000-8000-000000000001",
localConnectUrl: "ws://127.0.0.1/unused",
enableRunnerPreviewIngress: true,
});
expect(result.mode).toBe("provider_ingress");
});
it("lets resolved authorization override the deprecated ingress alias", async () => {
const getRunnerIngressEndpoint = vi.fn(async () => ingress());
const target: AdapterExecutionTarget = {
kind: "remote",
@ -82,9 +103,18 @@ describe("paperclip runner transport routing", () => {
runId: "00000000-0000-4000-8000-000000000001",
localConnectUrl: "ws://127.0.0.1/unused",
runnerPublicUrl: "wss://paperclip.example.test",
enableRunnerPreviewIngress: false,
runnerIngressAuthorized: false,
enableRunnerPreviewIngress: true,
}),
).rejects.toMatchObject({ code: "runner_ingress_unavailable" });
const missingAuthorization = {
target,
runId: "00000000-0000-4000-8000-000000000002",
localConnectUrl: "ws://127.0.0.1/unused",
} as Parameters<typeof resolvePaperclipRunnerTransport>[0];
await expect(
resolvePaperclipRunnerTransport(missingAuthorization),
).rejects.toMatchObject({ code: "runner_ingress_unavailable" });
expect(getRunnerIngressEndpoint).not.toHaveBeenCalled();
});
@ -110,7 +140,7 @@ describe("paperclip runner transport routing", () => {
localConnectUrl: "ws://127.0.0.1/unused",
runnerPublicUrl: "wss://paperclip.example.test/runner-base/",
runnerCaBundlePath: "/etc/paperclip/runner-ca.pem",
enableRunnerPreviewIngress: false,
runnerIngressAuthorized: false,
});
expect(result).toEqual({
mode: "direct_outbound",
@ -138,7 +168,7 @@ describe("paperclip runner transport routing", () => {
runId: "00000000-0000-4000-8000-000000000001",
localConnectUrl: "ws://127.0.0.1/unused",
runnerPublicUrl: "wss://paperclip.example.test",
enableRunnerPreviewIngress: true,
runnerIngressAuthorized: true,
}),
).rejects.toThrow("preview unavailable");
});
@ -161,7 +191,7 @@ describe("paperclip runner transport routing", () => {
runId: "00000000-0000-4000-8000-000000000001",
localConnectUrl: "ws://127.0.0.1/unused",
runnerPublicUrl: "wss://paperclip.example.test",
enableRunnerPreviewIngress: true,
runnerIngressAuthorized: true,
}),
).rejects.toMatchObject({ code: "runner_ingress_unavailable" });
});

View File

@ -35,6 +35,19 @@ export type PaperclipRunnerTransport =
readonly ingress: RunnerIngressEndpoint;
};
type RunnerIngressAuthorization =
| {
/** Per-run authorization resolved by the native runtime selection policy. */
readonly runnerIngressAuthorized: boolean;
/** @deprecated Use runnerIngressAuthorized. Retained for API compatibility. */
readonly enableRunnerPreviewIngress?: boolean;
}
| {
readonly runnerIngressAuthorized?: never;
/** @deprecated Use runnerIngressAuthorized. Retained for API compatibility. */
readonly enableRunnerPreviewIngress: boolean;
};
export class PaperclipRunnerTransportError extends Error {
readonly code:
| "runner_transport_ineligible"
@ -98,13 +111,12 @@ export async function resolvePaperclipRunnerTransport(input: {
localConnectUrl: string;
runnerPublicUrl?: string | null;
runnerCaBundlePath?: string | null;
enableRunnerPreviewIngress: boolean;
getRunnerIngressEndpoint?: (input: {
leaseId: string;
port: number;
path: string;
}) => Promise<RunnerIngressEndpoint>;
}): Promise<PaperclipRunnerTransport> {
} & RunnerIngressAuthorization): Promise<PaperclipRunnerTransport> {
if (input.target.kind === "local") {
return { mode: "local_loopback", connectUrl: input.localConnectUrl };
}
@ -124,10 +136,12 @@ export async function resolvePaperclipRunnerTransport(input: {
input.target.transport === "sandbox" &&
input.target.effectiveCapabilities?.runnerWebSocketIngress === true
) {
if (!input.enableRunnerPreviewIngress) {
const ingressAuthorized =
input.runnerIngressAuthorized ?? input.enableRunnerPreviewIngress ?? false;
if (!ingressAuthorized) {
throw new PaperclipRunnerTransportError(
"runner_ingress_unavailable",
"Runner preview ingress is disabled for this Paperclip instance.",
"Runner ingress is not authorized for this Paperclip Runner run.",
);
}
const getRunnerIngressEndpoint =

View File

@ -53,7 +53,7 @@ export const INSTANCE_FEATURE_CATALOG: Record<InstanceFeatureKey, FeatureCatalog
enableNativeRunner: {
title: "Paperclip Runner",
description:
"Allow new Codex agents to use the experimental Rust Paperclip Runner transport.",
"Allow new Codex agents to use the experimental Rust Paperclip Runner, including authenticated sandbox ingress when required.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
@ -278,9 +278,9 @@ export const INSTANCE_FEATURE_CATALOG: Record<InstanceFeatureKey, FeatureCatalog
selfHostedDefault: false,
},
enableRunnerPreviewIngress: {
title: "Runner Preview Ingress",
title: "Runner Preview Ingress (Deprecated)",
description:
"Let paperclip_runner connect through an authenticated sandbox-provider WebSocket ingress. Legacy adapters never use this path.",
"Compatibility-only key retained for older managed configs. Runner ingress follows the Paperclip Runner setting.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,

View File

@ -103,7 +103,10 @@ export interface InstanceExperimentalSettings {
* redeploy.
*/
enableSandboxDuplexBridge: boolean;
/** Default-off rollout gate for paperclip_runner provider WebSocket ingress. */
/**
* @deprecated Compatibility-only. Provider WebSocket ingress now follows
* enableNativeRunner and this value has no runtime effect.
*/
enableRunnerPreviewIngress: boolean;
/**
* Worktree preview instances (`PAPERCLIP_IN_WORKTREE=true`) suppress the

View File

@ -76,6 +76,8 @@ export const instanceExperimentalSettingsSchema = z.object({
// off the host keeps the file bridge for every run with no manifest change and
// no redeploy. The host reads this per run before it selects the transport.
enableSandboxDuplexBridge: z.boolean().default(false),
// Deprecated compatibility key. Runner ingress follows enableNativeRunner;
// this remains accepted so older stored rows and managed configs keep loading.
enableRunnerPreviewIngress: z.boolean().default(false),
enableWorktreeRunExecution: z.boolean().default(false),
worktreeRunExecutionActivatedAt: z.string().datetime().nullable().default(null),

View File

@ -71,6 +71,13 @@ describe("instance settings service", () => {
expect(normalizeExperimentalSettings({ enablePipelines: true }).enableApps).toBe(false);
});
it("retains the deprecated ingress key for stored-settings compatibility", () => {
expect(
normalizeExperimentalSettings({ enableRunnerPreviewIngress: true })
.enableRunnerPreviewIngress,
).toBe(true);
});
it("defaults enableConferenceRoomChat to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableConferenceRoomChat).toBe(false);
expect(normalizeExperimentalSettings({}).enableConferenceRoomChat).toBe(false);

View File

@ -57,6 +57,14 @@ describe("parseManagedConfigEnv", () => {
});
});
it("accepts the deprecated runner ingress key for managed-config compatibility", () => {
const config = parseManagedConfigEnv(
envWith(validDoc({ features: { enableRunnerPreviewIngress: true } })),
);
expect(config?.features).toEqual({ enableRunnerPreviewIngress: true });
});
it("accepts empty features {} and autoInstall [] sections", () => {
const config = parseManagedConfigEnv(
envWith(validDoc({ features: {}, plugins: { autoInstall: [] } })),

View File

@ -128,6 +128,7 @@ import {
executePaperclipNativeSession,
finalizeNativeRun,
isNativeSessionId,
isRunnerIngressAuthorized,
materializeLegacyQuestionResponseWakeProjection,
materializeNativeInteractionResponses,
NativeCancellationPendingRecoveryError,
@ -20613,9 +20614,8 @@ export function heartbeatService(
: {}),
},
runnerExecutionTarget: executionTarget,
enableRunnerPreviewIngress:
resolvedInstanceSettings.experimental
.enableRunnerPreviewIngress === true,
runnerIngressAuthorized:
isRunnerIngressAuthorized(nativeRuntimeResolution),
runnerPublicUrl:
runtimeEnv.PAPERCLIP_RUNNER_PUBLIC_URL?.trim() || null,
runnerCaBundlePath:

View File

@ -127,6 +127,7 @@ import {
readRemoteProviderPackManifest,
providerSessionIdentityTransitionIsAllowed,
providerPlanMarkdown,
resolveRemoteRunnerTransportMode,
renewNativeSessionExecutionLease,
runtimeInputLifecycleMetric,
runtimeQuestionFallbackFromEvent,
@ -931,6 +932,35 @@ describe("remote runner build metadata", () => {
});
});
describe("remote runner transport authorization", () => {
const ingressTarget = {
kind: "remote",
transport: "sandbox",
providerKey: "daytona",
remoteCwd: "/workspace",
leaseId: "lease-1",
effectiveCapabilities: { runnerWebSocketIngress: true },
} as const;
it("fails before selecting sandbox ingress for an unauthorized run", () => {
expect(() =>
resolveRemoteRunnerTransportMode({
target: ingressTarget as never,
runnerIngressAuthorized: false,
}),
).toThrow("runner_ingress_unavailable");
});
it("selects sandbox ingress for a resolved native run", () => {
expect(
resolveRemoteRunnerTransportMode({
target: ingressTarget as never,
runnerIngressAuthorized: true,
}),
).toBe("listen_ws");
});
});
describe("runtime question fallback", () => {
const questionSet = {

View File

@ -2728,7 +2728,8 @@ export async function executePaperclipNativeSession(input: {
/** Resolved adapter env; the runner transport applies a provider allowlist before spawn. */
runnerEnvironment?: NodeJS.ProcessEnv;
runnerExecutionTarget?: AdapterExecutionTarget | null;
enableRunnerPreviewIngress?: boolean;
/** Resolved per-run authorization; not an independent instance setting. */
runnerIngressAuthorized?: boolean;
runnerPublicUrl?: string | null;
runnerCaBundlePath?: string | null;
runnerRemoteBinaryPath?: string | null;
@ -4622,6 +4623,29 @@ function createRemoteRunnerProcessLauncher(input: {
};
}
/**
* Select the remote runner transport before any artifact is staged or provider
* endpoint is acquired. Sandbox ingress is available only to a run already
* authorized by native runtime selection.
*/
export function resolveRemoteRunnerTransportMode(input: {
target: AdapterExecutionTarget;
runnerIngressAuthorized: boolean;
}): "listen_ws" | "dial_wss" {
if (input.target.kind !== "remote") {
throw new Error("runner_transport_ineligible: remote target is required");
}
const requiredMode =
input.target.transport === "sandbox" &&
input.target.effectiveCapabilities?.runnerWebSocketIngress === true
? "listen_ws"
: "dial_wss";
if (requiredMode === "listen_ws" && !input.runnerIngressAuthorized) {
throw new Error("runner_ingress_unavailable");
}
return requiredMode;
}
/** Production runnerd backend seam, exported so provider wiring can be regression tested. */
export async function createRunnerdBackend(input: {
db: Db;
@ -4635,7 +4659,8 @@ export async function createRunnerdBackend(input: {
}) => Promise<void>;
runnerEnvironment?: NodeJS.ProcessEnv;
runnerExecutionTarget?: AdapterExecutionTarget | null;
enableRunnerPreviewIngress?: boolean;
/** Resolved per-run authorization; not an independent instance setting. */
runnerIngressAuthorized?: boolean;
runnerPublicUrl?: string | null;
runnerCaBundlePath?: string | null;
runnerRemoteBinaryPath?: string | null;
@ -6104,17 +6129,11 @@ export async function createRunnerdBackend(input: {
};
}
const requiredMode =
target.transport === "sandbox" &&
target.effectiveCapabilities?.runnerWebSocketIngress === true
? "listen_ws"
: "dial_wss";
if (
requiredMode === "listen_ws" &&
input.enableRunnerPreviewIngress !== true
) {
throw new Error("runner_ingress_unavailable");
}
const requiredMode = resolveRemoteRunnerTransportMode({
target,
runnerIngressAuthorized:
input.runnerIngressAuthorized === true,
});
let transport: PaperclipRunnerTransport;
if (requiredMode === "dial_wss") {
// Validate eligibility before staging any artifact.
@ -6128,8 +6147,8 @@ export async function createRunnerdBackend(input: {
localConnectUrl: "ws://127.0.0.1/unused",
runnerPublicUrl: input.runnerPublicUrl,
runnerCaBundlePath: input.runnerCaBundlePath,
enableRunnerPreviewIngress:
input.enableRunnerPreviewIngress === true,
runnerIngressAuthorized:
input.runnerIngressAuthorized === true,
}),
);
if (
@ -6164,7 +6183,7 @@ export async function createRunnerdBackend(input: {
localConnectUrl: "ws://127.0.0.1/unused",
runnerPublicUrl: input.runnerPublicUrl,
runnerCaBundlePath: input.runnerCaBundlePath,
enableRunnerPreviewIngress: true,
runnerIngressAuthorized: true,
}),
);
}

View File

@ -4,6 +4,7 @@ import { BUILTIN_ADAPTER_TYPES } from "../../adapters/builtin-adapter-types.js";
import {
NativeRunnerSelectionError,
NativeRuntimeEligibilityError,
isRunnerIngressAuthorized,
resolveHeartbeatNativeRuntimeMode,
resolveHeartbeatRuntimeMode,
resolveNativeRuntimeMode,
@ -122,6 +123,40 @@ describe("resolveNativeRuntimeMode", () => {
}));
});
it("authorizes ingress from the native runtime decision without a second flag", () => {
const freshNative = resolveHeartbeatNativeRuntimeMode({
...eligible,
persisted: {
runtimeMode: null,
runtimeModeReason: null,
runtimeModeResolvedAt: null,
},
});
const persistedNative = resolveHeartbeatNativeRuntimeMode({
...eligible,
enabled: false,
persisted: {
runtimeMode: "native",
runtimeModeReason: "eligible_opt_in",
runtimeModeResolvedAt: new Date(),
},
});
const directLegacy = resolveHeartbeatNativeRuntimeMode({
...eligible,
enabled: false,
agent: { ...eligible.agent, adapterType: "codex_local" },
persisted: {
runtimeMode: null,
runtimeModeReason: null,
runtimeModeResolvedAt: null,
},
});
expect(isRunnerIngressAuthorized(freshNative)).toBe(true);
expect(isRunnerIngressAuthorized(persistedNative)).toBe(true);
expect(isRunnerIngressAuthorized(directLegacy)).toBe(false);
});
it.each(["paused", "terminated", "pending_approval"])(
"refuses persisted native recovery for a %s agent",
(status) => {

View File

@ -46,6 +46,17 @@ export type NativeRuntimeResolution =
authorityDecision: NativeStatusDecision;
};
/**
* Runner ingress follows the resolved runtime decision, not a second instance
* flag. Persisted native runs therefore keep their transport during recovery
* after the rollout flag is disabled, while legacy runs never gain ingress.
*/
export function isRunnerIngressAuthorized(
resolution: NativeRuntimeResolution,
): boolean {
return resolution.kind === "native";
}
export class NativeRunnerSelectionError extends Error {
constructor(readonly code: string, message: string) {
super(message);

View File

@ -70,8 +70,6 @@ const STATUS_CARDS_TOGGLE_SELECTOR =
'button[aria-label="Toggle status cards experimental setting"]';
const AUTO_RECOVERY_TOGGLE_SELECTOR =
'button[aria-label="Toggle task graph liveness auto-recovery"]';
const RUNNER_PREVIEW_INGRESS_TOGGLE_SELECTOR =
'button[aria-label="Toggle runner preview ingress experimental setting"]';
const PAPERCLIP_RUNNER_TOGGLE_SELECTOR =
'button[aria-label="Toggle Paperclip Runner experimental setting"]';
@ -305,24 +303,14 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233)
});
});
it("renders and patches the Runner Preview Ingress experimental toggle", async () => {
it("does not expose the retired Runner Preview Ingress setting separately", async () => {
currentExperimentalSettings.enableRunnerPreviewIngress = true;
await renderPage();
expect(container.textContent).toContain("Runner Preview Ingress");
const toggle = container.querySelector<HTMLButtonElement>(
RUNNER_PREVIEW_INGRESS_TOGGLE_SELECTOR,
);
expect(toggle?.getAttribute("aria-checked")).toBe("false");
await act(async () => {
toggle?.click();
});
await flushReact();
expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenCalledWith({
enableRunnerPreviewIngress: true,
});
expect(toggle?.getAttribute("aria-checked")).toBe("true");
expect(container.textContent).not.toContain("Runner Preview Ingress");
expect(container.querySelector(
'button[aria-label="Toggle runner preview ingress experimental setting"]',
)).toBeNull();
});
it("keeps Paperclip Runner default-off and exposes an explicit opt-in", async () => {

View File

@ -364,8 +364,6 @@ export function InstanceExperimentalSettings() {
);
const enableEnvironments = experimentalQuery.data?.enableEnvironments === true;
const enableNativeRunner = experimentalQuery.data?.enableNativeRunner === true;
const enableRunnerPreviewIngress =
experimentalQuery.data?.enableRunnerPreviewIngress === true;
const enableManagedSandboxOnly = experimentalQuery.data?.enableManagedSandboxOnly === true;
const enableIsolatedWorkspaces = experimentalQuery.data?.enableIsolatedWorkspaces === true;
const enableApps = experimentalQuery.data?.enableApps === true;
@ -736,7 +734,7 @@ export function InstanceExperimentalSettings() {
<ExperimentalToggleCard
title="Paperclip Runner"
description="Allow new Codex agents to select the experimental Rust Paperclip Runner. Onboarding continues to use legacy adapters. Turning this off hides the choice without affecting existing native runs."
description="Allow new Codex agents to select the experimental Rust Paperclip Runner, including authenticated runner ingress when a sandbox requires it. Onboarding continues to use legacy adapters. Turning this off hides the choice without affecting existing native runs."
checked={enableNativeRunner}
onCheckedChange={(checked) =>
toggleMutation.mutate({ enableNativeRunner: checked })
@ -747,19 +745,6 @@ export function InstanceExperimentalSettings() {
ariaLabel="Toggle Paperclip Runner experimental setting"
/>
<ExperimentalToggleCard
title="Runner Preview Ingress"
description="Let Paperclip Runner agents connect through an authenticated sandbox-provider WebSocket preview. Legacy adapters continue using their existing transports."
checked={enableRunnerPreviewIngress}
onCheckedChange={(checked) =>
toggleMutation.mutate({ enableRunnerPreviewIngress: checked })
}
disabled={toggleMutation.isPending}
settingKey="enableRunnerPreviewIngress"
managed={managedKeys.enableRunnerPreviewIngress}
ariaLabel="Toggle runner preview ingress experimental setting"
/>
{inWorktree ? (
<Card className="block p-5">
<div className="flex flex-col gap-4">