mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-29 02:41:07 +00:00
ACP: harden startup and move configured routing behind plugin seams (#48197)
* ACPX: keep plugin-local runtime installs out of dist * Gateway: harden ACP startup and service PATH * ACP: reinitialize error-state configured bindings * ACP: classify pre-turn runtime failures as session init failures * Plugins: move configured ACP routing behind channel seams * Telegram tests: align startup probe assertions after rebase * Discord: harden ACP configured binding recovery * ACP: recover Discord bindings after stale runtime exits * ACPX: replace dead sessions during ensure * Discord: harden ACP binding recovery * Discord: fix review follow-ups * ACP bindings: load channel snapshots across workspaces * ACP bindings: cache snapshot channel plugin resolution * Experiments: add ACP pluginification holy grail plan * Experiments: rename ACP pluginification plan doc * Experiments: drop old ACP pluginification doc path * ACP: move configured bindings behind plugin services * Experiments: update bindings capability architecture plan * Bindings: isolate configured binding routing and targets * Discord tests: fix runtime env helper path * Tests: fix channel binding CI regressions * Tests: normalize ACP workspace assertion on Windows * Bindings: isolate configured binding registry * Bindings: finish configured binding cleanup * Bindings: finish generic cleanup * Bindings: align runtime approval callbacks * ACP: delete residual bindings barrel * Bindings: restore legacy compatibility * Revert "Bindings: restore legacy compatibility" This reverts commit ac2ed68fa2426ecc874d68278c71c71ad363fcfe. * Tests: drop ACP route legacy helper names * Discord/ACP: fix binding regressions --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,8 @@ import type {
|
||||
SessionBindingAdapter,
|
||||
SessionBindingRecord,
|
||||
} from "../infra/outbound/session-binding-service.js";
|
||||
import { createEmptyPluginRegistry } from "./registry.js";
|
||||
import { setActivePluginRegistry } from "./runtime.js";
|
||||
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-binding-"));
|
||||
const approvalsPath = path.join(tempRoot, "plugin-binding-approvals.json");
|
||||
@@ -145,6 +147,7 @@ describe("plugin conversation binding approvals", () => {
|
||||
beforeEach(() => {
|
||||
sessionBindingState.reset();
|
||||
__testing.reset();
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
fs.rmSync(approvalsPath, { force: true });
|
||||
unregisterSessionBindingAdapter({ channel: "discord", accountId: "default" });
|
||||
unregisterSessionBindingAdapter({ channel: "discord", accountId: "work" });
|
||||
@@ -366,6 +369,118 @@ describe("plugin conversation binding approvals", () => {
|
||||
expect(currentBinding?.detachHint).toBe("/codex_detach");
|
||||
});
|
||||
|
||||
it("notifies the owning plugin when a bind approval is approved", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const onResolved = vi.fn(async () => undefined);
|
||||
registry.conversationBindingResolvedHandlers.push({
|
||||
pluginId: "codex",
|
||||
pluginRoot: "/plugins/callback-test",
|
||||
handler: onResolved,
|
||||
source: "/plugins/callback-test/index.ts",
|
||||
rootDir: "/plugins/callback-test",
|
||||
});
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
const request = await requestPluginConversationBinding({
|
||||
pluginId: "codex",
|
||||
pluginName: "Codex App Server",
|
||||
pluginRoot: "/plugins/callback-test",
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: "isolated",
|
||||
conversationId: "channel:callback-test",
|
||||
},
|
||||
binding: { summary: "Bind this conversation to Codex thread abc." },
|
||||
});
|
||||
|
||||
expect(request.status).toBe("pending");
|
||||
if (request.status !== "pending") {
|
||||
throw new Error("expected pending bind request");
|
||||
}
|
||||
|
||||
const approved = await resolvePluginConversationBindingApproval({
|
||||
approvalId: request.approvalId,
|
||||
decision: "allow-once",
|
||||
senderId: "user-1",
|
||||
});
|
||||
|
||||
expect(approved.status).toBe("approved");
|
||||
expect(onResolved).toHaveBeenCalledWith({
|
||||
status: "approved",
|
||||
binding: expect.objectContaining({
|
||||
pluginId: "codex",
|
||||
pluginRoot: "/plugins/callback-test",
|
||||
conversationId: "channel:callback-test",
|
||||
}),
|
||||
decision: "allow-once",
|
||||
request: {
|
||||
summary: "Bind this conversation to Codex thread abc.",
|
||||
detachHint: undefined,
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: "isolated",
|
||||
conversationId: "channel:callback-test",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies the owning plugin when a bind approval is denied", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const onResolved = vi.fn(async () => undefined);
|
||||
registry.conversationBindingResolvedHandlers.push({
|
||||
pluginId: "codex",
|
||||
pluginRoot: "/plugins/callback-deny",
|
||||
handler: onResolved,
|
||||
source: "/plugins/callback-deny/index.ts",
|
||||
rootDir: "/plugins/callback-deny",
|
||||
});
|
||||
setActivePluginRegistry(registry);
|
||||
|
||||
const request = await requestPluginConversationBinding({
|
||||
pluginId: "codex",
|
||||
pluginName: "Codex App Server",
|
||||
pluginRoot: "/plugins/callback-deny",
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "8460800771",
|
||||
},
|
||||
binding: { summary: "Bind this conversation to Codex thread deny." },
|
||||
});
|
||||
|
||||
expect(request.status).toBe("pending");
|
||||
if (request.status !== "pending") {
|
||||
throw new Error("expected pending bind request");
|
||||
}
|
||||
|
||||
const denied = await resolvePluginConversationBindingApproval({
|
||||
approvalId: request.approvalId,
|
||||
decision: "deny",
|
||||
senderId: "user-1",
|
||||
});
|
||||
|
||||
expect(denied.status).toBe("denied");
|
||||
expect(onResolved).toHaveBeenCalledWith({
|
||||
status: "denied",
|
||||
binding: undefined,
|
||||
decision: "deny",
|
||||
request: {
|
||||
summary: "Bind this conversation to Codex thread deny.",
|
||||
detachHint: undefined,
|
||||
requestedBySenderId: "user-1",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
conversationId: "8460800771",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns and detaches only bindings owned by the requesting plugin root", async () => {
|
||||
const request = await requestPluginConversationBinding({
|
||||
pluginId: "codex",
|
||||
|
||||
Reference in New Issue
Block a user