fix(ai): retain account modes in session adoption checks

Exercise responsible-user to delegated adoption with real prepared account configurations and preserve all other fingerprint boundaries.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-02 17:16:41 -05:00
1 parent d134e3902a
commit b4bb78eb70
3 files changed
+38 -3

No files matched your search

@@ -8,6 +8,7 @@ import { createDb, companies, agents, companyMemberships, connectionGrants, plug
import { startEmbeddedPostgresTestDatabase } from "@paperclipai/db/test-embedded-postgres";
import type { AiConnectionPoolMember, AiConnectionRouterRequest, AiConnectionRouterResult, PaperclipPluginManifestV1 } from "@paperclipai/shared";
import { aiConnectionRouterService, AiConnectionPoolExhausted, poolMemberRuntimeConfig, applyAiConnectionRouterTaskSettings } from "../services/ai-connection-router.js";
import { aiConnectionSessionCompatibilityInputs } from "../services/ai-connection-session.js";
import { projectPaperclipRunnerTaskConfig, resolvePaperclipRunnerNativeProviderInput } from "../services/native-runtime/provider-profile.js";
import { aiConnectionService } from "../services/ai-connections.js";
import { instanceSettingsService } from "../services/instance-settings.js";
@@ -18,7 +19,7 @@ import express from "express";
import request from "supertest";
import { aiConnectionRoutes } from "../routes/ai-connections.js";
import { errorHandler } from "../middleware/index.js";
import { heartbeatService } from "../services/heartbeat.js";
import { heartbeatService, buildEffectiveRunSessionConfigMetadata, resolveTaskSessionConfigFreshness } from "../services/heartbeat.js";
import { connectionIntentService } from "../services/connection-intents.js";
import { legacyExecutionNeedsReconciliation } from "../services/legacy-execution-recovery.js";
import { getServerAdapter, registerServerAdapter, unregisterServerAdapter } from "../adapters/index.js";
@@ -275,6 +276,27 @@ describe("durable, authorized connection routing", () => {
try { await expect(resolve(pool.id, "new")).rejects.toThrow("plugin"); expect(await resolve(pool.id, "recovery", { persisted: selected })).toEqual(selected); await db.delete(toolConnections).where(eq(toolConnections.id, pool.id)); expect(await resolve(pool.id, "recovery", { persisted: selected })).toEqual(selected); }
finally { await db.update(plugins).set({ status: "ready" }).where(eq(plugins.id, pluginId)); }
});
it("resumes a responsible-user session through a delegated pool using real managed account configurations", async () => {
await aiConnectionService(db).setDefault(companyId, "alice", members[0]!.binding.grantId!);
const pool = await makePool({ members: [members[0]!] });
const selected = await resolve(pool.id, "managed-mode-adoption", { requireExisting: true, existingGrantId: members[0]!.binding.grantId });
const directBinding = { mode: "responsible_user" as const, provider: "openai" as const, method: "api_key" as const };
const prepare = { companyId, agentId, responsibleUserId: "alice", adapterType: "paperclip_runner", config: selected.runtimeConfig };
const before = await prepareManagedAiRuntime(db, { ...prepare, binding: directBinding });
const after = await prepareManagedAiRuntime(db, { ...prepare, binding: selected.binding });
try {
expect(before.config.managedAiConnection.mode).toBe("responsible_user");
expect(after.config.managedAiConnection.mode).toBe("delegated");
expect(after.sessionIdentity).toBe(before.sessionIdentity);
const common = { adapterType: "paperclip_runner", issueOverrides: null, workspaceConfig: {}, environment: {}, environmentEnv: null, projectEnv: null, routineEnv: null, runtimeSkills: [], agentConfigRevision: null };
const previous = await buildEffectiveRunSessionConfigMetadata({ ...common, effectiveAdapterConfig: before.config, managedAiHome: before.home, agentRuntimeConfig: { aiConnection: directBinding } });
const currentInput = { ...common, effectiveAdapterConfig: after.config, managedAiHome: after.home, agentRuntimeConfig: { aiConnection: { mode: "router", connectionId: pool.id } } };
const current = await buildEffectiveRunSessionConfigMetadata(currentInput);
const alternatives = aiConnectionSessionCompatibilityInputs({ ...currentInput, originalIssueOverrides: null, binding: selected.binding, router: true, storedIdentity: before.sessionIdentity, sessionIdentity: after.sessionIdentity, credentialIdentity: after.identity, revisions: [] });
const compatibleConfigMetadata = await Promise.all(alternatives.map(candidate => buildEffectiveRunSessionConfigMetadata({ ...currentInput, ...candidate })));
expect(resolveTaskSessionConfigFreshness({ hasTaskSession: true, configuredModel: String(selected.runtimeConfig.model), taskSessionParams: { __paperclipConfiguredModel: selected.runtimeConfig.model, __paperclipConfigFingerprint: previous.fingerprint, __paperclipConfigFingerprintVersion: previous.version, __paperclipConfigCategoryFingerprints: previous.categoryFingerprints }, configMetadata: current, compatibleConfigMetadata }).reset).toBe(false);
} finally { await before.cleanup(); await after.cleanup(); }
});
it("keeps session identity and fingerprints stable on authenticated refresh, but changes them on manual replacement", async () => {
const input = { companyId, agentId, responsibleUserId: "alice", adapterType: "paperclip_runner", binding: members[0]!.binding, config: { provider: "codex", model: "gpt-5.6-sol" } };
const first = await prepareManagedAiRuntime(db, input);
@@ -26,7 +26,7 @@ async function decision(previous: Partial<MetadataInput>, next: Partial<Metadata
describe("AI pool session adoption", () => {
it.each([binding, { ...binding, mode: "delegated" }, { mode: "responsible_user", provider: "openai", method: "api_key" }])("preserves the exact existing configuration through a compatible binding change: %j", async aiConnection => {
expect((await decision({ agentRuntimeConfig: { aiConnection } })).reset).toBe(false);
expect((await decision({ agentRuntimeConfig: { aiConnection }, effectiveAdapterConfig: { ...config, managedAiConnection: { ...config.managedAiConnection, mode: aiConnection.mode } } })).reset).toBe(false);
});
it("bridges only account-binding edits in revision history", async () => {
const first = revision("revision-1", null, binding);
+14 -1
View File
@@ -86,5 +86,18 @@ export function aiConnectionSessionCompatibilityInputs(input: {
}
const overrides = input.router && !isDeepStrictEqual(input.issueOverrides, input.originalIssueOverrides)
? [input.issueOverrides, input.originalIssueOverrides] : [input.issueOverrides];
return variants.flatMap(variant => configs.flatMap(effectiveAdapterConfig => overrides.map(issueOverrides => ({ ...variant, effectiveAdapterConfig, issueOverrides }))));
return variants.flatMap(variant => {
const previousMode = record(record(variant.agentRuntimeConfig).aiConnection).mode;
const modes = input.router
? ["responsible_user", "shared", "delegated"].includes(String(previousMode))
? [previousMode] : ["responsible_user", "shared", "delegated"]
: [record(input.effectiveAdapterConfig.managedAiConnection).mode];
return configs.flatMap(config => modes.flatMap(mode => overrides.map(issueOverrides => ({
...variant,
// Binding mode is also recorded in the managed account attribution.
// Keep grant, user, credential epoch, and every other setting unchanged.
effectiveAdapterConfig: { ...config, managedAiConnection: { ...record(config.managedAiConnection), mode } },
issueOverrides,
}))));
});
}