Replies: 1 comment
|
这个提案我举双手赞成,而且我大概是最有资格证明它必要的人之一:我们就是你描述的那种"仓库之外的第三方插件",而且我们已经被这条白名单逼着绕过去了。 下面是第一手的现状报告——它同时是给你的一条"今天就能用"的路,和一份支持你这个 RFC 的证据。 我们撞上的就是这堵墙,绕法是自己造一整个数据面我维护 pi2dsh(把 Pi 生态插件翻译成 DSH 原生插件的兼容层)。它的产品层 按理说这该是
也就是说: 这正好印证你写的那句"等于要求每个第三方插件的消费者 fork 框架":我们没 fork,我们付的是另一种代价——重写一遍配置的数据面。 这条绕法对你有用,但它的代价值得写进提案如果你现在就要让 elasticsearch-plugins 的配置出现在网页上,上面两步今天可用(我们在真 DSH loop 上跑着)。但我建议你把这个代价明确写进 RFC 的动机段,因为它比"要 fork 框架"更能说明问题——绕得过去,恰恰证明这不是安全边界,只是摩擦:
换句话说:白名单没有挡住任何东西,它只是把插件推去实现一个更差的 对 API 形状的两点意见你的设计我基本认同(声明式 opt-in、跟随 fiber 生命周期、按 surface 建模)。两点: 1. 建议至少在文档上把它定成"开放词汇"( 2. (凭证这条对我们尤其现实:我们的铁律是凭证只经环境变量注入、永不落盘、永不回显,配置里只放 边界与利益相关我们不修 DSH 自家组件—— 这条不推销:你要的是这个 RFC 被接受,装我们的东西对此毫无帮助。上面那条 最后一句实在话:你这份提案里最有力的部分是那段源码注释引用——"Moving that declaration to |
Uh oh!
There was an error while loading. Please reload this page.
现状
ctx.settings.register(ns, schema, { base, applies, validate })只解决"注册 + 分层解析",不表达任何暴露语义:任何插件都能注册命名空间,但注册本身不会让配置面可读写。settings.*wire)能服务的命名空间由packages/host/apiproxy/src/api-proxy.ts里硬编码的白名单决定:可配置模型提供方 +WEB_SETTINGS_NAMESPACES(agent-loop/shell/locale/permission/ui-conversation/ui-theme/web-search-deepseek)+PRODUCT_SETTINGS_NAMESPACES(ui-onboarding、agent-presets)。settings-not-exposed——卡片不渲染、写入被拒。要让它的配置出现在网页上,必须改动 apiproxy 这一行白名单,等于要求每个第三方插件的消费者 fork 框架。settings.register(), so a plugin can expose its own configuration without a change in this package」。需求
register()绝不能让一个分节变得可被远程读写。暴露必须是 owner 在注册时的显式声明;部署方的决策保持为"挂载了哪些插件"。代码改动点
1.
packages/settings/settings/src/index.ts新增类型 + 注册选项:
注册记录与
register()透传:interface SettingsRegistration { ns: SettingsNamespace schema: z<unknown> base: unknown applies: SettingsApplies + /** Remote configuration surfaces the owner opted into; none by default. */ + expose?: SettingsExposure /** Owner-supplied check for constraints the schema cannot express. */ validate?: (value: unknown) => voidconst registration: SettingsRegistration = { ns, schema: schema as z<unknown>, base: options?.base, applies: options?.applies ?? 'live', + ...options?.expose === undefined + ? {} + : { expose: options.expose }, ...options?.validate === undefined ? {} : { validate: options.validate as (value: unknown) => void },新增查询方法(放在
get(ns)之后):get(ns: SettingsNamespace): unknown { return this.registrations.get(ns)?.resolved } + /** + * The registered namespaces whose owner opted into remote configuration + * serving through `expose`. Read live on every call: a registration is an + * effect on its owner's fiber, so a disposed (or reloaded) plugin leaves + * this set the moment its registration does. A wire surface unions the set + * for ITS OWN surface with its internal allowlists before serving a + * namespace — the filter is the check the declaration promises. + * @param surface - only namespaces declared for this surface; every declared + * namespace when omitted. + * @returns the currently exposed namespaces. + */ + exposedNamespaces(surface?: SettingsExposure): ReadonlySet<SettingsNamespace> { + const exposed = new Set<SettingsNamespace>() + for (const registration of this.registrations.values()) { + if (registration.expose === undefined) continue + if (surface === undefined || registration.expose === surface) exposed.add(registration.ns) + } + return exposed + }installSettingsSection的 hooks 与透传:const scope = sctx.settings.register(ns, schema, { base: entry, + ...hooks.expose === undefined + ? {} + : { expose: hooks.expose }, ...hooks.validate === undefined ? {} : { validate: hooks.validate }, })2.
packages/host/apiproxy/src/api-proxy.ts白名单注释(删除 deferred work 表述):
门禁并入插件声明集合(读、写共用同一函数):
function exposedNamespaces(): Set<string> { const exposed = modelProviderNamespaces() for (const ns of WEB_SETTINGS_NAMESPACES) exposed.add(ns) for (const ns of PRODUCT_SETTINGS_NAMESPACES) exposed.add(ns) + // Read live on every call: registrations ride their owner's fiber, so a + // disposed or reloaded plugin leaves this set the moment it unregisters. + // The surface filter is this wire's check: a namespace declared for some + // other configuration client is not thereby served to the browser. + const settings = ctx.get('settings') + if (settings !== undefined) { + for (const ns of settings.exposedNamespaces('web')) exposed.add(String(ns)) + } return exposed }3. 插件侧用法(零 apiproxy 改动)
4. 测试
packages/settings/settings/tests/settings.spec.ts:packages/host/apiproxy/tests/api-proxy-config.spec.ts:使用方式:自定义卡片 +
expose: 'web'expose: 'web'只解决数据面(让命名空间可被浏览器读写),它不会自动生成卡片。完整流程需要两半各做一步:expose: 'web';settings.plugin.itemslot。① Host 侧:注册 + 声明暴露
不写
expose的字段/分节,对浏览器依旧完全不可见——默认拒绝不变。② 浏览器侧:注册卡片(需要 browser half)
卡片的宿主是
ui-settings-plugins声明的settings.plugin.item列表 slot。插件要自带卡片,需要以dsh.client.platform: "web"的 client 插件形态注册一个贡献:卡片控制器复用
ui-settings-plugins的表单设施(CardForm暂存式表单、PluginCard外壳、ValueField/SecretField控件):All reactions