mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 02:07:51 +00:00
refactor: centralize channel ingress access
This commit is contained in:
61
src/plugin-sdk/access-groups.test.ts
Normal file
61
src/plugin-sdk/access-groups.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
expandAllowFromWithAccessGroups,
|
||||
resolveAccessGroupAllowFromState,
|
||||
} from "./access-groups.js";
|
||||
|
||||
describe("access group allowlists", () => {
|
||||
it("reports static, missing, unsupported, failed, and compatibility expansion states", async () => {
|
||||
const cfg = {
|
||||
accessGroups: {
|
||||
admins: { type: "message.senders", members: { "*": ["global"], test: ["local"] } },
|
||||
audience: { type: "discord.channelAudience", guildId: "guild-1", channelId: "channel-1" },
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
await expect(
|
||||
resolveAccessGroupAllowFromState({
|
||||
accessGroups: cfg.accessGroups,
|
||||
allowFrom: ["accessGroup:admins", "accessGroup:missing", "accessGroup:audience"],
|
||||
channel: "test",
|
||||
accountId: "default",
|
||||
senderId: "local",
|
||||
isSenderAllowed: (senderId, allowFrom) => allowFrom.includes(senderId),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
referenced: ["admins", "missing", "audience"],
|
||||
matched: ["admins"],
|
||||
missing: ["missing"],
|
||||
unsupported: ["audience"],
|
||||
failed: [],
|
||||
matchedAllowFromEntries: ["accessGroup:admins"],
|
||||
hasReferences: true,
|
||||
hasMatch: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
resolveAccessGroupAllowFromState({
|
||||
accessGroups: cfg.accessGroups,
|
||||
allowFrom: ["accessGroup:audience"],
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
senderId: "discord:123",
|
||||
resolveMembership: async () => {
|
||||
throw new Error("discord lookup failed");
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({ referenced: ["audience"], failed: ["audience"], hasMatch: false });
|
||||
|
||||
await expect(
|
||||
expandAllowFromWithAccessGroups({
|
||||
cfg,
|
||||
allowFrom: ["accessGroup:admins"],
|
||||
channel: "test",
|
||||
accountId: "default",
|
||||
senderId: "local",
|
||||
isSenderAllowed: (senderId, allowFrom) => allowFrom.includes(senderId),
|
||||
}),
|
||||
).resolves.toEqual(["accessGroup:admins", "local"]);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,12 @@
|
||||
import {
|
||||
ACCESS_GROUP_ALLOW_FROM_PREFIX,
|
||||
parseAccessGroupAllowFromEntry,
|
||||
} from "../channels/allow-from.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.public.js";
|
||||
import type { AccessGroupConfig } from "../config/types.access-groups.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
export const ACCESS_GROUP_ALLOW_FROM_PREFIX = "accessGroup:";
|
||||
export { ACCESS_GROUP_ALLOW_FROM_PREFIX, parseAccessGroupAllowFromEntry };
|
||||
|
||||
export type AccessGroupMembershipResolver = (params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -13,14 +17,24 @@ export type AccessGroupMembershipResolver = (params: {
|
||||
senderId: string;
|
||||
}) => boolean | Promise<boolean>;
|
||||
|
||||
export function parseAccessGroupAllowFromEntry(entry: string): string | null {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed.startsWith(ACCESS_GROUP_ALLOW_FROM_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
const name = trimmed.slice(ACCESS_GROUP_ALLOW_FROM_PREFIX.length).trim();
|
||||
return name.length > 0 ? name : null;
|
||||
}
|
||||
export type AccessGroupMembershipLookup = (params: {
|
||||
name: string;
|
||||
group: AccessGroupConfig;
|
||||
channel: ChannelId;
|
||||
accountId: string;
|
||||
senderId: string;
|
||||
}) => boolean | Promise<boolean>;
|
||||
|
||||
export type ResolvedAccessGroupAllowFromState = {
|
||||
referenced: string[];
|
||||
matched: string[];
|
||||
missing: string[];
|
||||
unsupported: string[];
|
||||
failed: string[];
|
||||
matchedAllowFromEntries: string[];
|
||||
hasReferences: boolean;
|
||||
hasMatch: boolean;
|
||||
};
|
||||
|
||||
function resolveMessageSenderGroupEntries(params: {
|
||||
group: AccessGroupConfig;
|
||||
@@ -32,6 +46,83 @@ function resolveMessageSenderGroupEntries(params: {
|
||||
return [...(params.group.members["*"] ?? []), ...(params.group.members[params.channel] ?? [])];
|
||||
}
|
||||
|
||||
export async function resolveAccessGroupAllowFromState(params: {
|
||||
accessGroups?: Record<string, AccessGroupConfig>;
|
||||
allowFrom: Array<string | number> | null | undefined;
|
||||
channel: ChannelId;
|
||||
accountId: string;
|
||||
senderId: string;
|
||||
isSenderAllowed?: (senderId: string, allowFrom: string[]) => boolean;
|
||||
resolveMembership?: AccessGroupMembershipLookup;
|
||||
}): Promise<ResolvedAccessGroupAllowFromState> {
|
||||
const names = Array.from(
|
||||
new Set(
|
||||
(params.allowFrom ?? [])
|
||||
.map((entry) => parseAccessGroupAllowFromEntry(String(entry)))
|
||||
.filter((entry): entry is string => entry != null),
|
||||
),
|
||||
);
|
||||
const state: ResolvedAccessGroupAllowFromState = {
|
||||
referenced: names,
|
||||
matched: [],
|
||||
missing: [],
|
||||
unsupported: [],
|
||||
failed: [],
|
||||
matchedAllowFromEntries: [],
|
||||
hasReferences: names.length > 0,
|
||||
hasMatch: false,
|
||||
};
|
||||
const groups = params.accessGroups;
|
||||
for (const name of names) {
|
||||
const group = groups?.[name];
|
||||
if (!group) {
|
||||
state.missing.push(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
const senderEntries = resolveMessageSenderGroupEntries({
|
||||
group,
|
||||
channel: params.channel,
|
||||
});
|
||||
if (
|
||||
senderEntries.length > 0 &&
|
||||
params.isSenderAllowed?.(params.senderId, senderEntries) === true
|
||||
) {
|
||||
state.matched.push(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!params.resolveMembership) {
|
||||
if (group.type !== "message.senders") {
|
||||
state.unsupported.push(name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let allowed = false;
|
||||
try {
|
||||
allowed = await params.resolveMembership({
|
||||
name,
|
||||
group,
|
||||
channel: params.channel,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
});
|
||||
} catch {
|
||||
state.failed.push(name);
|
||||
continue;
|
||||
}
|
||||
if (allowed) {
|
||||
state.matched.push(name);
|
||||
}
|
||||
}
|
||||
state.matchedAllowFromEntries = state.matched.map(
|
||||
(name) => `${ACCESS_GROUP_ALLOW_FROM_PREFIX}${name}`,
|
||||
);
|
||||
state.hasMatch = state.matchedAllowFromEntries.length > 0;
|
||||
return state;
|
||||
}
|
||||
|
||||
export async function resolveAccessGroupAllowFromMatches(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
allowFrom: Array<string | number> | null | undefined;
|
||||
@@ -42,60 +133,24 @@ export async function resolveAccessGroupAllowFromMatches(params: {
|
||||
resolveMembership?: AccessGroupMembershipResolver;
|
||||
}): Promise<string[]> {
|
||||
const cfg = params.cfg;
|
||||
const groups = cfg?.accessGroups;
|
||||
if (!groups) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const names = Array.from(
|
||||
new Set(
|
||||
(params.allowFrom ?? [])
|
||||
.map((entry) => parseAccessGroupAllowFromEntry(String(entry)))
|
||||
.filter((entry): entry is string => entry != null),
|
||||
),
|
||||
);
|
||||
if (names.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matched: string[] = [];
|
||||
for (const name of names) {
|
||||
const group = groups[name];
|
||||
if (!group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const senderEntries = resolveMessageSenderGroupEntries({
|
||||
group,
|
||||
channel: params.channel,
|
||||
});
|
||||
if (
|
||||
senderEntries.length > 0 &&
|
||||
params.isSenderAllowed?.(params.senderId, senderEntries) === true
|
||||
) {
|
||||
matched.push(`${ACCESS_GROUP_ALLOW_FROM_PREFIX}${name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let allowed = false;
|
||||
try {
|
||||
allowed =
|
||||
(await params.resolveMembership?.({
|
||||
cfg,
|
||||
name,
|
||||
group,
|
||||
channel: params.channel,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
})) === true;
|
||||
} catch {
|
||||
allowed = false;
|
||||
}
|
||||
if (allowed) {
|
||||
matched.push(`${ACCESS_GROUP_ALLOW_FROM_PREFIX}${name}`);
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
const resolveMembership = params.resolveMembership;
|
||||
const state = await resolveAccessGroupAllowFromState({
|
||||
accessGroups: cfg?.accessGroups,
|
||||
allowFrom: params.allowFrom,
|
||||
channel: params.channel,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
isSenderAllowed: params.isSenderAllowed,
|
||||
resolveMembership:
|
||||
resolveMembership && cfg
|
||||
? async (lookupParams) =>
|
||||
await resolveMembership({
|
||||
cfg,
|
||||
...lookupParams,
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
return state.matchedAllowFromEntries;
|
||||
}
|
||||
|
||||
export async function expandAllowFromWithAccessGroups(params: {
|
||||
|
||||
1
src/plugin-sdk/channel-access-compat.ts
Normal file
1
src/plugin-sdk/channel-access-compat.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "../security/dm-policy-shared.js";
|
||||
146
src/plugin-sdk/channel-ingress-runtime.test.ts
Normal file
146
src/plugin-sdk/channel-ingress-runtime.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { describe, expect, expectTypeOf, it, vi } from "vitest";
|
||||
import type { AccessFacts } from "../channels/turn/types.js";
|
||||
import {
|
||||
resolveChannelMessageIngress,
|
||||
type ChannelIngressIdentityDescriptor,
|
||||
type ResolveChannelMessageIngressParams,
|
||||
} from "./channel-ingress-runtime.js";
|
||||
import { projectIngressAccessFacts } from "./channel-ingress.js";
|
||||
|
||||
const identity = {
|
||||
primary: { normalize: (value) => value.trim().toLowerCase(), sensitivity: "pii" },
|
||||
} satisfies ChannelIngressIdentityDescriptor;
|
||||
|
||||
async function resolve(input: Partial<ResolveChannelMessageIngressParams> = {}) {
|
||||
return await resolveChannelMessageIngress({
|
||||
channelId: "runtime-test",
|
||||
accountId: "default",
|
||||
identity,
|
||||
subject: { stableId: "owner" },
|
||||
conversation: { kind: "direct", id: "dm-1" },
|
||||
event: { kind: "message", authMode: "inbound", mayPair: true },
|
||||
policy: { dmPolicy: "allowlist", groupPolicy: "disabled", ...input.policy },
|
||||
allowFrom: ["owner"],
|
||||
...input,
|
||||
});
|
||||
}
|
||||
|
||||
describe("plugin-sdk/channel-ingress-runtime", () => {
|
||||
it("omits projected command facts unless command policy was requested", async () => {
|
||||
const normalMessage = await resolve();
|
||||
|
||||
expect(projectIngressAccessFacts(normalMessage.ingress).commands).toBeUndefined();
|
||||
|
||||
const commandMessage = await resolve({
|
||||
command: { useAccessGroups: true, allowTextCommands: true, hasControlCommand: true },
|
||||
});
|
||||
|
||||
expect(projectIngressAccessFacts(commandMessage.ingress).commands).toMatchObject({
|
||||
authorized: true,
|
||||
authorizers: [],
|
||||
useAccessGroups: true,
|
||||
allowTextCommands: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps command authorizers required on public AccessFacts", () => {
|
||||
expectTypeOf<NonNullable<AccessFacts["commands"]>["authorizers"]>().toEqualTypeOf<
|
||||
Array<{ configured: boolean; allowed: boolean }>
|
||||
>();
|
||||
});
|
||||
|
||||
it("derives store allowlists, command auth, sender separation, and redaction", async () => {
|
||||
const sender = "Secret-Sender@example.test";
|
||||
const readStoreAllowFrom = vi.fn(async () => ["secret-sender@example.test"]);
|
||||
const allowed = await resolve({
|
||||
subject: { stableId: sender },
|
||||
policy: { dmPolicy: "pairing", groupPolicy: "disabled" },
|
||||
allowFrom: [],
|
||||
readStoreAllowFrom,
|
||||
command: { useAccessGroups: true, allowTextCommands: true, hasControlCommand: true },
|
||||
});
|
||||
expect(readStoreAllowFrom).toHaveBeenCalledOnce();
|
||||
expect(allowed.ingress).toMatchObject({ admission: "dispatch", decision: "allow" });
|
||||
expect(allowed.commandAccess.authorized).toBe(true);
|
||||
expect(JSON.stringify(allowed.state)).not.toContain(sender);
|
||||
expect(JSON.stringify(allowed.ingress)).not.toContain(sender);
|
||||
|
||||
const blockedBeforeCommand = await resolve({
|
||||
route: { id: "route:disabled", enabled: false },
|
||||
command: { useAccessGroups: true, allowTextCommands: true, hasControlCommand: true },
|
||||
});
|
||||
expect(blockedBeforeCommand.ingress.reasonCode).toBe("route_blocked");
|
||||
expect(blockedBeforeCommand.commandAccess.authorized).toBe(false);
|
||||
|
||||
const unauthorizedCommand = await resolve({
|
||||
conversation: { kind: "group", id: "room-1" },
|
||||
event: { kind: "message", authMode: "inbound", mayPair: false },
|
||||
policy: {
|
||||
dmPolicy: "pairing",
|
||||
groupPolicy: "open",
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
},
|
||||
command: {
|
||||
useAccessGroups: true,
|
||||
allowTextCommands: true,
|
||||
hasControlCommand: true,
|
||||
groupOwnerAllowFrom: "none",
|
||||
commandGroupAllowFromFallbackToAllowFrom: false,
|
||||
},
|
||||
});
|
||||
expect(unauthorizedCommand.ingress.reasonCode).toBe("control_command_unauthorized");
|
||||
expect(unauthorizedCommand.senderAccess).toMatchObject({
|
||||
decision: "allow",
|
||||
reasonCode: "group_policy_open",
|
||||
});
|
||||
expect(unauthorizedCommand.commandAccess.shouldBlockControlCommand).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps normalized compatibility entries scoped to the intended identifier kind", async () => {
|
||||
const prefixedIdentity = {
|
||||
primary: {
|
||||
key: "user-id",
|
||||
normalizeEntry: (value) =>
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^users\//, "") || null,
|
||||
normalizeSubject: (value) =>
|
||||
value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^users\//, ""),
|
||||
},
|
||||
aliases: [
|
||||
{
|
||||
key: "email",
|
||||
kind: "plugin:test-email",
|
||||
normalizeEntry(value) {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized.startsWith("users/") || !normalized.includes("@") ? null : normalized;
|
||||
},
|
||||
normalizeSubject: (value) => value.trim().toLowerCase(),
|
||||
dangerous: true,
|
||||
},
|
||||
],
|
||||
} satisfies ChannelIngressIdentityDescriptor;
|
||||
|
||||
const result = await resolveChannelMessageIngress({
|
||||
channelId: "runtime-test",
|
||||
accountId: "default",
|
||||
identity: prefixedIdentity,
|
||||
subject: { stableId: "users/123", aliases: { email: "jane@example.test" } },
|
||||
conversation: { kind: "direct", id: "dm-1" },
|
||||
event: { kind: "message", authMode: "inbound", mayPair: false },
|
||||
policy: {
|
||||
dmPolicy: "allowlist",
|
||||
groupPolicy: "disabled",
|
||||
mutableIdentifierMatching: "enabled",
|
||||
},
|
||||
allowFrom: ["users/jane@example.test"],
|
||||
});
|
||||
|
||||
expect(result.senderAccess.effectiveAllowFrom).toEqual(["jane@example.test"]);
|
||||
expect(result.senderAccess.decision).toBe("block");
|
||||
});
|
||||
});
|
||||
44
src/plugin-sdk/channel-ingress-runtime.ts
Normal file
44
src/plugin-sdk/channel-ingress-runtime.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* High-level runtime resolver for inbound channel access decisions.
|
||||
*
|
||||
* Channel plugins should use this subpath for new receive paths. It accepts
|
||||
* platform facts, raw allowlists, route descriptors, command facts, and access
|
||||
* group config, then returns sender/route/command/activation projections plus
|
||||
* the ordered ingress graph.
|
||||
*/
|
||||
export {
|
||||
channelIngressRoutes,
|
||||
createChannelIngressResolver,
|
||||
defineStableChannelIngressIdentity,
|
||||
readChannelIngressStoreAllowFromForDmPolicy,
|
||||
resolveChannelMessageIngress,
|
||||
resolveStableChannelMessageIngress,
|
||||
} from "../channels/message-access/index.js";
|
||||
export type {
|
||||
AccessGroupMembershipFact,
|
||||
ChannelIngressDecision,
|
||||
ChannelIngressAccessGroupMembershipResolver,
|
||||
ChannelIngressCommandPresetInput,
|
||||
ChannelIngressConfigInput,
|
||||
ChannelIngressEventInput,
|
||||
ChannelIngressEventPresetInput,
|
||||
ChannelIngressIdentityDescriptor,
|
||||
ChannelIngressIdentityAlias,
|
||||
ChannelIngressIdentityField,
|
||||
ChannelIngressIdentitySubjectInput,
|
||||
ChannelIngressIdentifierKind,
|
||||
ChannelIngressPolicyInput,
|
||||
ChannelIngressRouteAccess,
|
||||
ChannelIngressRouteDescriptor,
|
||||
ChannelIngressResolver,
|
||||
ChannelIngressResolverMessageParams,
|
||||
ChannelIngressStateInput,
|
||||
ChannelIngressState,
|
||||
ChannelMessageIngressCommandInput,
|
||||
CreateChannelIngressResolverParams,
|
||||
IngressReasonCode,
|
||||
ResolvedChannelMessageIngress,
|
||||
ResolveChannelMessageIngressParams,
|
||||
ResolveStableChannelMessageIngressParams,
|
||||
StableChannelIngressIdentityParams,
|
||||
} from "../channels/message-access/index.js";
|
||||
620
src/plugin-sdk/channel-ingress.ts
Normal file
620
src/plugin-sdk/channel-ingress.ts
Normal file
@@ -0,0 +1,620 @@
|
||||
import {
|
||||
decideChannelIngress,
|
||||
resolveChannelIngressState as resolveChannelIngressStateInternal,
|
||||
} from "../channels/message-access/index.js";
|
||||
import type {
|
||||
AccessGraphGate,
|
||||
ChannelIngressDecision,
|
||||
ChannelIngressIdentifierKind,
|
||||
ChannelIngressPolicyInput,
|
||||
ChannelIngressState,
|
||||
ChannelIngressStateInput as MessageAccessChannelIngressStateInput,
|
||||
IngressGateKind,
|
||||
IngressGatePhase,
|
||||
InternalChannelIngressAdapter,
|
||||
InternalChannelIngressNormalizeResult,
|
||||
InternalChannelIngressSubject,
|
||||
InternalMatchMaterial,
|
||||
InternalNormalizedEntry,
|
||||
IngressReasonCode,
|
||||
} from "../channels/message-access/index.js";
|
||||
import type { AccessFacts, ChannelTurnAdmission } from "../channels/turn/types.js";
|
||||
import type {
|
||||
DmGroupAccessDecision,
|
||||
DmGroupAccessReasonCode,
|
||||
} from "../security/dm-policy-shared.js";
|
||||
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
||||
|
||||
export { decideChannelIngress };
|
||||
export type {
|
||||
AccessGraph,
|
||||
AccessGraphGate,
|
||||
AccessGroupMembershipFact,
|
||||
ChannelIngressAdmission,
|
||||
ChannelIngressChannelId,
|
||||
ChannelIngressDecision,
|
||||
ChannelIngressEventInput,
|
||||
ChannelIngressIdentifierKind,
|
||||
ChannelIngressNormalizedEntry,
|
||||
ChannelIngressPolicyInput,
|
||||
ChannelIngressState,
|
||||
IngressGateEffect,
|
||||
IngressGateKind,
|
||||
IngressGatePhase,
|
||||
IngressReasonCode,
|
||||
MatchableIdentifier,
|
||||
RedactedChannelIngressEvent,
|
||||
RedactedIngressAllowlistFacts,
|
||||
RedactedIngressEntryDiagnostic,
|
||||
RedactedIngressMatch,
|
||||
ResolvedIngressAllowlist,
|
||||
ResolvedRouteGateFacts,
|
||||
RouteGateFacts,
|
||||
RouteGateState,
|
||||
RouteSenderAllowlistSource,
|
||||
RouteSenderPolicy,
|
||||
} from "../channels/message-access/index.js";
|
||||
|
||||
export type ChannelIngressSubjectIdentifier = InternalMatchMaterial;
|
||||
export type ChannelIngressSubject = InternalChannelIngressSubject;
|
||||
export type ChannelIngressAdapterEntry = InternalNormalizedEntry;
|
||||
export type ChannelIngressAdapterNormalizeResult = InternalChannelIngressNormalizeResult;
|
||||
export type ChannelIngressAdapter = InternalChannelIngressAdapter;
|
||||
export type ChannelIngressStateInput = MessageAccessChannelIngressStateInput;
|
||||
|
||||
declare const CHANNEL_INGRESS_PLUGIN_ID: unique symbol;
|
||||
|
||||
export type ChannelIngressPluginId = string & {
|
||||
readonly [CHANNEL_INGRESS_PLUGIN_ID]: true;
|
||||
};
|
||||
|
||||
export type ChannelIngressGateSelector = {
|
||||
phase: IngressGatePhase;
|
||||
kind: IngressGateKind;
|
||||
};
|
||||
|
||||
export type ChannelIngressDecisionBundle = {
|
||||
dm: ChannelIngressDecision;
|
||||
group: ChannelIngressDecision;
|
||||
dmCommand: ChannelIngressDecision;
|
||||
groupCommand: ChannelIngressDecision;
|
||||
};
|
||||
|
||||
export type ChannelIngressSideEffectResult =
|
||||
| { kind: "none" }
|
||||
| { kind: "pairing-reply-sent" }
|
||||
| { kind: "pairing-reply-failed"; errorCode?: string }
|
||||
| { kind: "command-reply-sent" }
|
||||
| { kind: "command-reply-failed"; errorCode?: string }
|
||||
| { kind: "pending-history-recorded" }
|
||||
| { kind: "local-event-handled" };
|
||||
|
||||
export type RedactedIngressDiagnostics = {
|
||||
decisiveGateId?: string;
|
||||
reasonCode: IngressReasonCode;
|
||||
};
|
||||
|
||||
export const CHANNEL_INGRESS_GATE_SELECTORS = {
|
||||
command: { phase: "command", kind: "command" },
|
||||
activation: { phase: "activation", kind: "mention" },
|
||||
dmSender: { phase: "sender", kind: "dmSender" },
|
||||
groupSender: { phase: "sender", kind: "groupSender" },
|
||||
event: { phase: "event", kind: "event" },
|
||||
} as const satisfies Record<string, ChannelIngressGateSelector>;
|
||||
|
||||
export type ChannelIngressSubjectIdentifierInput = {
|
||||
value: string;
|
||||
opaqueId?: string;
|
||||
kind?: ChannelIngressIdentifierKind;
|
||||
dangerous?: boolean;
|
||||
sensitivity?: "normal" | "pii";
|
||||
};
|
||||
|
||||
export type CreateChannelIngressStringAdapterParams = {
|
||||
kind?: ChannelIngressIdentifierKind;
|
||||
normalizeEntry?: (value: string) => string | null | undefined;
|
||||
normalizeSubject?: (value: string) => string | null | undefined;
|
||||
isWildcardEntry?: (value: string) => boolean;
|
||||
resolveEntryId?: (params: { entry: string; index: number }) => string;
|
||||
dangerous?: boolean | ((entry: string) => boolean);
|
||||
sensitivity?: "normal" | "pii";
|
||||
};
|
||||
|
||||
export type CreateChannelIngressMultiIdentifierAdapterParams = {
|
||||
normalizeEntry: (entry: string, index: number) => readonly ChannelIngressAdapterEntry[];
|
||||
getEntryMatchKey?: (entry: ChannelIngressAdapterEntry) => string | null | undefined;
|
||||
getSubjectMatchKeys?: (
|
||||
identifier: ChannelIngressSubjectIdentifier,
|
||||
) => readonly (string | null | undefined)[];
|
||||
isWildcardEntry?: (entry: ChannelIngressAdapterEntry) => boolean;
|
||||
};
|
||||
|
||||
export type ChannelIngressDmGroupAccessProjection = {
|
||||
decision: DmGroupAccessDecision;
|
||||
reasonCode: DmGroupAccessReasonCode;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type ChannelIngressSenderGroupAccessProjection = {
|
||||
allowed: boolean;
|
||||
groupPolicy: ChannelIngressPolicyInput["groupPolicy"];
|
||||
providerMissingFallbackApplied: boolean;
|
||||
reason: "allowed" | "disabled" | "empty_allowlist" | "sender_not_allowlisted";
|
||||
};
|
||||
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export type ResolveChannelIngressAccessParams = ChannelIngressStateInput & {
|
||||
policy: ChannelIngressPolicyInput;
|
||||
effectiveAllowFrom?: readonly string[];
|
||||
effectiveGroupAllowFrom?: readonly string[];
|
||||
};
|
||||
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export type ResolvedChannelIngressAccess = {
|
||||
state: ChannelIngressState;
|
||||
ingress: ChannelIngressDecision;
|
||||
isGroup: boolean;
|
||||
senderReasonCode: IngressReasonCode;
|
||||
access: ChannelIngressDmGroupAccessProjection & {
|
||||
effectiveAllowFrom: string[];
|
||||
effectiveGroupAllowFrom: string[];
|
||||
};
|
||||
commandAuthorized: boolean;
|
||||
shouldBlockControlCommand: boolean;
|
||||
};
|
||||
|
||||
function defaultNormalize(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeMatchValue(
|
||||
value: string,
|
||||
normalize: (value: string) => string | null | undefined,
|
||||
): string | null {
|
||||
const normalized = normalize(value);
|
||||
return normalized == null ? null : normalized.trim() || null;
|
||||
}
|
||||
|
||||
function resolveDangerous(
|
||||
dangerous: CreateChannelIngressStringAdapterParams["dangerous"],
|
||||
entry: string,
|
||||
): boolean | undefined {
|
||||
return typeof dangerous === "function" ? dangerous(entry) : dangerous;
|
||||
}
|
||||
|
||||
function defaultIngressMatchKey(params: {
|
||||
kind: ChannelIngressIdentifierKind;
|
||||
value: string;
|
||||
}): string {
|
||||
return `${params.kind}:${params.value}`;
|
||||
}
|
||||
|
||||
export function findChannelIngressGate(
|
||||
decision: ChannelIngressDecision,
|
||||
selector: ChannelIngressGateSelector,
|
||||
): AccessGraphGate | undefined {
|
||||
return decision.graph.gates.find(
|
||||
(gate) => gate.phase === selector.phase && gate.kind === selector.kind,
|
||||
);
|
||||
}
|
||||
|
||||
export function findChannelIngressSenderGate(
|
||||
decision: ChannelIngressDecision,
|
||||
params: { isGroup: boolean },
|
||||
): AccessGraphGate | undefined {
|
||||
return findChannelIngressGate(
|
||||
decision,
|
||||
params.isGroup
|
||||
? CHANNEL_INGRESS_GATE_SELECTORS.groupSender
|
||||
: CHANNEL_INGRESS_GATE_SELECTORS.dmSender,
|
||||
);
|
||||
}
|
||||
|
||||
export function findChannelIngressCommandGate(
|
||||
decision: ChannelIngressDecision,
|
||||
): AccessGraphGate | undefined {
|
||||
return findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.command);
|
||||
}
|
||||
|
||||
export function decideChannelIngressBundle(params: {
|
||||
directState: ChannelIngressState;
|
||||
groupState: ChannelIngressState;
|
||||
basePolicy: ChannelIngressPolicyInput;
|
||||
commandPolicy: ChannelIngressPolicyInput;
|
||||
}): ChannelIngressDecisionBundle {
|
||||
return {
|
||||
dm: decideChannelIngress(params.directState, params.basePolicy),
|
||||
group: decideChannelIngress(params.groupState, params.basePolicy),
|
||||
dmCommand: decideChannelIngress(params.directState, params.commandPolicy),
|
||||
groupCommand: decideChannelIngress(params.groupState, params.commandPolicy),
|
||||
};
|
||||
}
|
||||
|
||||
function projectGroupPolicy(
|
||||
gate: AccessGraphGate | undefined,
|
||||
): NonNullable<AccessFacts["group"]>["policy"] {
|
||||
const policy = gate?.sender?.policy;
|
||||
return policy === "open" || policy === "disabled" ? policy : "allowlist";
|
||||
}
|
||||
|
||||
function projectMentionFacts(gate: AccessGraphGate | undefined): AccessFacts["mentions"] {
|
||||
const activation = gate?.activation;
|
||||
if (!activation?.hasMentionFacts) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
canDetectMention: activation.canDetectMention ?? false,
|
||||
wasMentioned: activation.wasMentioned ?? false,
|
||||
hasAnyMention: activation.hasAnyMention,
|
||||
implicitMentionKinds: activation.implicitMentionKinds
|
||||
? [...activation.implicitMentionKinds]
|
||||
: undefined,
|
||||
requireMention: activation.requireMention,
|
||||
effectiveWasMentioned: activation.effectiveWasMentioned,
|
||||
shouldSkip: activation.shouldSkip,
|
||||
};
|
||||
}
|
||||
|
||||
function projectDmDecision(
|
||||
decision: ChannelIngressDecision,
|
||||
dmSender: AccessGraphGate | undefined,
|
||||
): NonNullable<AccessFacts["dm"]>["decision"] {
|
||||
if (decision.decision === "pairing") {
|
||||
return "pairing";
|
||||
}
|
||||
if (dmSender) {
|
||||
return dmSender.allowed ? "allow" : "deny";
|
||||
}
|
||||
return decision.admission === "drop" ? "deny" : "allow";
|
||||
}
|
||||
|
||||
export function projectIngressAccessFacts(decision: ChannelIngressDecision): AccessFacts {
|
||||
const command = findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.command);
|
||||
const activation = findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.activation);
|
||||
const dmSender = findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.dmSender);
|
||||
const groupSender = findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.groupSender);
|
||||
const event = findChannelIngressGate(decision, CHANNEL_INGRESS_GATE_SELECTORS.event);
|
||||
return {
|
||||
dm: {
|
||||
decision: projectDmDecision(decision, dmSender),
|
||||
reason: dmSender?.reasonCode ?? decision.reasonCode,
|
||||
allowFrom: [],
|
||||
allowlist: dmSender?.allowlist,
|
||||
},
|
||||
group: {
|
||||
policy: projectGroupPolicy(groupSender),
|
||||
routeAllowed: !decision.graph.gates.some(
|
||||
(gate) => gate.phase === "route" && gate.effect === "block-dispatch",
|
||||
),
|
||||
senderAllowed: groupSender?.allowed ?? dmSender?.allowed ?? false,
|
||||
allowFrom: [],
|
||||
requireMention: activation?.activation?.requireMention ?? false,
|
||||
allowlist: groupSender?.allowlist,
|
||||
},
|
||||
commands: command?.command
|
||||
? {
|
||||
authorized: command.allowed,
|
||||
shouldBlockControlCommand: command.command.shouldBlockControlCommand,
|
||||
reasonCode: command.reasonCode,
|
||||
useAccessGroups: command.command.useAccessGroups,
|
||||
allowTextCommands: command.command.allowTextCommands,
|
||||
modeWhenAccessGroupsOff: command.command.modeWhenAccessGroupsOff,
|
||||
authorizers: [],
|
||||
}
|
||||
: undefined,
|
||||
event: event?.event
|
||||
? {
|
||||
...event.event,
|
||||
authorized: event.allowed,
|
||||
reasonCode: event.reasonCode,
|
||||
}
|
||||
: undefined,
|
||||
mentions: projectMentionFacts(activation),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapChannelIngressDecisionToTurnAdmission(
|
||||
decision: ChannelIngressDecision,
|
||||
sideEffect: ChannelIngressSideEffectResult,
|
||||
): ChannelTurnAdmission {
|
||||
if (decision.admission === "dispatch") {
|
||||
return { kind: "dispatch", reason: decision.reasonCode };
|
||||
}
|
||||
if (decision.admission === "observe") {
|
||||
return { kind: "observeOnly", reason: decision.reasonCode };
|
||||
}
|
||||
if (decision.admission === "pairing-required") {
|
||||
return sideEffect.kind === "pairing-reply-sent"
|
||||
? { kind: "handled", reason: decision.reasonCode }
|
||||
: { kind: "drop", reason: decision.reasonCode };
|
||||
}
|
||||
if (decision.admission === "skip") {
|
||||
return sideEffect.kind === "pending-history-recorded" ||
|
||||
sideEffect.kind === "local-event-handled" ||
|
||||
sideEffect.kind === "command-reply-sent"
|
||||
? { kind: "handled", reason: decision.reasonCode }
|
||||
: { kind: "drop", reason: decision.reasonCode, recordHistory: false };
|
||||
}
|
||||
return sideEffect.kind === "local-event-handled" || sideEffect.kind === "command-reply-sent"
|
||||
? { kind: "handled", reason: decision.reasonCode }
|
||||
: { kind: "drop", reason: decision.reasonCode };
|
||||
}
|
||||
|
||||
export function createChannelIngressPluginId(id: string): ChannelIngressPluginId {
|
||||
const trimmed = id.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("Channel ingress plugin id must be non-empty.");
|
||||
}
|
||||
return trimmed as ChannelIngressPluginId;
|
||||
}
|
||||
|
||||
export function createChannelIngressSubject(
|
||||
input:
|
||||
| ChannelIngressSubjectIdentifierInput
|
||||
| { identifiers: readonly ChannelIngressSubjectIdentifierInput[] },
|
||||
): ChannelIngressSubject {
|
||||
const identifiers = "identifiers" in input ? input.identifiers : [input];
|
||||
return {
|
||||
identifiers: identifiers.map((identifier, index) => ({
|
||||
opaqueId: identifier.opaqueId ?? `subject-${index + 1}`,
|
||||
kind: identifier.kind ?? "stable-id",
|
||||
value: identifier.value,
|
||||
dangerous: identifier.dangerous,
|
||||
sensitivity: identifier.sensitivity,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function createChannelIngressStringAdapter(
|
||||
params: CreateChannelIngressStringAdapterParams = {},
|
||||
): ChannelIngressAdapter {
|
||||
const kind = params.kind ?? "stable-id";
|
||||
const normalizeEntry = params.normalizeEntry ?? defaultNormalize;
|
||||
const normalizeSubject = params.normalizeSubject ?? normalizeEntry;
|
||||
const isWildcardEntry = params.isWildcardEntry ?? ((entry: string) => entry === "*");
|
||||
return {
|
||||
normalizeEntries({ entries }) {
|
||||
const matchable = normalizeStringEntries(entries).flatMap((entry, index) => {
|
||||
const value = isWildcardEntry(entry) ? "*" : normalizeMatchValue(entry, normalizeEntry);
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
opaqueEntryId: params.resolveEntryId?.({ entry, index }) ?? `entry-${index + 1}`,
|
||||
kind,
|
||||
value,
|
||||
dangerous: resolveDangerous(params.dangerous, entry),
|
||||
sensitivity: params.sensitivity,
|
||||
},
|
||||
];
|
||||
});
|
||||
return {
|
||||
matchable,
|
||||
invalid: [],
|
||||
disabled: [],
|
||||
};
|
||||
},
|
||||
matchSubject({ subject, entries }) {
|
||||
const values = new Set(
|
||||
subject.identifiers.flatMap((identifier) => {
|
||||
if (identifier.kind !== kind) {
|
||||
return [];
|
||||
}
|
||||
const value = normalizeMatchValue(identifier.value, normalizeSubject);
|
||||
return value ? [value] : [];
|
||||
}),
|
||||
);
|
||||
const matchedEntryIds = entries
|
||||
.filter((entry) => entry.kind === kind && (entry.value === "*" || values.has(entry.value)))
|
||||
.map((entry) => entry.opaqueEntryId);
|
||||
return {
|
||||
matched: matchedEntryIds.length > 0,
|
||||
matchedEntryIds,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createChannelIngressMultiIdentifierAdapter(
|
||||
params: CreateChannelIngressMultiIdentifierAdapterParams,
|
||||
): ChannelIngressAdapter {
|
||||
const getEntryMatchKey = params.getEntryMatchKey ?? defaultIngressMatchKey;
|
||||
const getSubjectMatchKeys =
|
||||
params.getSubjectMatchKeys ??
|
||||
((identifier: ChannelIngressSubjectIdentifier) => [defaultIngressMatchKey(identifier)]);
|
||||
const isWildcardEntry = params.isWildcardEntry ?? ((entry) => entry.value === "*");
|
||||
return {
|
||||
normalizeEntries({ entries }) {
|
||||
return {
|
||||
matchable: entries.flatMap((entry, index) => params.normalizeEntry(entry, index)),
|
||||
invalid: [],
|
||||
disabled: [],
|
||||
};
|
||||
},
|
||||
matchSubject({ subject, entries }) {
|
||||
const subjectKeys = new Set(
|
||||
subject.identifiers.flatMap((identifier) =>
|
||||
getSubjectMatchKeys(identifier).filter((key): key is string => Boolean(key)),
|
||||
),
|
||||
);
|
||||
const matchedEntryIds = entries
|
||||
.filter((entry) => {
|
||||
if (isWildcardEntry(entry)) {
|
||||
return true;
|
||||
}
|
||||
const key = getEntryMatchKey(entry);
|
||||
return key ? subjectKeys.has(key) : false;
|
||||
})
|
||||
.map((entry) => entry.opaqueEntryId);
|
||||
return {
|
||||
matched: matchedEntryIds.length > 0,
|
||||
matchedEntryIds,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function assertNeverChannelIngressReason(reasonCode: never): never {
|
||||
throw new Error(`Unhandled channel ingress reason code: ${String(reasonCode)}`);
|
||||
}
|
||||
|
||||
/** @deprecated Use `senderAccess.reasonCode` from `resolveChannelMessageIngress(...)` or typed gate selectors. */
|
||||
export function findChannelIngressSenderReasonCode(
|
||||
decision: ChannelIngressDecision,
|
||||
params: { isGroup: boolean },
|
||||
): IngressReasonCode {
|
||||
return findChannelIngressSenderGate(decision, params)?.reasonCode ?? decision.reasonCode;
|
||||
}
|
||||
|
||||
/** @deprecated Use `senderAccess.reasonCode` from `resolveChannelMessageIngress(...)`. */
|
||||
export function mapChannelIngressReasonCodeToDmGroupAccessReason(params: {
|
||||
reasonCode: IngressReasonCode;
|
||||
isGroup: boolean;
|
||||
}): DmGroupAccessReasonCode {
|
||||
switch (params.reasonCode) {
|
||||
case "group_policy_open":
|
||||
case "group_policy_allowed":
|
||||
return "group_policy_allowed";
|
||||
case "group_policy_disabled":
|
||||
return "group_policy_disabled";
|
||||
case "route_sender_empty":
|
||||
case "group_policy_empty_allowlist":
|
||||
return "group_policy_empty_allowlist";
|
||||
case "group_policy_not_allowlisted":
|
||||
return "group_policy_not_allowlisted";
|
||||
case "dm_policy_open":
|
||||
return "dm_policy_open";
|
||||
case "dm_policy_disabled":
|
||||
return "dm_policy_disabled";
|
||||
case "dm_policy_allowlisted":
|
||||
return "dm_policy_allowlisted";
|
||||
case "dm_policy_pairing_required":
|
||||
return "dm_policy_pairing_required";
|
||||
default:
|
||||
return params.isGroup ? "group_policy_not_allowlisted" : "dm_policy_not_allowlisted";
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use `senderAccess.reason` from `resolveChannelMessageIngress(...)`. */
|
||||
export function formatChannelIngressPolicyReason(params: {
|
||||
reasonCode: DmGroupAccessReasonCode;
|
||||
dmPolicy: string;
|
||||
groupPolicy: string;
|
||||
}): string {
|
||||
switch (params.reasonCode) {
|
||||
case "group_policy_allowed":
|
||||
return `groupPolicy=${params.groupPolicy}`;
|
||||
case "group_policy_disabled":
|
||||
return "groupPolicy=disabled";
|
||||
case "group_policy_empty_allowlist":
|
||||
return "groupPolicy=allowlist (empty allowlist)";
|
||||
case "group_policy_not_allowlisted":
|
||||
return "groupPolicy=allowlist (not allowlisted)";
|
||||
case "dm_policy_open":
|
||||
return "dmPolicy=open";
|
||||
case "dm_policy_disabled":
|
||||
return "dmPolicy=disabled";
|
||||
case "dm_policy_allowlisted":
|
||||
return `dmPolicy=${params.dmPolicy} (allowlisted)`;
|
||||
case "dm_policy_pairing_required":
|
||||
return "dmPolicy=pairing (not allowlisted)";
|
||||
case "dm_policy_not_allowlisted":
|
||||
return `dmPolicy=${params.dmPolicy} (not allowlisted)`;
|
||||
}
|
||||
const exhaustive: never = params.reasonCode;
|
||||
return exhaustive;
|
||||
}
|
||||
|
||||
/** @deprecated Use `senderAccess.groupAccess` from `resolveChannelMessageIngress(...)`. */
|
||||
export function projectChannelIngressSenderGroupAccess(params: {
|
||||
reasonCode: IngressReasonCode;
|
||||
decisionAllowed: boolean;
|
||||
groupPolicy: ChannelIngressPolicyInput["groupPolicy"];
|
||||
providerMissingFallbackApplied?: boolean;
|
||||
}): ChannelIngressSenderGroupAccessProjection {
|
||||
const reasonCode = mapChannelIngressReasonCodeToDmGroupAccessReason({
|
||||
reasonCode: params.reasonCode,
|
||||
isGroup: true,
|
||||
});
|
||||
const reason =
|
||||
params.groupPolicy === "disabled" || reasonCode === "group_policy_disabled"
|
||||
? "disabled"
|
||||
: reasonCode === "group_policy_empty_allowlist"
|
||||
? "empty_allowlist"
|
||||
: reasonCode === "group_policy_not_allowlisted"
|
||||
? "sender_not_allowlisted"
|
||||
: "allowed";
|
||||
return {
|
||||
allowed: reason === "allowed" && params.decisionAllowed,
|
||||
groupPolicy: params.groupPolicy,
|
||||
providerMissingFallbackApplied: params.providerMissingFallbackApplied ?? false,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
|
||||
/** @deprecated Use `senderAccess` from `resolveChannelMessageIngress(...)`. */
|
||||
export function projectChannelIngressDmGroupAccess(params: {
|
||||
ingress: ChannelIngressDecision;
|
||||
isGroup: boolean;
|
||||
dmPolicy: string;
|
||||
groupPolicy: string;
|
||||
}): ChannelIngressDmGroupAccessProjection {
|
||||
const reasonCode = mapChannelIngressReasonCodeToDmGroupAccessReason({
|
||||
reasonCode: findChannelIngressSenderReasonCode(params.ingress, { isGroup: params.isGroup }),
|
||||
isGroup: params.isGroup,
|
||||
});
|
||||
const decision: DmGroupAccessDecision =
|
||||
reasonCode === "dm_policy_pairing_required"
|
||||
? "pairing"
|
||||
: params.ingress.decision === "allow"
|
||||
? "allow"
|
||||
: "block";
|
||||
const reason = formatChannelIngressPolicyReason({
|
||||
reasonCode,
|
||||
dmPolicy: params.dmPolicy,
|
||||
groupPolicy: params.groupPolicy,
|
||||
});
|
||||
return {
|
||||
decision,
|
||||
reasonCode,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveChannelIngressState(
|
||||
input: ChannelIngressStateInput,
|
||||
): Promise<ChannelIngressState> {
|
||||
return await resolveChannelIngressStateInternal(input);
|
||||
}
|
||||
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export async function resolveChannelIngressAccess(
|
||||
params: ResolveChannelIngressAccessParams,
|
||||
): Promise<ResolvedChannelIngressAccess> {
|
||||
const { policy, effectiveAllowFrom, effectiveGroupAllowFrom, ...stateInput } = params;
|
||||
const state = await resolveChannelIngressState(stateInput);
|
||||
const ingress = decideChannelIngress(state, policy);
|
||||
const isGroup = params.conversation.kind !== "direct";
|
||||
const senderReasonCode = findChannelIngressSenderReasonCode(ingress, { isGroup });
|
||||
const access = projectChannelIngressDmGroupAccess({
|
||||
ingress,
|
||||
isGroup,
|
||||
dmPolicy: policy.dmPolicy,
|
||||
groupPolicy: policy.groupPolicy,
|
||||
});
|
||||
const commandGate = findChannelIngressCommandGate(ingress);
|
||||
return {
|
||||
state,
|
||||
ingress,
|
||||
isGroup,
|
||||
senderReasonCode,
|
||||
access: {
|
||||
...access,
|
||||
effectiveAllowFrom: [...(effectiveAllowFrom ?? [])],
|
||||
effectiveGroupAllowFrom: [...(effectiveGroupAllowFrom ?? [])],
|
||||
},
|
||||
commandAuthorized: commandGate?.allowed === true,
|
||||
shouldBlockControlCommand: commandGate?.command?.shouldBlockControlCommand === true,
|
||||
};
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export {
|
||||
resolveDmGroupAccessWithLists,
|
||||
resolveEffectiveAllowFromLists,
|
||||
resolveOpenDmAllowlistAccess,
|
||||
} from "../security/dm-policy-shared.js";
|
||||
} from "./channel-access-compat.js";
|
||||
export {
|
||||
evaluateGroupRouteAccessForPolicy,
|
||||
evaluateSenderGroupAccessForPolicy,
|
||||
|
||||
@@ -2,12 +2,26 @@ export {
|
||||
buildCommandTextFromArgs,
|
||||
findCommandByNativeName,
|
||||
formatCommandArgMenuTitle,
|
||||
listChatCommands,
|
||||
listNativeCommandSpecs,
|
||||
listNativeCommandSpecsForConfig,
|
||||
maybeResolveTextAlias,
|
||||
normalizeCommandBody,
|
||||
parseCommandArgs,
|
||||
serializeCommandArgs,
|
||||
resolveCommandArgMenu,
|
||||
} from "../auto-reply/commands-registry.js";
|
||||
export type { CommandArgs } from "../auto-reply/commands-registry.js";
|
||||
export type {
|
||||
ChatCommandDefinition,
|
||||
CommandArgDefinition,
|
||||
CommandArgValues,
|
||||
CommandArgs,
|
||||
NativeCommandSpec,
|
||||
} from "../auto-reply/commands-registry.js";
|
||||
export {
|
||||
hasControlCommand,
|
||||
shouldComputeCommandAuthorized,
|
||||
} from "../auto-reply/command-detection.js";
|
||||
export {
|
||||
resolveCommandAuthorizedFromAuthorizers,
|
||||
resolveControlCommandGate,
|
||||
@@ -18,3 +32,6 @@ export {
|
||||
type CommandAuthorization,
|
||||
} from "../auto-reply/command-auth.js";
|
||||
export { resolveStoredModelOverride } from "../auto-reply/reply/stored-model-override.js";
|
||||
export type { ModelsProviderData } from "../auto-reply/reply/commands-models.js";
|
||||
export { listSkillCommandsForAgents } from "../auto-reply/skill-commands.js";
|
||||
export { listProviderPluginCommandSpecs } from "../plugins/command-specs.js";
|
||||
|
||||
@@ -5,17 +5,20 @@ import {
|
||||
} from "../auto-reply/command-status-builders.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.public.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveDmGroupAccessWithLists } from "../security/dm-policy-shared.js";
|
||||
import {
|
||||
expandAllowFromWithAccessGroups,
|
||||
type AccessGroupMembershipResolver,
|
||||
} from "./access-groups.js";
|
||||
import { resolveDmGroupAccessWithLists } from "./channel-access-compat.js";
|
||||
export {
|
||||
ACCESS_GROUP_ALLOW_FROM_PREFIX,
|
||||
expandAllowFromWithAccessGroups,
|
||||
parseAccessGroupAllowFromEntry,
|
||||
resolveAccessGroupAllowFromMatches,
|
||||
resolveAccessGroupAllowFromState,
|
||||
type AccessGroupMembershipResolver,
|
||||
type AccessGroupMembershipLookup,
|
||||
type ResolvedAccessGroupAllowFromState,
|
||||
} from "./access-groups.js";
|
||||
export { buildCommandsPaginationKeyboard } from "./telegram-command-ui.js";
|
||||
export {
|
||||
@@ -100,6 +103,7 @@ export type { ModelsProviderData } from "../auto-reply/reply/commands-models.js"
|
||||
export { resolveStoredModelOverride } from "../auto-reply/reply/stored-model-override.js";
|
||||
export type { StoredModelOverride } from "../auto-reply/reply/stored-model-override.js";
|
||||
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export type ResolveSenderCommandAuthorizationParams = {
|
||||
cfg: OpenClawConfig;
|
||||
rawBody: string;
|
||||
@@ -114,12 +118,14 @@ export type ResolveSenderCommandAuthorizationParams = {
|
||||
resolveAccessGroupMembership?: AccessGroupMembershipResolver;
|
||||
readAllowFromStore: () => Promise<string[]>;
|
||||
shouldComputeCommandAuthorized: (rawBody: string, cfg: OpenClawConfig) => boolean;
|
||||
resolveCommandAuthorizedFromAuthorizers: (params: {
|
||||
/** @deprecated Command authorization is resolved by channel ingress. Kept for runtime injection compatibility. */
|
||||
resolveCommandAuthorizedFromAuthorizers?: (params: {
|
||||
useAccessGroups: boolean;
|
||||
authorizers: Array<{ configured: boolean; allowed: boolean }>;
|
||||
}) => boolean;
|
||||
};
|
||||
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export type CommandAuthorizationRuntime = {
|
||||
shouldComputeCommandAuthorized: (rawBody: string, cfg: OpenClawConfig) => boolean;
|
||||
resolveCommandAuthorizedFromAuthorizers: (params: {
|
||||
@@ -128,6 +134,7 @@ export type CommandAuthorizationRuntime = {
|
||||
}) => boolean;
|
||||
};
|
||||
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export type ResolveSenderCommandAuthorizationWithRuntimeParams = Omit<
|
||||
ResolveSenderCommandAuthorizationParams,
|
||||
"shouldComputeCommandAuthorized" | "resolveCommandAuthorizedFromAuthorizers"
|
||||
@@ -135,7 +142,7 @@ export type ResolveSenderCommandAuthorizationWithRuntimeParams = Omit<
|
||||
runtime: CommandAuthorizationRuntime;
|
||||
};
|
||||
|
||||
/** Fast-path DM command authorization when only policy and sender allowlist state matter. */
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export function resolveDirectDmAuthorizationOutcome(params: {
|
||||
isGroup: boolean;
|
||||
dmPolicy: string;
|
||||
@@ -153,7 +160,7 @@ export function resolveDirectDmAuthorizationOutcome(params: {
|
||||
return "allowed";
|
||||
}
|
||||
|
||||
/** Runtime-backed wrapper around sender command authorization for grouped helper surfaces. */
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export async function resolveSenderCommandAuthorizationWithRuntime(
|
||||
params: ResolveSenderCommandAuthorizationWithRuntimeParams,
|
||||
): ReturnType<typeof resolveSenderCommandAuthorization> {
|
||||
@@ -164,7 +171,7 @@ export async function resolveSenderCommandAuthorizationWithRuntime(
|
||||
});
|
||||
}
|
||||
|
||||
/** Compute effective allowlists and command authorization for one inbound sender. */
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export async function resolveSenderCommandAuthorization(
|
||||
params: ResolveSenderCommandAuthorizationParams,
|
||||
): Promise<{
|
||||
@@ -236,13 +243,13 @@ export async function resolveSenderCommandAuthorization(
|
||||
const ownerAllowedForCommands = params.isSenderAllowed(params.senderId, effectiveAllowFrom);
|
||||
const groupAllowedForCommands = params.isSenderAllowed(params.senderId, effectiveGroupAllowFrom);
|
||||
const commandAuthorized = shouldComputeAuth
|
||||
? params.resolveCommandAuthorizedFromAuthorizers({
|
||||
? (params.resolveCommandAuthorizedFromAuthorizers?.({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: effectiveAllowFrom.length > 0, allowed: ownerAllowedForCommands },
|
||||
{ configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands },
|
||||
],
|
||||
})
|
||||
}) ?? senderAllowedForCommands)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
|
||||
@@ -109,4 +109,4 @@ export {
|
||||
resolvePluginConversationBindingApproval,
|
||||
toPluginConversationBinding,
|
||||
} from "../plugins/conversation-binding.js";
|
||||
export { resolvePinnedMainDmOwnerFromAllowlist } from "../security/dm-policy-shared.js";
|
||||
export { resolvePinnedMainDmOwnerFromAllowlist } from "./channel-access-compat.js";
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import type { ChannelId } from "../channels/plugins/types.public.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
readStoreAllowFromForDmPolicy,
|
||||
resolveDmGroupAccessWithLists,
|
||||
type DmGroupAccessReasonCode,
|
||||
} from "../security/dm-policy-shared.js";
|
||||
import {
|
||||
expandAllowFromWithAccessGroups,
|
||||
type AccessGroupMembershipResolver,
|
||||
} from "./access-groups.js";
|
||||
import { DM_GROUP_ACCESS_REASON, type DmGroupAccessReasonCode } from "./channel-access-compat.js";
|
||||
import {
|
||||
readStoreAllowFromForDmPolicy,
|
||||
resolveDmGroupAccessWithLists,
|
||||
} from "./channel-access-compat.js";
|
||||
export type { AccessGroupMembershipResolver } from "./access-groups.js";
|
||||
|
||||
export type DirectDmCommandAuthorizationRuntime = {
|
||||
shouldComputeCommandAuthorized: (rawBody: string, cfg: OpenClawConfig) => boolean;
|
||||
resolveCommandAuthorizedFromAuthorizers: (params: {
|
||||
/** @deprecated Command authorization is resolved by channel ingress. Kept for runtime injection compatibility. */
|
||||
resolveCommandAuthorizedFromAuthorizers?: (params: {
|
||||
useAccessGroups: boolean;
|
||||
authorizers: Array<{ configured: boolean; allowed: boolean }>;
|
||||
modeWhenAccessGroupsOff?: "allow" | "deny" | "configured";
|
||||
}) => boolean;
|
||||
};
|
||||
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export type ResolvedInboundDirectDmAccess = {
|
||||
access: {
|
||||
decision: "allow" | "block" | "pairing";
|
||||
@@ -32,7 +34,20 @@ export type ResolvedInboundDirectDmAccess = {
|
||||
commandAuthorized: boolean | undefined;
|
||||
};
|
||||
|
||||
/** Resolve direct-DM policy, effective allowlists, and optional command auth in one place. */
|
||||
function toLegacyDmReasonCode(reasonCode: string): DmGroupAccessReasonCode {
|
||||
switch (reasonCode) {
|
||||
case DM_GROUP_ACCESS_REASON.DM_POLICY_OPEN:
|
||||
case DM_GROUP_ACCESS_REASON.DM_POLICY_DISABLED:
|
||||
case DM_GROUP_ACCESS_REASON.DM_POLICY_ALLOWLISTED:
|
||||
case DM_GROUP_ACCESS_REASON.DM_POLICY_PAIRING_REQUIRED:
|
||||
case DM_GROUP_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED:
|
||||
return reasonCode;
|
||||
default:
|
||||
return DM_GROUP_ACCESS_REASON.DM_POLICY_NOT_ALLOWLISTED;
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export async function resolveInboundDirectDmAccessWithRuntime(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel: ChannelId;
|
||||
@@ -48,6 +63,10 @@ export async function resolveInboundDirectDmAccessWithRuntime(params: {
|
||||
readStoreAllowFrom?: (provider: ChannelId, accountId: string) => Promise<string[]>;
|
||||
}): Promise<ResolvedInboundDirectDmAccess> {
|
||||
const dmPolicy = params.dmPolicy ?? "pairing";
|
||||
const shouldComputeAuth = params.runtime.shouldComputeCommandAuthorized(
|
||||
params.rawBody,
|
||||
params.cfg,
|
||||
);
|
||||
const storeAllowFrom =
|
||||
dmPolicy === "pairing"
|
||||
? await readStoreAllowFromForDmPolicy({
|
||||
@@ -77,7 +96,6 @@ export async function resolveInboundDirectDmAccessWithRuntime(params: {
|
||||
resolveMembership: params.resolveAccessGroupMembership,
|
||||
}),
|
||||
]);
|
||||
|
||||
const access = resolveDmGroupAccessWithLists({
|
||||
isGroup: false,
|
||||
dmPolicy,
|
||||
@@ -86,17 +104,13 @@ export async function resolveInboundDirectDmAccessWithRuntime(params: {
|
||||
groupAllowFromFallbackToAllowFrom: false,
|
||||
isSenderAllowed: (allowEntries) => params.isSenderAllowed(params.senderId, allowEntries),
|
||||
});
|
||||
|
||||
const shouldComputeAuth = params.runtime.shouldComputeCommandAuthorized(
|
||||
params.rawBody,
|
||||
params.cfg,
|
||||
);
|
||||
const reasonCode = toLegacyDmReasonCode(access.reasonCode);
|
||||
const senderAllowedForCommands = params.isSenderAllowed(
|
||||
params.senderId,
|
||||
access.effectiveAllowFrom,
|
||||
);
|
||||
const commandAuthorized = shouldComputeAuth
|
||||
? params.runtime.resolveCommandAuthorizedFromAuthorizers({
|
||||
? (params.runtime.resolveCommandAuthorizedFromAuthorizers?.({
|
||||
useAccessGroups: params.cfg.commands?.useAccessGroups !== false,
|
||||
authorizers: [
|
||||
{
|
||||
@@ -105,13 +119,13 @@ export async function resolveInboundDirectDmAccessWithRuntime(params: {
|
||||
},
|
||||
],
|
||||
modeWhenAccessGroupsOff: params.modeWhenAccessGroupsOff,
|
||||
})
|
||||
}) ?? senderAllowedForCommands)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
access: {
|
||||
decision: access.decision,
|
||||
reasonCode: access.reasonCode,
|
||||
reasonCode,
|
||||
reason: access.reason,
|
||||
effectiveAllowFrom: access.effectiveAllowFrom,
|
||||
},
|
||||
@@ -121,7 +135,7 @@ export async function resolveInboundDirectDmAccessWithRuntime(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert resolved DM policy into a pre-crypto allow/block/pairing callback. */
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export function createPreCryptoDirectDmAuthorizer(params: {
|
||||
resolveAccess: (
|
||||
senderId: string,
|
||||
|
||||
@@ -9,41 +9,36 @@ export type SenderGroupAccessReason =
|
||||
| "disabled"
|
||||
| "empty_allowlist"
|
||||
| "sender_not_allowlisted";
|
||||
|
||||
export type SenderGroupAccessDecision = {
|
||||
allowed: boolean;
|
||||
groupPolicy: GroupPolicy;
|
||||
providerMissingFallbackApplied: boolean;
|
||||
reason: SenderGroupAccessReason;
|
||||
};
|
||||
|
||||
export type GroupRouteAccessReason =
|
||||
| "allowed"
|
||||
| "disabled"
|
||||
| "empty_allowlist"
|
||||
| "route_not_allowlisted"
|
||||
| "route_disabled";
|
||||
|
||||
export type GroupRouteAccessDecision = {
|
||||
allowed: boolean;
|
||||
groupPolicy: GroupPolicy;
|
||||
reason: GroupRouteAccessReason;
|
||||
};
|
||||
|
||||
export type MatchedGroupAccessReason =
|
||||
| "allowed"
|
||||
| "disabled"
|
||||
| "missing_match_input"
|
||||
| "empty_allowlist"
|
||||
| "not_allowlisted";
|
||||
|
||||
export type MatchedGroupAccessDecision = {
|
||||
allowed: boolean;
|
||||
groupPolicy: GroupPolicy;
|
||||
reason: MatchedGroupAccessReason;
|
||||
};
|
||||
|
||||
/** Downgrade sender-scoped group policy to open mode when no allowlist is configured. */
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export function resolveSenderScopedGroupPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
groupAllowFrom: string[];
|
||||
@@ -54,7 +49,7 @@ export function resolveSenderScopedGroupPolicy(params: {
|
||||
return params.groupAllowFrom.length > 0 ? "allowlist" : "open";
|
||||
}
|
||||
|
||||
/** Evaluate route-level group access after policy, route match, and enablement checks. */
|
||||
/** @deprecated Use route descriptors with `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export function evaluateGroupRouteAccessForPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
routeAllowlistConfigured: boolean;
|
||||
@@ -62,46 +57,23 @@ export function evaluateGroupRouteAccessForPolicy(params: {
|
||||
routeEnabled?: boolean;
|
||||
}): GroupRouteAccessDecision {
|
||||
if (params.groupPolicy === "disabled") {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
reason: "disabled",
|
||||
};
|
||||
return { allowed: false, groupPolicy: params.groupPolicy, reason: "disabled" };
|
||||
}
|
||||
|
||||
if (params.routeMatched && params.routeEnabled === false) {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
reason: "route_disabled",
|
||||
};
|
||||
return { allowed: false, groupPolicy: params.groupPolicy, reason: "route_disabled" };
|
||||
}
|
||||
|
||||
if (params.groupPolicy === "allowlist") {
|
||||
if (!params.routeAllowlistConfigured) {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
reason: "empty_allowlist",
|
||||
};
|
||||
return { allowed: false, groupPolicy: params.groupPolicy, reason: "empty_allowlist" };
|
||||
}
|
||||
if (!params.routeMatched) {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
reason: "route_not_allowlisted",
|
||||
};
|
||||
return { allowed: false, groupPolicy: params.groupPolicy, reason: "route_not_allowlisted" };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
groupPolicy: params.groupPolicy,
|
||||
reason: "allowed",
|
||||
};
|
||||
return { allowed: true, groupPolicy: params.groupPolicy, reason: "allowed" };
|
||||
}
|
||||
|
||||
/** Evaluate generic allowlist match state for channels that compare derived group identifiers. */
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export function evaluateMatchedGroupAccessForPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
allowlistConfigured: boolean;
|
||||
@@ -110,45 +82,23 @@ export function evaluateMatchedGroupAccessForPolicy(params: {
|
||||
hasMatchInput?: boolean;
|
||||
}): MatchedGroupAccessDecision {
|
||||
if (params.groupPolicy === "disabled") {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
reason: "disabled",
|
||||
};
|
||||
return { allowed: false, groupPolicy: params.groupPolicy, reason: "disabled" };
|
||||
}
|
||||
|
||||
if (params.groupPolicy === "allowlist") {
|
||||
if (params.requireMatchInput && !params.hasMatchInput) {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
reason: "missing_match_input",
|
||||
};
|
||||
return { allowed: false, groupPolicy: params.groupPolicy, reason: "missing_match_input" };
|
||||
}
|
||||
if (!params.allowlistConfigured) {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
reason: "empty_allowlist",
|
||||
};
|
||||
return { allowed: false, groupPolicy: params.groupPolicy, reason: "empty_allowlist" };
|
||||
}
|
||||
if (!params.allowlistMatched) {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
reason: "not_allowlisted",
|
||||
};
|
||||
return { allowed: false, groupPolicy: params.groupPolicy, reason: "not_allowlisted" };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
groupPolicy: params.groupPolicy,
|
||||
reason: "allowed",
|
||||
};
|
||||
return { allowed: true, groupPolicy: params.groupPolicy, reason: "allowed" };
|
||||
}
|
||||
|
||||
/** Evaluate sender access for an already-resolved group policy and allowlist. */
|
||||
/** @deprecated Use `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export function evaluateSenderGroupAccessForPolicy(params: {
|
||||
groupPolicy: GroupPolicy;
|
||||
providerMissingFallbackApplied?: boolean;
|
||||
@@ -156,11 +106,12 @@ export function evaluateSenderGroupAccessForPolicy(params: {
|
||||
senderId: string;
|
||||
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
|
||||
}): SenderGroupAccessDecision {
|
||||
const providerMissingFallbackApplied = Boolean(params.providerMissingFallbackApplied);
|
||||
if (params.groupPolicy === "disabled") {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
||||
providerMissingFallbackApplied,
|
||||
reason: "disabled",
|
||||
};
|
||||
}
|
||||
@@ -169,7 +120,7 @@ export function evaluateSenderGroupAccessForPolicy(params: {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
||||
providerMissingFallbackApplied,
|
||||
reason: "empty_allowlist",
|
||||
};
|
||||
}
|
||||
@@ -177,21 +128,20 @@ export function evaluateSenderGroupAccessForPolicy(params: {
|
||||
return {
|
||||
allowed: false,
|
||||
groupPolicy: params.groupPolicy,
|
||||
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
||||
providerMissingFallbackApplied,
|
||||
reason: "sender_not_allowlisted",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
groupPolicy: params.groupPolicy,
|
||||
providerMissingFallbackApplied: Boolean(params.providerMissingFallbackApplied),
|
||||
providerMissingFallbackApplied,
|
||||
reason: "allowed",
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve provider fallback policy first, then evaluate sender access against that result. */
|
||||
/** @deprecated Use `resolveOpenProviderRuntimeGroupPolicy` plus `resolveChannelMessageIngress` from `openclaw/plugin-sdk/channel-ingress-runtime`. */
|
||||
export function evaluateSenderGroupAccess(params: {
|
||||
providerConfigPresent: boolean;
|
||||
configuredGroupPolicy?: GroupPolicy;
|
||||
|
||||
@@ -4,7 +4,7 @@ type RouteLike = {
|
||||
};
|
||||
|
||||
type RoutePeerLike = {
|
||||
kind: string;
|
||||
kind: "direct" | "group" | "channel";
|
||||
id: string | number;
|
||||
};
|
||||
|
||||
|
||||
@@ -8,13 +8,16 @@ export * from "../secrets/shared.js";
|
||||
export type * from "../secrets/target-registry-types.js";
|
||||
export * from "../security/channel-metadata.js";
|
||||
export * from "../security/context-visibility.js";
|
||||
export * from "../security/dm-policy-shared.js";
|
||||
export * from "./channel-access-compat.js";
|
||||
export {
|
||||
ACCESS_GROUP_ALLOW_FROM_PREFIX,
|
||||
expandAllowFromWithAccessGroups,
|
||||
parseAccessGroupAllowFromEntry,
|
||||
resolveAccessGroupAllowFromMatches,
|
||||
resolveAccessGroupAllowFromState,
|
||||
type AccessGroupMembershipResolver,
|
||||
type AccessGroupMembershipLookup,
|
||||
type ResolvedAccessGroupAllowFromState,
|
||||
} from "./access-groups.js";
|
||||
export * from "../security/external-content.js";
|
||||
export * from "../security/safe-regex.js";
|
||||
|
||||
@@ -265,7 +265,9 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
|
||||
OriginatingChannel: params.channel,
|
||||
OriginatingTo: params.reply.originatingTo,
|
||||
CommandAuthorized: params.access?.commands
|
||||
? params.access.commands.authorizers.some((entry) => entry.allowed)
|
||||
? (params.access.commands.authorized ??
|
||||
params.access.commands.authorizers?.some((entry) => entry.allowed) ??
|
||||
false)
|
||||
: false,
|
||||
...params.extra,
|
||||
}) as ReturnType<PluginRuntime["channel"]["turn"]["buildContext"]>,
|
||||
@@ -636,6 +638,8 @@ export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> =
|
||||
},
|
||||
turn: {
|
||||
run: runChannelTurnMock,
|
||||
runAssembled:
|
||||
dispatchAssembledChannelTurnMock as unknown as PluginRuntime["channel"]["turn"]["runAssembled"],
|
||||
runResolved: vi.fn(
|
||||
async (params: Parameters<PluginRuntime["channel"]["turn"]["runResolved"]>[0]) =>
|
||||
await runChannelTurnMock({
|
||||
|
||||
Reference in New Issue
Block a user