@capybara-hq/plugin-sdk 0.0.0-stage → 0.1.0-rc.0

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 (70) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +27 -2
  4. package/dist/agent-template.d.ts +47 -0
  5. package/dist/agent-template.js +123 -0
  6. package/dist/bootstrap.d.ts +67 -0
  7. package/dist/bootstrap.js +109 -0
  8. package/dist/cards.d.ts +64 -0
  9. package/dist/cards.js +217 -0
  10. package/dist/channel.d.ts +230 -0
  11. package/dist/channel.js +26 -0
  12. package/dist/connector-auth.d.ts +64 -0
  13. package/dist/connector-auth.js +96 -0
  14. package/dist/connector.d.ts +154 -0
  15. package/dist/connector.js +1 -0
  16. package/dist/ctx.d.ts +176 -0
  17. package/dist/ctx.js +1 -0
  18. package/dist/define-extension.d.ts +41 -0
  19. package/dist/define-extension.js +85 -0
  20. package/dist/directory.d.ts +29 -0
  21. package/dist/directory.js +1 -0
  22. package/dist/extension-point.d.ts +29 -0
  23. package/dist/extension-point.js +36 -0
  24. package/dist/file-preview.d.ts +64 -0
  25. package/dist/file-preview.js +72 -0
  26. package/dist/frontmatter.d.ts +12 -0
  27. package/dist/frontmatter.js +36 -0
  28. package/dist/hooks.d.ts +194 -0
  29. package/dist/hooks.js +14 -0
  30. package/dist/identity.d.ts +44 -0
  31. package/dist/identity.js +1 -0
  32. package/dist/index.d.ts +27 -0
  33. package/dist/index.js +27 -0
  34. package/dist/marketplace.d.ts +99 -0
  35. package/dist/marketplace.js +256 -0
  36. package/dist/model-params.d.ts +82 -0
  37. package/dist/model-params.js +222 -0
  38. package/dist/model.d.ts +227 -0
  39. package/dist/model.js +152 -0
  40. package/dist/permissions.d.ts +67 -0
  41. package/dist/permissions.js +271 -0
  42. package/dist/plugin-manifest.d.ts +141 -0
  43. package/dist/plugin-manifest.js +322 -0
  44. package/dist/plugin-package.d.ts +139 -0
  45. package/dist/plugin-package.js +448 -0
  46. package/dist/sandbox.d.ts +152 -0
  47. package/dist/sandbox.js +96 -0
  48. package/dist/secret.d.ts +5 -0
  49. package/dist/secret.js +8 -0
  50. package/dist/semver-range.d.ts +6 -0
  51. package/dist/semver-range.js +12 -0
  52. package/dist/skill-invocation.d.ts +83 -0
  53. package/dist/skill-invocation.js +201 -0
  54. package/dist/skill-keys.d.ts +9 -0
  55. package/dist/skill-keys.js +16 -0
  56. package/dist/skill.d.ts +77 -0
  57. package/dist/skill.js +244 -0
  58. package/dist/storage.d.ts +96 -0
  59. package/dist/storage.js +9 -0
  60. package/dist/tool.d.ts +154 -0
  61. package/dist/tool.js +218 -0
  62. package/dist/ui.d.ts +265 -0
  63. package/dist/ui.js +358 -0
  64. package/dist/url.d.ts +2 -0
  65. package/dist/url.js +12 -0
  66. package/package.json +40 -3
  67. package/schemas/bootstrap.v1.schema.json +125 -0
  68. package/schemas/connector-template.v1.schema.json +116 -0
  69. package/schemas/marketplace.v1.schema.json +270 -0
  70. package/schemas/plugin.v1.schema.json +330 -0
