fix: resolve session ids in session tools

This commit is contained in:
Peter Steinberger
2026-01-24 11:09:06 +00:00
parent 1bbbb10abf
commit ab000398be
13 changed files with 604 additions and 130 deletions

View File

@@ -1,11 +1,12 @@
import type { ClawdbotConfig } from "../../config/config.js";
import { callGateway } from "../../gateway/call.js";
import { sanitizeUserFacingText } from "../pi-embedded-helpers.js";
import {
stripDowngradedToolCallText,
stripMinimaxToolCallXml,
stripThinkingTagsFromText,
} from "../pi-embedded-utils.js";
import { normalizeMainKey } from "../../routing/session-key.js";
import { isAcpSessionKey, normalizeMainKey } from "../../routing/session-key.js";
export type SessionKind = "main" | "group" | "cron" | "hook" | "node" | "other";
@@ -62,6 +63,195 @@ export function resolveInternalSessionKey(params: { key: string; alias: string;
return params.key;
}
export type AgentToAgentPolicy = {
enabled: boolean;
matchesAllow: (agentId: string) => boolean;
isAllowed: (requesterAgentId: string, targetAgentId: string) => boolean;
};
export function createAgentToAgentPolicy(cfg: ClawdbotConfig): AgentToAgentPolicy {
const routingA2A = cfg.tools?.agentToAgent;
const enabled = routingA2A?.enabled === true;
const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : [];
const matchesAllow = (agentId: string) => {
if (allowPatterns.length === 0) return true;
return allowPatterns.some((pattern) => {
const raw = String(pattern ?? "").trim();
if (!raw) return false;
if (raw === "*") return true;
if (!raw.includes("*")) return raw === agentId;
const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const re = new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`, "i");
return re.test(agentId);
});
};
const isAllowed = (requesterAgentId: string, targetAgentId: string) => {
if (requesterAgentId === targetAgentId) return true;
if (!enabled) return false;
return matchesAllow(requesterAgentId) && matchesAllow(targetAgentId);
};
return { enabled, matchesAllow, isAllowed };
}
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export function looksLikeSessionId(value: string): boolean {
return SESSION_ID_RE.test(value.trim());
}
export function looksLikeSessionKey(value: string): boolean {
const raw = value.trim();
if (!raw) return false;
// These are canonical key shapes that should never be treated as sessionIds.
if (raw === "main" || raw === "global" || raw === "unknown") return true;
if (isAcpSessionKey(raw)) return true;
if (raw.startsWith("agent:")) return true;
if (raw.startsWith("cron:") || raw.startsWith("hook:")) return true;
if (raw.startsWith("node-") || raw.startsWith("node:")) return true;
if (raw.includes(":group:") || raw.includes(":channel:")) return true;
return false;
}
export function shouldResolveSessionIdInput(value: string): boolean {
// Treat anything that doesn't look like a well-formed key as a sessionId candidate.
return looksLikeSessionId(value) || !looksLikeSessionKey(value);
}
export type SessionReferenceResolution =
| {
ok: true;
key: string;
displayKey: string;
resolvedViaSessionId: boolean;
}
| { ok: false; status: "error" | "forbidden"; error: string };
async function resolveSessionKeyFromSessionId(params: {
sessionId: string;
alias: string;
mainKey: string;
requesterInternalKey?: string;
restrictToSpawned: boolean;
}): Promise<SessionReferenceResolution> {
try {
// Resolve via gateway so we respect store routing and visibility rules.
const result = (await callGateway({
method: "sessions.resolve",
params: {
sessionId: params.sessionId,
spawnedBy: params.restrictToSpawned ? params.requesterInternalKey : undefined,
includeGlobal: !params.restrictToSpawned,
includeUnknown: !params.restrictToSpawned,
},
})) as { key?: unknown };
const key = typeof result?.key === "string" ? result.key.trim() : "";
if (!key) {
throw new Error(
`Session not found: ${params.sessionId} (use the full sessionKey from sessions_list)`,
);
}
return {
ok: true,
key,
displayKey: resolveDisplaySessionKey({
key,
alias: params.alias,
mainKey: params.mainKey,
}),
resolvedViaSessionId: true,
};
} catch (err) {
if (params.restrictToSpawned) {
return {
ok: false,
status: "forbidden",
error: `Session not visible from this sandboxed agent session: ${params.sessionId}`,
};
}
const message = err instanceof Error ? err.message : String(err);
return {
ok: false,
status: "error",
error:
message ||
`Session not found: ${params.sessionId} (use the full sessionKey from sessions_list)`,
};
}
}
async function resolveSessionKeyFromKey(params: {
key: string;
alias: string;
mainKey: string;
requesterInternalKey?: string;
restrictToSpawned: boolean;
}): Promise<SessionReferenceResolution | null> {
try {
// Try key-based resolution first so non-standard keys keep working.
const result = (await callGateway({
method: "sessions.resolve",
params: {
key: params.key,
spawnedBy: params.restrictToSpawned ? params.requesterInternalKey : undefined,
},
})) as { key?: unknown };
const key = typeof result?.key === "string" ? result.key.trim() : "";
if (!key) return null;
return {
ok: true,
key,
displayKey: resolveDisplaySessionKey({
key,
alias: params.alias,
mainKey: params.mainKey,
}),
resolvedViaSessionId: false,
};
} catch {
return null;
}
}
export async function resolveSessionReference(params: {
sessionKey: string;
alias: string;
mainKey: string;
requesterInternalKey?: string;
restrictToSpawned: boolean;
}): Promise<SessionReferenceResolution> {
const raw = params.sessionKey.trim();
if (shouldResolveSessionIdInput(raw)) {
// Prefer key resolution to avoid misclassifying custom keys as sessionIds.
const resolvedByKey = await resolveSessionKeyFromKey({
key: raw,
alias: params.alias,
mainKey: params.mainKey,
requesterInternalKey: params.requesterInternalKey,
restrictToSpawned: params.restrictToSpawned,
});
if (resolvedByKey) return resolvedByKey;
return await resolveSessionKeyFromSessionId({
sessionId: raw,
alias: params.alias,
mainKey: params.mainKey,
requesterInternalKey: params.requesterInternalKey,
restrictToSpawned: params.restrictToSpawned,
});
}
const resolvedKey = resolveInternalSessionKey({
key: raw,
alias: params.alias,
mainKey: params.mainKey,
});
const displayKey = resolveDisplaySessionKey({
key: resolvedKey,
alias: params.alias,
mainKey: params.mainKey,
});
return { ok: true, key: resolvedKey, displayKey, resolvedViaSessionId: false };
}
export function classifySessionKind(params: {
key: string;
gatewayKind?: string | null;