mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 08:10:43 +00:00
fix(agents): route requester roles into child origins
This commit is contained in:
@@ -464,6 +464,7 @@ async function dispatchDiscordComponentEvent(params: {
|
||||
SenderTag: senderTag,
|
||||
GroupSubject: groupSubject,
|
||||
GroupChannel: groupChannel,
|
||||
MemberRoleIds: interactionCtx.memberRoleIds,
|
||||
GroupSystemPrompt: interactionCtx.isDirectMessage ? undefined : groupSystemPrompt,
|
||||
GroupSpace: guildInfo?.id ?? guildInfo?.slug ?? interactionCtx.rawGuildId ?? undefined,
|
||||
OwnerAllowFrom: ownerAllowFrom,
|
||||
|
||||
@@ -1083,6 +1083,7 @@ export async function preflightDiscordMessage(
|
||||
messageChannelId,
|
||||
author,
|
||||
sender,
|
||||
memberRoleIds,
|
||||
channelInfo,
|
||||
channelName,
|
||||
isGuildMessage,
|
||||
|
||||
@@ -44,6 +44,7 @@ export type DiscordMessagePreflightContext = DiscordMessagePreflightSharedFields
|
||||
messageChannelId: string;
|
||||
author: User;
|
||||
sender: DiscordSenderIdentity;
|
||||
memberRoleIds: string[];
|
||||
|
||||
channelInfo: DiscordChannelInfo | null;
|
||||
channelName?: string;
|
||||
|
||||
@@ -144,6 +144,7 @@ export async function processDiscordMessage(
|
||||
displayChannelSlug,
|
||||
guildInfo,
|
||||
guildSlug,
|
||||
memberRoleIds,
|
||||
channelConfig,
|
||||
baseSessionKey,
|
||||
boundSessionKey,
|
||||
@@ -481,6 +482,7 @@ export async function processDiscordMessage(
|
||||
SenderTag: senderTag,
|
||||
GroupSubject: groupSubject,
|
||||
GroupChannel: groupChannel,
|
||||
MemberRoleIds: memberRoleIds,
|
||||
UntrustedContext: untrustedContext,
|
||||
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
|
||||
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
|
||||
|
||||
@@ -50,6 +50,7 @@ describe("buildDiscordNativeCommandContext", () => {
|
||||
interactionId: "interaction-1",
|
||||
channelId: "chan-1",
|
||||
threadParentId: "parent-1",
|
||||
memberRoleIds: ["admin"],
|
||||
guildName: "Ops",
|
||||
channelTopic: "Production alerts only",
|
||||
channelConfig: {
|
||||
@@ -82,6 +83,8 @@ describe("buildDiscordNativeCommandContext", () => {
|
||||
expect(ctx.ChatType).toBe("channel");
|
||||
expect(ctx.ConversationLabel).toBe("chan-1");
|
||||
expect(ctx.GroupSubject).toBe("Ops");
|
||||
expect(ctx.GroupSpace).toBe("guild-1");
|
||||
expect(ctx.MemberRoleIds).toEqual(["admin"]);
|
||||
expect(ctx.GroupSystemPrompt).toBe("Use the runbook.");
|
||||
expect(ctx.OwnerAllowFrom).toEqual(["user-1"]);
|
||||
expect(ctx.MessageThreadId).toBe("chan-1");
|
||||
|
||||
@@ -13,6 +13,8 @@ export type BuildDiscordNativeCommandContextParams = {
|
||||
interactionId: string;
|
||||
channelId: string;
|
||||
threadParentId?: string;
|
||||
memberRoleIds?: string[];
|
||||
guildId?: string;
|
||||
guildName?: string;
|
||||
channelTopic?: string;
|
||||
channelConfig?: DiscordChannelConfigResolved | null;
|
||||
@@ -67,6 +69,10 @@ export function buildDiscordNativeCommandContext(params: BuildDiscordNativeComma
|
||||
ChatType: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
|
||||
ConversationLabel: conversationLabel,
|
||||
GroupSubject: params.isGuild ? params.guildName : undefined,
|
||||
GroupSpace: params.isGuild
|
||||
? (params.guildInfo?.id ?? params.guildInfo?.slug ?? params.guildId)
|
||||
: undefined,
|
||||
MemberRoleIds: params.memberRoleIds,
|
||||
GroupSystemPrompt: groupSystemPrompt,
|
||||
UntrustedContext: untrustedContext,
|
||||
OwnerAllowFrom: ownerAllowFrom,
|
||||
|
||||
@@ -1183,6 +1183,8 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
interactionId,
|
||||
channelId,
|
||||
threadParentId,
|
||||
memberRoleIds,
|
||||
guildId: interaction.guild?.id,
|
||||
guildName: interaction.guild?.name,
|
||||
channelTopic: channel && "topic" in channel ? (channel.topic ?? undefined) : undefined,
|
||||
channelConfig,
|
||||
|
||||
@@ -108,6 +108,8 @@ export type SpawnAcpContext = {
|
||||
agentGroupId?: string;
|
||||
/** Group space label (guild/team id) from the originating channel context. */
|
||||
agentGroupSpace?: string | null;
|
||||
/** Trusted provider role ids for the requester in this group turn. */
|
||||
agentMemberRoleIds?: string[];
|
||||
sandboxed?: boolean;
|
||||
};
|
||||
|
||||
@@ -764,6 +766,7 @@ function resolveAcpSpawnRequesterState(params: {
|
||||
requesterTo: params.ctx.agentTo,
|
||||
requesterThreadId: params.ctx.agentThreadId,
|
||||
requesterGroupSpace: params.ctx.agentGroupSpace,
|
||||
requesterMemberRoleIds: params.ctx.agentMemberRoleIds,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -592,6 +592,41 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
).toBe("bot-alpha-room-a");
|
||||
});
|
||||
|
||||
it("sessions_spawn uses requester roles for role-scoped target-agent accounts", async () => {
|
||||
expect(
|
||||
await executeBoundAccountSpawn({
|
||||
callId: "call-role-scoped-account",
|
||||
agentId: "bot-alpha",
|
||||
context: {
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "discord",
|
||||
agentAccountId: "bot-beta",
|
||||
agentTo: "channel:ops",
|
||||
agentGroupSpace: "guild-current",
|
||||
agentMemberRoleIds: ["admin"],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
type: "route",
|
||||
agentId: "bot-alpha",
|
||||
match: { channel: "discord", accountId: "bot-alpha-default" },
|
||||
},
|
||||
{
|
||||
type: "route",
|
||||
agentId: "bot-alpha",
|
||||
match: {
|
||||
channel: "discord",
|
||||
guildId: "guild-current",
|
||||
roles: ["admin"],
|
||||
peer: { kind: "channel", id: "channel:ops" },
|
||||
accountId: "bot-alpha-admin",
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBe("bot-alpha-admin");
|
||||
});
|
||||
|
||||
it("sessions_spawn strips channel-side prefixes from agentTo before bound-account lookup", async () => {
|
||||
const rawRoomId = "!exampleRoomId:example.org";
|
||||
// agentTo arrives in delivery-target format (room:<id>), while the binding
|
||||
|
||||
@@ -288,6 +288,7 @@ export function createOpenClawTools(
|
||||
agentGroupId: options?.agentGroupId,
|
||||
agentGroupChannel: options?.agentGroupChannel,
|
||||
agentGroupSpace: options?.agentGroupSpace,
|
||||
agentMemberRoleIds: options?.agentMemberRoleIds,
|
||||
sandboxed: options?.sandboxed,
|
||||
requesterAgentIdOverride: options?.requesterAgentIdOverride,
|
||||
workspaceDir: spawnWorkspaceDir,
|
||||
|
||||
@@ -687,6 +687,7 @@ export async function runEmbeddedPiAgent(
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
memberRoleIds: params.memberRoleIds,
|
||||
spawnedBy: params.spawnedBy,
|
||||
isCanonicalWorkspace,
|
||||
senderId: params.senderId,
|
||||
|
||||
@@ -487,6 +487,7 @@ export async function runEmbeddedAttempt(
|
||||
groupId: params.groupId,
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
memberRoleIds: params.memberRoleIds,
|
||||
spawnedBy: params.spawnedBy,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
|
||||
@@ -40,6 +40,8 @@ export type RunEmbeddedPiAgentParams = {
|
||||
groupChannel?: string | null;
|
||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||
groupSpace?: string | null;
|
||||
/** Trusted provider role ids for the requester in this group turn. */
|
||||
memberRoleIds?: string[];
|
||||
/** Parent session key for subagent policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
/** Whether workspaceDir points at the canonical agent workspace for bootstrap purposes. */
|
||||
|
||||
@@ -310,6 +310,8 @@ export function createOpenClawCodingTools(options?: {
|
||||
groupChannel?: string | null;
|
||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
||||
groupSpace?: string | null;
|
||||
/** Trusted provider role ids for the requester in this group turn. */
|
||||
memberRoleIds?: string[];
|
||||
/** Parent session key for subagent group policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
senderId?: string | null;
|
||||
@@ -570,6 +572,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
agentGroupId: options?.groupId ?? null,
|
||||
agentGroupChannel: options?.groupChannel ?? null,
|
||||
agentGroupSpace: options?.groupSpace ?? null,
|
||||
agentMemberRoleIds: options?.memberRoleIds,
|
||||
agentDir: options?.agentDir,
|
||||
sandboxRoot,
|
||||
sandboxContainerWorkdir: sandbox?.containerWorkdir,
|
||||
|
||||
@@ -15,6 +15,7 @@ export type SpawnedToolContext = {
|
||||
agentGroupId?: string | null;
|
||||
agentGroupChannel?: string | null;
|
||||
agentGroupSpace?: string | null;
|
||||
agentMemberRoleIds?: string[];
|
||||
workspaceDir?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ export type SpawnSubagentContext = {
|
||||
agentGroupId?: string | null;
|
||||
agentGroupChannel?: string | null;
|
||||
agentGroupSpace?: string | null;
|
||||
agentMemberRoleIds?: string[];
|
||||
requesterAgentIdOverride?: string;
|
||||
/** Explicit workspace directory for subagent to inherit (optional). */
|
||||
workspaceDir?: string;
|
||||
@@ -468,6 +469,7 @@ export async function spawnSubagentDirect(
|
||||
requesterTo: ctx.agentTo,
|
||||
requesterThreadId: ctx.agentThreadId,
|
||||
requesterGroupSpace: ctx.agentGroupSpace,
|
||||
requesterMemberRoleIds: ctx.agentMemberRoleIds,
|
||||
});
|
||||
let childSessionOrigin = requesterOrigin;
|
||||
if (targetAgentId !== requesterAgentId) {
|
||||
|
||||
@@ -244,6 +244,7 @@ export function createSessionsSpawnTool(
|
||||
agentThreadId: opts?.agentThreadId,
|
||||
agentGroupId: opts?.agentGroupId ?? undefined,
|
||||
agentGroupSpace: opts?.agentGroupSpace,
|
||||
agentMemberRoleIds: opts?.agentMemberRoleIds,
|
||||
sandboxed: opts?.sandboxed,
|
||||
},
|
||||
);
|
||||
@@ -338,6 +339,7 @@ export function createSessionsSpawnTool(
|
||||
agentGroupId: opts?.agentGroupId,
|
||||
agentGroupChannel: opts?.agentGroupChannel,
|
||||
agentGroupSpace: opts?.agentGroupSpace,
|
||||
agentMemberRoleIds: opts?.agentMemberRoleIds,
|
||||
requesterAgentIdOverride: opts?.requesterAgentIdOverride,
|
||||
workspaceDir: opts?.workspaceDir,
|
||||
},
|
||||
|
||||
@@ -158,6 +158,7 @@ describe("agent-runner-utils", () => {
|
||||
Provider: "OpenAI",
|
||||
To: "channel-1",
|
||||
SenderId: "sender-1",
|
||||
MemberRoleIds: ["admin", " ", "operator"],
|
||||
},
|
||||
hasRepliedRef: undefined,
|
||||
provider: "anthropic",
|
||||
@@ -173,6 +174,7 @@ describe("agent-runner-utils", () => {
|
||||
agentId: run.agentId,
|
||||
messageProvider: "openai",
|
||||
messageTo: "channel-1",
|
||||
memberRoleIds: ["admin", "operator"],
|
||||
});
|
||||
expect(resolved.senderContext).toEqual({
|
||||
senderId: "sender-1",
|
||||
|
||||
@@ -238,6 +238,7 @@ export function buildEmbeddedContextFromTemplate(params: {
|
||||
to: params.sessionCtx.To,
|
||||
}),
|
||||
messageThreadId: params.sessionCtx.MessageThreadId ?? undefined,
|
||||
memberRoleIds: normalizeMemberRoleIds(params.sessionCtx.MemberRoleIds),
|
||||
// Provider threading context for tool auto-injection
|
||||
...buildThreadingToolContext({
|
||||
sessionCtx: params.sessionCtx,
|
||||
@@ -247,6 +248,15 @@ export function buildEmbeddedContextFromTemplate(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMemberRoleIds(value: TemplateContext["MemberRoleIds"]): string[] | undefined {
|
||||
const roles = Array.isArray(value)
|
||||
? value
|
||||
.map((roleId) => normalizeOptionalString(roleId))
|
||||
.filter((roleId): roleId is string => Boolean(roleId))
|
||||
: [];
|
||||
return roles.length > 0 ? roles : undefined;
|
||||
}
|
||||
|
||||
export function buildTemplateSenderContext(sessionCtx: TemplateContext) {
|
||||
return {
|
||||
senderId: normalizeOptionalString(sessionCtx.SenderId),
|
||||
|
||||
@@ -126,6 +126,8 @@ export type MsgContext = {
|
||||
/** Human label for channel-like group conversations (e.g. #general, #support). */
|
||||
GroupChannel?: string;
|
||||
GroupSpace?: string;
|
||||
/** Trusted provider role ids for the sender in this group turn. */
|
||||
MemberRoleIds?: string[];
|
||||
GroupMembers?: string;
|
||||
GroupSystemPrompt?: string;
|
||||
/** Untrusted metadata that must not be treated as system instructions. */
|
||||
|
||||
@@ -265,6 +265,26 @@ describe("resolveDeliveryTarget", () => {
|
||||
expect(result.accountId).toBe("peer-first");
|
||||
});
|
||||
|
||||
it("does not infer scoped bound accountId for peerless cron delivery", async () => {
|
||||
setMainSessionEntry(undefined);
|
||||
const cfg = makeCfg({
|
||||
bindings: [
|
||||
{
|
||||
agentId: AGENT_ID,
|
||||
match: {
|
||||
channel: "telegram",
|
||||
guildId: "guild-1",
|
||||
accountId: "tenant-account",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const result = await resolveForAgent({ cfg });
|
||||
|
||||
expect(result.accountId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves session lastAccountId when present", async () => {
|
||||
setMainSessionEntry({
|
||||
sessionId: "sess-1",
|
||||
|
||||
Reference in New Issue
Block a user