feat(dot): add a saved experimental setting with prerequisites

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-06 21:37:33 -05:00
1 parent ad1ff9b329
commit 88dddb8298
20 files changed
+126 -19

No files matched your search

+4 -2
View File
@@ -13,8 +13,10 @@ controller deployments are not qualified. The feature is off by default.
## Enable and pair
1. Configure `PAPERCLIP_PUBLIC_URL` to the instance's stable HTTPS origin and
enable Public MCP and Paperclip Runner in experimental settings. Set
`PAPERCLIP_ENABLE_OPENAI_DOT=1` on the server.
enable **Assistant connections (MCP)**, **Paperclip Runner**, and **OpenAI Dot**
in **Instance settings → Experimental**. The instance must use authenticated
sign-in. These settings are saved and take effect without a restart; the old
`PAPERCLIP_ENABLE_OPENAI_DOT` environment flag no longer enables the provider.
2. Create an approved Paperclip Runner agent with provider **OpenAI Dot**.
Acknowledge that its provider billing is external and unmetered. Save it.
3. In the agent configuration, choose **Pair Dot**. Connect a private ChatGPT
+7
View File
@@ -120,6 +120,13 @@ export const INSTANCE_FEATURE_CATALOG: Record<InstanceFeatureKey, FeatureCatalog
cloudDefault: false,
selfHostedDefault: false,
},
enableOpenAiDot: {
title: "OpenAI Dot",
description: "Let an OpenAI Dot work as a Paperclip Runner agent through MCP Events. Requires Paperclip Runner, Assistant connections (MCP), and an authenticated instance with a public HTTPS URL.",
tier: "managed",
cloudDefault: false,
selfHostedDefault: false,
},
enableChatConnectors: {
title: "Chat connectors",
description:
+2
View File
@@ -73,6 +73,8 @@ export interface InstanceExperimentalSettings {
enableChatConnectors: boolean;
/** Allow person-authorized assistant tools and task event delivery. */
enablePublicMcp: boolean;
/** Enable the experimental OpenAI Dot provider and its dedicated agent MCP connection. */
enableOpenAiDot: boolean;
/** @deprecated Compatibility key only. MCP aggregators are always enabled. */
enableMcpAggregators: boolean;
/** Show experimental memory connection setup. Existing connections remain usable. */
@@ -52,6 +52,7 @@ export const instanceExperimentalSettingsSchema = z.object({
// configs continue to load during upgrades.
enableApps: z.boolean().default(true),
enablePublicMcp: z.boolean().default(false),
enableOpenAiDot: z.boolean().default(false),
enableChatConnectors: z.boolean().default(false),
// Compatibility only: old stored and managed values must still parse.
enableMcpAggregators: z.boolean().default(true),
+36 -4
View File
@@ -26,6 +26,7 @@ import { executePaperclipNativeSession } from "../services/native-runtime/native
import { documentService } from "../services/documents.js";
import { setupRunnerPrpWebSocketServer } from "../realtime/runner-prp-ws.js";
import { finalizeNativeRun } from "../services/native-runtime/native-run-finalizer.js";
import { resolveHeartbeatNativeRuntimeMode, resolveNativeRuntimeMode } from "../services/native-runtime/runtime-mode.js";
describe("durable Dot Runner integration", () => {
let temporary: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
@@ -37,14 +38,45 @@ describe("durable Dot Runner integration", () => {
temporary = await startEmbeddedPostgresTestDatabase("paperclip-dot-runner-");
db = createDb(temporary.connectionString);
root = await mkdtemp(join(tmpdir(), "paperclip-dot-state-"));
vi.stubEnv("PAPERCLIP_ENABLE_OPENAI_DOT", "1");
vi.stubEnv("PAPERCLIP_RUNNER_BINARY", join(runnerRoot, "runner/target/release", process.platform === "win32" ? "paperclip-runnerd.exe" : "paperclip-runnerd"));
vi.stubEnv("PAPERCLIP_RUNNER_STATE_DIR", join(root, "runner-state"));
vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY", randomBytes(32).toString("base64"));
await instanceSettingsService(db).updateExperimental({ enablePublicMcp: true });
await instanceSettingsService(db).updateExperimental({ enablePublicMcp: true, enableOpenAiDot: true, enableNativeRunner: true });
}, 360000);
afterAll(async () => { await temporary?.cleanup(); await rm(root, { recursive: true, force: true }); vi.unstubAllEnvs(); });
it("requires each persisted prerequisite for pairing and new work without relying on the retired environment flag", async () => {
const settings = instanceSettingsService(db);
const broker = dotRunnerBroker(db);
for (const key of ["enableOpenAiDot", "enablePublicMcp", "enableNativeRunner"] as const) {
await settings.updateExperimental({ [key]: false });
try {
vi.stubEnv("PAPERCLIP_ENABLE_OPENAI_DOT", "1");
expect(await broker.enabled()).toBe(false);
await expect(broker.createPairing({ companyId: randomUUID(), agentId: randomUUID(), operatorId: randomUUID() })).rejects.toThrow("experimental settings");
} finally { await settings.updateExperimental({ [key]: true }); }
}
vi.stubEnv("PAPERCLIP_ENABLE_OPENAI_DOT", "0");
expect(await broker.enabled()).toBe(true);
});
it("gates fresh Dot runtime selection while preserving the existing run recovery path", () => {
const input = {
enabled: true,
runtimeConfig: {},
adapterConfig: { provider: "openai_dot", dotBindingId: randomUUID(), allowUnmeteredProvider: true },
agent: { status: "active", adapterType: "paperclip_runner" },
issue: { id: randomUUID(), workMode: "standard" },
target: { kind: "local" },
workspaceId: null,
};
expect(() => resolveNativeRuntimeMode(input)).toThrow("experimental settings");
expect(resolveNativeRuntimeMode({ ...input, dotEnabled: true })).toMatchObject({ kind: "native", profile: { backend: "openai_dot_mcp" } });
expect(resolveHeartbeatNativeRuntimeMode({ ...input, enabled: false, dotEnabled: false,
persisted: { runtimeMode: "native", runtimeModeReason: null, runtimeModeResolvedAt: new Date(), driverKind: "openai_dot_mcp" },
})).toMatchObject({ kind: "native", profile: { backend: "openai_dot_mcp" } });
});
async function fixture() {
const userId = randomUUID();
await db.insert(authUsers).values({ id: userId, name: "Dot operator", email: userId + "@example.test", createdAt: new Date(), updatedAt: new Date() });
@@ -240,11 +272,11 @@ describe("durable Dot Runner integration", () => {
const renewed = await f.oauth.token({ grant_type: "refresh_token", client_id: client.client_id, resource: f.oauth.config.resource, refresh_token: tokens.refresh_token });
expect((await f.oauth.authenticate(renewed.access_token)).grant.purpose).toBe("agent");
await expect(f.oauth.token(exchange)).rejects.toMatchObject({ code: "invalid_grant" });
vi.stubEnv("PAPERCLIP_ENABLE_OPENAI_DOT", "0");
await instanceSettingsService(db).updateExperimental({ enableOpenAiDot: false });
try {
expect((await request(app).get("/.well-known/oauth-protected-resource/mcp/runner")).status).toBe(503);
expect((await request(app).get("/.well-known/oauth-protected-resource/mcp/paperclip")).status).toBe(200);
} finally { vi.stubEnv("PAPERCLIP_ENABLE_OPENAI_DOT", "1"); }
} finally { await instanceSettingsService(db).updateExperimental({ enableOpenAiDot: true }); }
await f.events.unsubscribe(f.principal, f.subscription);
}, 30000);
@@ -7,6 +7,14 @@ import {
} from "../services/instance-settings.js";
describe("instance settings service", () => {
it("keeps OpenAI Dot opt-in and persists its setting independently of its prerequisites", () => {
expect(normalizeExperimentalSettings({}).enableOpenAiDot).toBe(false);
const enabled = applyExperimentalSettingsPatch({}, { enableOpenAiDot: true });
expect(normalizeExperimentalSettings(JSON.parse(JSON.stringify(enabled)))).toMatchObject({
enableOpenAiDot: true, enablePublicMcp: false,
});
expect(applyExperimentalSettingsPatch(enabled, { enableOpenAiDot: false }).enableOpenAiDot).toBe(false);
});
it("keeps chat connectors opt-in across legacy storage and patches without disabling Apps", () => {
for (const stored of [undefined, {}, { enableApps: true }, { enableConferenceRoomChat: true }]) {
expect(normalizeExperimentalSettings(stored).enableChatConnectors).toBe(false);
@@ -46,6 +54,7 @@ describe("instance settings service", () => {
enableMcpAggregators: true,
enableAgentChat: false,
enablePublicMcp: false,
enableOpenAiDot: false,
enableCombinedInboxTasks: false,
enableChatConnectors: false,
enableMemoryConnectors: false,
+2 -1
View File
@@ -3602,11 +3602,12 @@ export function agentRoutes(
: inputAdapterConfig;
}
if (type === "paperclip_runner" && inputAdapterConfig.provider === "openai_dot") {
const dotEnabled = await dotRunnerBroker(db).enabled();
const binding = savedAgentId ? await dotRunnerBroker(db).bindingForAgent(companyId, savedAgentId) : null;
let resource: ReturnType<typeof publicMcpConfig> = null;
try { resource = publicMcpConfig(process.env); } catch { /* diagnostic below */ }
const checks: AdapterEnvironmentCheck[] = [
{ code: "dot_enabled", level: dotRunnerBroker(db).enabled() ? "info" : "error", message: dotRunnerBroker(db).enabled() ? "Dot is enabled." : "Enable PAPERCLIP_ENABLE_OPENAI_DOT=1 on the server." },
{ code: "dot_enabled", level: dotEnabled ? "info" : "error", message: dotEnabled ? "Dot is enabled." : "Enable OpenAI Dot, Paperclip Runner, and Assistant connections (MCP) in experimental settings." },
{ code: "dot_public_endpoint", level: resource?.origin.startsWith("https://") ? "info" : "error", message: resource?.origin.startsWith("https://") ? "Public HTTPS MCP origin is configured." : "Configure a stable public HTTPS PAPERCLIP_PUBLIC_URL." },
{ code: "dot_unmetered", level: inputAdapterConfig.allowUnmeteredProvider === true ? "info" : "error", message: "Dot usage and provider cost are unavailable. Explicit externally billed provider acknowledgement is required." },
{ code: "dot_binding", level: binding?.status === "ready" && binding.subscriptionVerified && binding.id === inputAdapterConfig.dotBindingId ? "info" : "warn", message: binding?.status === "ready" && binding.subscriptionVerified ? "Binding has a verified event subscription and completed readiness challenge." : "Save this agent, pair it, subscribe to mailbox events and complete the event test." },
+1 -1
View File
@@ -23,7 +23,7 @@ export function dotRunnerRoutes(db: Db, resourceUrl?: string) {
router.get(path, async (req, res) => {
const input = scope(req);
const config = resourceUrl ? null : publicMcpConfig(process.env);
res.json({ enabled: broker.enabled(), resourceUrl: resourceUrl ?? (config ? config.origin + "/mcp/runner" : null), binding: await broker.bindingForAgent(input.companyId, input.agentId) });
res.json({ enabled: await broker.enabled(), resourceUrl: resourceUrl ?? (config ? config.origin + "/mcp/runner" : null), binding: await broker.bindingForAgent(input.companyId, input.agentId) });
});
router.post(path, async (req, res) => {
const input = scope(req, true);
+11 -6
View File
@@ -9,9 +9,9 @@ import { McpOAuthError, type McpPrincipal } from "./public-mcp/oauth.js";
import type { PublicMcpToolExtension } from "./public-mcp/dot-runner.js";
import { boardAuthService } from "./board-auth.js";
import { logActivity } from "./activity-log.js";
import { instanceSettingsService } from "./instance-settings.js";
const hash = (text: string) => createHash("sha256").update(text).digest("hex");
const enabled = () => process.env.PAPERCLIP_ENABLE_OPENAI_DOT === "1";
const fail = (message: string) => new McpOAuthError("access_denied", message, 409);
const instances = new WeakMap<Db, ReturnType<typeof createBroker>>();
export function dotRunnerBroker(db: Db) {
@@ -21,6 +21,11 @@ export function dotRunnerBroker(db: Db) {
}
function createBroker(db: Db) {
const settings = instanceSettingsService(db);
async function enabled() {
const experimental = await settings.getExperimental();
return experimental.enableOpenAiDot && experimental.enableNativeRunner && experimental.enablePublicMcp;
}
const ports = new Map<string, { token: symbol; send: (op: ExternalProviderOperation) => Promise<void>; revoke?: () => Promise<void> }>();
async function principalBinding(principal: McpPrincipal, requireReady = true, allowPaused = false) {
if (principal.grant.purpose !== "agent" || !principal.grant.scopes.includes("paperclip:agent")) throw fail("Use the dedicated Dot agent connection.");
@@ -87,7 +92,7 @@ function createBroker(db: Db) {
return {
enabled,
async createPairing(input: { companyId: string; agentId: string; operatorId: string; dotUrl?: string }) {
if (!enabled()) throw fail("OpenAI Dot is disabled on this instance.");
if (!await enabled()) throw fail("Enable OpenAI Dot, Paperclip Runner, and Assistant connections (MCP) in experimental settings.");
if (input.dotUrl && !/^https:\/\/chatgpt\.com\/dots\/[A-Za-z0-9-]+$/.test(input.dotUrl)) throw fail("Use the Dot's ChatGPT URL.");
const code = randomBytes(24).toString("base64url");
const binding = await db.transaction(async tx => {
@@ -104,7 +109,7 @@ function createBroker(db: Db) {
instructions: "Connect the private Paperclip Dot plugin at /mcp/runner, approve agent access, then call paperclip_dot_pair with this code. Subscribe to paperclip.dot.mailbox_updated for the returned binding. Run the event test before assigning work." };
},
async pair(principal: McpPrincipal, code: string) {
if (!enabled() || principal.grant.purpose !== "agent" || !principal.grant.scopes.includes("paperclip:agent")) throw fail("A dedicated Dot agent grant is required.");
if (!await enabled() || principal.grant.purpose !== "agent" || !principal.grant.scopes.includes("paperclip:agent")) throw fail("A dedicated Dot agent grant is required.");
return db.transaction(async tx => {
const [b] = await tx.select().from(bindings).where(and(eq(bindings.pairingCodeHash, hash(code)),
eq(bindings.companyId, principal.grant.companyId), eq(bindings.operatorId, principal.grant.userId), isNull(bindings.revokedAt))).for("update");
@@ -135,7 +140,7 @@ function createBroker(db: Db) {
assignment: active ? { ...active, attentionRequired: active.status === "accepted" && active.lastActivityAt.getTime() < Date.now() - 15 * 60_000 } : null };
},
async snapshot(companyId: string, agentId: string, bindingId: string): Promise<DotBindingSnapshot> {
if (!enabled()) throw fail("OpenAI Dot is disabled for new work.");
if (!await enabled()) throw fail("OpenAI Dot is disabled for new work.");
const state = await this.bindingForAgent(companyId, agentId);
if (!state || state.id !== bindingId || state.status !== "ready" || !state.subscriptionVerified) throw fail("Dot must be paired with a verified event subscription and a completed event test.");
const [binding] = await db.select().from(bindings).where(eq(bindings.id, bindingId));
@@ -196,7 +201,7 @@ function createBroker(db: Db) {
async requestWork(principal: McpPrincipal, issueId: string, requestId: string) {
const b = await principalBinding(principal);
if (!z.uuid().safeParse(requestId).success) throw fail("Use a stable UUID requestId.");
if (!enabled() || !(await this.bindingForAgent(b.companyId, b.agentId))?.subscriptionVerified) throw fail("Dot admission is unavailable.");
if (!await enabled() || !(await this.bindingForAgent(b.companyId, b.agentId))?.subscriptionVerified) throw fail("Dot admission is unavailable.");
const [issue] = await db.select().from(issues).where(and(eq(issues.companyId, b.companyId), eq(issues.id, issueId), eq(issues.assigneeAgentId, b.agentId)));
if (!issue || !["todo", "in_progress"].includes(issue.status)) throw fail("Request work only for an eligible task assigned to this agent.");
const key = `dot-work:${b.id}:${b.generation}:${requestId}`;
@@ -237,7 +242,7 @@ function createBroker(db: Db) {
},
async challenge(companyId: string, agentId: string) {
const state = await this.bindingForAgent(companyId, agentId);
if (!enabled() || !state || !state.subscriptionVerified) throw fail("Connect and subscribe the Dot first.");
if (!await enabled() || !state || !state.subscriptionVerified) throw fail("Connect and subscribe the Dot first.");
const nonce = randomBytes(24).toString("base64url");
await db.transaction(async tx => {
const [b] = await tx.select().from(bindings).where(eq(bindings.id, state.id)).for("update");
+2
View File
@@ -23710,6 +23710,8 @@ export function heartbeatService(
persisted: run,
enabled:
resolvedInstanceSettings.experimental.enableNativeRunner === true,
dotEnabled: resolvedInstanceSettings.experimental.enableOpenAiDot === true
&& resolvedInstanceSettings.experimental.enablePublicMcp === true,
runtimeConfig: agent.runtimeConfig,
adapterConfig: agent.adapterConfig,
agent: {
+2
View File
@@ -234,6 +234,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableApps: true,
enableMcpAggregators: true,
enablePublicMcp: parsed.data.enablePublicMcp ?? false,
enableOpenAiDot: parsed.data.enableOpenAiDot ?? false,
enableChatConnectors: parsed.data.enableChatConnectors ?? false,
enableMemoryConnectors: parsed.data.enableMemoryConnectors ?? false,
enablePipelines: parsed.data.enablePipelines ?? false,
@@ -280,6 +281,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableApps: true,
enableMcpAggregators: true,
enablePublicMcp: false,
enableOpenAiDot: false,
enableChatConnectors: false,
enableMemoryConnectors: false,
enablePipelines: false,
@@ -97,6 +97,7 @@ function ineligible(
export function resolveNativeRuntimeMode(input: {
enabled: boolean;
dotEnabled?: boolean;
runtimeConfig: unknown;
adapterConfig?: unknown;
agent: { id?: string; status: string; adapterType: string | null };
@@ -131,7 +132,7 @@ export function resolveNativeRuntimeMode(input: {
}
throw error;
}
if (runnerProfile.provider === "openai_dot" && process.env.PAPERCLIP_ENABLE_OPENAI_DOT !== "1") throw ineligible("paperclip_runner_dot_disabled", "OpenAI Dot is disabled for new work on this instance.");
if (runnerProfile.provider === "openai_dot" && input.dotEnabled !== true) throw ineligible("paperclip_runner_dot_disabled", "Enable OpenAI Dot and Assistant connections (MCP) in experimental settings before assigning new work.");
if (
input.agent.adapterType !== "paperclip_runner"
|| input.agent.status !== "active" && input.agent.status !== "running"
@@ -183,6 +184,7 @@ export function resolveHeartbeatRuntimeMode(input: {
runtimeModeResolvedAt: Date | null;
};
enabled: boolean;
dotEnabled?: boolean;
adapterType: string | null;
adapterConfig: unknown;
agentStatus: string;
@@ -209,6 +211,7 @@ export function resolveHeartbeatRuntimeMode(input: {
try {
resolution = resolveNativeRuntimeMode({
enabled: input.enabled,
dotEnabled: input.dotEnabled,
runtimeConfig: {},
adapterConfig: input.adapterConfig,
agent: {
@@ -264,6 +267,7 @@ export function resolveHeartbeatNativeRuntimeMode(input: {
driverKind?: string | null;
};
enabled: boolean;
dotEnabled?: boolean;
runtimeConfig: unknown;
adapterConfig?: unknown;
agent: { id?: string; status: string; adapterType: string | null };
+4 -1
View File
@@ -71,7 +71,10 @@ export function createPublicMcpOAuth(db: Db, config: PublicMcpConfig, options: {
const issuer = agentConnection ? config.origin + DOT_RUNNER_MCP_PATH + "/oauth" : config.origin;
const resolveMetadata = createClientMetadataResolver(options.metadataFetch);
const settings = instanceSettingsService(db);
const isEnabled = async (queryDb: Db = db) => (!agentConnection || process.env.PAPERCLIP_ENABLE_OPENAI_DOT === "1") && (await (queryDb === db ? settings : instanceSettingsService(queryDb)).getExperimental()).enablePublicMcp === true;
const isEnabled = async (queryDb: Db = db) => {
const experimental = await (queryDb === db ? settings : instanceSettingsService(queryDb)).getExperimental();
return experimental.enablePublicMcp && (!agentConnection || experimental.enableOpenAiDot);
};
async function assertEnabled(queryDb: Db = db) {
if (!await isEnabled(queryDb)) throw new PublicMcpDisabledError();
}
@@ -25,7 +25,7 @@ async function renderMarkup(node: ReactNode, expand?: string): Promise<string> {
return html;
}
async function renderRunner(config: Record<string, unknown>, expand?: string): Promise<string> {
async function renderRunner(config: Record<string, unknown>, expand?: string, openAiDotEnabled = false): Promise<string> {
return renderMarkup(
<TooltipProvider>
<CodexLocalConfigFields
@@ -39,6 +39,7 @@ async function renderRunner(config: Record<string, unknown>, expand?: string): P
mark={() => undefined}
models={[]}
hideInstructionsFile
openAiDotEnabled={openAiDotEnabled}
/>
</TooltipProvider>,
expand,
@@ -46,6 +47,10 @@ async function renderRunner(config: Record<string, unknown>, expand?: string): P
}
describe("Paperclip Runner Codex configuration", () => {
it("only offers Dot for new selections when its prerequisites are enabled", async () => {
expect(await renderRunner({ provider: "codex" }, "Harness")).not.toContain("OpenAI Dot");
expect(await renderRunner({ provider: "codex" }, "Harness", true)).toContain("OpenAI Dot");
});
it.each([
[undefined, "Full auto (approve all)"],
["approve-paperclip", "Automatic Paperclip actions"],
@@ -63,6 +63,7 @@ export function CodexLocalConfigFields({
models,
hideInstructionsFile,
managedSandboxOnly,
openAiDotEnabled,
}: AdapterConfigFieldsProps) {
const runnerManaged = adapterType === "paperclip_runner";
// The execution engine picks which binary runs on the execution host, and the
@@ -252,7 +253,7 @@ export function CodexLocalConfigFields({
>
<SelectTrigger className="w-full" aria-label="Harness"><SelectValue /></SelectTrigger>
<SelectContent>
{runnerHarnessOptions.map((option) => (
{runnerHarnessOptions.filter((option) => option.value !== "openai_dot" || openAiDotEnabled || runnerProvider === "openai_dot").map((option) => (
<SelectItem key={option.value} value={option.value}>
<AdapterMark type={option.adapter} className="size-4" />
{option.label}
+2
View File
@@ -51,6 +51,8 @@ export interface AdapterConfigFieldsProps {
* so a stored path never flashes before the policy resolves.
*/
managedSandboxOnly?: boolean;
/** Show Dot for new selections only when its experimental prerequisites are enabled. */
openAiDotEnabled?: boolean;
}
export interface UIAdapterModule extends TranscriptParserSource {
+3
View File
@@ -966,6 +966,9 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
// every adapter without a per-adapter edit.
hideInstructionsFile: hideInstructionsFile || hideHostPaths || isDotRunner,
managedSandboxOnly: hideHostPaths || isDotRunner,
openAiDotEnabled: experimentalSettings?.enableOpenAiDot === true
&& experimentalSettings?.enablePublicMcp === true
&& experimentalSettings?.enableNativeRunner === true,
};
// Section toggle state — advanced always starts collapsed
+1 -1
View File
@@ -41,7 +41,7 @@ export function DotRunnerConnection({ companyId, agentId, bindingId, onBinding }
const b = state.data?.binding;
return <div className="space-y-3">
<p className="text-sm text-muted-foreground">Dot manages its model and external tools. Paperclip supplies task tools through a dedicated agent connection. Provider usage and cost are unavailable; stopping a Paperclip run revokes access without confirming an external stop.</p>
{state.data && !state.data.enabled && <p className="text-sm text-muted-foreground">This instance has not enabled OpenAI Dot.</p>}
{state.data && !state.data.enabled && <p className="text-sm text-muted-foreground">Enable OpenAI Dot, Paperclip Runner, and Assistant connections (MCP) in experimental settings.</p>}
{state.data?.resourceUrl && <p className="text-sm break-all">Private plugin MCP URL: <code>{state.data.resourceUrl}</code></p>}
{b && <p className="text-sm">Connection: {b.status}. Event subscription: {b.subscriptionVerified ? "verified" : "required"}.</p>}
{b?.assignment && <p className="text-sm">Assignment: {b.assignment.status === "offered" ? "waiting for Dot to accept" : "accepted by Dot"}.</p>}
@@ -86,6 +86,7 @@ function defaultExperimentalSettings(): InstanceExperimentalSettingsPayload {
enableApps: true,
enableMcpAggregators: true,
enablePublicMcp: false,
enableOpenAiDot: false,
enableChatConnectors: false,
enableMemoryConnectors: false,
enablePipelines: false,
@@ -269,6 +270,19 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233)
}
});
it("names Dot prerequisites and saves its opt-in independently in both directions", async () => {
await renderPage();
const selector = 'button[aria-label="Toggle OpenAI Dot experimental setting"]';
expect(container.querySelector(selector)?.getAttribute("aria-checked")).toBe("false");
expect(container.textContent).toContain("Requires Paperclip Runner, Assistant connections (MCP), and an authenticated instance with a public HTTPS URL");
for (const enabled of [true, false]) {
await act(() => container.querySelector<HTMLButtonElement>(selector)!.click());
await flushReact();
expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenLastCalledWith({ enableOpenAiDot: enabled });
expect(container.querySelector(selector)?.getAttribute("aria-checked")).toBe(String(enabled));
}
});
it("defaults chat connectors off and persists an explicit toggle in both directions", async () => {
await renderPage();
const selector = 'button[aria-label="Toggle chat connectors experimental setting"]';
@@ -469,6 +469,18 @@ export function InstanceExperimentalSettings() {
ariaLabel="Toggle memory connectors experimental setting"
/>
<ExperimentalToggleCard
title="OpenAI Dot"
description="Let your OpenAI Dot take assignments and use Paperclip task tools through the Runner. Pair the Dot and verify event delivery before assigning work."
footnote="Requires Paperclip Runner, Assistant connections (MCP), and an authenticated instance with a public HTTPS URL. Turning this off blocks Dot calls and new work; saved connections are kept."
checked={experimentalQuery.data?.enableOpenAiDot === true}
onCheckedChange={(checked) => toggleMutation.mutate({ enableOpenAiDot: checked })}
disabled={toggleMutation.isPending}
settingKey="enableOpenAiDot"
managed={managedKeys.enableOpenAiDot}
ariaLabel="Toggle OpenAI Dot experimental setting"
/>
<ExperimentalToggleCard
title="Paperclip Runner"
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."