@axiom-lattice/protocols 4.1.0 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +12 -0
  3. package/dist/index.d.mts +1021 -26
  4. package/dist/index.d.ts +1021 -26
  5. package/dist/index.js +448 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +420 -1
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +3 -2
  10. package/src/BindingProtocol.ts +87 -11
  11. package/src/CapabilityBundleStoreProtocol.ts +78 -0
  12. package/src/CapabilityRuntimeProtocol.ts +82 -0
  13. package/src/ChannelInstallationStoreProtocol.ts +23 -4
  14. package/src/ExactDataSnapshot.ts +119 -0
  15. package/src/PluginProtocol.ts +59 -7
  16. package/src/ProjectBotMembershipStoreProtocol.ts +48 -0
  17. package/src/ProjectMembershipStoreProtocol.ts +58 -0
  18. package/src/ProjectRoomMessageStoreProtocol.ts +26 -0
  19. package/src/ProjectRoomProtocol.ts +143 -0
  20. package/src/ProjectRoomRealtimeProtocol.ts +349 -0
  21. package/src/ProjectRoomStoreProtocol.ts +16 -0
  22. package/src/SkillStoreProtocol.ts +30 -0
  23. package/src/TaskBeliefProtocol.ts +6 -1
  24. package/src/TaskStoreProtocol.ts +66 -2
  25. package/src/TaskWorkItemProtocol.ts +138 -0
  26. package/src/TrustedRunContextProtocol.ts +119 -0
  27. package/src/WorkspaceStoreProtocol.ts +33 -0
  28. package/src/__tests__/BindingProtocol.test.ts +36 -0
  29. package/src/__tests__/ExactDataSnapshot.test.ts +105 -0
  30. package/src/__tests__/ProjectRoomProtocol.test.ts +48 -0
  31. package/src/__tests__/ProjectRoomRealtimeProtocol.test.ts +185 -0
  32. package/src/__tests__/ProjectRoomStores.test.ts +363 -0
  33. package/src/__tests__/ProjectTaskProtocol.test.ts +29 -0
  34. package/src/__tests__/TaskWorkItemProtocol.test.ts +111 -0
  35. package/src/__tests__/TrustedRunContextProtocol.test.ts +265 -0
  36. package/src/__tests__/capability-bundle-types.test.ts +177 -0
  37. package/src/index.ts +13 -0
  38. package/tsconfig.type-tests.json +9 -0
  39. package/type-tests/task-work-item-store-compatibility.ts +37 -0