@@ -0,0 +1,154 @@
1
+ import type { ZodTypeAny } from "zod";
2
+ import type { FormAnswer, ToolContent, ToolResult, ToolSpec } from "./tool.js";
3
+ /** 注入到出站请求的鉴权信息。 */
4
+ export interface AuthInjection {
5
+ headers: Record<string, string>;
6
+ query?: Record<string, string>;
7
+ }
8
+ /** 连接器与远端服务的一次会话。 */
9
+ export interface ConnectorSession {
10
+ listTools(): Promise<ToolSpec[]>;
11
+ /**
12
+ * `ctx.spec`:内核快照里该工具已复核的定义(`connector_tools.spec`,绝不是待复核的新定义),含 listTools 时给出的
13
+ * `connectorData`。连接器应按它路由调用,而不是重新读取远端描述(远端变化须先经漂移复核)。旧内核不传,须判空。
14
+ */
15
+ callTool(name: string, input: unknown, signal: AbortSignal, ctx?: {
16
+ spec: ToolSpec;
17
+ }): Promise<ToolResult>;
18
+ listResources?(): Promise<{
19
+ uri: string;
20
+ name: string;
21
+ description?: string;
22
+ mimeType?: string;
23
+ }[]>;
24
+ readResource?(uri: string): Promise<ToolResult>;
25
+ /** 可选:远端提供的提示模板(如 MCP prompts)。不支持时不实现,内核须判空。 */
26
+ listPrompts?(): Promise<PromptInfo[]>;
27
+ /** 可选:按参数渲染一个提示模板,返回消息(如 MCP `prompts/get`)。失败抛 ToolError(英文、不含凭据)。 */
28
+ getPrompt?(name: string, args: Record<string, string>): Promise<RenderedPrompt>;
29
+ close(): Promise<void>;
30
+ }
31
+ /** 远端提示模板(`/连接器:提示名` 斜杠命令)。 */
32
+ export interface PromptInfo {
33
+ name: string;
34
+ title?: string;
35
+ description?: string;
36
+ arguments: {
37
+ name: string;
38
+ description?: string;
39
+ required?: boolean;
40
+ }[];
41
+ }
42
+ /** 渲染后的提示:按顺序的消息,内容块同工具结果(非文本给类型说明)。 */
43
+ export interface RenderedPrompt {
44
+ description?: string;
45
+ messages: {
46
+ role: "user" | "assistant";
47
+ content: ToolContent[];
48
+ }[];
49
+ }
50
+ /**
51
+ * 远端在一次调用中途向用户要输入(MCP `elicitation/create`):
52
+ * - `form`:`requestedSchema` 是远端给的扁平 JSON Schema(object + 原始类型属性),由内核映射成交互表单;
53
+ * - `url`:请用户在浏览器里打开 `url` 完成某步(授权、付款、填敏感信息),完成与否由用户确认。
54
+ */
55
+ export type ElicitationRequest = {
56
+ mode: "form";
57
+ message: string;
58
+ requestedSchema: Record<string, unknown>;
59
+ } | {
60
+ mode: "url";
61
+ message: string;
62
+ url: string;
63
+ elicitationId: string;
64
+ };
65
+ /** 对 elicitation 的答复(与 MCP `ElicitResult` 对应):接受(带表单内容)/ 拒绝 / 取消。 */
66
+ export type ElicitationResponse = {
67
+ action: "accept";
68
+ content?: FormAnswer;
69
+ } | {
70
+ action: "decline";
71
+ } | {
72
+ action: "cancel";
73
+ };
74
+ /** 内核打开会话时传给连接器的参数。 */
75
+ export interface ConnectorOpenArgs {
76
+ /** 已按 endpointSchema 校验。 */
77
+ endpoint: unknown;
78
+ /** 每次请求前调用,凭据的刷新由内核负责。 */
79
+ auth: () => Promise<AuthInjection>;
80
+ /** 收到 401 时调用一次;内核刷新凭据后,插件重试一次。 */
81
+ onUnauthorized: () => Promise<void>;
82
+ signal: AbortSignal;
83
+ /**
84
+ * 会话期间远端报告工具清单变了(如 MCP `notifications/tools/list_changed`)时调用,可能多次;内核据此安排一次
85
+ * 漂移检查(新的 / 变了的工具在管理员复核前不可用)。可选:旧内核不提供,连接器须判空。不要等待它、不要因它抛错。
86
+ */
87
+ onToolsChanged?: () => void;
88
+ /**
89
+ * Called when the remote asks the user for input mid-call (MCP `elicitation/create`, the URL-mode
90
+ * `-32042` error); the connector passes the returned answer back to the remote as is. Optional:
91
+ * when the kernel does not provide it, the connector does not declare the MCP elicitation
92
+ * capability (so the remote never starts one).
93
+ *
94
+ * 内核不能在等用户回答期间保持远端调用(运行会暂停、worker 释放、会话关闭):需要问用户时,内核**中止本次调用**
95
+ * (经 `callTool` 的 signal)并让运行进入等待;用户回答后内核重新发起同一调用,远端再次发起同样的 elicitation 时
96
+ * 由本回调立即用用户的答案答复。所以连接器只需「收到就调用、拿到就回」,不要自己缓存或重试;需要问用户时本回调
97
+ * 以错误拒绝(调用已被中止),连接器让该错误原样结束本次调用即可。
98
+ */
99
+ onElicit?: (request: ElicitationRequest) => Promise<ElicitationResponse>;
100
+ }
101
+ /**
102
+ * 远端描述里声明的一种认证方式,映射到内核连接器认证(ConnectorAuth)的形状,供安装向导预填。
103
+ * `scheme` 是远端描述里的名字(如 OpenAPI `securitySchemes` 的键),`description` 原样给出(可能缺省)。
104
+ * 只列内核支持的方式;其余(cookie 里的 API key、OpenID Connect、implicit / password 流等)不出现。
105
+ */
106
+ export type AuthHint = {
107
+ scheme: string;
108
+ description?: string;
109
+ } & ({
110
+ type: "bearer";
111
+ } | {
112
+ type: "basic";
113
+ } | {
114
+ type: "api_key";
115
+ in: "header" | "query";
116
+ name: string;
117
+ } | {
118
+ type: "oauth_client";
119
+ tokenUrl: string;
120
+ scopes: string[];
121
+ } | {
122
+ type: "oauth_code";
123
+ authorizationUrl: string;
124
+ tokenUrl: string;
125
+ scopes: string[];
126
+ });
127
+ /** 远端描述里的一个操作(如 OpenAPI operation),供安装向导勾选。 */
128
+ export interface ConnectorOperation {
129
+ /** 端点配置里勾选用的 id(如 OpenAPI 的 operationId)。 */
130
+ id: string;
131
+ /** 大写 HTTP 方法。 */
132
+ method: string;
133
+ path: string;
134
+ summary?: string;
135
+ }
136
+ /** `Connector.inspect` 的结果。 */
137
+ export interface ConnectorInspection {
138
+ authHints: AuthHint[];
139
+ operations?: ConnectorOperation[];
140
+ }
141
+ /** `connector@1`:插件实现、内核调用。 */
142
+ export interface Connector {
143
+ kind: string;
144
+ endpointSchema: ZodTypeAny;
145
+ open(args: ConnectorOpenArgs): Promise<ConnectorSession>;
146
+ /**
147
+ * 可选:不带凭据读取端点的描述(安装向导的预览用),返回声明的认证方式与可选的操作清单。
148
+ * `endpoint` 已按 endpointSchema 校验。失败抛 ToolError(消息英文、不含凭据)。旧连接器不实现,内核须判空。
149
+ */
150
+ inspect?(args: {
151
+ endpoint: unknown;
152
+ signal: AbortSignal;
153
+ }): Promise<ConnectorInspection>;
154
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/ctx.d.ts ADDED
@@ -0,0 +1,176 @@
1
+ import type { Kysely } from "kysely";
2
+ import type { ExtensionPointRef } from "./extension-point.js";
3
+ import type { AvailableHook, HookHandler, HookOptions } from "./hooks.js";
4
+ import type { ObjectHead, PresignedUpload, PresignGetOptions } from "./storage.js";
5
+ import type { ToolHandler, ToolSpec } from "./tool.js";
6
+ import type { EmbedClaims, UiActionArgs, UiActionResult, UiRenderArgs, UiView } from "./ui.js";
7
+ /** 日志接口。 */
8
+ export interface Logger {
9
+ info(msg: string, fields?: Record<string, unknown>): void;
10
+ warn(msg: string, fields?: Record<string, unknown>): void;
11
+ error(msg: string, fields?: Record<string, unknown>): void;
12
+ }
13
+ /** 分布式锁租约。 */
14
+ export interface Lease {
15
+ renew(): Promise<boolean>;
16
+ release(): Promise<void>;
17
+ }
18
+ /**
19
+ * 插件的对象存储(`ctx.storage`,需 `permissions.storage`):与 `ObjectStore` 同形,但每个方法的第一个参数是
20
+ * `orgId`,键与前缀由内核自动加上 `plugins/<插件名>/<orgId>/`——插件只能看到自己在该组织下的对象。
21
+ * 底层是部署配置的 `object-store@1` 提供方;没有提供方时各方法抛错。
22
+ */
23
+ export interface ExtensionStorage {
24
+ put(orgId: string, key: string, body: Uint8Array | ReadableStream<Uint8Array>, opts: {
25
+ contentType: string;
26
+ size?: number;
27
+ signal?: AbortSignal;
28
+ }): Promise<ObjectHead>;
29
+ /**
30
+ * 不存在返回 null。`range` 为闭区间字节范围,缺省 `end` 表示读到末尾。
31
+ * 带 `range` 时 `head.size` 仍是整个对象的大小;存储忽略 Range 时 body 可能是整个对象。
32
+ */
33
+ get(orgId: string, key: string, opts?: {
34
+ signal?: AbortSignal;
35
+ range?: {
36
+ start: number;
37
+ end?: number;
38
+ };
39
+ }): Promise<{
40
+ head: ObjectHead;
41
+ body: ReadableStream<Uint8Array>;
42
+ } | null>;
43
+ head(orgId: string, key: string): Promise<ObjectHead | null>;
44
+ /** 不存在也算成功。 */
45
+ delete(orgId: string, key: string): Promise<void>;
46
+ deletePrefix(orgId: string, prefix: string): Promise<number>;
47
+ list(orgId: string, prefix: string, opts?: {
48
+ cursor?: string;
49
+ limit?: number;
50
+ }): Promise<{
51
+ items: ObjectHead[];
52
+ cursor?: string;
53
+ }>;
54
+ presignGet(orgId: string, key: string, opts: PresignGetOptions): Promise<string>;
55
+ presignPut(orgId: string, key: string, opts: {
56
+ contentType: string;
57
+ maxSize: number;
58
+ ttlSeconds: number;
59
+ }): Promise<PresignedUpload>;
60
+ }
61
+ /** 传给插件 `setup` 的上下文。不提供定时器与进程内共享状态;kv 写入必须带 TTL。 */
62
+ export interface ExtensionContext {
63
+ kv: {
64
+ get(key: string): Promise<string | null>;
65
+ set(key: string, value: string, opts: {
66
+ ttlSeconds: number;
67
+ }): Promise<void>;
68
+ del(key: string): Promise<void>;
69
+ incr(key: string, opts: {
70
+ ttlSeconds: number;
71
+ }): Promise<number>;
72
+ };
73
+ lock: {
74
+ acquire(key: string, opts: {
75
+ ttlMs: number;
76
+ }): Promise<Lease | null>;
77
+ };
78
+ bus: {
79
+ publish(topic: string, payload: unknown): Promise<void>;
80
+ subscribe(topic: string, handler: (payload: unknown) => void): Promise<() => Promise<void>>;
81
+ };
82
+ queue: {
83
+ define<T>(name: string, handler: (data: T) => Promise<void>, opts?: {
84
+ concurrency?: number;
85
+ }): void;
86
+ add<T>(name: string, data: T, opts?: {
87
+ delayMs?: number;
88
+ jobId?: string;
89
+ }): Promise<void>;
90
+ };
91
+ secrets: {
92
+ get(orgId: string, key: string): Promise<string | null>;
93
+ set(orgId: string, key: string, value: string): Promise<void>;
94
+ };
95
+ /** 对象存储(需 `permissions.storage`,见 {@link ExtensionStorage})。 */
96
+ storage: ExtensionStorage;
97
+ /** 已切到本插件 schema(withSchema)的 Kysely 实例。 */
98
+ db: Kysely<any>;
99
+ http: {
100
+ /** 实际挂载到 `/plugins/<name><path>`。 */
101
+ route(method: "GET" | "POST" | "PUT" | "DELETE", path: string, handler: (req: Request) => Promise<Response>): void;
102
+ };
103
+ tools: {
104
+ /** 注册一个工具;只能在 `setup` 期间调用,名称不可重复。 */
105
+ register(spec: ToolSpec, handler: ToolHandler): void;
106
+ };
107
+ /**
108
+ * Consume extension points provided by other extensions: only non-brokered refs declared in the
109
+ * manifest's `requires` / `optional`. Without `orgId` it is the deployment-level view and returns
110
+ * synchronously; with `orgId` it returns a Promise, and orgScoped extension points (such as
111
+ * `channel`, `directory`) are filtered by that org's enablement (queried every time, not cached);
112
+ * `get` rejects with `ExtensionPointUnavailableError` when the provider is not enabled in that org.
113
+ * When there is no provider, `get` throws `ExtensionPointUnavailableError` and `list` returns an
114
+ * empty list.
115
+ */
116
+ extensionPoints: {
117
+ /** `provider` may be omitted for a single-select extension point; required for multi-select. */
118
+ get<T>(ref: ExtensionPointRef, provider?: string): T;
119
+ get<T>(ref: ExtensionPointRef, provider: string | undefined, opts: {
120
+ orgId: string;
121
+ }): Promise<T>;
122
+ list<T>(ref: ExtensionPointRef): {
123
+ plugin: string;
124
+ impl: T;
125
+ }[];
126
+ list<T>(ref: ExtensionPointRef, opts: {
127
+ orgId: string;
128
+ }): Promise<{
129
+ plugin: string;
130
+ impl: T;
131
+ }[]>;
132
+ };
133
+ /**
134
+ * 界面贡献的运行时(清单 `ui` + `permissions.ui`):
135
+ * - `view` / `action`:只能在 `setup` 期间注册,id 是清单里 kind view 的贡献 id;
136
+ * `action` 收到视图里按钮或表单提交(`args.actionId` 区分是哪一个);
137
+ * - `invalidate`:告诉内核「这个贡献变了」,打开着它的网页会重新取视图(经 bus,跨副本);
138
+ * - `verifyEmbedToken`:校验 embed 页面拿到的嵌入令牌,只认签给本插件、未过期的,且此刻插件仍在该组织启用、
139
+ * 调用者仍是 active 成员(组织页面还要求 member 以上),否则返回 null;返回的 `role` 是此刻的角色。
140
+ */
141
+ ui: {
142
+ view(id: string, render: (args: UiRenderArgs) => Promise<UiView>): void;
143
+ action(id: string, handler: (args: UiActionArgs) => Promise<UiActionResult>): void;
144
+ invalidate(target: {
145
+ orgId: string;
146
+ conversationId?: string;
147
+ contribution: string;
148
+ }): Promise<void>;
149
+ verifyEmbedToken(token: string): Promise<EmbedClaims | null>;
150
+ };
151
+ /**
152
+ * Hooks (`permissions.hooks`): observe model and tool calls, or deny them with a policy hook.
153
+ * `on` is only allowed during `setup`; the mount point must be declared in `permissions.hooks` and be
154
+ * one of `HOOKS_AVAILABLE`; `mode: "policy"` only on `POLICY_HOOKS` and only for mount points also
155
+ * declared in `permissions.policyHooks`. Hooks fire only for organizations
156
+ * that enabled the plugin, only in chat conversations (never in eval replays); see `HookOptions` for
157
+ * how policy and observe hooks run. Unloading, upgrading or disabling the plugin instance revokes
158
+ * them (in-flight hook calls are drained like tool calls).
159
+ */
160
+ hooks: {
161
+ on<K extends AvailableHook>(mount: K, handler: HookHandler<K>, opts: HookOptions): void;
162
+ };
163
+ provide(extensionPoint: ExtensionPointRef, impl: unknown): void;
164
+ /**
165
+ * 本插件实例的生命周期信号:实例被撤销(停用、卸载、升级)时、在途调用排空之后中止。
166
+ * 插件自己起的长任务(轮询循环、长连接等)据此收尾。
167
+ */
168
+ signal: AbortSignal;
169
+ /**
170
+ * 部署级配置,已按插件 schema 校验(启动配置与企业管理员在后台保存的值合并后)。这是一个 getter:
171
+ * 标了 `hot`(`.meta({ hot: true })`)的字段改动时内核就地替换,插件**在用时读** `ctx.config` 才能拿到新值,
172
+ * 不要在 setup 里把 hot 字段拷进闭包;改了非 hot 字段时内核重新加载插件实例(新的 setup)。
173
+ */
174
+ readonly config: unknown;
175
+ log: Logger;
176
+ }
package/dist/ctx.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import type { z } from "zod";
2
+ import type { ExtensionContext } from "./ctx.js";
3
+ import { type ExtensionPointRef } from "./extension-point.js";
4
+ import { type ExtensionPermissions } from "./permissions.js";
5
+ import type { UiContribution } from "./ui.js";
6
+ /** 插件清单。 */
7
+ export interface ExtensionManifest {
8
+ name: string;
9
+ version: string;
10
+ sdk: string;
11
+ provides: ExtensionPointRef[];
12
+ requires?: ExtensionPointRef[];
13
+ /** 可选依赖:缺失时插件照常加载,运行时经 ctx.extensionPoints 判断。 */
14
+ optional?: ExtensionPointRef[];
15
+ /** 权限清单(治理而非沙箱)。 */
16
+ permissions?: ExtensionPermissions;
17
+ /** 界面贡献:对话页标签与组织页面;id 须同时列在 `permissions.ui` 里。 */
18
+ ui?: UiContribution[];
19
+ config?: {
20
+ deployment?: z.ZodTypeAny;
21
+ org?: z.ZodTypeAny;
22
+ };
23
+ migrations?: string;
24
+ setup(ctx: ExtensionContext): void | Promise<void>;
25
+ /**
26
+ * 可选:插件实例被撤销(停用、卸载、升级换新实例、依赖方随提供方重新加载)时调用。
27
+ * 调用时内核已经:停止接新的调用、等在途调用结束(最多 `extensionHost.drainSeconds`)、中止 `ctx.signal`、
28
+ * 撤销本实例经 ctx 做过的全部登记(工具、路由、队列 worker、bus 订阅、锁租约、界面、provide)。
29
+ * 这里只收拾插件自己在 ctx 之外开的东西(如自建的连接);不要再经 ctx 登记任何东西。
30
+ * setup 抛错时不调用(已登记的由内核撤销)。
31
+ */
32
+ dispose?(ctx: ExtensionContext): void | Promise<void>;
33
+ }
34
+ /** 插件清单不合法。 */
35
+ export declare class ExtensionManifestError extends Error {
36
+ constructor(plugin: string, message: string);
37
+ }
38
+ /** 插件名规则(清单 `name`;连接器模板的 `plugin` 也按它校验)。 */
39
+ export declare const EXTENSION_NAME_RE: RegExp;
40
+ /** 校验并原样返回插件清单;不合法抛 ExtensionManifestError。 */
41
+ export declare function defineExtension(m: ExtensionManifest): ExtensionManifest;
@@ -0,0 +1,85 @@
1
+ import semver from "semver";
2
+ import { EXTENSION_POINTS, parseExtensionPoint, } from "./extension-point.js";
3
+ import { validatePermissions, validateUiContributions, } from "./permissions.js";
4
+ /** 插件清单不合法。 */
5
+ export class ExtensionManifestError extends Error {
6
+ constructor(plugin, message) {
7
+ super(`extension ${plugin}: ${message}`);
8
+ this.name = "ExtensionManifestError";
9
+ }
10
+ }
11
+ /** 插件名规则(清单 `name`;连接器模板的 `plugin` 也按它校验)。 */
12
+ export const EXTENSION_NAME_RE = /^[a-z][a-z0-9-]{1,40}$/;
13
+ const NAME_RE = EXTENSION_NAME_RE;
14
+ /** 保留的插件名:内核用 `kernel` 作为自己的 secrets 命名空间,插件不能与之相同。 */
15
+ const RESERVED_NAMES = new Set(["kernel"]);
16
+ function checkRefs(label, refs, fail, forbidBrokered = false) {
17
+ for (const ref of refs) {
18
+ let name;
19
+ try {
20
+ name = parseExtensionPoint(ref).name;
21
+ }
22
+ catch {
23
+ fail(`${label} entry "${ref}" is not a valid extension point ref`);
24
+ }
25
+ if (!Object.hasOwn(EXTENSION_POINTS, name)) {
26
+ fail(`${label} entry "${ref}" references unregistered extension point "${name}"`);
27
+ }
28
+ if (forbidBrokered && EXTENSION_POINTS[name]?.brokered) {
29
+ fail(`${label} entry "${ref}" is a brokered extension point and is used through the platform`);
30
+ }
31
+ }
32
+ }
33
+ /** 校验并原样返回插件清单;不合法抛 ExtensionManifestError。 */
34
+ export function defineExtension(m) {
35
+ const plugin = typeof m?.name === "string" && m.name !== "" ? m.name : "<unnamed>";
36
+ const fail = (msg) => {
37
+ throw new ExtensionManifestError(plugin, msg);
38
+ };
39
+ if (typeof m.name !== "string" || !NAME_RE.test(m.name)) {
40
+ fail(`field "name" must match ${NAME_RE}`);
41
+ }
42
+ if (RESERVED_NAMES.has(m.name))
43
+ fail(`field "name" "${m.name}" is reserved`);
44
+ if (typeof m.version !== "string" || semver.valid(m.version) === null) {
45
+ fail(`field "version" must be a valid semver version`);
46
+ }
47
+ if (typeof m.sdk !== "string" || semver.validRange(m.sdk) === null) {
48
+ fail(`field "sdk" must be a valid semver range`);
49
+ }
50
+ if (typeof m.setup !== "function")
51
+ fail(`field "setup" must be a function`);
52
+ if (m.dispose !== undefined && typeof m.dispose !== "function") {
53
+ fail(`field "dispose" must be a function when present`);
54
+ }
55
+ if (!Array.isArray(m.provides))
56
+ fail(`field "provides" must be an array`);
57
+ checkRefs("provides", m.provides, fail);
58
+ if (new Set(m.provides).size !== m.provides.length) {
59
+ fail(`field "provides" has duplicate entries`);
60
+ }
61
+ const requires = m.requires ?? [];
62
+ checkRefs("requires", requires, fail, true);
63
+ for (const r of requires) {
64
+ if (m.provides.includes(r))
65
+ fail(`field "requires" entry "${r}" is also provided by the same plugin`);
66
+ }
67
+ const optional = m.optional ?? [];
68
+ if (!Array.isArray(optional))
69
+ fail(`field "optional" must be an array`);
70
+ checkRefs("optional", optional, fail, true);
71
+ for (const r of optional) {
72
+ if (m.provides.includes(r) || requires.includes(r)) {
73
+ fail(`field "optional" entry "${r}" overlaps with "provides" or "requires"`);
74
+ }
75
+ }
76
+ if (m.permissions !== undefined) {
77
+ const errs = validatePermissions(plugin, m.permissions);
78
+ if (errs.length > 0)
79
+ fail(`invalid permissions: ${errs.join("; ")}`);
80
+ }
81
+ const uiErrs = validateUiContributions(m.ui, m.permissions);
82
+ if (uiErrs.length > 0)
83
+ fail(`invalid ui: ${uiErrs.join("; ")}`);
84
+ return m;
85
+ }
@@ -0,0 +1,29 @@
1
+ export interface DirectoryMember {
2
+ externalId: string;
3
+ tenantId: string;
4
+ email?: string;
5
+ emailVerified: boolean;
6
+ displayName: string;
7
+ active: boolean;
8
+ }
9
+ export interface Directory {
10
+ /**
11
+ * Optional: whether this org-level config has the directory fully set up (e.g. every required
12
+ * credential filled in). Implement it in an extension whose directory fields are optional in the
13
+ * org-level config (the same extension also provides other extension points such as a channel, and
14
+ * an org that uses only those leaves the directory config empty). When it returns false the kernel
15
+ * treats the directory as "not configured": the hourly scan, `directory_changed` and the
16
+ * pre-removal recheck are skipped silently (no job, no audit, no warning), and a manual sync by an
17
+ * admin returns 409 `directory.not_configured`. When not implemented, any config that satisfies
18
+ * the schema counts as configured. Must be a pure function (no requests, never throws).
19
+ */
20
+ configured?(orgConfig: unknown): boolean;
21
+ listMembers(args: {
22
+ orgConfig: unknown;
23
+ cursor?: string;
24
+ signal: AbortSignal;
25
+ }): Promise<{
26
+ members: DirectoryMember[];
27
+ next?: string;
28
+ }>;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ /** Extension point ref, of the form `channel@1` (name@major). */
2
+ export type ExtensionPointRef = `${string}@${number}`;
3
+ /** Extension point cardinality: `single` allows one provider at a time, `multi` allows several. */
4
+ export type Cardinality = "single" | "multi";
5
+ /** Registry entry of an extension point. */
6
+ export interface ExtensionPointInfo {
7
+ cardinality: Cardinality;
8
+ /** Brokered by the platform API (extensions may not `requires` it; they use it only through `ctx`). */
9
+ brokered: boolean;
10
+ /** Enabled per org (consumers may pass `orgId` to check that the org enabled the provider). */
11
+ orgScoped: boolean;
12
+ }
13
+ /** The registered extension points. */
14
+ export declare const EXTENSION_POINTS: Record<string, ExtensionPointInfo>;
15
+ /** Cardinality of the registered extension points (derived from EXTENSION_POINTS). */
16
+ export declare const EXTENSION_POINT_CARDINALITY: Record<string, Cardinality>;
17
+ /** Connector extension point ref (see `connector.ts`). */
18
+ export declare const CONNECTOR_EXTENSION_POINT = "connector@1";
19
+ /** IM channel extension point ref (see `channel.ts`). */
20
+ export declare const CHANNEL_EXTENSION_POINT = "channel@1";
21
+ /** Third-party login identity source extension point ref (see `identity.ts`). */
22
+ export declare const IDENTITY_SOURCE_EXTENSION_POINT = "identity-source@1";
23
+ /** Directory sync extension point ref (see `directory.ts`). */
24
+ export declare const DIRECTORY_EXTENSION_POINT = "directory@1";
25
+ /** Parses an extension point ref; throws when the format is invalid. */
26
+ export declare function parseExtensionPoint(ref: string): {
27
+ name: string;
28
+ major: number;
29
+ };
@@ -0,0 +1,36 @@
1
+ const info = (cardinality, brokered = false, orgScoped = false) => ({
2
+ cardinality,
3
+ brokered,
4
+ orgScoped,
5
+ });
6
+ /** The registered extension points. */
7
+ export const EXTENSION_POINTS = {
8
+ sandbox: info("single", true),
9
+ "config-source": info("single"),
10
+ "secret-store": info("single", true),
11
+ "object-store": info("single", true),
12
+ channel: info("multi", false, true),
13
+ "model-provider": info("multi", true),
14
+ "identity-source": info("multi"),
15
+ directory: info("multi", false, true),
16
+ counter: info("multi"),
17
+ connector: info("multi", true),
18
+ "file-preview": info("multi", false, true),
19
+ };
20
+ /** Cardinality of the registered extension points (derived from EXTENSION_POINTS). */
21
+ export const EXTENSION_POINT_CARDINALITY = Object.fromEntries(Object.entries(EXTENSION_POINTS).map(([k, v]) => [k, v.cardinality]));
22
+ /** Connector extension point ref (see `connector.ts`). */
23
+ export const CONNECTOR_EXTENSION_POINT = "connector@1";
24
+ /** IM channel extension point ref (see `channel.ts`). */
25
+ export const CHANNEL_EXTENSION_POINT = "channel@1";
26
+ /** Third-party login identity source extension point ref (see `identity.ts`). */
27
+ export const IDENTITY_SOURCE_EXTENSION_POINT = "identity-source@1";
28
+ /** Directory sync extension point ref (see `directory.ts`). */
29
+ export const DIRECTORY_EXTENSION_POINT = "directory@1";
30
+ /** Parses an extension point ref; throws when the format is invalid. */
31
+ export function parseExtensionPoint(ref) {
32
+ const m = /^([a-z][a-z0-9-]*)@(\d+)$/.exec(ref);
33
+ if (!m)
34
+ throw new Error(`invalid extension point ref "${ref}", expected "<name>@<major>"`);
35
+ return { name: m[1], major: Number(m[2]) };
36
+ }
@@ -0,0 +1,64 @@
1
+ import type { Locale } from "./cards.js";
2
+ import type { UiView } from "./ui.js";
3
+ /** File previewer extension point ref (UI refresh spec §5.8). */
4
+ export declare const FILE_PREVIEW_EXTENSION_POINT = "file-preview@1";
5
+ /** Largest file a previewer may declare (`FilePreviewer.maxBytes`): 20 MiB. */
6
+ export declare const FILE_PREVIEW_MAX_BYTES: number;
7
+ /**
8
+ * One previewer of a `file-preview@1` provider: which files it previews and how. It renders either a
9
+ * view (`render`, drawn by the web app like any `UiView`) or an embed page (`embed`, the id of a ui
10
+ * contribution `{ kind: "embed", place: "file-preview" }` of the same extension, which receives the
11
+ * file over the message bridge). Exactly one of the two.
12
+ */
13
+ export interface FilePreviewer {
14
+ /** Unique within the extension, `[a-z0-9-]{1,40}`. */
15
+ id: string;
16
+ title: {
17
+ zh: string;
18
+ en: string;
19
+ };
20
+ /**
21
+ * Files it accepts: MIME types (`image/*` wildcards allowed) and / or file extensions (with or
22
+ * without the dot; matched case-insensitively). At least one entry.
23
+ */
24
+ accepts: {
25
+ mimeTypes?: string[];
26
+ extensions?: string[];
27
+ };
28
+ /** Largest file it previews, at most {@link FILE_PREVIEW_MAX_BYTES}. */
29
+ maxBytes: number;
30
+ render?(args: FilePreviewArgs): Promise<UiView>;
31
+ embed?: string;
32
+ }
33
+ /** What a `file-preview@1` extension provides (`ctx.provide("file-preview@1", provider)`). */
34
+ export interface FilePreviewProvider {
35
+ previewers: FilePreviewer[];
36
+ }
37
+ /** Arguments of {@link FilePreviewer.render}; identities come from the kernel after authentication. */
38
+ export interface FilePreviewArgs {
39
+ orgId: string;
40
+ accountId: string;
41
+ conversationId: string;
42
+ locale: Locale;
43
+ /** Aborted at the kernel's time limit: stop reading and rendering. */
44
+ signal: AbortSignal;
45
+ file: {
46
+ id: string;
47
+ name: string;
48
+ contentType: string;
49
+ size: number;
50
+ };
51
+ /**
52
+ * The read grant: this one file's bytes, valid only during this call (rejects afterwards), and
53
+ * rejects a file over the previewer's `maxBytes`. The extension never gets a storage key or URL.
54
+ */
55
+ read(): Promise<Uint8Array>;
56
+ }
57
+ /** Checks a `file-preview@1` provider; returns the problems (empty = valid). */
58
+ export declare function validateFilePreviewProvider(p: unknown): string[];
59
+ /** Whether a previewer takes this file: its type or extension is accepted and it is within `maxBytes`. */
60
+ export declare function previewerAccepts(p: Pick<FilePreviewer, "accepts" | "maxBytes">, file: {
61
+ name: string;
62
+ contentType: string;
63
+ size: number;
64
+ }): boolean;