fix(dot): honor standalone rollout in imports and dev startup

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-07 17:52:55 -05:00
1 parent c3a53dfca0
commit 5a1bdea4ca
5 files changed
+58 -12

No files matched your search

+5
View File
@@ -65,6 +65,11 @@ Database migration `0312_broken_sentinels.sql` adds only Dot tables and extensio
after the merged gateway migrations. It is safe to reapply. The earlier
prototype migration number is retired; published master migrations are intact.
Company package imports use the same independent Dot option. An unpaired imported
Dot can be saved after the external billing acknowledgement; pair it before
assigning work. The dev launcher also prepares the shared Runner binary when
Dot is enabled, even when the general Runner option is off.
## Troubleshoot plugin OAuth discovery
Use the exact server URL `<origin>/mcp/runner` with OAuth authentication. The
@@ -103,7 +103,7 @@ const agentInstructionsSvc = {
};
const instanceSettingsSvc = {
getExperimental: vi.fn(async () => ({ enableNativeRunner: false })),
getExperimental: vi.fn(async (): Promise<{ enableNativeRunner: boolean; enableOpenAiDot?: boolean }> => ({ enableNativeRunner: false })),
};
const managedAgentProfileSvc = {
@@ -6030,6 +6030,33 @@ describe("company portability", () => {
expect(preview.plan.issuePlans).toHaveLength(0);
});
it("imports an unpaired Dot using its own option without enabling other Runner providers", async () => {
const portability = companyPortabilityService({} as any);
const exported = await portability.exportBundle("company-1", { include: { company: false, agents: true, projects: false, issues: false } });
agentSvc.list.mockResolvedValue([]);
agentSvc.create.mockImplementation(async (_companyId: string, input: Record<string, unknown>) => ({ id: "agent-created", ...input }));
const input = {
source: { type: "inline" as const, rootPath: exported.rootPath, files: exported.files },
include: { company: false, agents: true, projects: false, issues: false },
target: { mode: "existing_company" as const, companyId: "company-1" },
agents: "all" as const, collisionStrategy: "rename" as const,
adapterOverrides: { claudecoder: { adapterType: "paperclip_runner", adapterConfig: { provider: "openai_dot", allowUnmeteredProvider: true, lifecycleMode: "per_turn" } } },
};
instanceSettingsSvc.getExperimental.mockResolvedValue({ enableNativeRunner: true, enableOpenAiDot: false });
await expect(portability.importBundle(input, "user-1")).rejects.toMatchObject({ status: 422, details: { code: "paperclip_runner_dot_disabled" } });
expect(agentSvc.create).not.toHaveBeenCalled();
instanceSettingsSvc.getExperimental.mockResolvedValue({ enableNativeRunner: false, enableOpenAiDot: true });
await expect(portability.importBundle({ ...input, adapterOverrides: { claudecoder: { adapterType: "paperclip_runner", adapterConfig: { provider: "codex" } } } }, "user-1"))
.rejects.toMatchObject({ status: 422, details: { code: "paperclip_runner_rollout_disabled" } });
expect(agentSvc.create).not.toHaveBeenCalled();
await portability.importBundle(input, "user-1");
expect(agentSvc.create).toHaveBeenCalledWith("company-1", expect.objectContaining({ adapterType: "paperclip_runner", adapterConfig: expect.objectContaining({ provider: "openai_dot", allowUnmeteredProvider: true }) }));
const createdConfig = agentSvc.create.mock.calls[0]![1].adapterConfig;
expect(createdConfig.dotBindingId).toBeUndefined();
const { resolvePaperclipRunnerProviderProfile } = await import("../services/native-runtime/provider-profile.js");
expect(() => resolvePaperclipRunnerProviderProfile(createdConfig)).toThrow(expect.objectContaining({ code: "paperclip_runner_dot_config_invalid" }));
});
it("rejects runner imports while disabled and accepts the same selection when enabled", async () => {
const portability = companyPortabilityService({} as any);
const exported = await portability.exportBundle("company-1", {
+2 -1
View File
@@ -49,8 +49,9 @@ async function main(): Promise<void> {
console.log(
JSON.stringify({
nativeRunnerRequired:
experimental.enableNativeRunner === true || persistedNativeRun,
experimental.enableNativeRunner === true || experimental.enableOpenAiDot === true || persistedNativeRun,
rolloutEnabled: experimental.enableNativeRunner === true,
dotRolloutEnabled: experimental.enableOpenAiDot === true,
persistedNativeRun,
persistedActiveNativeRun,
persistedRetryableFailedNativeRun,
+22 -9
View File
@@ -109,6 +109,7 @@ import type {
import {
PaperclipRunnerProviderProfileError,
resolvePaperclipRunnerProviderProfile,
validatePaperclipRunnerDotConfig,
} from "./native-runtime/provider-profile.js";
import { managedAgentProfileService } from "./managed-agent-profiles.js";
import { remoteAgentProfileService } from "./remote-agent-profiles.js";
@@ -3600,6 +3601,10 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
if (adapterType === "paperclip_runner") {
let profile;
try {
if (adapterConfig.provider === "openai_dot") {
validatePaperclipRunnerDotConfig(adapterConfig, false);
return;
}
profile = resolvePaperclipRunnerProviderProfile(adapterConfig);
} catch (error) {
if (error instanceof PaperclipRunnerProviderProfileError) {
@@ -5248,19 +5253,27 @@ export function companyPortabilityService(db: Db, storage?: StorageService) {
.filter((entry) => entry.action !== "skip")
.map((entry) => entry.slug),
);
const selectsNativeRunner = sourceManifest.agents.some((agent) =>
const runnerSelections = sourceManifest.agents.filter((agent) =>
importedAgentSlugs.has(agent.slug)
&& (input.adapterOverrides?.[agent.slug]?.adapterType ?? agent.adapterType)
=== "paperclip_runner",
);
if (
selectsNativeRunner
&& (await instanceSettingsService(db).getExperimental()).enableNativeRunner !== true
) {
throw unprocessable(
"Paperclip Runner is experimental and disabled on this instance.",
{ code: "paperclip_runner_rollout_disabled" },
);
if (runnerSelections.length > 0) {
const experimental = await instanceSettingsService(db).getExperimental();
for (const agent of runnerSelections) {
const config = input.adapterOverrides?.[agent.slug]?.adapterConfig ?? agent.adapterConfig;
if (config.provider === "openai_dot") {
if (experimental.enableOpenAiDot !== true) throw unprocessable(
"OpenAI Dot is experimental and disabled on this instance.",
{ code: "paperclip_runner_dot_disabled" },
);
} else if (experimental.enableNativeRunner !== true) {
throw unprocessable(
"Paperclip Runner is experimental and disabled on this instance.",
{ code: "paperclip_runner_rollout_disabled" },
);
}
}
}
}
+1 -1
View File
@@ -95,7 +95,7 @@ function createBroker(db: Db) {
return {
enabled,
async createPairing(input: { companyId: string; agentId: string; operatorId: string; dotUrl?: string }) {
if (!await enabled()) throw fail("Enable OpenAI Dot, Paperclip Runner, and Assistant connections (MCP) in experimental settings.");
if (!await enabled()) throw fail("Enable OpenAI Dot 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 => {