mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
fix(gateway): preserve agentId through gateway send path
Landed from #23249 by @Sid-Qin. Includes extra regression tests for agentId precedence + blank fallback. Co-authored-by: Sid <201593046+Sid-Qin@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/macOS beta onboarding: remove Anthropic OAuth sign-in and the legacy `oauth.json` onboarding path that exposed the PKCE verifier via OAuth `state`; this impacted the macOS beta onboarding path only. Anthropic subscription auth is now setup-token-only and will ship in the next npm release (`2026.2.25`). Thanks @zdi-disclosures for reporting.
|
||||
- Security/Nextcloud Talk: drop replayed signed webhook events with persistent per-account replay dedupe across restarts, and reject unexpected webhook backend origins when account base URL is configured. Thanks @aristorechina for reporting.
|
||||
- Security/Gateway: harden `agents.files` path handling to block out-of-workspace symlink targets for `agents.files.get`/`agents.files.set`, keep in-workspace symlink targets supported, and add gateway regression coverage for both blocked escapes and allowed in-workspace symlinks. Thanks @tdjackey for reporting.
|
||||
- Gateway/Message media roots: thread `agentId` through gateway `send` RPC and prefer explicit `agentId` over session/default resolution so non-default agent workspace media sends no longer fail with `LocalMediaAccessError`; added regression coverage for agent precedence and blank-agent fallback. (#23249) Thanks @Sid-Qin.
|
||||
- Security/Nextcloud Talk: reject unsigned webhook traffic before full body reads, reducing unauthenticated request-body exposure, with auth-order regression coverage. (#26118) Thanks @bmendonca3.
|
||||
- Security/Nextcloud Talk: stop treating DM pairing-store entries as group allowlist senders, so group authorization remains bounded to configured group allowlists. (#26116) Thanks @bmendonca3.
|
||||
- Security/IRC: keep pairing-store approvals DM-only and out of IRC group allowlist authorization, with policy regression tests for allowlist resolution. (#26112) Thanks @bmendonca3.
|
||||
|
||||
@@ -408,6 +408,7 @@ public struct SendParams: Codable, Sendable {
|
||||
public let gifplayback: Bool?
|
||||
public let channel: String?
|
||||
public let accountid: String?
|
||||
public let agentid: String?
|
||||
public let threadid: String?
|
||||
public let sessionkey: String?
|
||||
public let idempotencykey: String
|
||||
@@ -420,6 +421,7 @@ public struct SendParams: Codable, Sendable {
|
||||
gifplayback: Bool?,
|
||||
channel: String?,
|
||||
accountid: String?,
|
||||
agentid: String?,
|
||||
threadid: String?,
|
||||
sessionkey: String?,
|
||||
idempotencykey: String)
|
||||
@@ -431,6 +433,7 @@ public struct SendParams: Codable, Sendable {
|
||||
self.gifplayback = gifplayback
|
||||
self.channel = channel
|
||||
self.accountid = accountid
|
||||
self.agentid = agentid
|
||||
self.threadid = threadid
|
||||
self.sessionkey = sessionkey
|
||||
self.idempotencykey = idempotencykey
|
||||
@@ -444,6 +447,7 @@ public struct SendParams: Codable, Sendable {
|
||||
case gifplayback = "gifPlayback"
|
||||
case channel
|
||||
case accountid = "accountId"
|
||||
case agentid = "agentId"
|
||||
case threadid = "threadId"
|
||||
case sessionkey = "sessionKey"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
|
||||
@@ -408,6 +408,7 @@ public struct SendParams: Codable, Sendable {
|
||||
public let gifplayback: Bool?
|
||||
public let channel: String?
|
||||
public let accountid: String?
|
||||
public let agentid: String?
|
||||
public let threadid: String?
|
||||
public let sessionkey: String?
|
||||
public let idempotencykey: String
|
||||
@@ -420,6 +421,7 @@ public struct SendParams: Codable, Sendable {
|
||||
gifplayback: Bool?,
|
||||
channel: String?,
|
||||
accountid: String?,
|
||||
agentid: String?,
|
||||
threadid: String?,
|
||||
sessionkey: String?,
|
||||
idempotencykey: String)
|
||||
@@ -431,6 +433,7 @@ public struct SendParams: Codable, Sendable {
|
||||
self.gifplayback = gifplayback
|
||||
self.channel = channel
|
||||
self.accountid = accountid
|
||||
self.agentid = agentid
|
||||
self.threadid = threadid
|
||||
self.sessionkey = sessionkey
|
||||
self.idempotencykey = idempotencykey
|
||||
@@ -444,6 +447,7 @@ public struct SendParams: Codable, Sendable {
|
||||
case gifplayback = "gifPlayback"
|
||||
case channel
|
||||
case accountid = "accountId"
|
||||
case agentid = "agentId"
|
||||
case threadid = "threadId"
|
||||
case sessionkey = "sessionKey"
|
||||
case idempotencykey = "idempotencyKey"
|
||||
|
||||
@@ -22,6 +22,8 @@ export const SendParamsSchema = Type.Object(
|
||||
gifPlayback: Type.Optional(Type.Boolean()),
|
||||
channel: Type.Optional(Type.String()),
|
||||
accountId: Type.Optional(Type.String()),
|
||||
/** Optional agent id for per-agent media root resolution on gateway sends. */
|
||||
agentId: Type.Optional(Type.String()),
|
||||
/** Thread id (channel-specific meaning, e.g. Telegram forum topic id). */
|
||||
threadId: Type.Optional(Type.String()),
|
||||
/** Optional session key for mirroring delivered output back into the transcript. */
|
||||
|
||||
@@ -342,6 +342,96 @@ describe("gateway send mirroring", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses explicit agentId for delivery when sessionKey is not provided", async () => {
|
||||
mockDeliverySuccess("m-agent");
|
||||
|
||||
await runSend({
|
||||
to: "channel:C1",
|
||||
message: "hello",
|
||||
channel: "slack",
|
||||
agentId: "work",
|
||||
idempotencyKey: "idem-agent-explicit",
|
||||
});
|
||||
|
||||
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "work",
|
||||
mirror: expect.objectContaining({
|
||||
sessionKey: "agent:work:slack:channel:resolved",
|
||||
agentId: "work",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses sessionKey agentId when explicit agentId is omitted", async () => {
|
||||
mockDeliverySuccess("m-session-agent");
|
||||
|
||||
await runSend({
|
||||
to: "channel:C1",
|
||||
message: "hello",
|
||||
channel: "slack",
|
||||
sessionKey: "agent:work:slack:channel:c1",
|
||||
idempotencyKey: "idem-session-agent",
|
||||
});
|
||||
|
||||
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "work",
|
||||
mirror: expect.objectContaining({
|
||||
sessionKey: "agent:work:slack:channel:c1",
|
||||
agentId: "work",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers explicit agentId over sessionKey agent for delivery and mirror", async () => {
|
||||
mockDeliverySuccess("m-agent-precedence");
|
||||
|
||||
await runSend({
|
||||
to: "channel:C1",
|
||||
message: "hello",
|
||||
channel: "slack",
|
||||
agentId: "work",
|
||||
sessionKey: "agent:main:slack:channel:c1",
|
||||
idempotencyKey: "idem-agent-precedence",
|
||||
});
|
||||
|
||||
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "work",
|
||||
mirror: expect.objectContaining({
|
||||
sessionKey: "agent:main:slack:channel:c1",
|
||||
agentId: "work",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores blank explicit agentId and falls back to sessionKey agent", async () => {
|
||||
mockDeliverySuccess("m-agent-blank");
|
||||
|
||||
await runSend({
|
||||
to: "channel:C1",
|
||||
message: "hello",
|
||||
channel: "slack",
|
||||
agentId: " ",
|
||||
sessionKey: "agent:work:slack:channel:c1",
|
||||
idempotencyKey: "idem-agent-blank",
|
||||
});
|
||||
|
||||
expect(mocks.deliverOutboundPayloads).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "work",
|
||||
mirror: expect.objectContaining({
|
||||
sessionKey: "agent:work:slack:channel:c1",
|
||||
agentId: "work",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards threadId to outbound delivery when provided", async () => {
|
||||
mockDeliverySuccess("m-thread");
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
gifPlayback?: boolean;
|
||||
channel?: string;
|
||||
accountId?: string;
|
||||
agentId?: string;
|
||||
threadId?: string;
|
||||
sessionKey?: string;
|
||||
idempotencyKey: string;
|
||||
@@ -206,13 +207,21 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
typeof request.sessionKey === "string" && request.sessionKey.trim()
|
||||
? request.sessionKey.trim().toLowerCase()
|
||||
: undefined;
|
||||
const derivedAgentId = resolveSessionAgentId({ config: cfg });
|
||||
const explicitAgentId =
|
||||
typeof request.agentId === "string" && request.agentId.trim()
|
||||
? request.agentId.trim()
|
||||
: undefined;
|
||||
const sessionAgentId = providedSessionKey
|
||||
? resolveSessionAgentId({ sessionKey: providedSessionKey, config: cfg })
|
||||
: undefined;
|
||||
const defaultAgentId = resolveSessionAgentId({ config: cfg });
|
||||
const effectiveAgentId = explicitAgentId ?? sessionAgentId ?? defaultAgentId;
|
||||
// If callers omit sessionKey, derive a target session key from the outbound route.
|
||||
const derivedRoute = !providedSessionKey
|
||||
? await resolveOutboundSessionRoute({
|
||||
cfg,
|
||||
channel,
|
||||
agentId: derivedAgentId,
|
||||
agentId: effectiveAgentId,
|
||||
accountId,
|
||||
target: resolved.to,
|
||||
threadId,
|
||||
@@ -221,7 +230,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
if (derivedRoute) {
|
||||
await ensureOutboundSessionEntry({
|
||||
cfg,
|
||||
agentId: derivedAgentId,
|
||||
agentId: effectiveAgentId,
|
||||
channel,
|
||||
accountId,
|
||||
route: derivedRoute,
|
||||
@@ -233,23 +242,21 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
to: resolved.to,
|
||||
accountId,
|
||||
payloads: [{ text: message, mediaUrl, mediaUrls }],
|
||||
agentId: providedSessionKey
|
||||
? resolveSessionAgentId({ sessionKey: providedSessionKey, config: cfg })
|
||||
: derivedAgentId,
|
||||
agentId: effectiveAgentId,
|
||||
gifPlayback: request.gifPlayback,
|
||||
threadId: threadId ?? null,
|
||||
deps: outboundDeps,
|
||||
mirror: providedSessionKey
|
||||
? {
|
||||
sessionKey: providedSessionKey,
|
||||
agentId: resolveSessionAgentId({ sessionKey: providedSessionKey, config: cfg }),
|
||||
agentId: effectiveAgentId,
|
||||
text: mirrorText || message,
|
||||
mediaUrls: mirrorMediaUrls.length > 0 ? mirrorMediaUrls : undefined,
|
||||
}
|
||||
: derivedRoute
|
||||
? {
|
||||
sessionKey: derivedRoute.sessionKey,
|
||||
agentId: derivedAgentId,
|
||||
agentId: effectiveAgentId,
|
||||
text: mirrorText || message,
|
||||
mediaUrls: mirrorMediaUrls.length > 0 ? mirrorMediaUrls : undefined,
|
||||
}
|
||||
|
||||
@@ -194,6 +194,35 @@ describe("gateway url override hardening", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards explicit agentId in gateway send params", async () => {
|
||||
setRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "mattermost",
|
||||
source: "test",
|
||||
plugin: {
|
||||
...createMattermostLikePlugin({ onSendText: () => {} }),
|
||||
outbound: { deliveryMode: "gateway" },
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
callGatewayMock.mockResolvedValueOnce({ messageId: "m-agent" });
|
||||
await sendMessage({
|
||||
cfg: {},
|
||||
to: "channel:town-square",
|
||||
content: "hi",
|
||||
channel: "mattermost",
|
||||
agentId: "work",
|
||||
});
|
||||
|
||||
const call = callGatewayMock.mock.calls[0]?.[0] as {
|
||||
params?: Record<string, unknown>;
|
||||
};
|
||||
expect(call.params?.agentId).toBe("work");
|
||||
});
|
||||
});
|
||||
|
||||
const emptyRegistry = createTestRegistry([]);
|
||||
|
||||
@@ -251,6 +251,7 @@ export async function sendMessage(params: MessageSendParams): Promise<MessageSen
|
||||
mediaUrls: mirrorMediaUrls.length ? mirrorMediaUrls : params.mediaUrls,
|
||||
gifPlayback: params.gifPlayback,
|
||||
accountId: params.accountId,
|
||||
agentId: params.agentId,
|
||||
channel,
|
||||
sessionKey: params.mirror?.sessionKey,
|
||||
idempotencyKey: params.idempotencyKey ?? randomIdempotencyKey(),
|
||||
|
||||
Reference in New Issue
Block a user