fix: derive canonicalize agentId from target key, not current session
When a non-default agent (e.g. agent:shopping-claw:main) calls restart or config/update with sessionKey="main", the gateway treats "main" as resolveMainSessionKey(cfg) = the default agent's session. Previously, isTargetingOtherSession canonicalized the target key using the CURRENT session's agentId, so "main" mapped to the current agent's main session rather than the default agent's — falsely treating a cross-agent request as same-session and forwarding the wrong chat's deliveryContext. Fix: canonicalize each key using its own agentId (resolveAgentIdFromSessionKey on the key itself). For bare "main", this returns DEFAULT_AGENT_ID so "main" → "agent:main:main" regardless of which agent is calling. Applied to both the restart path and the RPC path (resolveGatewayWriteMeta). Add two regression tests covering the cross-agent alias scenario.
This commit is contained in:
parent
3849101443
commit
045bf781a4
@ -167,4 +167,53 @@ describe("createGatewayTool – live delivery context guard", () => {
|
||||
expect(sentinelPayload?.deliveryContext?.channel).toBe("discord");
|
||||
expect(sentinelPayload?.deliveryContext?.to).toBe("123456789");
|
||||
});
|
||||
|
||||
it("does not forward live RPC delivery context when a non-default agent passes sessionKey='main'", async () => {
|
||||
// "main" resolves to "agent:main:main" (default agent), which differs from the
|
||||
// current session "agent:shopping-claw:main". Live context must NOT be forwarded.
|
||||
mocks.callGatewayTool.mockClear();
|
||||
const tool = createGatewayTool({
|
||||
agentSessionKey: "agent:shopping-claw:main",
|
||||
agentChannel: "discord",
|
||||
agentTo: "123456789",
|
||||
});
|
||||
|
||||
await execTool(tool, {
|
||||
action: "config.patch",
|
||||
raw: '{"key":"value"}',
|
||||
baseHash: "abc123",
|
||||
sessionKey: "main", // targets default agent — different from current shopping-claw session
|
||||
note: "cross-agent patch",
|
||||
});
|
||||
|
||||
const forwardedParams = getCallArg<Record<string, unknown>>(mocks.callGatewayTool, 0, 2);
|
||||
expect(forwardedParams?.deliveryContext).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not forward live restart context when a non-default agent passes sessionKey='main'", async () => {
|
||||
// "main" resolves to "agent:main:main" (default agent), which differs from the
|
||||
// current session "agent:shopping-claw:main". Sentinel must not carry this session's thread.
|
||||
mocks.writeRestartSentinel.mockClear();
|
||||
mocks.extractDeliveryInfo.mockReturnValueOnce({
|
||||
deliveryContext: { channel: "telegram", to: "+19995550001", accountId: undefined },
|
||||
threadId: undefined,
|
||||
});
|
||||
|
||||
const tool = createGatewayTool({
|
||||
agentSessionKey: "agent:shopping-claw:main",
|
||||
agentChannel: "discord",
|
||||
agentTo: "123456789",
|
||||
});
|
||||
|
||||
await execTool(tool, { action: "restart", sessionKey: "main" });
|
||||
|
||||
const sentinelPayload = getCallArg<{ deliveryContext?: { channel?: string; to?: string } }>(
|
||||
mocks.writeRestartSentinel,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
// Should fall back to extractDeliveryInfo() for the targeted session, not current session's live context
|
||||
expect(sentinelPayload?.deliveryContext?.channel).toBe("telegram");
|
||||
expect(sentinelPayload?.deliveryContext?.to).toBe("+19995550001");
|
||||
});
|
||||
});
|
||||
|
||||
@ -116,9 +116,29 @@ export function createGatewayTool(opts?: {
|
||||
// session, the live context belongs to the wrong session and would
|
||||
// misroute the post-restart reply. Fall back to extractDeliveryInfo()
|
||||
// so the server uses the correct routing for the target session.
|
||||
// Canonicalize both keys before comparing so that aliases like "main"
|
||||
// and "agent:main:main" are treated as the same session. Without this,
|
||||
// an operator passing sessionKey="main" would be incorrectly treated as
|
||||
// targeting a different session, suppressing live deliveryContext and
|
||||
// falling back to the stale session store. See #18612.
|
||||
const ownKey = opts?.agentSessionKey?.trim() || undefined;
|
||||
const agentId = resolveAgentIdFromSessionKey(ownKey);
|
||||
// Canonicalize each key using its OWN agentId — not the current session's.
|
||||
// If a non-default agent passes sessionKey="main", resolveAgentIdFromSessionKey
|
||||
// returns DEFAULT_AGENT_ID ("main") so "main" → "agent:main:main". Using the
|
||||
// current session's agentId instead would map "main" to the current agent's main
|
||||
// session, falsely treating a cross-agent request as same-session. See #18612.
|
||||
const canonicalizeOwn = (k: string) =>
|
||||
canonicalizeMainSessionAlias({ cfg: opts?.config, agentId, sessionKey: k });
|
||||
const canonicalizeTarget = (k: string) =>
|
||||
canonicalizeMainSessionAlias({
|
||||
cfg: opts?.config,
|
||||
agentId: resolveAgentIdFromSessionKey(k),
|
||||
sessionKey: k,
|
||||
});
|
||||
const isTargetingOtherSession =
|
||||
explicitSessionKey != null &&
|
||||
explicitSessionKey !== (opts?.agentSessionKey?.trim() || undefined);
|
||||
canonicalizeTarget(explicitSessionKey) !== (ownKey ? canonicalizeOwn(ownKey) : undefined);
|
||||
// Only forward live context when both channel and to are present.
|
||||
// Forwarding a partial context (channel without to) causes the server
|
||||
// to write a sentinel without `to`, and scheduleRestartSentinelWake
|
||||
@ -228,12 +248,26 @@ export function createGatewayTool(opts?: {
|
||||
? Math.floor(params.restartDelayMs)
|
||||
: undefined;
|
||||
// Only forward live context when the target session is this agent's
|
||||
// own session. When an explicit sessionKey points to a different
|
||||
// session, omit deliveryContext so the server falls back to
|
||||
// extractDeliveryInfo(sessionKey) which uses that session's routing.
|
||||
// own session. Canonicalize both keys before comparing so that aliases
|
||||
// like "main" and "agent:main:main" are treated as the same session.
|
||||
// When an explicit sessionKey points to a different session, omit
|
||||
// deliveryContext so the server falls back to extractDeliveryInfo(sessionKey).
|
||||
const rpcOwnKey = opts?.agentSessionKey?.trim() || undefined;
|
||||
const rpcAgentId = resolveAgentIdFromSessionKey(rpcOwnKey);
|
||||
// Same cross-agent alias fix as the restart path: derive agentId from each key
|
||||
// independently so that "main" resolves to the default agent, not the current one.
|
||||
const rpcCanonicalizeOwn = (k: string) =>
|
||||
canonicalizeMainSessionAlias({ cfg: opts?.config, agentId: rpcAgentId, sessionKey: k });
|
||||
const rpcCanonicalizeTarget = (k: string) =>
|
||||
canonicalizeMainSessionAlias({
|
||||
cfg: opts?.config,
|
||||
agentId: resolveAgentIdFromSessionKey(k),
|
||||
sessionKey: k,
|
||||
});
|
||||
const isTargetingOtherSession =
|
||||
explicitSessionKey != null &&
|
||||
explicitSessionKey !== (opts?.agentSessionKey?.trim() || undefined);
|
||||
rpcCanonicalizeTarget(explicitSessionKey) !==
|
||||
(rpcOwnKey ? rpcCanonicalizeOwn(rpcOwnKey) : undefined);
|
||||
const deliveryContext = isTargetingOtherSession ? undefined : liveDeliveryContextForRpc;
|
||||
return { sessionKey, note, restartDelayMs, deliveryContext };
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user