Plugins: migrate legacy plugin binding records

This commit is contained in:
huntharo 2026-03-14 08:34:49 -04:00 committed by Vincent Koc
parent 54fead1508
commit a81dbf109d
2 changed files with 89 additions and 5 deletions

View File

@ -386,4 +386,61 @@ describe("plugin conversation binding approvals", () => {
"This conversation is already bound by core routing and cannot be claimed by a plugin.",
});
});
it("migrates a legacy plugin binding record through the new approval flow", async () => {
sessionBindingState.setRecord({
bindingId: "binding-legacy",
targetSessionKey: "plugin-binding:codex:legacy123",
targetKind: "session",
conversation: {
channel: "telegram",
accountId: "default",
conversationId: "-10099:topic:77",
},
status: "active",
boundAt: Date.now(),
metadata: {
label: "legacy plugin bind",
},
});
const request = await requestPluginConversationBinding({
pluginId: "codex",
pluginName: "Codex App Server",
pluginRoot: "/plugins/codex-a",
requestedBySenderId: "user-1",
conversation: {
channel: "telegram",
accountId: "default",
conversationId: "-10099:topic:77",
parentConversationId: "-10099",
threadId: "77",
},
binding: { summary: "Bind this conversation to Codex thread abc." },
});
expect(["pending", "bound"]).toContain(request.status);
const binding =
request.status === "pending"
? await resolvePluginConversationBindingApproval({
approvalId: request.approvalId,
decision: "allow-once",
senderId: "user-1",
}).then((approved) => {
expect(approved.status).toBe("approved");
if (approved.status !== "approved") {
throw new Error("expected approved bind result");
}
return approved.binding;
})
: request.binding;
expect(binding).toEqual(
expect.objectContaining({
pluginId: "codex",
pluginRoot: "/plugins/codex-a",
conversationId: "-10099:topic:77",
}),
);
});
});

View File

@ -196,6 +196,23 @@ function buildPluginBindingSessionKey(params: {
return `${PLUGIN_BINDING_SESSION_PREFIX}:${params.pluginId}:${hash}`;
}
function isLegacyPluginBindingRecord(params: {
record:
| {
targetSessionKey: string;
metadata?: Record<string, unknown>;
}
| null
| undefined;
pluginId: string;
}): boolean {
if (!params.record || isPluginOwnedBindingMetadata(params.record.metadata)) {
return false;
}
const targetSessionKey = params.record.targetSessionKey.trim();
return targetSessionKey.startsWith(`${PLUGIN_BINDING_SESSION_PREFIX}:${params.pluginId}:`);
}
function buildDiscordButtonRow(
approvalId: string,
labels?: { once?: string; always?: string; deny?: string },
@ -525,12 +542,22 @@ export async function requestPluginConversationBinding(params: {
const ref = toConversationRef(conversation);
const existing = getSessionBindingService().resolveByConversation(ref);
const existingPluginBinding = toPluginConversationBinding(existing);
const existingLegacyPluginBinding = isLegacyPluginBindingRecord({
record: existing,
pluginId: params.pluginId,
});
if (existing && !existingPluginBinding) {
return {
status: "error",
message:
"This conversation is already bound by core routing and cannot be claimed by a plugin.",
};
if (existingLegacyPluginBinding) {
log.info(
`plugin binding migrating legacy record plugin=${params.pluginId} root=${params.pluginRoot} channel=${ref.channel} account=${ref.accountId} conversation=${ref.conversationId}`,
);
} else {
return {
status: "error",
message:
"This conversation is already bound by core routing and cannot be claimed by a plugin.",
};
}
}
if (existingPluginBinding && existingPluginBinding.pluginRoot !== params.pluginRoot) {
return {