@@ -27,14 +27,97 @@ export interface CreateBindingInput {
27
27
  tenantId: string;
28
28
  senderId: string;
29
29
  agentId: string;
30
+ threadId?: string;
30
31
  threadMode?: "fixed" | "per_conversation";
31
32
  senderDisplayName?: string;
32
33
  senderMetadata?: Record<string, unknown>;
33
34
  workspaceId?: string;
34
35
  projectId?: string;
36
+ /** Whether the binding is eligible for inbound resolution immediately after creation. */
37
+ enabled?: boolean;
38
+ }
39
+
40
+ /** Filters binding records before pagination is applied. */
41
+ export interface BindingListParams {
42
+ tenantId: string;
43
+ channel?: string;
44
+ agentId?: string;
45
+ channelInstallationId?: string;
46
+ /** Installation ID prefixes excluded before pagination, for internal namespaces. */
47
+ excludeInstallationIdPrefixes?: string[];
48
+ excludeChannels?: string[];
49
+ limit?: number;
50
+ offset?: number;
51
+ }
52
+
53
+ /**
54
+ * Fields that may change after a binding is created.
55
+ *
56
+ * Binding identity (`id`, tenant, channel, installation, sender, and timestamps) is intentionally
57
+ * absent so every persistence backend can enforce tenant-scoped mutation without identity drift.
58
+ */
59
+ export interface BindingMutablePatch {
60
+ /** Agent that receives messages for this subject. */
61
+ agentId?: string;
62
+ /** Fixed thread used when `threadMode` is `fixed`. */
63
+ threadId?: string;
64
+ /** Optional workspace execution scope. */
65
+ workspaceId?: string;
66
+ /** Optional project execution scope. */
67
+ projectId?: string;
68
+ /** Whether messages share one thread or create one per conversation. */
69
+ threadMode?: "fixed" | "per_conversation";
70
+ /** Human-readable sender label. */
71
+ senderDisplayName?: string;
72
+ /** Mutable sender metadata supplied by trusted internal callers. */
73
+ senderMetadata?: Record<string, unknown>;
74
+ /** Whether inbound resolution may use this binding. */
75
+ enabled?: boolean;
76
+ }
77
+
78
+ /** Raised when a channel installation already has a binding for the same tenant and sender. */
79
+ export class DuplicateChannelBindingSubjectError extends Error {
80
+ constructor() {
81
+ super("A binding already exists for this channel subject");
82
+ this.name = "DuplicateChannelBindingSubjectError";
83
+ }
84
+ }
85
+
86
+ /** A duplicate subject found while upgrading a local channel-binding database. */
87
+ export interface ChannelBindingMigrationConflict {
88
+ tenantId: string;
89
+ channel: string;
90
+ channelInstallationId: string;
91
+ senderId: string;
92
+ count: number;
93
+ }
94
+
95
+ /**
96
+ * Raised when a local binding uniqueness migration requires operator reconciliation.
97
+ *
98
+ * The conflict list contains only subject identifiers and row counts; binding metadata and other
99
+ * potentially sensitive payloads are never included.
100
+ */
101
+ export class ChannelBindingMigrationConflictError extends Error {
102
+ readonly conflicts: ChannelBindingMigrationConflict[];
103
+
104
+ constructor(conflicts: ChannelBindingMigrationConflict[]) {
105
+ super(`Channel binding migration found ${conflicts.length} duplicate subject(s)`);
106
+ this.name = "ChannelBindingMigrationConflictError";
107
+ this.conflicts = conflicts;
108
+ }
35
109
  }
36
110
 
