mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:30:42 +00:00
fix: isolate external direct-message runtime policy
This commit is contained in:
@@ -43,7 +43,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/startup: tolerate transient bundled-channel catalog/metadata drift while auto-enabling configured plugins, so CLI and gateway startup no longer crash when a channel id is known but its display metadata is unavailable.
|
||||
- CLI/Claude: report CLI-backed reply runs as streaming while Claude/Codex CLI turns are still in flight, so WebChat keeps visible response state until the backend finishes. Fixes #70125.
|
||||
- Codex harness: rotate the shared app-server websocket client when the configured bearer token changes, so auth-token refreshes reconnect with the new `Authorization` header instead of reusing a stale socket. (#70328) Thanks @Lucenx9.
|
||||
- Telegram/sandbox: keep Telegram bot DMs on per-account sender session keys even when `session.dmScope=main`, so sandbox/tool policy can distinguish Telegram-originated direct chats from the agent main session.
|
||||
- Channels/sandbox: derive runtime policy keys for external direct messages that share the main conversation, so sandbox/tool policy no longer treats channel-originated DMs as local main-session runs.
|
||||
- Config/models: merge provider-scoped model allowlist updates and protect model/provider map writes from accidental full replacement, adding `config set --merge` for additive updates and `--replace` for intentional clobbers. Fixes #65920, #68392, and #68653.
|
||||
- Agents/Pi auth: preserve AWS SDK-authenticated Bedrock runs for IMDS and task-role setups, clear stale refresh timers on sentinel fallback, and log unexpected runtime-auth prep failures instead of silently leaving the provider unauthenticated. Thanks @wirjo.
|
||||
- Config/gateway: restore last-known-good config on critical clobber signatures such as missing metadata, missing `gateway.mode`, or sharp size drops, preventing gateway crash loops when a valid backup exists. Fixes #70336.
|
||||
|
||||
@@ -23,15 +23,13 @@ host configuration.
|
||||
|
||||
## Session key shapes (examples)
|
||||
|
||||
Most direct messages collapse to the agent’s **main** session:
|
||||
Direct messages collapse to the agent’s **main** session by default:
|
||||
|
||||
- `agent:<agentId>:<mainKey>` (default: `agent:main:main`)
|
||||
|
||||
Telegram bot direct messages are isolated per bot account and sender even when
|
||||
`session.dmScope` is `main`, so sandbox and tool policy decisions can distinguish
|
||||
channel-originated DMs from the agent main session:
|
||||
|
||||
- `agent:<agentId>:telegram:<accountId>:direct:<senderId>`
|
||||
Even when direct-message conversation history is shared with main, sandbox and
|
||||
tool policy use a derived per-account direct-chat runtime key for external DMs
|
||||
so channel-originated messages are not treated like local main-session runs.
|
||||
|
||||
Groups and channels remain isolated per channel:
|
||||
|
||||
|
||||
@@ -72,13 +72,6 @@ openclaw pairing approve telegram <CODE>
|
||||
Token resolution order is account-aware. In practice, config values win over env fallback, and `TELEGRAM_BOT_TOKEN` only applies to the default account.
|
||||
</Note>
|
||||
|
||||
## Session isolation
|
||||
|
||||
Telegram bot DMs use per-account sender session keys, for example
|
||||
`agent:main:telegram:default:direct:814912386`. This keeps Telegram-originated
|
||||
tool and sandbox policy distinct from the agent main session even when the
|
||||
global `session.dmScope` setting is `main`.
|
||||
|
||||
## Telegram side settings
|
||||
|
||||
<AccordionGroup>
|
||||
|
||||
@@ -76,12 +76,10 @@ describe("buildTelegramMessageContext dm thread sessions", () => {
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx?.ctxPayload?.MessageThreadId).toBe(42);
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe(
|
||||
"agent:main:telegram:default:direct:42:thread:1234:42",
|
||||
);
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:1234:42");
|
||||
});
|
||||
|
||||
it("uses the Telegram direct session key when no thread id", async () => {
|
||||
it("uses the main session key when no thread id", async () => {
|
||||
const ctx = await buildContext({
|
||||
message_id: 2,
|
||||
chat: { id: 1234, type: "private" },
|
||||
@@ -92,7 +90,7 @@ describe("buildTelegramMessageContext dm thread sessions", () => {
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx?.ctxPayload?.MessageThreadId).toBeUndefined();
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:telegram:default:direct:42");
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ describe("buildTelegramMessageContext named-account DM fallback", () => {
|
||||
expect(ctx).toBeNull();
|
||||
});
|
||||
|
||||
it("uses a per-account session key for default-account DMs", async () => {
|
||||
it("uses the main session key for default-account DMs", async () => {
|
||||
setRuntimeConfigSnapshot(baseCfg);
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
@@ -154,7 +154,7 @@ describe("buildTelegramMessageContext named-account DM fallback", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:telegram:default:direct:42");
|
||||
expect(getLastUpdateLastRoute()?.sessionKey).toBe("agent:main:telegram:default:direct:42");
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main");
|
||||
expect(getLastUpdateLastRoute()?.sessionKey).toBe("agent:main:main");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
import { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import type { TelegramDirectConfig, TelegramGroupConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing";
|
||||
import { DEFAULT_ACCOUNT_ID, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
|
||||
import { normalizeAccountId, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveDefaultTelegramAccountId } from "./accounts.js";
|
||||
import { withTelegramApiErrorLogging } from "./api-logging.js";
|
||||
import { firstDefined, normalizeAllowFrom, normalizeDmAllowFromWithStore } from "./bot-access.js";
|
||||
import { resolveTelegramInboundBody } from "./bot-message-context.body.js";
|
||||
@@ -234,7 +235,10 @@ export const buildTelegramMessageContext = async ({
|
||||
});
|
||||
const requiresExplicitAccountBinding = (
|
||||
candidate: ReturnType<typeof resolveTelegramConversationRoute>["route"],
|
||||
): boolean => candidate.accountId !== DEFAULT_ACCOUNT_ID && candidate.matchedBy === "default";
|
||||
): boolean =>
|
||||
normalizeAccountId(candidate.accountId) !==
|
||||
normalizeAccountId(resolveDefaultTelegramAccountId(freshCfg)) &&
|
||||
candidate.matchedBy === "default";
|
||||
const isNamedAccountFallback = requiresExplicitAccountBinding(route);
|
||||
// Named-account groups still require an explicit binding; DMs get a
|
||||
// per-account fallback session key below to preserve isolation.
|
||||
|
||||
@@ -1697,7 +1697,7 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.AccountId).toBe("opie");
|
||||
expect(payload.SessionKey).toBe("agent:opie:telegram:opie:direct:999");
|
||||
expect(payload.SessionKey).toBe("agent:opie:main");
|
||||
});
|
||||
|
||||
it("reloads DM routing bindings between messages without recreating the bot", async () => {
|
||||
@@ -1705,7 +1705,12 @@ describe("createTelegramBot", () => {
|
||||
const configForAgent = (agentId: string) => ({
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultAccount: "work",
|
||||
accounts: {
|
||||
work: {
|
||||
botToken: "tok-work",
|
||||
dmPolicy: "open",
|
||||
},
|
||||
opie: {
|
||||
botToken: "tok-opie",
|
||||
dmPolicy: "open",
|
||||
@@ -1809,7 +1814,12 @@ describe("createTelegramBot", () => {
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultAccount: "work",
|
||||
accounts: {
|
||||
work: {
|
||||
botToken: "tok-work",
|
||||
dmPolicy: "open",
|
||||
},
|
||||
opie: {
|
||||
botToken: "tok-opie",
|
||||
dmPolicy: "open",
|
||||
|
||||
@@ -2190,9 +2190,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.CommandTargetSessionKey).toBe(
|
||||
"agent:main:telegram:default:direct:12345:thread:12345:99",
|
||||
);
|
||||
expect(payload.CommandTargetSessionKey).toBe("agent:main:main:thread:12345:99");
|
||||
});
|
||||
|
||||
it("allows native DM commands for paired users", async () => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { resolveTelegramConversationBaseSessionKey } from "./conversation-route.
|
||||
describe("resolveTelegramConversationBaseSessionKey", () => {
|
||||
const cfg: OpenClawConfig = {};
|
||||
|
||||
it("uses a per-account key for default-account DMs", () => {
|
||||
it("keeps default-account DMs on the route session key", () => {
|
||||
expect(
|
||||
resolveTelegramConversationBaseSessionKey({
|
||||
cfg,
|
||||
@@ -20,7 +20,34 @@ describe("resolveTelegramConversationBaseSessionKey", () => {
|
||||
isGroup: false,
|
||||
senderId: 12345,
|
||||
}),
|
||||
).toBe("agent:main:telegram:default:direct:12345");
|
||||
).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("keeps configured default-account DMs on the route session key", () => {
|
||||
expect(
|
||||
resolveTelegramConversationBaseSessionKey({
|
||||
cfg: {
|
||||
channels: {
|
||||
telegram: {
|
||||
defaultAccount: "work",
|
||||
accounts: {
|
||||
work: {},
|
||||
personal: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
route: {
|
||||
agentId: "main",
|
||||
accountId: "work",
|
||||
matchedBy: "default",
|
||||
sessionKey: "agent:main:main",
|
||||
},
|
||||
chatId: 12345,
|
||||
isGroup: false,
|
||||
senderId: 12345,
|
||||
}),
|
||||
).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("uses the per-account fallback key for named-account DMs without an explicit binding", () => {
|
||||
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
import {
|
||||
buildAgentSessionKey,
|
||||
deriveLastRoutePolicy,
|
||||
normalizeAccountId,
|
||||
resolveAgentRoute,
|
||||
} from "openclaw/plugin-sdk/routing";
|
||||
import { buildAgentMainSessionKey, sanitizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { resolveDefaultTelegramAccountId } from "./accounts.js";
|
||||
import {
|
||||
buildTelegramGroupPeerId,
|
||||
buildTelegramParentPeer,
|
||||
@@ -147,10 +149,13 @@ export function resolveTelegramConversationBaseSessionKey(params: {
|
||||
isGroup: boolean;
|
||||
senderId?: string | number | null;
|
||||
}): string {
|
||||
if (params.isGroup || params.route.matchedBy === "binding.channel") {
|
||||
const routeAccountId = normalizeAccountId(params.route.accountId);
|
||||
const defaultAccountId = normalizeAccountId(resolveDefaultTelegramAccountId(params.cfg));
|
||||
const isNamedAccountFallback =
|
||||
routeAccountId !== defaultAccountId && params.route.matchedBy === "default";
|
||||
if (!isNamedAccountFallback || params.isGroup) {
|
||||
return params.route.sessionKey;
|
||||
}
|
||||
const configuredDmScope = params.cfg.session?.dmScope;
|
||||
return normalizeLowercaseStringOrEmpty(
|
||||
buildAgentSessionKey({
|
||||
agentId: params.route.agentId,
|
||||
@@ -163,10 +168,7 @@ export function resolveTelegramConversationBaseSessionKey(params: {
|
||||
senderId: params.senderId,
|
||||
}),
|
||||
},
|
||||
dmScope:
|
||||
configuredDmScope && configuredDmScope !== "main"
|
||||
? configuredDmScope
|
||||
: "per-account-channel-peer",
|
||||
dmScope: "per-account-channel-peer",
|
||||
identityLinks: params.cfg.session?.identityLinks,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -95,6 +95,7 @@ export const registerProviderStreamForModelMock: Mock<(params?: unknown) => unkn
|
||||
export const applyExtraParamsToAgentMock = vi.fn(() => ({ effectiveExtraParams: {} }));
|
||||
export const resolveAgentTransportOverrideMock: Mock<(params?: unknown) => string | undefined> =
|
||||
vi.fn(() => undefined);
|
||||
export const resolveSandboxContextMock = vi.fn(async () => null);
|
||||
|
||||
export function resetCompactSessionStateMocks(): void {
|
||||
sanitizeSessionHistoryMock.mockReset();
|
||||
@@ -131,6 +132,8 @@ export function resetCompactSessionStateMocks(): void {
|
||||
applyExtraParamsToAgentMock.mockReturnValue({ effectiveExtraParams: {} });
|
||||
resolveAgentTransportOverrideMock.mockReset();
|
||||
resolveAgentTransportOverrideMock.mockReturnValue(undefined);
|
||||
resolveSandboxContextMock.mockReset();
|
||||
resolveSandboxContextMock.mockResolvedValue(null);
|
||||
}
|
||||
|
||||
export function resetCompactHooksHarnessMocks(): void {
|
||||
@@ -296,7 +299,7 @@ export async function loadCompactHooksHarness(): Promise<{
|
||||
}));
|
||||
|
||||
vi.doMock("../sandbox.js", () => ({
|
||||
resolveSandboxContext: vi.fn(async () => null),
|
||||
resolveSandboxContext: resolveSandboxContextMock,
|
||||
}));
|
||||
|
||||
vi.doMock("../session-file-repair.js", () => ({
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
resolveEmbeddedAgentStreamFnMock,
|
||||
resolveMemorySearchConfigMock,
|
||||
resolveModelMock,
|
||||
resolveSandboxContextMock,
|
||||
resolveSessionAgentIdMock,
|
||||
resetCompactHooksHarnessMocks,
|
||||
resetCompactSessionStateMocks,
|
||||
@@ -210,6 +211,22 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses sandboxSessionKey only for compaction sandbox resolution", async () => {
|
||||
await compactEmbeddedPiSessionDirect({
|
||||
sessionId: "session-1",
|
||||
sessionKey: "agent:main:main",
|
||||
sandboxSessionKey: "agent:main:telegram:default:direct:12345",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
|
||||
expect(resolveSandboxContextMock).toHaveBeenCalledWith({
|
||||
config: undefined,
|
||||
sessionKey: "agent:main:telegram:default:direct:12345",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
});
|
||||
|
||||
it("routes compaction through shared stream resolution and extra params", async () => {
|
||||
const resolvedStreamFn = vi.fn();
|
||||
resolveEmbeddedAgentStreamFnMock.mockReturnValue(resolvedStreamFn);
|
||||
|
||||
@@ -412,7 +412,8 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
}
|
||||
|
||||
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
||||
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
|
||||
const sandboxSessionKey =
|
||||
params.sandboxSessionKey?.trim() || params.sessionKey?.trim() || params.sessionId;
|
||||
const sandbox = await resolveSandboxContext({
|
||||
config: params.config,
|
||||
sessionKey: sandboxSessionKey,
|
||||
|
||||
@@ -8,6 +8,8 @@ export type CompactEmbeddedPiSessionParams = {
|
||||
sessionId: string;
|
||||
runId?: string;
|
||||
sessionKey?: string;
|
||||
/** Session key used only for runtime policy/sandbox resolution. Defaults to sessionKey. */
|
||||
sandboxSessionKey?: string;
|
||||
messageChannel?: string;
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
|
||||
@@ -689,6 +689,7 @@ export async function runEmbeddedPiAgent(
|
||||
const attempt = await runEmbeddedAttemptWithBackend({
|
||||
sessionId: params.sessionId,
|
||||
sessionKey: resolvedSessionKey,
|
||||
sandboxSessionKey: params.sandboxSessionKey,
|
||||
trigger: params.trigger,
|
||||
memoryFlushWritePath: params.memoryFlushWritePath,
|
||||
messageChannel: params.messageChannel,
|
||||
|
||||
@@ -438,7 +438,8 @@ export async function runEmbeddedAttempt(
|
||||
|
||||
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
||||
|
||||
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
|
||||
const sandboxSessionKey =
|
||||
params.sandboxSessionKey?.trim() || params.sessionKey?.trim() || params.sessionId;
|
||||
const sandbox = await resolveSandboxContext({
|
||||
config: params.config,
|
||||
sessionKey: sandboxSessionKey,
|
||||
|
||||
@@ -22,6 +22,8 @@ export type EmbeddedRunTrigger = "cron" | "heartbeat" | "manual" | "memory" | "o
|
||||
export type RunEmbeddedPiAgentParams = {
|
||||
sessionId: string;
|
||||
sessionKey?: string;
|
||||
/** Session-like key for sandbox and tool-policy resolution. Defaults to sessionKey. */
|
||||
sandboxSessionKey?: string;
|
||||
agentId?: string;
|
||||
messageChannel?: string;
|
||||
messageProvider?: string;
|
||||
|
||||
@@ -172,6 +172,35 @@ describe("runReplyAgent runtime config", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes the derived runtime-policy key to pre-run maintenance", async () => {
|
||||
const { followupRun, replyParams } = createDirectRuntimeReplyParams({
|
||||
shouldFollowup: false,
|
||||
isActive: false,
|
||||
});
|
||||
const runtimePolicySessionKey = "agent:main:telegram:default:direct:test";
|
||||
followupRun.run.sessionKey = "agent:main:main";
|
||||
followupRun.run.runtimePolicySessionKey = runtimePolicySessionKey;
|
||||
replyParams.sessionKey = "agent:main:main";
|
||||
replyParams.runtimePolicySessionKey = runtimePolicySessionKey;
|
||||
runPreflightCompactionIfNeededMock.mockResolvedValue(undefined);
|
||||
runMemoryFlushIfNeededMock.mockRejectedValue(sentinelError);
|
||||
|
||||
await expect(runReplyAgent(replyParams)).rejects.toBe(sentinelError);
|
||||
|
||||
expect(runPreflightCompactionIfNeededMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:main",
|
||||
runtimePolicySessionKey,
|
||||
}),
|
||||
);
|
||||
expect(runMemoryFlushIfNeededMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:main",
|
||||
runtimePolicySessionKey,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not resolve secrets before the enqueue-followup queue path", async () => {
|
||||
const { followupRun, resolvedQueue, replyParams } = createDirectRuntimeReplyParams({
|
||||
shouldFollowup: true,
|
||||
|
||||
@@ -582,6 +582,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
resetSessionAfterRoleOrderingConflict: (reason: string) => Promise<boolean>;
|
||||
isHeartbeat: boolean;
|
||||
sessionKey?: string;
|
||||
runtimePolicySessionKey?: string;
|
||||
getActiveSessionEntry: () => SessionEntry | undefined;
|
||||
activeSessionStore?: Record<string, SessionEntry>;
|
||||
storePath?: string;
|
||||
@@ -1018,6 +1019,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
groupSpace: normalizeOptionalString(params.sessionCtx.GroupSpace),
|
||||
...senderContext,
|
||||
...runBaseParams,
|
||||
sandboxSessionKey: params.runtimePolicySessionKey,
|
||||
prompt: params.commandBody,
|
||||
extraSystemPrompt: params.followupRun.run.extraSystemPrompt,
|
||||
toolResultFormat: (() => {
|
||||
|
||||
@@ -8,9 +8,14 @@ import {
|
||||
registerMemoryFlushPlanResolver,
|
||||
} from "../../plugins/memory-state.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import { runMemoryFlushIfNeeded, setAgentRunnerMemoryTestDeps } from "./agent-runner-memory.js";
|
||||
import {
|
||||
runMemoryFlushIfNeeded,
|
||||
runPreflightCompactionIfNeeded,
|
||||
setAgentRunnerMemoryTestDeps,
|
||||
} from "./agent-runner-memory.js";
|
||||
import { createTestFollowupRun, writeTestSessionStore } from "./agent-runner.test-fixtures.js";
|
||||
|
||||
const compactEmbeddedPiSessionMock = vi.fn();
|
||||
const runWithModelFallbackMock = vi.fn();
|
||||
const runEmbeddedPiAgentMock = vi.fn();
|
||||
const refreshQueuedFollowupSessionMock = vi.fn();
|
||||
@@ -43,6 +48,11 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
model,
|
||||
attempts: [],
|
||||
}));
|
||||
compactEmbeddedPiSessionMock.mockReset().mockResolvedValue({
|
||||
ok: true,
|
||||
compacted: true,
|
||||
result: { tokensAfter: 42 },
|
||||
});
|
||||
runEmbeddedPiAgentMock.mockReset().mockResolvedValue({ payloads: [], meta: {} });
|
||||
refreshQueuedFollowupSessionMock.mockReset();
|
||||
incrementCompactionCountMock.mockReset().mockImplementation(async (params) => {
|
||||
@@ -67,6 +77,7 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
return nextEntry.compactionCount;
|
||||
});
|
||||
setAgentRunnerMemoryTestDeps({
|
||||
compactEmbeddedPiSession: compactEmbeddedPiSessionMock as never,
|
||||
runWithModelFallback: runWithModelFallbackMock as never,
|
||||
runEmbeddedPiAgent: runEmbeddedPiAgentMock as never,
|
||||
refreshQueuedFollowupSession: refreshQueuedFollowupSessionMock as never,
|
||||
@@ -184,6 +195,98 @@ describe("runMemoryFlushIfNeeded", () => {
|
||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses runtime policy session key when checking memory-flush sandbox writability", async () => {
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
updatedAt: Date.now(),
|
||||
totalTokens: 80_000,
|
||||
compactionCount: 1,
|
||||
};
|
||||
|
||||
const entry = await runMemoryFlushIfNeeded({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main",
|
||||
scope: "agent",
|
||||
workspaceAccess: "ro",
|
||||
},
|
||||
compaction: {
|
||||
memoryFlush: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
followupRun: createTestFollowupRun({
|
||||
sessionKey: "agent:main:main",
|
||||
runtimePolicySessionKey: "agent:main:telegram:default:direct:12345",
|
||||
}),
|
||||
sessionCtx: { Provider: "telegram" } as unknown as TemplateContext,
|
||||
defaultModel: "anthropic/claude-opus-4-6",
|
||||
agentCfgContextTokens: 100_000,
|
||||
resolvedVerboseLevel: "off",
|
||||
sessionEntry,
|
||||
sessionStore: { "agent:main:main": sessionEntry },
|
||||
sessionKey: "agent:main:main",
|
||||
runtimePolicySessionKey: "agent:main:telegram:default:direct:12345",
|
||||
isHeartbeat: false,
|
||||
replyOperation: createReplyOperation(),
|
||||
});
|
||||
|
||||
expect(entry).toBe(sessionEntry);
|
||||
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes runtime policy session key to preflight compaction sandbox resolution", async () => {
|
||||
const sessionFile = path.join(rootDir, "session.jsonl");
|
||||
await fs.writeFile(
|
||||
sessionFile,
|
||||
`${JSON.stringify({ message: { role: "user", content: "x".repeat(5_000) } })}\n`,
|
||||
"utf8",
|
||||
);
|
||||
registerMemoryFlushPlanResolver(() => ({
|
||||
softThresholdTokens: 1,
|
||||
forceFlushTranscriptBytes: 1_000_000_000,
|
||||
reserveTokensFloor: 0,
|
||||
prompt: "Pre-compaction memory flush.\nNO_REPLY",
|
||||
systemPrompt: "Write memory to memory/YYYY-MM-DD.md.",
|
||||
relativePath: "memory/2023-11-14.md",
|
||||
}));
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
sessionFile,
|
||||
updatedAt: Date.now(),
|
||||
totalTokensFresh: false,
|
||||
};
|
||||
|
||||
await runPreflightCompactionIfNeeded({
|
||||
cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } },
|
||||
followupRun: createTestFollowupRun({
|
||||
sessionId: "session",
|
||||
sessionFile,
|
||||
sessionKey: "agent:main:main",
|
||||
runtimePolicySessionKey: "agent:main:telegram:default:direct:12345",
|
||||
}),
|
||||
defaultModel: "anthropic/claude-opus-4-6",
|
||||
agentCfgContextTokens: 100,
|
||||
sessionEntry,
|
||||
sessionStore: { "agent:main:main": sessionEntry },
|
||||
sessionKey: "agent:main:main",
|
||||
runtimePolicySessionKey: "agent:main:telegram:default:direct:12345",
|
||||
storePath: path.join(rootDir, "sessions.json"),
|
||||
isHeartbeat: false,
|
||||
replyOperation: createReplyOperation(),
|
||||
});
|
||||
|
||||
expect(compactEmbeddedPiSessionMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:main",
|
||||
sandboxSessionKey: "agent:main:telegram:default:direct:12345",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses configured prompts and stored bootstrap warning signatures", async () => {
|
||||
const sessionEntry: SessionEntry = {
|
||||
sessionId: "session",
|
||||
|
||||
@@ -361,6 +361,7 @@ export async function runPreflightCompactionIfNeeded(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
runtimePolicySessionKey?: string;
|
||||
storePath?: string;
|
||||
isHeartbeat: boolean;
|
||||
replyOperation: ReplyOperation;
|
||||
@@ -463,6 +464,7 @@ export async function runPreflightCompactionIfNeeded(params: {
|
||||
const result = await memoryDeps.compactEmbeddedPiSession({
|
||||
sessionId: entry.sessionId,
|
||||
sessionKey: params.sessionKey,
|
||||
sandboxSessionKey: params.runtimePolicySessionKey,
|
||||
allowGatewaySubagentBinding: true,
|
||||
messageChannel: params.followupRun.run.messageProvider,
|
||||
groupId: entry.groupId ?? params.followupRun.run.groupId,
|
||||
@@ -523,6 +525,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
runtimePolicySessionKey?: string;
|
||||
storePath?: string;
|
||||
isHeartbeat: boolean;
|
||||
replyOperation: ReplyOperation;
|
||||
@@ -538,7 +541,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
}
|
||||
const runtime = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionKey: params.runtimePolicySessionKey ?? params.sessionKey,
|
||||
});
|
||||
if (!runtime.sandboxed) {
|
||||
return true;
|
||||
@@ -762,6 +765,7 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
...embeddedContext,
|
||||
...senderContext,
|
||||
...runBaseParams,
|
||||
sandboxSessionKey: params.runtimePolicySessionKey,
|
||||
allowGatewaySubagentBinding: true,
|
||||
silentExpected: true,
|
||||
trigger: "memory",
|
||||
|
||||
@@ -227,6 +227,7 @@ export function buildEmbeddedContextFromTemplate(params: {
|
||||
return {
|
||||
sessionId: params.run.sessionId,
|
||||
sessionKey: params.run.sessionKey,
|
||||
sandboxSessionKey: params.run.runtimePolicySessionKey,
|
||||
agentId: params.run.agentId,
|
||||
messageProvider: resolveOriginMessageProvider({
|
||||
originatingChannel: params.sessionCtx.OriginatingChannel,
|
||||
|
||||
@@ -873,6 +873,7 @@ export async function runReplyAgent(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
runtimePolicySessionKey?: string;
|
||||
storePath?: string;
|
||||
defaultModel: string;
|
||||
agentCfgContextTokens?: number;
|
||||
@@ -907,6 +908,7 @@ export async function runReplyAgent(params: {
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
runtimePolicySessionKey,
|
||||
storePath,
|
||||
defaultModel,
|
||||
agentCfgContextTokens,
|
||||
@@ -1105,6 +1107,7 @@ export async function runReplyAgent(params: {
|
||||
sessionEntry: activeSessionEntry,
|
||||
sessionStore: activeSessionStore,
|
||||
sessionKey,
|
||||
runtimePolicySessionKey,
|
||||
storePath,
|
||||
isHeartbeat,
|
||||
replyOperation,
|
||||
@@ -1124,6 +1127,7 @@ export async function runReplyAgent(params: {
|
||||
sessionEntry: activeSessionEntry,
|
||||
sessionStore: activeSessionStore,
|
||||
sessionKey,
|
||||
runtimePolicySessionKey,
|
||||
storePath,
|
||||
isHeartbeat,
|
||||
replyOperation,
|
||||
@@ -1208,6 +1212,7 @@ export async function runReplyAgent(params: {
|
||||
resetSessionAfterRoleOrderingConflict,
|
||||
isHeartbeat,
|
||||
sessionKey,
|
||||
runtimePolicySessionKey,
|
||||
getActiveSessionEntry: () => activeSessionEntry,
|
||||
activeSessionStore,
|
||||
storePath,
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { ReplyPayload } from "../types.js";
|
||||
import { buildDisabledCommandReply } from "./command-gates.js";
|
||||
import { formatElevatedUnavailableMessage } from "./elevated-unavailable.js";
|
||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js";
|
||||
|
||||
const CHAT_BASH_SCOPE_KEY = "chat:bash";
|
||||
const DEFAULT_FOREGROUND_MS = 2000;
|
||||
@@ -210,7 +211,11 @@ export async function handleBashChatCommand(params: {
|
||||
if (!params.elevated.enabled || !params.elevated.allowed) {
|
||||
const runtimeSandboxed = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionKey: resolveRuntimePolicySessionKey({
|
||||
cfg: params.cfg,
|
||||
ctx: params.ctx,
|
||||
sessionKey: params.sessionKey,
|
||||
}),
|
||||
}).sandboxed;
|
||||
return {
|
||||
text: formatElevatedUnavailableMessage({
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { WorkspaceBootstrapFile } from "../../agents/workspace.js";
|
||||
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
||||
import { buildTtsSystemPromptHint } from "../../tts/tts.js";
|
||||
import type { HandleCommandsParams } from "./commands-types.js";
|
||||
import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js";
|
||||
|
||||
export type CommandsSystemPromptBundle = {
|
||||
systemPrompt: string;
|
||||
@@ -43,7 +44,16 @@ export async function resolveCommandsSystemPromptBundle(
|
||||
});
|
||||
const sandboxRuntime = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.sessionKey ?? params.ctx.SessionKey,
|
||||
sessionKey: resolveRuntimePolicySessionKey({
|
||||
cfg: params.cfg,
|
||||
ctx: params.ctx,
|
||||
sessionKey: params.sessionKey ?? params.ctx.SessionKey,
|
||||
}),
|
||||
});
|
||||
const toolPolicySessionKey = resolveRuntimePolicySessionKey({
|
||||
cfg: params.cfg,
|
||||
ctx: params.ctx,
|
||||
sessionKey: params.sessionKey,
|
||||
});
|
||||
const skillsSnapshot = (() => {
|
||||
try {
|
||||
@@ -73,7 +83,7 @@ export async function resolveCommandsSystemPromptBundle(
|
||||
config: params.cfg,
|
||||
agentId: sessionAgentId,
|
||||
workspaceDir,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionKey: toolPolicySessionKey,
|
||||
allowGatewaySubagentBinding: true,
|
||||
messageProvider: params.command.channel,
|
||||
groupId: targetSessionEntry?.groupId ?? undefined,
|
||||
|
||||
@@ -88,6 +88,7 @@ export async function applyInlineDirectivesFastLane(
|
||||
currentVerboseLevel,
|
||||
currentReasoningLevel,
|
||||
currentElevatedLevel,
|
||||
ctx,
|
||||
surface: ctx.Surface,
|
||||
gatewayClientScopes: ctx.GatewayClientScopes,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "./directive-handling.shared.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel } from "./directives.js";
|
||||
import { refreshQueuedFollowupSession } from "./queue.js";
|
||||
import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js";
|
||||
|
||||
export async function handleDirectiveOnly(
|
||||
params: HandleDirectiveOnlyParams,
|
||||
@@ -73,7 +74,11 @@ export async function handleDirectiveOnly(
|
||||
const agentDir = resolveAgentDir(params.cfg, activeAgentId);
|
||||
const runtimeIsSandboxed = resolveSandboxRuntimeStatus({
|
||||
cfg: params.cfg,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionKey: resolveRuntimePolicySessionKey({
|
||||
cfg: params.cfg,
|
||||
ctx: params.ctx,
|
||||
sessionKey: params.sessionKey,
|
||||
}),
|
||||
}).sandboxed;
|
||||
const shouldHintDirectRuntime = directives.hasElevatedDirective && !runtimeIsSandboxed;
|
||||
const allowInternalExecPersistence = canPersistInternalExecDirective({
|
||||
|
||||
@@ -31,6 +31,7 @@ export type HandleDirectiveOnlyCoreParams = {
|
||||
};
|
||||
|
||||
export type HandleDirectiveOnlyParams = HandleDirectiveOnlyCoreParams & {
|
||||
ctx?: MsgContext;
|
||||
messageProvider?: string;
|
||||
currentThinkLevel?: ThinkLevel;
|
||||
currentFastMode?: boolean;
|
||||
|
||||
@@ -317,6 +317,7 @@ export async function applyInlineDirectiveOverrides(params: {
|
||||
currentVerboseLevel,
|
||||
currentReasoningLevel,
|
||||
currentElevatedLevel,
|
||||
ctx,
|
||||
messageProvider: ctx.Provider,
|
||||
surface: ctx.Surface,
|
||||
gatewayClientScopes: ctx.GatewayClientScopes,
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "./model-selection.js";
|
||||
import { formatElevatedUnavailableMessage, resolveElevatedPermissions } from "./reply-elevated.js";
|
||||
import { stripInlineStatus } from "./reply-inline.js";
|
||||
import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js";
|
||||
import type { TypingController } from "./typing.js";
|
||||
|
||||
type AgentDefaults = NonNullable<OpenClawConfig["agents"]>["defaults"];
|
||||
@@ -386,7 +387,7 @@ export async function resolveReplyDirectives(params: {
|
||||
typing.cleanup();
|
||||
const runtimeSandboxed = resolveSandboxRuntimeStatus({
|
||||
cfg,
|
||||
sessionKey: ctx.SessionKey,
|
||||
sessionKey: resolveRuntimePolicySessionKey({ cfg, ctx, sessionKey: ctx.SessionKey }),
|
||||
}).sandboxed;
|
||||
return {
|
||||
kind: "reply",
|
||||
|
||||
@@ -50,6 +50,7 @@ import { resolveOriginMessageProvider } from "./origin-routing.js";
|
||||
import { buildReplyPromptBodies } from "./prompt-prelude.js";
|
||||
import { resolveActiveRunQueueAction } from "./queue-policy.js";
|
||||
import { resolveQueueSettings } from "./queue/settings-runtime.js";
|
||||
import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js";
|
||||
import { resolveBareSessionResetPromptState } from "./session-reset-prompt.js";
|
||||
import { resolveBareResetBootstrapFileAccess } from "./session-reset-prompt.js";
|
||||
import { drainFormattedSystemEvents } from "./session-system-events.js";
|
||||
@@ -233,6 +234,11 @@ export async function runPreparedReply(
|
||||
workspaceDir,
|
||||
sessionStore,
|
||||
} = params;
|
||||
const runtimePolicySessionKey = resolveRuntimePolicySessionKey({
|
||||
cfg,
|
||||
ctx,
|
||||
sessionKey,
|
||||
});
|
||||
let {
|
||||
sessionEntry,
|
||||
resolvedThinkLevel,
|
||||
@@ -689,6 +695,7 @@ export async function runPreparedReply(
|
||||
agentDir,
|
||||
sessionId: preparedSessionState.sessionId,
|
||||
sessionKey,
|
||||
runtimePolicySessionKey,
|
||||
messageProvider: resolveOriginMessageProvider({
|
||||
originatingChannel: ctx.OriginatingChannel ?? sessionCtx.OriginatingChannel,
|
||||
// Prefer Provider over Surface for fallback channel identity.
|
||||
@@ -779,6 +786,7 @@ export async function runPreparedReply(
|
||||
sessionEntry: preparedSessionState.sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
runtimePolicySessionKey,
|
||||
storePath,
|
||||
defaultModel,
|
||||
agentCfgContextTokens: agentCfg?.contextTokens,
|
||||
|
||||
@@ -47,6 +47,7 @@ export type FollowupRun = {
|
||||
agentDir: string;
|
||||
sessionId: string;
|
||||
sessionKey?: string;
|
||||
runtimePolicySessionKey?: string;
|
||||
messageProvider?: string;
|
||||
agentAccountId?: string;
|
||||
groupId?: string;
|
||||
|
||||
119
src/auto-reply/reply/runtime-policy-session-key.test.ts
Normal file
119
src/auto-reply/reply/runtime-policy-session-key.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox/runtime-status.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { resolveRuntimePolicySessionKey } from "./runtime-policy-session-key.js";
|
||||
|
||||
describe("resolveRuntimePolicySessionKey", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: { mode: "non-main", scope: "agent" },
|
||||
},
|
||||
list: [{ id: "main" }],
|
||||
},
|
||||
};
|
||||
|
||||
it("derives an external direct-chat policy key when the conversation uses main", () => {
|
||||
const sessionKey = resolveRuntimePolicySessionKey({
|
||||
cfg,
|
||||
sessionKey: "agent:main:main",
|
||||
ctx: {
|
||||
SessionKey: "agent:main:main",
|
||||
OriginatingChannel: "whatsapp" as MsgContext["OriginatingChannel"],
|
||||
AccountId: "personal",
|
||||
ChatType: "direct",
|
||||
SenderId: "15555550123",
|
||||
},
|
||||
});
|
||||
|
||||
expect(sessionKey).toBe("agent:main:whatsapp:personal:direct:15555550123");
|
||||
expect(resolveSandboxRuntimeStatus({ cfg, sessionKey }).sandboxed).toBe(true);
|
||||
});
|
||||
|
||||
it("normalizes dm chat type aliases", () => {
|
||||
expect(
|
||||
resolveRuntimePolicySessionKey({
|
||||
cfg,
|
||||
sessionKey: "agent:main:main",
|
||||
ctx: {
|
||||
SessionKey: "agent:main:main",
|
||||
OriginatingChannel: "slack" as MsgContext["OriginatingChannel"],
|
||||
ChatType: "dm",
|
||||
SenderId: "U123",
|
||||
},
|
||||
}),
|
||||
).toBe("agent:main:slack:default:direct:u123");
|
||||
});
|
||||
|
||||
it("leaves local main-session runs unsandboxed in non-main mode", () => {
|
||||
const sessionKey = resolveRuntimePolicySessionKey({
|
||||
cfg,
|
||||
sessionKey: "agent:main:main",
|
||||
ctx: {
|
||||
SessionKey: "agent:main:main",
|
||||
Provider: "webchat",
|
||||
ChatType: "direct",
|
||||
SenderId: "operator",
|
||||
},
|
||||
});
|
||||
|
||||
expect(sessionKey).toBe("agent:main:main");
|
||||
expect(resolveSandboxRuntimeStatus({ cfg, sessionKey }).sandboxed).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps already-isolated sessions unchanged", () => {
|
||||
expect(
|
||||
resolveRuntimePolicySessionKey({
|
||||
cfg,
|
||||
sessionKey: "agent:main:discord:channel:123:thread:456",
|
||||
ctx: {
|
||||
SessionKey: "agent:main:discord:channel:123:thread:456",
|
||||
OriginatingChannel: "discord" as MsgContext["OriginatingChannel"],
|
||||
ChatType: "channel",
|
||||
SenderId: "u1",
|
||||
},
|
||||
}),
|
||||
).toBe("agent:main:discord:channel:123:thread:456");
|
||||
});
|
||||
|
||||
it("uses native command target sessions as the policy base", () => {
|
||||
expect(
|
||||
resolveRuntimePolicySessionKey({
|
||||
cfg,
|
||||
sessionKey: "agent:main:main",
|
||||
ctx: {
|
||||
SessionKey: "telegram:slash:status",
|
||||
CommandTargetSessionKey: "agent:main:main",
|
||||
OriginatingChannel: "telegram" as MsgContext["OriginatingChannel"],
|
||||
AccountId: "default",
|
||||
ChatType: "direct",
|
||||
NativeDirectUserId: "42",
|
||||
},
|
||||
}),
|
||||
).toBe("agent:main:telegram:default:direct:42");
|
||||
});
|
||||
|
||||
it("applies identity links for derived direct-chat policy keys", () => {
|
||||
expect(
|
||||
resolveRuntimePolicySessionKey({
|
||||
cfg: {
|
||||
...cfg,
|
||||
session: {
|
||||
identityLinks: {
|
||||
alice: ["telegram:42"],
|
||||
},
|
||||
},
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
ctx: {
|
||||
SessionKey: "agent:main:main",
|
||||
OriginatingChannel: "telegram" as MsgContext["OriginatingChannel"],
|
||||
AccountId: "default",
|
||||
ChatType: "direct",
|
||||
SenderId: "42",
|
||||
},
|
||||
}),
|
||||
).toBe("agent:main:telegram:default:direct:alice");
|
||||
});
|
||||
});
|
||||
125
src/auto-reply/reply/runtime-policy-session-key.ts
Normal file
125
src/auto-reply/reply/runtime-policy-session-key.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { normalizeChatType } from "../../channels/chat-type.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import {
|
||||
buildAgentMainSessionKey,
|
||||
buildAgentPeerSessionKey,
|
||||
normalizeAgentId,
|
||||
normalizeMainKey,
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "../../routing/session-key.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
|
||||
type RuntimePolicyContext = Pick<
|
||||
MsgContext,
|
||||
| "AccountId"
|
||||
| "ChatType"
|
||||
| "CommandTargetSessionKey"
|
||||
| "From"
|
||||
| "NativeDirectUserId"
|
||||
| "OriginatingChannel"
|
||||
| "OriginatingTo"
|
||||
| "Provider"
|
||||
| "RuntimePolicySessionKey"
|
||||
| "SenderE164"
|
||||
| "SenderId"
|
||||
| "SenderUsername"
|
||||
| "SessionKey"
|
||||
| "Surface"
|
||||
| "To"
|
||||
>;
|
||||
|
||||
function resolvePolicyChannel(ctx?: RuntimePolicyContext): string | undefined {
|
||||
const raw = normalizeOptionalString(ctx?.OriginatingChannel ?? ctx?.Provider ?? ctx?.Surface);
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const channel = normalizeLowercaseStringOrEmpty(raw);
|
||||
return channel && channel !== "webchat" ? channel : undefined;
|
||||
}
|
||||
|
||||
function resolvePolicyDirectPeerId(ctx?: RuntimePolicyContext): string | undefined {
|
||||
return normalizeOptionalString(
|
||||
ctx?.NativeDirectUserId ??
|
||||
ctx?.SenderId ??
|
||||
ctx?.SenderE164 ??
|
||||
ctx?.SenderUsername ??
|
||||
ctx?.OriginatingTo ??
|
||||
ctx?.From ??
|
||||
ctx?.To,
|
||||
);
|
||||
}
|
||||
|
||||
function isMainSessionAlias(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
agentId: string;
|
||||
sessionKey: string;
|
||||
}): boolean {
|
||||
const raw = normalizeLowercaseStringOrEmpty(params.sessionKey);
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
const agentId = normalizeAgentId(params.agentId);
|
||||
const mainKey = normalizeMainKey(params.cfg?.session?.mainKey);
|
||||
const agentMainSessionKey = buildAgentMainSessionKey({
|
||||
agentId,
|
||||
mainKey,
|
||||
});
|
||||
const agentMainAliasKey = buildAgentMainSessionKey({
|
||||
agentId,
|
||||
mainKey: "main",
|
||||
});
|
||||
return (
|
||||
raw === "main" ||
|
||||
raw === mainKey ||
|
||||
raw === agentMainSessionKey ||
|
||||
raw === agentMainAliasKey ||
|
||||
raw === buildAgentMainSessionKey({ agentId: "main", mainKey }) ||
|
||||
raw === buildAgentMainSessionKey({ agentId: "main", mainKey: "main" }) ||
|
||||
(params.cfg?.session?.scope === "global" && raw === "global")
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveRuntimePolicySessionKey(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
ctx?: RuntimePolicyContext;
|
||||
sessionKey?: string | null;
|
||||
}): string | undefined {
|
||||
const explicitPolicySessionKey = normalizeOptionalString(params.ctx?.RuntimePolicySessionKey);
|
||||
if (explicitPolicySessionKey) {
|
||||
return explicitPolicySessionKey;
|
||||
}
|
||||
const sessionKey = normalizeOptionalString(
|
||||
params.sessionKey ?? params.ctx?.CommandTargetSessionKey ?? params.ctx?.SessionKey,
|
||||
);
|
||||
if (!sessionKey) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
if (!isMainSessionAlias({ cfg: params.cfg, agentId, sessionKey })) {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
if (normalizeChatType(params.ctx?.ChatType) !== "direct") {
|
||||
return sessionKey;
|
||||
}
|
||||
const channel = resolvePolicyChannel(params.ctx);
|
||||
const peerId = resolvePolicyDirectPeerId(params.ctx);
|
||||
if (!channel || !peerId) {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
return buildAgentPeerSessionKey({
|
||||
agentId,
|
||||
channel,
|
||||
accountId: params.ctx?.AccountId,
|
||||
peerKind: "direct",
|
||||
peerId,
|
||||
dmScope: "per-account-channel-peer",
|
||||
identityLinks: params.cfg?.session?.identityLinks,
|
||||
});
|
||||
}
|
||||
@@ -55,6 +55,11 @@ export type MsgContext = {
|
||||
From?: string;
|
||||
To?: string;
|
||||
SessionKey?: string;
|
||||
/**
|
||||
* Session-like key used for runtime policy (sandbox/tool policy) when the
|
||||
* conversation key intentionally remains broader, such as a main-session DM.
|
||||
*/
|
||||
RuntimePolicySessionKey?: string;
|
||||
/** Provider account id (multi-account). */
|
||||
AccountId?: string;
|
||||
ParentSessionKey?: string;
|
||||
|
||||
Reference in New Issue
Block a user