Files
PaperClipAI/server/src/__tests__/dot-oauth-onboarding.test.ts
T
DottaandPaperclip 556daf9a99 feat(dot): run cloud assignments in managed sandboxes (#15691)
## Thinking Path

> - Paperclip manages agents and governs their work.
> - The Runner owns execution and task authority.
> - Dot executes at OpenAI and uses Paperclip through MCP.
> - Dot previously required a self-hosted controller.
> - Cloud must admit Dot work in a managed sandbox and keep protocol
traffic in the tenant.
> - This pull request adds managed Dot execution and dedicated pairing
routes.
> - The same Runner path can later host sandboxed workspace tools.

## Linked Issues or Issue Description

**Agent or provider**

OpenAI Dot, through the existing dedicated MCP adapter. Related changes:
#15402, #15414, and #15581.

**Why this adapter is useful**

Cloud users need to assign work to Dot and let Dot request work while
idle. Neither path should execute agent commands on the control-plane
host or require an idle sandbox.

**How the agent is invoked**

The tenant serves OAuth, signed mailbox events, and scoped tools. Normal
admission creates a managed sandbox for each assignment. A verified Rust
Runner owns that assignment and relays its tools and results.

## What Changed

- Enable the external-agent invitation in Cloud behind the standalone
Dot flag.
- Launch Dot through the existing managed process launcher and
authenticated PRP transport.
- Verify the artifact digest and Dot capability before dispatch. Read
provider checkpoints from the execution target for recovery.
- Reject local execution in Cloud and reject managed workspace access
for this release.
- Add dedicated public Dot pairing aliases. Preserve the personal
assistant consent paths.
- Keep Dot events in the tenant when a personal Cloud broker is
configured.
- Use the app-configured heartbeat for Dot-initiated work. Report
terminal admission failures and fence stranded offers before new
admission.
- Offer normal comments through the active assignment's mailbox. Keep
their wakes deferred until a successful history receipt contains the
exact IDs; preserve unread comments through finish, batching and
recovery without replaying consumed input.
- Require a sandbox process fingerprint before dispatch and allow a
bounded five-minute controller reconnect gap. Preserve remote process
birth without inspecting a colliding controller PID.
- Recognize authenticated remote shutdown receipts even without an OS
exit code.
- Document dedicated pairing aliases and teach route coverage to inspect
literal path arrays.
- Add opt-in private Daytona qualification and focused contract tests.

## Verification

- Workspace typecheck, production build, and UI token gates passed.
- Focused onboarding, OAuth, managed environment, artifact metadata, and
terminal-fence tests passed. One environment-test timeout passed on an
isolated repeat.
- A private Daytona fixture passed with the real Rust Runner, signed
readiness, assigned app tools, document writes, duplicate receipts,
authority fences, completion, and sandbox deletion.
- A real OpenAI Dot completed both an operator-assigned task and a
Dot-initiated task in managed Daytona sandboxes. Both leases were
released.
- A real accepted Dot assignment survived a controller gap longer than
one minute with the same sandbox process, Runner, session, and
assignment. Dot consumed the normal task-comment follow-up and completed
exactly once; the lease was released.
- A real cancellation fenced an accepted assignment, released its lease,
and received Dot's acknowledgement without claiming a global provider
stop.
- Final local checks: 74 Dot execution, OAuth onboarding and API
contract tests after integrating the current agent-lifecycle changes, 9
real Rust driver tests, 21 remote-runtime tests plus the 3
detached-process cleanup tests, and 17 lifecycle tests passed. The final
live Daytona fixture passed and deleted its sandbox.
- A final real Dot follow-up stayed pending until its exact comment
appeared in a successful history receipt. Dot completed the original
managed assignment once; its lease was released.
- The broad local suite crossed a rebase and was stopped; its failures
are not a qualification of this final source. Focused repeats passed.
The final head passed all CI gates: workspace types, build, general and
serialized test suites, Rust and Runner verification, all browser
shards, and the canary dry run. Greptile is 5/5 with both findings
resolved.

## Risks

- Deploy the tenant app with its current Linux Runner artifact and the
corresponding front-door protocol routes together.
- This branch has not been deployed to a Cloud tenant. A deployed tenant
still needs an OAuth and assignment smoke test.
- Dot provider usage and cost are unmetered. Revocation fences Paperclip
tools and does not prove a global OpenAI stop.
- Missing or changed recovery identity fails closed. No replacement Dot
thread is invented.
- No schema migration is added. Managed Dot workspace access remains
unavailable.

## Model Used

OpenAI Codex, based on GPT-6, with reasoning, tool use, code execution,
and browser testing. The exact runtime model identifier and
context-window size are not exposed in this session.

## 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 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
- [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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-09 12:58:35 -05:00

252 lines
20 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { createHash, randomBytes, randomUUID } from "node:crypto";
import express from "express";
import request from "supertest";
import { and, eq, sql } from "drizzle-orm";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createDb, authUsers, companies, companyMemberships, agents, dotAgentBindings, dotMailboxItems, mcpEventSubscriptions, mcpOauthGrants, mcpOauthRequests, mcpOauthTokens } from "@paperclipai/db";
import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
import { createPublicMcpOAuth, publicMcpConfig, hashMcpSecret, DEVICE_GRANT } from "../services/public-mcp/oauth.js";
import { instanceSettingsService } from "../services/instance-settings.js";
import { dotRunnerBroker } from "../services/dot-runner-broker.js";
import { publicMcpManagementRoutes, publicMcpIngressRoutes } from "../routes/public-mcp.js";
const config = { origin: "https://paperclip.example", resource: "https://paperclip.example/mcp/runner" };
const callback = "https://chatgpt.com/connector_platform_oauth_redirect";
describe("Dot onboarding with an operator-issued pairing capability", () => {
let temp: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
let db: ReturnType<typeof createDb>;
beforeAll(async () => {
temp = await startEmbeddedPostgresTestDatabase("paperclip-dot-onboarding-");
db = createDb(temp.connectionString);
}, 120000);
beforeEach(async () => { await instanceSettingsService(db).updateExperimental({ enablePublicMcp: true, enableOpenAiDot: true, enableNativeRunner: false }); });
afterAll(async () => { await temp?.cleanup(); });
async function fixture(authorizationOrigin?: string, lifecycleState = "ready") {
const userId = randomUUID();
await db.insert(authUsers).values({ id: userId, name: "Operator", email: userId + "@example.test", createdAt: new Date(), updatedAt: new Date() });
const [company] = await db.insert(companies).values({ name: "Dot onboarding", issuePrefix: "DO" + randomBytes(3).toString("hex") }).returning();
await db.insert(companyMemberships).values({ companyId: company!.id, principalType: "user", principalId: userId, membershipRole: "owner", status: "active" });
const [agent] = await db.insert(agents).values({ companyId: company!.id, name: "Dot", adapterType: "paperclip_runner", adapterConfig: { provider: "openai_dot" }, lifecycleState, status: lifecycleState === "ready" ? "active" : "paused" }).returning();
const oauth = createPublicMcpOAuth(db, { ...config, ...(authorizationOrigin ? { authorizationOrigin } : {}) });
const client = await oauth.register({ client_name: "Dot", redirect_uris: [callback], grant_types: ["authorization_code", "refresh_token", DEVICE_GRANT] }, randomUUID());
const verifier = randomBytes(32).toString("base64url");
const input = { client_id: client.client_id, redirect_uri: callback, response_type: "code", resource: config.resource,
scope: "paperclip:agent offline_access", code_challenge: createHash("sha256").update(verifier).digest("base64url"), code_challenge_method: "S256" };
const begin = async (extra = {}) => (await oauth.authorize({ ...input, ...extra })).split("/").at(-1)!;
const pairing = await dotRunnerBroker(db).createPairing({ companyId: company!.id, agentId: agent!.id, operatorId: userId });
const id = await begin();
return { userId, company: company!, agent: agent!, oauth, client, verifier, input, pairing, id, begin };
}
async function connect(f: Awaited<ReturnType<typeof fixture>>) {
const consent = await f.oauth.consentDotPairing(f.id, f.pairing.pairingCode);
return f.oauth.token({ grant_type: "authorization_code", client_id: f.client.client_id,
redirect_uri: callback, resource: config.resource, code: new URL(consent.redirectUrl).searchParams.get("code"), code_verifier: f.verifier });
}
it("invalidates setup when a broker pairing is applied or revoked", async () => {
const f = await fixture(undefined, "preparing");
const consent = await f.oauth.consent(f.id, { type: "board", source: "session", userId: f.userId },
{ decision: "approve", companyId: f.company.id, allowWrites: false });
const tokens = await f.oauth.token({ grant_type: "authorization_code", client_id: f.client.client_id,
redirect_uri: callback, resource: config.resource, code: new URL(consent.redirectUrl).searchParams.get("code"), code_verifier: f.verifier });
const principal = await f.oauth.authenticate(tokens.access_token);
const broker = dotRunnerBroker(db);
await broker.pair(principal, f.pairing.pairingCode);
const [paired] = await db.select().from(agents).where(eq(agents.id, f.agent.id));
expect(paired.adapterConfig.dotBindingId).toBe(f.pairing.bindingId);
expect(paired.lifecycleVersion).toBe(f.agent.lifecycleVersion + 1);
await expect(broker.pair(principal, f.pairing.pairingCode)).rejects.toThrow("Pairing code expired or was consumed");
await broker.revoke(f.company.id, f.agent.id, f.userId);
const [revoked] = await db.select().from(agents).where(eq(agents.id, f.agent.id));
expect(revoked.adapterConfig).not.toHaveProperty("dotBindingId");
expect(revoked.lifecycleVersion).toBe(paired.lifecycleVersion + 1);
await expect(f.oauth.authenticate(tokens.access_token)).rejects.toThrow();
});
it.each(["preparing", "verifying"])("allows Dot setup during %s without allowing task access", async (state) => {
const f = await fixture(undefined, state);
const tokens = await connect(f);
const principal = await f.oauth.authenticate(tokens.access_token);
const broker = dotRunnerBroker(db);
await db.insert(mcpEventSubscriptions).values({ id: randomUUID(), companyId: f.company.id,
grantId: principal.grant.id, name: "paperclip.dot.mailbox_updated", bindingId: f.pairing.bindingId,
arguments: {}, deliveryMaterial: {}, verifiedAt: new Date(), expiresAt: new Date(Date.now() + 60_000) });
await db.insert(dotMailboxItems).values({ companyId: f.company.id, bindingId: f.pairing.bindingId,
bindingGeneration: 1, kind: "assignment", sourceEventId: randomUUID(), references: { task: "hidden" } });
await broker.challenge(f.company.id, f.agent.id);
const inbox = await broker.mailbox(principal);
expect(inbox.items.map(item => item.kind)).toEqual(["readiness_challenge"]);
expect(inbox.nextCursor).toBe(0);
await expect(broker.confirmChallenge(principal, String(inbox.items[0]!.references.challenge))).resolves.toEqual({ status: "ready" });
expect(await broker.capabilities(principal)).toMatchObject({ ready: false });
await expect(broker.tasks(principal)).rejects.toThrow("authority");
await expect(broker.snapshot(f.company.id, f.agent.id, f.pairing.bindingId)).rejects.toThrow("authority");
await db.update(agents).set({ lifecycleState: "paused" }).where(eq(agents.id, f.agent.id));
expect((await broker.mailbox(principal)).items).toEqual([]);
await expect(broker.confirmChallenge(principal, "already-used")).rejects.toThrow("authority");
});
it("refreshes a Dot connection after years of inactivity, still rotates and revokes on replay", async () => {
const f = await fixture();
const tokens = await connect(f);
const [stored] = await db.select().from(mcpOauthTokens).where(eq(mcpOauthTokens.tokenHash, hashMcpSecret(tokens.refresh_token!)));
expect(stored!.expiresAt).toBeNull();
const refresh = { grant_type: "refresh_token", client_id: f.client.client_id, resource: config.resource, refresh_token: tokens.refresh_token };
vi.useFakeTimers({ toFake: ["Date"] });
try {
vi.setSystemTime(new Date(Date.now() + 2 * 365 * 24 * 60 * 60_000));
await expect(f.oauth.authenticate(tokens.access_token)).rejects.toThrow();
const next = await f.oauth.token(refresh);
expect(next.refresh_token).not.toBe(tokens.refresh_token);
expect(next.expires_in).toBe(900);
expect((await f.oauth.authenticate(next.access_token)).actor).toMatchObject({ type: "agent", agentId: f.agent.id });
const [rotated] = await db.select().from(mcpOauthTokens).where(eq(mcpOauthTokens.tokenHash, hashMcpSecret(next.refresh_token!)));
expect(rotated!.expiresAt).toBeNull();
await expect(f.oauth.token(refresh)).rejects.toThrow();
await expect(f.oauth.authenticate(next.access_token)).rejects.toThrow();
expect(await dotRunnerBroker(db).bindingForAgent(f.company.id, f.agent.id)).toBeNull();
} finally { vi.useRealTimers(); }
});
it("upgrades valid Dot refresh tokens without reviving expired, used or revoked credentials", async () => {
const f = await fixture();
const tokens = await connect(f);
const grant = (await f.oauth.authenticate(tokens.access_token)).grant;
const validExpiry = new Date(Date.now() + 30 * 24 * 60 * 60_000);
await db.update(mcpOauthTokens).set({ expiresAt: validExpiry }).where(eq(mcpOauthTokens.tokenHash, hashMcpSecret(tokens.refresh_token!)));
const stale = await fixture();
const staleTokens = await connect(stale);
const expired = new Date(0);
await db.update(mcpOauthTokens).set({ expiresAt: expired }).where(eq(mcpOauthTokens.tokenHash, hashMcpSecret(staleTokens.refresh_token!)));
const personalId = randomUUID();
const revokedId = randomUUID();
const usedHash = randomUUID();
await db.insert(mcpOauthGrants).values([
{ ...grant, id: personalId, purpose: "personal", agentId: null, resource: config.origin + "/mcp/paperclip" },
{ ...grant, id: revokedId, revokedAt: new Date() },
]);
await db.insert(mcpOauthTokens).values([
{ grantId: personalId, tokenHash: randomUUID(), kind: "refresh", expiresAt: validExpiry },
{ grantId: revokedId, tokenHash: randomUUID(), kind: "refresh", expiresAt: validExpiry },
{ grantId: grant.id, tokenHash: usedHash, kind: "refresh", expiresAt: validExpiry, usedAt: new Date() },
]);
const migration = await readFile(new URL("../../../packages/db/src/migrations/0319_heavy_captain_midlands.sql", import.meta.url), "utf8");
for (let attempt = 0; attempt < 2; attempt++) {
for (const statement of migration.split("--> statement-breakpoint")) await db.execute(sql.raw(statement));
}
const [personal] = await db.select().from(mcpOauthTokens).where(eq(mcpOauthTokens.grantId, personalId));
const [revoked] = await db.select().from(mcpOauthTokens).where(eq(mcpOauthTokens.grantId, revokedId));
const [used] = await db.select().from(mcpOauthTokens).where(eq(mcpOauthTokens.tokenHash, usedHash));
const [staleToken] = await db.select().from(mcpOauthTokens).where(eq(mcpOauthTokens.tokenHash, hashMcpSecret(staleTokens.refresh_token!)));
expect(personal!.expiresAt).toEqual(validExpiry);
expect(revoked!.expiresAt).toEqual(validExpiry);
expect(used!.expiresAt).toEqual(validExpiry);
expect(staleToken!.expiresAt).toEqual(expired);
await expect(stale.oauth.token({ grant_type: "refresh_token", client_id: stale.client.client_id, resource: config.resource, refresh_token: staleTokens.refresh_token })).rejects.toThrow();
const next = await f.oauth.token({ grant_type: "refresh_token", client_id: f.client.client_id, resource: config.resource, refresh_token: tokens.refresh_token });
expect((await f.oauth.authenticate(next.access_token)).actor).toMatchObject({ type: "agent", agentId: f.agent.id });
await f.oauth.revokeToken(next.access_token, f.client.client_id);
await expect(f.oauth.token({ grant_type: "refresh_token", client_id: f.client.client_id, resource: config.resource, refresh_token: next.refresh_token })).rejects.toThrow();
});
it("pins browser ingress without changing the issuer, resource or token endpoint", async () => {
const browserOrigin = "https://paperclip-browser.example:10000";
const f = await fixture(browserOrigin);
const personal = createPublicMcpOAuth(db, { ...config, authorizationOrigin: browserOrigin, resource: config.origin + "/mcp/paperclip" });
const app = express(); app.use(express.json());
app.use((req, _res, next) => { req.actor = { type: "none" }; next(); });
app.use(publicMcpIngressRoutes(f.oauth, vi.fn()));
app.use("/api", publicMcpManagementRoutes(personal, f.oauth));
const metadata = await request(app).get("/.well-known/oauth-authorization-server/mcp/runner/oauth");
expect(metadata.body).toMatchObject({ issuer: config.origin + "/mcp/runner/oauth",
authorization_endpoint: browserOrigin + "/mcp/runner/oauth/authorize", token_endpoint: config.origin + "/mcp/runner/oauth/token" });
const authorization = await request(app).get("/mcp/runner/oauth/authorize").query(f.input);
expect(authorization.status).toBe(303);
expect(new URL(authorization.headers.location).origin).toBe(browserOrigin);
expect(new URL(authorization.headers.location).pathname).toMatch(/^\/dot-connect\/pcmcp_request_[A-Za-z0-9_-]{43}$/);
const path = `/api/mcp/requests/${f.id}/dot-pairing`;
expect((await request(app).post(path).set("Origin", "https://other.example").send({ pairingCode: f.pairing.pairingCode })).status).toBe(403);
const approval = await request(app).post(path).set("Origin", browserOrigin).send({ pairingCode: f.pairing.pairingCode });
expect(approval.status).toBe(200);
expect(new URL(approval.body.redirectUrl).searchParams.get("iss")).toBe(config.origin + "/mcp/runner/oauth");
const device = await f.oauth.deviceAuthorize({ client_id: f.client.client_id, resource: config.resource, scope: "paperclip:agent" }, randomUUID());
expect(device.verification_uri).toBe(browserOrigin + "/mcp-device");
});
it("validates a separately configured browser origin", () => {
expect(publicMcpConfig({ PAPERCLIP_PUBLIC_URL: config.origin, PAPERCLIP_MCP_AUTHORIZATION_ORIGIN: "https://browser.example:10000" }))
.toMatchObject({ origin: config.origin, resource: config.origin + "/mcp/paperclip", authorizationOrigin: "https://browser.example:10000" });
for (const invalid of ["http://browser.example", "https://secret@browser.example", "https://browser.example/path", "https://browser.example?next=evil", "https://browser.example#fragment"])
expect(() => publicMcpConfig({ PAPERCLIP_PUBLIC_URL: config.origin, PAPERCLIP_MCP_AUTHORIZATION_ORIGIN: invalid })).toThrow();
});
it.each(["mcp", "dot-mcp"])("connects via %s without a board session, binds the exact agent, and consumes the capability once", async prefix => {
const f = await fixture();
const personal = createPublicMcpOAuth(db, { ...config, resource: config.origin + "/mcp/paperclip" });
const app = express(); app.use(express.json());
app.use((req, _res, next) => { req.actor = { type: "none" }; next(); });
app.use("/api", publicMcpManagementRoutes(personal, f.oauth));
const path = `/api/${prefix}/requests/${f.id}/dot-pairing`;
expect((await request(app).get(`/api/${prefix}/requests/${f.id}`)).body.agentConnection).toBe(true);
expect((await request(app).post(path).send({ pairingCode: f.pairing.pairingCode })).status).toBe(403);
const preview = await request(app).post(path + "/preview").set("Origin", config.origin).send({ pairingCode: f.pairing.pairingCode });
expect(preview.status).toBe(200);
expect(preview.body.accessDuration).toBe("Ongoing until revoked. This connection does not expire from inactivity.");
expect(preview.body).toMatchObject({ company: { id: f.company.id, name: f.company.name }, agent: { id: f.agent.id, name: f.agent.name } });
expect(JSON.stringify(preview.body)).not.toContain(f.pairing.pairingCode);
expect(await db.select().from(mcpOauthGrants).where(eq(mcpOauthGrants.userId, f.userId))).toHaveLength(0);
const approval = await request(app).post(path).set("Origin", config.origin).send({ pairingCode: f.pairing.pairingCode });
expect(approval.status).toBe(200);
const redirect = new URL(approval.body.redirectUrl);
expect(redirect.origin + redirect.pathname).toBe(callback);
const exchange = { grant_type: "authorization_code", client_id: f.client.client_id, redirect_uri: callback, resource: config.resource,
code: redirect.searchParams.get("code"), code_verifier: f.verifier };
await expect(f.oauth.token({ ...exchange, code_verifier: "x".repeat(43) })).rejects.toThrow();
const tokens = await f.oauth.token(exchange);
const principal = await f.oauth.authenticate(tokens.access_token);
expect(principal.actor).toMatchObject({ type: "agent", agentId: f.agent.id, companyId: f.company.id });
expect(principal.grant.scopes).toEqual(["paperclip:agent", "offline_access"]);
await expect(personal.authenticate(tokens.access_token)).rejects.toThrow();
expect(await dotRunnerBroker(db).mailbox(principal)).toMatchObject({ bindingId: f.pairing.bindingId });
const [binding] = await db.select().from(dotAgentBindings).where(eq(dotAgentBindings.id, f.pairing.bindingId));
expect(binding).toMatchObject({ status: "connected", pairingCodeHash: null, pairingExpiresAt: null });
expect((await db.select().from(agents).where(eq(agents.id, f.agent.id)))[0]!.adapterConfig.dotBindingId).toBe(f.pairing.bindingId);
await expect(f.oauth.consentDotPairing(f.id, f.pairing.pairingCode)).rejects.toThrow();
await expect(f.oauth.consentDotPairing(await f.begin(), f.pairing.pairingCode)).rejects.toThrow();
});
it("does not expose personal assistant requests through the public Dot pairing lane", async () => {
const f = await fixture();
const personal = createPublicMcpOAuth(db, { ...config, resource: config.origin + "/mcp/paperclip" });
const client = await personal.register({ client_name: "Personal", redirect_uris: [callback], grant_types: ["authorization_code"] }, randomUUID());
const id = (await personal.authorize({ ...f.input, client_id: client.client_id, resource: config.origin + "/mcp/paperclip", scope: "paperclip:read" })).split("/").at(-1)!;
const app = express(); app.use(express.json());
app.use((req, _res, next) => { req.actor = { type: "none" }; next(); });
app.use("/api", publicMcpManagementRoutes(personal, f.oauth));
expect((await request(app).get(`/api/dot-mcp/requests/${id}`)).status).toBe(404);
for (const suffix of ["", "/preview"]) {
expect((await request(app).post(`/api/dot-mcp/requests/${id}/dot-pairing${suffix}`).set("Origin", config.origin).send({ pairingCode: f.pairing.pairingCode })).status).toBe(404);
}
expect((await request(app).get(`/api/mcp/requests/${id}`)).status).toBe(200);
});
it.each(["expired-code", "expired-request", "revoked", "paused", "viewer", "wrong-company", "dot-disabled", "mcp-disabled"])("rejects %s without creating a grant", async failure => {
const f = await fixture();
let id = f.id;
if (failure === "expired-code") await db.update(dotAgentBindings).set({ pairingExpiresAt: new Date(0) }).where(eq(dotAgentBindings.id, f.pairing.bindingId));
if (failure === "expired-request") await db.update(mcpOauthRequests).set({ expiresAt: new Date(0) }).where(eq(mcpOauthRequests.id, id));
if (failure === "revoked") await dotRunnerBroker(db).revoke(f.company.id, f.agent.id, f.userId);
if (failure === "paused") await db.update(agents).set({ status: "paused" }).where(eq(agents.id, f.agent.id));
if (failure === "viewer") await db.update(companyMemberships).set({ membershipRole: "viewer" }).where(and(eq(companyMemberships.companyId, f.company.id), eq(companyMemberships.principalId, f.userId)));
if (failure === "wrong-company") id = await f.begin({ company_id: randomUUID() });
if (failure === "dot-disabled") await instanceSettingsService(db).updateExperimental({ enableOpenAiDot: false });
if (failure === "mcp-disabled") await instanceSettingsService(db).updateExperimental({ enablePublicMcp: false });
await expect(f.oauth.describeDotPairing(id, f.pairing.pairingCode)).rejects.toThrow();
await expect(f.oauth.consentDotPairing(id, f.pairing.pairingCode)).rejects.toThrow();
expect(await db.select().from(mcpOauthGrants).where(eq(mcpOauthGrants.userId, f.userId))).toHaveLength(0);
});
it("prevents concurrent requests and the personal endpoint from spending the same capability", async () => {
const f = await fixture();
const personal = createPublicMcpOAuth(db, { ...config, resource: config.origin + "/mcp/paperclip" });
await expect(personal.consentDotPairing(f.id, f.pairing.pairingCode)).rejects.toMatchObject({ status: 403 });
const other = await f.begin();
const results = await Promise.allSettled([f.oauth.consentDotPairing(f.id, f.pairing.pairingCode), f.oauth.consentDotPairing(other, f.pairing.pairingCode)]);
expect(results.filter(result => result.status === "fulfilled")).toHaveLength(1);
expect(await db.select().from(mcpOauthGrants).where(eq(mcpOauthGrants.userId, f.userId))).toHaveLength(1);
});
});