37
111
  export interface BindingRegistry {
112
+ findById(tenantId: string, id: string): Promise<Binding | null>;
113
+
114
+ findBySubject(params: {
115
+ tenantId: string;
116
+ channel: string;
117
+ channelInstallationId: string;
118
+ senderId: string;
119
+ }): Promise<Binding | null>;
120
+
38
121
  resolve(params: {
39
122
  channel: string;
40
123
  senderId: string;
@@ -43,18 +126,11 @@ export interface BindingRegistry {
43
126
  }): Promise<Binding | null>;
44
127
 
45
128
  create(binding: CreateBindingInput): Promise<Binding>;
46
- update(id: string, patch: Partial<Binding>): Promise<Binding>;
47
- delete(id: string): Promise<void>;
129
+ update(tenantId: string, id: string, patch: BindingMutablePatch): Promise<Binding>;
130
+ delete(tenantId: string, id: string): Promise<void>;
48
131
 
49
- list(params: {
50
- channel?: string;
51
- agentId?: string;
52
- tenantId: string;
53
- channelInstallationId?: string;
54
- limit?: number;
55
- offset?: number;
56
- }): Promise<Binding[]>;
132
+ list(params: BindingListParams): Promise<Binding[]>;
57
133
 
58
- import(bindings: CreateBindingInput[]): Promise<Binding[]>;
134
+ import(tenantId: string, bindings: CreateBindingInput[]): Promise<Binding[]>;
59
135
  export(params: { tenantId: string }): Promise<Binding[]>;
60
136
  }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Capability bundle persistence protocol.
3
+ *
4
+ * A capability bundle is a tenant-scoped, flat collection of middleware
5
+ * configurations that can be selected by a project.
6
+ */
7
+
8
+ import type { AgentMiddlewareConfig } from "./AgentLatticeProtocol";
9
+
10
+ /** A tenant-scoped collection of middleware configurations. */
11
+ export interface CapabilityBundle {
12
+ /** Stable bundle identifier. */
13
+ readonly id: string;
14
+ /** Tenant that owns the bundle. */
15
+ tenantId: string;
16
+ /** Tenant-local unique key. */
17
+ key: string;
18
+ /** Human-readable bundle name. */
19
+ name: string;
20
+ /** Optional bundle description. */
21
+ description?: string;
22
+ /** Middleware configurations contained in the bundle. */
23
+ capabilities: AgentMiddlewareConfig[];
24
+ /** Creation timestamp in ISO string format. */
25
+ createdAt: string;
26
+ /** Last update timestamp in ISO string format. */
27
+ updatedAt: string;
28
+ }
29
+
30
+ /** Public input used to create a capability bundle; the tenant-local key is generated internally. */
31
+ export interface CreateCapabilityBundleInput {
32
+ /** Human-readable bundle name. */
33
+ name: string;
34
+ /** Optional bundle description. */
35
+ description?: string;
36
+ /** Middleware configurations contained in the bundle. */
37
+ capabilities: AgentMiddlewareConfig[];
38
+ }
39
+
40
+ /** Input with a generated key used by persistence implementations. */
41
+ export interface InternalCreateCapabilityBundleInput extends Omit<CreateCapabilityBundleInput, "key"> {
42
+ key: string;
43
+ }
44
+
45
+ /** Public partial bundle update; the stable key is immutable and the revision is mandatory for gateway updates. */
46
+ export interface UpdateCapabilityBundleInput extends Partial<CreateCapabilityBundleInput> {
47
+ expectedUpdatedAt: string;
48
+ }
49
+
50
+ /** Store input retained for internal maintenance callers that may omit CAS. */
51
+ export interface InternalUpdateCapabilityBundleInput extends Partial<CreateCapabilityBundleInput> {
52
+ expectedUpdatedAt?: string;
53
+ }
54
+
55
+ /** Result returned when an update's expected revision no longer matches. */
56
+ export interface CapabilityBundleUpdateConflict {
57
+ status: "conflict";
58
+ }
59
+
60
+ /** Result of deleting a bundle only when no tenant project references it. */
61
+ export type CapabilityBundleDeleteResult = "deleted" | "not_found" | "in_use";
62
+
63
+ /** Persistence operations for tenant-scoped capability bundles. */
64
+ export interface CapabilityBundleStore {
65
+ /** Lists all bundles owned by a tenant. */
66
+ listByTenant(tenantId: string): Promise<CapabilityBundle[]>;
67
+ /** Gets one bundle by tenant and identifier. */
68
+ getById(tenantId: string, id: string): Promise<CapabilityBundle | null>;
69
+ /** Gets bundles by tenant and identifiers. */
70
+ getManyByIds(tenantId: string, ids: string[]): Promise<CapabilityBundle[]>;
71
+ /** Creates a bundle for a tenant. */
72
+ create(tenantId: string, input: InternalCreateCapabilityBundleInput): Promise<CapabilityBundle>;
73
+ /** Updates a bundle, or returns null when it does not exist. */
74
+ /** Omitted expectedUpdatedAt is reserved for internal maintenance callers. */
75
+ update(tenantId: string, id: string, input: InternalUpdateCapabilityBundleInput): Promise<CapabilityBundle | CapabilityBundleUpdateConflict | null>;
76
+ /** Atomically deletes a bundle unless a tenant project references it. */
77
+ deleteIfUnreferenced(tenantId: string, id: string): Promise<CapabilityBundleDeleteResult>;
78
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Capability runtime and preview protocol definitions.
3
+ *
4
+ * These contracts describe the resolved middleware runtime and the
5
+ * provenance data exposed while composing project bundles.
6
+ */
7
+
8
+ import type { AgentMiddlewareConfig } from "./AgentLatticeProtocol";
9
+
10
+ /** Project configuration selecting ordered capability bundles. */
11
+ export interface ProjectCapabilityConfig {
12
+ /** Bundle identifiers in composition order. */
13
+ capabilityBundleIds: string[];
14
+ }
15
+
16
+ /** Resolved middleware available to an agent execution. */
17
+ export interface CapabilityRuntime {
18
+ /** Revision identifying the resolved capability set. */
19
+ revision: string;
20
+ /** Middleware indexed by middleware identifier. */
21
+ middleware: Record<string, AgentMiddlewareConfig>;
22
+ }
23
+
24
+ /** Provenance for a resolved capability field. */
25
+ export interface CapabilityFieldSource {
26
+ /** Middleware type owning the field. */
27
+ capabilityType: string;
28
+ /** RFC 6901 escaped JSON Pointer path of the field within the middleware configuration. */
29
+ fieldPath: string;
30
+ /** Bundle that supplied the field value. */
31
+ sourceBundleId: string;
32
+ /** Supplied field value. */
33
+ value: unknown;
34
+ }
35
+
36
+ /** A later bundle value replacing an earlier field value. */
37
+ export interface CapabilityOverride {
38
+ /** Middleware type owning the field. */
39
+ capabilityType: string;
40
+ /** RFC 6901 escaped JSON Pointer path of the overridden field. */
41
+ fieldPath: string;
42
+ /** Value supplied by the earlier bundle. */
43
+ previousValue: unknown;
44
+ /** Value supplied by the later bundle. */
45
+ nextValue: unknown;
46
+ /** Bundle that supplied the later value. */
47
+ sourceBundleId: string;
48
+ }
49
+
50
+ /** A warning or error produced while previewing bundle composition. */
51
+ export interface CapabilityPreviewIssue {
52
+ /** Stable issue code. */
53
+ code: string;
54
+ /** Human-readable issue description. */
55
+ message: string;
56
+ /** Related bundle, when applicable. */
57
+ bundleId?: string;
58
+ /** Related middleware type, when applicable. */
59
+ capabilityType?: string;
60
+ /** Related middleware field path, when applicable. */
61
+ fieldPath?: string;
62
+ }
63
+
64
+ /** Result of composing capability bundles for a project. */
65
+ export interface CapabilityPreview {
66
+ /** Bundle identifiers in composition order. */
67
+ bundleIds: string[];
68
+ /** Composed middleware configurations. */
69
+ capabilities: AgentMiddlewareConfig[];
70
+ /** Field replacements detected during composition. */
71
+ overrides: CapabilityOverride[];
72
+ /** Field-level provenance for composed values. */
73
+ sources: CapabilityFieldSource[];
74
+ /** Non-blocking composition issues. */
75
+ warnings: CapabilityPreviewIssue[];
76
+ /** Blocking composition issues. */
77
+ errors: CapabilityPreviewIssue[];
78
+ /** Revision identifying this preview. */
79
+ revision: string;
80
+ /** Bundle content revisions captured by this preview. */
81
+ bundleRevisions?: Record<string, string>;
82
+ }
@@ -1,5 +1,10 @@
1
- export type ChannelInstallationType = "lark" | "email" | "slack" | "wechat";
1
+ /** Channel types persisted by the installation store, including internal-only channels. */
2
+ export type ChannelInstallationType = "lark" | "email" | "slack" | "wechat" | "room";
2
3
 
4
+ /** Channel types exposed by the public installation management API. */
5
+ export type PublicChannelInstallationType = Exclude<ChannelInstallationType, "room">;
6
+
7
+ /** Credentials and routing configuration for a Lark installation. */
3
8
  export interface LarkChannelInstallationConfig {
4
9
  appId: string;
5
10
  appSecret: string;
@@ -8,11 +13,13 @@ export interface LarkChannelInstallationConfig {
8
13
  assistantId?: string;
9
14
  }
10
15
 
16
+ /** Credentials and identity configuration for a WeChat installation. */
11
17
  export interface WechatChannelInstallationConfig {
12
18
  botToken: string;
13
19
  uin?: string;
14
20
  }
15
21
 
22
+ /** A persisted tenant-scoped channel installation. */
16
23
  export interface ChannelInstallation<TConfig = unknown> {
17
24
  id: string;
18
25
  tenantId: string;
@@ -26,15 +33,26 @@ export interface ChannelInstallation<TConfig = unknown> {
26
33
  updatedAt: Date;
27
34
  }
28
35
 
29
- export interface CreateChannelInstallationRequest {
36
+ /** Internal input accepted by installation stores. */
37
+ export interface CreateChannelInstallationInput {
30
38
  channel: ChannelInstallationType;
31
39
  name?: string;
32
- config: LarkChannelInstallationConfig;
40
+ config: Record<string, unknown>;
33
41
  enabled?: boolean;
34
42
  fallbackAgentId?: string;
35
43
  rejectWhenNoBinding?: boolean;
36
44
  }
37
45
 
46
+ type PublicInstallationBase = Omit<CreateChannelInstallationInput, "channel" | "config">;
47
+
48
+ /** Public create request with channel-specific configuration and no internal room variant. */
49
+ export type CreateChannelInstallationRequest = PublicInstallationBase & (
50
+ | { channel: "lark"; config: LarkChannelInstallationConfig }
51
+ | { channel: "wechat"; config: WechatChannelInstallationConfig }
52
+ | { channel: "email" | "slack"; config: Record<string, unknown> }
53
+ );
54
+
55
+ /** Fields accepted when updating an existing public installation. */
38
56
  export interface UpdateChannelInstallationRequest {
39
57
  name?: string;
40
58
  config?: Record<string, unknown>;
@@ -43,6 +61,7 @@ export interface UpdateChannelInstallationRequest {
43
61
  rejectWhenNoBinding?: boolean;
44
62
  }
45
63
 
64
+ /** Persistence boundary for tenant-scoped channel installations. */
46
65
  export interface ChannelInstallationStore {
47
66
  getInstallationById(
48
67
  installationId: string,
@@ -64,7 +83,7 @@ export interface ChannelInstallationStore {
64
83
  createInstallation(
65
84
  tenantId: string,
66
85
  installationId: string,
67
- data: CreateChannelInstallationRequest,
86
+ data: CreateChannelInstallationInput,
68
87
  ): Promise<ChannelInstallation>;
69
88
 
70
89
  updateInstallation(
@@ -0,0 +1,119 @@
1
+ /** A safely extracted value from an own enumerable data-property descriptor. */
2
+ export interface DescriptorDataValue {
3
+ ok: true;
4
+ value: unknown;
5
+ }
6
+
7
+ function ownDescriptorField(descriptor: object, key: string): unknown {
8
+ const field = Object.getOwnPropertyDescriptor(descriptor, key);
9
+ return field && Object.prototype.hasOwnProperty.call(field, "value")
10
+ ? field.value
11
+ : undefined;
12
+ }
13
+
14
+ /**
15
+ * Reads an own enumerable data-property descriptor without consulting its prototype.
16
+ *
17
+ * @param descriptor Property descriptor to inspect.
18
+ * @returns The descriptor value when it is an own enumerable data descriptor, otherwise `undefined`.
19
+ */
20
+ export function descriptorDataValue(descriptor: unknown): DescriptorDataValue | undefined {
21
+ if (typeof descriptor !== "object" || descriptor === null) return undefined;
22
+ try {
23
+ const keys = Reflect.ownKeys(descriptor);
24
+ if (!keys.includes("value") || !keys.includes("enumerable")
25
+ || keys.includes("get") || keys.includes("set")) return undefined;
26
+ const valueField = Object.getOwnPropertyDescriptor(descriptor, "value");
27
+ if (!valueField || !Object.prototype.hasOwnProperty.call(valueField, "value")
28
+ || ownDescriptorField(descriptor, "enumerable") !== true) return undefined;
29
+ return { ok: true, value: valueField.value };
30
+ } catch {
31
+ return undefined;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Copies an object-like value into a local plain record using only exact own enumerable data descriptors.
37
+ *
38
+ * Prototypes are deliberately ignored so records from other JavaScript realms remain valid and inherited
39
+ * behavior can never participate in validation.
40
+ *
41
+ * @param value Untrusted value to snapshot.
42
+ * @param required Own string keys that must be present.
43
+ * @param optional Own string keys that may be present.
44
+ * @returns A canonical local record, or `undefined` for malformed descriptors, keys, arrays, or proxies.
45
+ */
46
+ export function snapshotExactRecord(
47
+ value: unknown,
48
+ required: readonly string[],
49
+ optional: readonly string[] = [],
50
+ ): Record<string, unknown> | undefined {
51
+ try {
52
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
53
+ const keys = Reflect.ownKeys(value);
54
+ const allowed = new Set([...required, ...optional]);
55
+ if (required.some((key) => !keys.includes(key))
56
+ || keys.some((key) => typeof key !== "string" || !allowed.has(key))) return undefined;
57
+ const descriptors = Object.getOwnPropertyDescriptors(value);
58
+ const descriptorKeys = Reflect.ownKeys(descriptors);
59
+ if (descriptorKeys.length !== keys.length || keys.some((key) => !descriptorKeys.includes(key))) return undefined;
60
+ const result: Record<string, unknown> = {};
61
+ for (const key of keys) {
62
+ if (typeof key !== "string") return undefined;
63
+ const descriptor = descriptors[key];
64
+ const data = descriptorDataValue(descriptor);
65
+ if (!data) return undefined;
66
+ Object.defineProperty(result, key, {
67
+ value: data.value,
68
+ enumerable: true,
69
+ configurable: true,
70
+ writable: true,
71
+ });
72
+ }
73
+ return result;
74
+ } catch {
75
+ return undefined;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Copies a dense cross-realm array using only own element data descriptors and the intrinsic length descriptor.
81
+ *
82
+ * @param value Untrusted value to snapshot.
83
+ * @returns A canonical local dense array, or `undefined` for holes, extras, accessors, or malformed proxies.
84
+ */
85
+ export function snapshotExactArray(value: unknown): unknown[] | undefined {
86
+ try {
87
+ if (!Array.isArray(value)) return undefined;
88
+ const keys = Reflect.ownKeys(value);
89
+ const descriptors = Object.getOwnPropertyDescriptors(value);
90
+ const descriptorKeys = Reflect.ownKeys(descriptors);
91
+ if (descriptorKeys.length !== keys.length || keys.some((key) => !descriptorKeys.includes(key))) return undefined;
92
+ const lengthDescriptorField = Object.getOwnPropertyDescriptor(descriptors, "length");
93
+ const lengthDescriptor = lengthDescriptorField
94
+ && Object.prototype.hasOwnProperty.call(lengthDescriptorField, "value")
95
+ ? lengthDescriptorField.value
96
+ : undefined;
97
+ if (typeof lengthDescriptor !== "object" || lengthDescriptor === null
98
+ || Reflect.ownKeys(lengthDescriptor).some((key) => key === "get" || key === "set")
99
+ || !Object.prototype.hasOwnProperty.call(lengthDescriptor, "value")) return undefined;
100
+ const length = ownDescriptorField(lengthDescriptor, "value");
101
+ if (ownDescriptorField(lengthDescriptor, "enumerable") !== false
102
+ || ownDescriptorField(lengthDescriptor, "configurable") !== false
103
+ || ownDescriptorField(lengthDescriptor, "writable") !== true
104
+ || !Number.isSafeInteger(length) || typeof length !== "number" || length < 0
105
+ || keys.length !== length + 1) return undefined;
106
+ const result: unknown[] = [];
107
+ for (let index = 0; index < length; index += 1) {
108
+ const key = String(index);
109
+ const descriptor = descriptors[key];
110
+ const data = descriptorDataValue(descriptor);
111
+ if (!keys.includes(key) || !data) return undefined;
112
+ result.push(data.value);
113
+ }
114
+ if (keys.some((key) => typeof key !== "string" || (key !== "length" && !/^(0|[1-9]\d*)$/.test(key)))) return undefined;
115
+ return result;
116
+ } catch {
117
+ return undefined;
118
+ }
119
+ }
@@ -65,19 +65,63 @@ export interface PluginConnection {
65
65
 
66
66
  /**
67
67
  * 工具元信息(用于前端 allowedTools 筛选)
68
+ *
69
+ * New connection-backed plugin tools that select one connection must accept
70
+ * the selected key as `args.connectionKey`. Existing legacy tools retain their
71
+ * established resource argument names.
68
72
  */
69
73
  export interface PluginToolMeta {
70
74
  name: string;
71
75
  description: string;
72
76
  }
73
77
 
78
+ /**
79
+ * A text file included in a plugin skill bundle.
80
+ *
81
+ * @property content - Text content written to the skill resource file.
82
+ * @property mimeType - Optional MIME type for consumers that need it.
83
+ */
84
+ export interface PluginSkillResource {
85
+ content: string;
86
+ mimeType?: string;
87
+ }
88
+
89
+ /**
90
+ * Versioned definition of a plugin-provided skill.
91
+ *
92
+ * @property version - Bundle version used to detect resource updates.
93
+ * @property content - Complete SKILL.md markdown content.
94
+ * @property resources - Optional text resources keyed by safe relative paths.
95
+ */
96
+ export interface PluginSkillDefinition {
97
+ version: string;
98
+ content: string;
99
+ resources?: Record<string, PluginSkillResource>;
100
+ }
101
+
102
+ /**
103
+ * Standard configuration for a new connection-backed plugin.
104
+ *
105
+ * `PluginMeta.type` is also the Connection Store type. `connections` selects
106
+ * connection keys of that type; `connectAll` opts into using all available
107
+ * connections of that type. Do not add a separate connection or resource type
108
+ * or selector field to this configuration.
109
+ */
110
+ export type PluginStandardConnectionConfig = {
111
+ connections: string[];
112
+ connectAll?: boolean;
113
+ };
114
+
74
115
  /**
75
116
  * 插件元数据(开发者声明)
76
117
  *
77
118
  * connectionSchema 不在此类型中——由 serializePluginMeta 自动推导后注入 PluginMetaOutput。
78
119
  */
79
120
  export interface PluginMeta {
80
- /** 插件唯一标识,如 "erp" */
121
+ /**
122
+ * 插件唯一标识,如 "erp"; for connection-backed plugins this is also the
123
+ * Connection Store type.
124
+ */
81
125
  type: string;
82
126
  /** 显示名称 */
83
127
  name: string;
@@ -91,9 +135,13 @@ export interface PluginMeta {
91
135
  icon?: string;
92
136
  /** 工具清单(可选,middleware 能自动提取时不需要写) */
93
137
  tools?: PluginToolMeta[];
94
- /** 中间件配置 schema(用于 agent 配置面板) */
138
+ /**
139
+ * 中间件配置 schema(用于 agent 配置面板)。新的 connection-backed
140
+ * plugins use `connections: string[]` and optional `connectAll?: boolean`.
141
+ * Do not introduce connectionType, resourceType, or resourceSelector fields.
142
+ */
95
143
  configSchema?: Record<string, unknown>;
96
- /** 默认配置 */
144
+ /** 默认配置;connection-backed plugins use PluginStandardConnectionConfig. */
97
145
  defaultConfig?: Record<string, unknown>;
98
146
  /** 推荐配置的 companion 插件 */
99
147
  recommends?: string[];
@@ -103,6 +151,8 @@ export interface PluginMeta {
103
151
  * 第三方插件可自定义分类名,前端会原样显示;未提供时归入 "Other"。
104
152
  */
105
153
  category?: string;
154
+ /** Whether this plugin's middleware may be included in capability bundles. */
155
+ capabilityBundleEligible?: boolean;
106
156
  }
107
157
 
108
158
  /**
@@ -133,7 +183,7 @@ export interface PluginMetaOutput extends PluginMeta {
133
183
  * const myPlugin: Plugin = {
134
184
  * meta: { ... },
135
185
  * middleware: (config, context) => {
136
- * const skills = context?.pluginSkillContents ?? {};
186
+ * const skills = context?.pluginSkills ?? {};
137
187
  * return createMyMiddleware({ ...config, pluginSkills: skills });
138
188
  * },
139
189
  * };
@@ -141,12 +191,14 @@ export interface PluginMetaOutput extends PluginMeta {
141
191
  */
142
192
  export interface PluginContext {
143
193
  /**
144
- * Cross-plugin aggregated skill contents (SKILL.md, keyed by skill name).
194
+ * Cross-plugin aggregated skill bundles, keyed by skill name.
145
195
  * Collected from all enabled plugins before the main middleware loop.
146
196
  * Most plugins should ignore this; only cross-plugin coordination
147
197
  * middleware (e.g. skillMiddleware) consumes it.
148
198
  */
149
- pluginSkillContents?: Record<string, string>;
199
+ pluginSkills?: Record<string, PluginSkillDefinition>;
200
+ /** Owning plugin type for each enabled plugin skill bundle. */
201
+ pluginSkillOwners?: Record<string, string>;
150
202
  }
151
203
 
152
204
  /**
@@ -197,7 +249,7 @@ export interface Plugin {
197
249
  * 名称必须以 "{pluginType}-" 为前缀,注册时校验。
198
250
  * Builder 在构建中间件之前从所有启用的插件中收集。
199
251
  */
200
- skills?: Record<string, string>;
252
+ skills?: Record<string, PluginSkillDefinition>;
201
253
  /**
202
254
  * 插件贡献的 Agent 定义(以 agent key 为键)。
203
255
  * 在租户首次访问时通过 ensurePluginAgentsForTenant 按租户注册到
@@ -0,0 +1,48 @@
1
+ import type {
2
+ ProjectBotMembership,
3
+ } from "./ProjectRoomProtocol";
4
+
5
+ /** Persistence operations for bot membership in a project room. */
6
+ export interface ProjectBotMembershipStore {
7
+ /** Lists bot memberships belonging to a project. */
8
+ list(tenantId: string, projectId: string): Promise<ProjectBotMembership[]>;
9
+
10
+ /** Finds a bot membership by identifier within a tenant. */
11
+ findById(tenantId: string, id: string): Promise<ProjectBotMembership | null>;
12
+
13
+ /** Finds a bot membership by assistant within a project. */
14
+ findByAssistant(tenantId: string, projectId: string, assistantId: string): Promise<ProjectBotMembership | null>;
15
+
16
+ /**
17
+ * Saves a bot membership. A new row returns `created`, an existing active or
18
+ * paused row returns `updated`, and a removed row returns `reactivated`.
19
+ * `coordinator_conflict` means a non-removed
20
+ * coordinator already exists for the Tenant+Project; `mention_conflict`
21
+ * means the mention name is already used by an active or paused membership
22
+ * in the Tenant+room.
23
+ */
24
+ save(
25
+ input: Omit<ProjectBotMembership, "joinedAt" | "updatedAt">,
26
+ ): Promise<
27
+ | { kind: "created" | "updated" | "reactivated"; membership: ProjectBotMembership }
28
+ | { kind: "coordinator_conflict" | "mention_conflict" }
29
+ >;
30
+
31
+ /**
32
+ * Updates a bot membership using optimistic concurrency and uniqueness
33
+ * safeguards. `not_found` means the membership is not in the tenant;
34
+ * `conflict` means `updatedAt` differs from `expectedUpdatedAt`.
35
+ * `coordinator_conflict` applies to the one non-removed Coordinator per
36
+ * Tenant+Project rule; `mention_conflict` applies to mention uniqueness
37
+ * among active and paused memberships in the Tenant+room.
38
+ */
39
+ update(input: {
40
+ tenantId: string;
41
+ id: string;
42
+ patch: Partial<Pick<ProjectBotMembership, "role" | "title" | "responsibility" | "mentionName" | "status">>;
43
+ expectedUpdatedAt: Date;
44
+ }): Promise<
45
+ | { kind: "updated"; membership: ProjectBotMembership }
46
+ | { kind: "not_found" | "conflict" | "coordinator_conflict" | "mention_conflict" }
47
+ >;
48
+ }