Skip to content

Commit e2524e0

Browse files
committed
fix(ci): break plugin import cycles
1 parent 58bab0c commit e2524e0

24 files changed

Lines changed: 364 additions & 330 deletions

src/agents/acp-spawn.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import {
2424
resolveChannelDefaultBindingPlacement,
2525
resolveInboundConversationResolution,
2626
} from "../channels/conversation-resolution.js";
27-
import { routeFromBindingRecord, routeToDeliveryFields } from "../channels/route-projection.js";
27+
import {
28+
formatConversationTarget,
29+
routeFromBindingRecord,
30+
routeToDeliveryFields,
31+
} from "../channels/route-projection.js";
2832
import {
2933
resolveThreadBindingIntroText,
3034
resolveThreadBindingThreadName,
@@ -65,11 +69,7 @@ import {
6569
} from "../routing/session-key.js";
6670
import { createRunningTaskRun } from "../tasks/detached-task-runtime.js";
6771
import { listTasksForOwnerKey } from "../tasks/runtime-internal.js";
68-
import {
69-
deliveryContextFromSession,
70-
formatConversationTarget,
71-
normalizeDeliveryContext,
72-
} from "../utils/delivery-context.js";
72+
import { deliveryContextFromSession, normalizeDeliveryContext } from "../utils/delivery-context.js";
7373
import {
7474
type AcpSpawnParentRelayHandle,
7575
resolveAcpSpawnStreamLogPath,

src/channels/message-access/dm-allow-state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
77
import type { ChannelId } from "../plugins/types.public.js";
8-
import { readChannelIngressStoreAllowFromForDmPolicy } from "./runtime.js";
8+
import { readChannelIngressStoreAllowFromForDmPolicy } from "./store-allow-from.js";
99

1010
export async function resolveDmAllowAuditState(params: {
1111
provider: ChannelId;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
2+
import { mergeDmAllowFromSources, resolveGroupAllowFromSources } from "../allow-from.js";
3+
4+
/**
5+
* Merge configured direct, group, and pairing-store allowlists into the
6+
* effective lists consumed by sender and context-visibility checks.
7+
*/
8+
export function resolveChannelIngressEffectiveAllowFromLists(params: {
9+
allowFrom?: Array<string | number> | null;
10+
groupAllowFrom?: Array<string | number> | null;
11+
storeAllowFrom?: Array<string | number> | null;
12+
dmPolicy?: string | null;
13+
groupAllowFromFallbackToAllowFrom?: boolean | null;
14+
}): {
15+
effectiveAllowFrom: string[];
16+
effectiveGroupAllowFrom: string[];
17+
} {
18+
const allowFrom = Array.isArray(params.allowFrom) ? params.allowFrom : undefined;
19+
const groupAllowFrom = Array.isArray(params.groupAllowFrom) ? params.groupAllowFrom : undefined;
20+
const storeAllowFrom = Array.isArray(params.storeAllowFrom) ? params.storeAllowFrom : undefined;
21+
const effectiveAllowFrom = normalizeStringEntries(
22+
mergeDmAllowFromSources({
23+
allowFrom,
24+
storeAllowFrom,
25+
dmPolicy: params.dmPolicy ?? undefined,
26+
}),
27+
);
28+
const effectiveGroupAllowFrom = normalizeStringEntries(
29+
resolveGroupAllowFromSources({
30+
allowFrom,
31+
groupAllowFrom,
32+
fallbackToAllowFrom: params.groupAllowFromFallbackToAllowFrom ?? undefined,
33+
}),
34+
);
35+
return { effectiveAllowFrom, effectiveGroupAllowFrom };
36+
}

src/channels/message-access/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ export { defineStableChannelIngressIdentity } from "./runtime-identity.js";
55
export {
66
channelIngressRoutes,
77
createChannelIngressResolver,
8-
readChannelIngressStoreAllowFromForDmPolicy,
98
resolveChannelMessageIngress,
109
resolveStableChannelMessageIngress,
1110
} from "./runtime.js";
11+
export { readChannelIngressStoreAllowFromForDmPolicy } from "./store-allow-from.js";
12+
export { resolveChannelIngressEffectiveAllowFromLists } from "./effective-allow-from.js";
1213
export { resolveChannelIngressState } from "./state.js";
1314
export type {
1415
ChannelIngressAccessGroupMembershipResolver,

src/channels/message-access/runtime.ts

Lines changed: 2 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import {
77
normalizeStringEntries,
88
uniqueStrings,
99
} from "@openclaw/normalization-core/string-normalization";
10-
import { readChannelAllowFromStore } from "../../pairing/pairing-store.js";
1110
import type { PairingChannel } from "../../pairing/pairing-store.types.js";
12-
import { mergeDmAllowFromSources, resolveGroupAllowFromSources } from "../allow-from.js";
1311
import { decideChannelIngress } from "./decision.js";
12+
import { resolveChannelIngressEffectiveAllowFromLists } from "./effective-allow-from.js";
1413
import {
1514
allReferencedAccessGroupNames,
1615
normalizeEffectiveEntries,
@@ -38,6 +37,7 @@ import type {
3837
ResolvedChannelMessageIngress,
3938
} from "./runtime-types.js";
4039
import { resolveChannelIngressState } from "./state.js";
40+
import { readChannelIngressStoreAllowFromForDmPolicy } from "./store-allow-from.js";
4141
import type {
4242
AccessGraphGate,
4343
ChannelIngressChannelId,
@@ -71,65 +71,6 @@ function shouldReadStore(params: {
7171
);
7272
}
7373

74-
/**
75-
* Merge configured direct, group, and pairing-store allowlists into the
76-
* effective lists consumed by sender and context-visibility checks.
77-
*/
78-
export function resolveChannelIngressEffectiveAllowFromLists(params: {
79-
allowFrom?: Array<string | number> | null;
80-
groupAllowFrom?: Array<string | number> | null;
81-
storeAllowFrom?: Array<string | number> | null;
82-
dmPolicy?: string | null;
83-
groupAllowFromFallbackToAllowFrom?: boolean | null;
84-
}): {
85-
effectiveAllowFrom: string[];
86-
effectiveGroupAllowFrom: string[];
87-
} {
88-
const allowFrom = Array.isArray(params.allowFrom) ? params.allowFrom : undefined;
89-
const groupAllowFrom = Array.isArray(params.groupAllowFrom) ? params.groupAllowFrom : undefined;
90-
const storeAllowFrom = Array.isArray(params.storeAllowFrom) ? params.storeAllowFrom : undefined;
91-
const effectiveAllowFrom = normalizeStringEntries(
92-
mergeDmAllowFromSources({
93-
allowFrom,
94-
storeAllowFrom,
95-
dmPolicy: params.dmPolicy ?? undefined,
96-
}),
97-
);
98-
const effectiveGroupAllowFrom = normalizeStringEntries(
99-
resolveGroupAllowFromSources({
100-
allowFrom,
101-
groupAllowFrom,
102-
fallbackToAllowFrom: params.groupAllowFromFallbackToAllowFrom ?? undefined,
103-
}),
104-
);
105-
return { effectiveAllowFrom, effectiveGroupAllowFrom };
106-
}
107-
108-
/**
109-
* Read pairing-store allowlist entries when a direct-message policy permits
110-
* store fallback.
111-
*/
112-
export async function readChannelIngressStoreAllowFromForDmPolicy(params: {
113-
provider: PairingChannel;
114-
accountId: string;
115-
dmPolicy?: string | null;
116-
shouldRead?: boolean | null;
117-
readStore?: (provider: PairingChannel, accountId: string) => Promise<string[]>;
118-
}): Promise<string[]> {
119-
if (
120-
params.shouldRead === false ||
121-
params.dmPolicy === "allowlist" ||
122-
params.dmPolicy === "open"
123-
) {
124-
return [];
125-
}
126-
const readStore =
127-
params.readStore ??
128-
((provider: PairingChannel, accountId: string) =>
129-
readChannelAllowFromStore(provider, process.env, accountId));
130-
return await readStore(params.provider, params.accountId).catch(() => []);
131-
}
132-
13374
async function readStoreAllowFrom(
13475
params: ResolveChannelMessageIngressParams & { channelId: ChannelIngressChannelId },
13576
): Promise<Array<string | number>> {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { PairingChannel } from "../../pairing/pairing-store.types.js";
2+
3+
/**
4+
* Read pairing-store allowlist entries when a direct-message policy permits
5+
* store fallback.
6+
*/
7+
export async function readChannelIngressStoreAllowFromForDmPolicy(params: {
8+
provider: PairingChannel;
9+
accountId: string;
10+
dmPolicy?: string | null;
11+
shouldRead?: boolean | null;
12+
readStore?: (provider: PairingChannel, accountId: string) => Promise<string[]>;
13+
}): Promise<string[]> {
14+
if (
15+
params.shouldRead === false ||
16+
params.dmPolicy === "allowlist" ||
17+
params.dmPolicy === "open"
18+
) {
19+
return [];
20+
}
21+
const readStore =
22+
params.readStore ??
23+
(async (provider: PairingChannel, accountId: string) => {
24+
// Pairing store loads channel adapters for legacy normalization; keep that
25+
// registry edge lazy so pure ingress policy imports stay acyclic.
26+
const { readChannelAllowFromStore } = await import("../../pairing/pairing-store.js");
27+
return await readChannelAllowFromStore(provider, process.env, accountId);
28+
});
29+
return await readStore(params.provider, params.accountId).catch(() => []);
30+
}

src/channels/plugins/bundled.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {
1313
BundledChannelLegacySessionSurface,
1414
BundledChannelLegacyStateMigrationDetector,
1515
BundledEntryModuleLoadOptions,
16-
} from "../../plugin-sdk/channel-entry-contract.js";
16+
} from "../../plugin-sdk/channel-entry-contract.types.js";
1717
import {
1818
listBundledChannelPluginMetadata,
1919
resolveBundledChannelGeneratedPath,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export type ChannelLegacyStateMigrationPlan =
2+
| {
3+
kind: "copy" | "move";
4+
label: string;
5+
sourcePath: string;
6+
targetPath: string;
7+
}
8+
| {
9+
kind: "plugin-state-import";
10+
label: string;
11+
sourcePath: string;
12+
targetPath: string;
13+
pluginId: string;
14+
namespace: string;
15+
maxEntries: number;
16+
scopeKey: string;
17+
stateDir?: string;
18+
cleanupSource?: "rename";
19+
cleanupWhenEmpty?: boolean;
20+
preview?: string;
21+
shouldReplaceExistingEntry?: (params: {
22+
key: string;
23+
existingValue: unknown;
24+
incomingValue: unknown;
25+
}) => boolean | Promise<boolean>;
26+
readEntries: () =>
27+
| Array<{ key: string; value: unknown; ttlMs?: number }>
28+
| Promise<Array<{ key: string; value: unknown; ttlMs?: number }>>;
29+
};

src/channels/plugins/types.core.ts

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { ChannelMessageActionName as ChannelMessageActionNameFromList } fro
2323
import type { ChannelMessageCapability } from "./message-capabilities.js";
2424

2525
export type { ChannelId } from "./channel-id.types.js";
26+
export type { ChannelLegacyStateMigrationPlan } from "./legacy-state-migration.types.js";
2627

2728
export type ChannelExposure = {
2829
configured?: boolean;
@@ -159,36 +160,6 @@ export type ChannelHeartbeatDeps = {
159160
hasActiveWebListener?: (accountId?: string) => boolean;
160161
};
161162

162-
export type ChannelLegacyStateMigrationPlan =
163-
| {
164-
kind: "copy" | "move";
165-
label: string;
166-
sourcePath: string;
167-
targetPath: string;
168-
}
169-
| {
170-
kind: "plugin-state-import";
171-
label: string;
172-
sourcePath: string;
173-
targetPath: string;
174-
pluginId: string;
175-
namespace: string;
176-
maxEntries: number;
177-
scopeKey: string;
178-
stateDir?: string;
179-
cleanupSource?: "rename";
180-
cleanupWhenEmpty?: boolean;
181-
preview?: string;
182-
shouldReplaceExistingEntry?: (params: {
183-
key: string;
184-
existingValue: unknown;
185-
incomingValue: unknown;
186-
}) => boolean | Promise<boolean>;
187-
readEntries: () =>
188-
| Array<{ key: string; value: unknown; ttlMs?: number }>
189-
| Promise<Array<{ key: string; value: unknown; ttlMs?: number }>>;
190-
};
191-
192163
/** User-facing metadata used in docs, pickers, and setup surfaces. */
193164
export type ChannelMeta = {
194165
id: ChannelId;

src/channels/route-projection.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { setActivePluginRegistry } from "../plugins/runtime.js";
44
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
55
import {
66
deliveryContextFromRoute,
7+
formatConversationTarget,
78
normalizeRoutableChannelRoute,
9+
resolveConversationDeliveryTarget,
810
routeFromBindingRecord,
911
routeFromConversationRef,
1012
routeFromDeliveryContext,
@@ -17,6 +19,30 @@ describe("channel route projection", () => {
1719
beforeEach(() => {
1820
setActivePluginRegistry(
1921
createTestRegistry([
22+
{
23+
pluginId: "room-chat",
24+
source: "test",
25+
plugin: {
26+
...createChannelTestPluginBase({ id: "room-chat", label: "Room chat" }),
27+
messaging: {
28+
resolveDeliveryTarget: ({
29+
conversationId,
30+
parentConversationId,
31+
}: {
32+
conversationId: string;
33+
parentConversationId?: string;
34+
}) =>
35+
conversationId.startsWith("$")
36+
? {
37+
to: parentConversationId ? `room:${parentConversationId}` : undefined,
38+
threadId: conversationId,
39+
}
40+
: {
41+
to: `room:${conversationId}`,
42+
},
43+
},
44+
},
45+
},
2046
{
2147
pluginId: "thread-chat",
2248
source: "test",
@@ -78,6 +104,35 @@ describe("channel route projection", () => {
78104
});
79105
});
80106

107+
it("formats plugin-defined conversation targets via channel messaging hooks", () => {
108+
expect(
109+
formatConversationTarget({ channel: "room-chat", conversationId: "!room:example" }),
110+
).toBe("room:!room:example");
111+
expect(
112+
formatConversationTarget({
113+
channel: "room-chat",
114+
conversationId: "$thread",
115+
parentConversationId: "!room:example",
116+
}),
117+
).toBe("room:!room:example");
118+
expect(
119+
formatConversationTarget({ channel: "room-chat", conversationId: " " }),
120+
).toBeUndefined();
121+
});
122+
123+
it("resolves delivery targets for plugin-defined child threads", () => {
124+
expect(
125+
resolveConversationDeliveryTarget({
126+
channel: "room-chat",
127+
conversationId: "$thread",
128+
parentConversationId: "!room:example",
129+
}),
130+
).toEqual({
131+
to: "room:!room:example",
132+
threadId: "$thread",
133+
});
134+
});
135+
81136
it("projects parent-child conversation refs through plugin delivery targets", () => {
82137
expect(
83138
routeFromConversationRef({

0 commit comments

Comments
 (0)