动态插件运行时:Host/Client 双半部
一句话版:
@deepseek-ai/dsh-cordis-host-runner在进程内存里保存不可变 Package、用node:vm跑 Host 半部、用一张 handler 表承接浏览器调用;@deepseek-ai/dsh-cordis-client-runner在页面里求值闭包、用白名单门面把插件挂进 loader;@deepseek-ai/dsh-client-ui-cordis把定义、运行状态和批准入口渲染成面板、键控卡片与@pluginId补全。
审计基线 0.1.5-alpha.1 @ 5dda764ed3:包名、ctx key、事件名、配置键、错误码与沙箱符号均与官方源码逐点核对。
运行时自省与动态 Cordis 插件 讲的是模型看到的那一面:tool-cordis 的七个工具、cordis_define 的参数形状、run/update/stop/undefine 的语义。这一篇往下走一层,讲跑起来的机制——谁保存定义、Host 半部在哪执行、Client 半部怎么进浏览器、UI 怎么显示状态。七个工具的目录不在这里重复,直接看那一篇。
一、四个包的分工
| 包 | 面 | ctx key / 注册点 | 职责 |
|---|---|---|---|
@deepseek-ai/dsh-tool-cordis | Host | 注册到 ctx.tools | 七个模型工具 + @pluginId 引用注入(见 运行时自省) |
@deepseek-ai/dsh-cordis-host-runner | Host | ctx.dynamicCordisRunner、ctx.cordisInspect | 定义注册表、vm 沙箱、Host 半部 fiber 生命周期、invoke handler 表 |
@deepseek-ai/dsh-cordis-client-runner | Client | 浏览器 ctx.dynamicCordisRunner | 求值 Client 半部、挂载/卸载、回答运行请求、守卫门面 |
@deepseek-ai/dsh-client-ui-cordis | Client | 注册 slots 与 input source | 全帧面板、四个键控工具卡片、@pluginId 补全 |
Web 组合里前三个由 packages/bundle/web-app/cordis.patch.yml 装载(cordis-host-runner / cordis-client-runner / ui-cordis 三行);tool-cordis 不在任何出厂组合里,是显式 opt-in——packages/preset/agent-presets/presets/cordis/agent.cordis.yml 里的 tool-cordis 行就是启用方式。没有 Host runner,工具即使被装载也永远等不到 ctx.dynamicCordisRunner。
二、cordis-host-runner:定义注册表与 Host 半部沙箱
服务是 DynamicCordisRunnerService(static inject = ['tools']),在 declare module '@deepseek-ai/cordis' 里挂成 ctx.dynamicCordisRunner。配置只有一个字段:
| 配置键 | 默认 | 含义 |
|---|---|---|
vmTimeoutMs | 5000 | 一次同步 vm 求值的最长毫秒数(z.number().min(1).default(5000)) |
定义注册表
DynamicCordisRegistry 是纯进程内存的 Map,没有任何持久化;DSH 重启即清空。身份由四个自增计数器铸造,格式可预测但全局唯一:
| 身份 | 铸造方法 | 形态 |
|---|---|---|
| Plugin ID | mintPluginId(prefix) | <prefix>-<n>,避开已用后缀 |
| Package ID | mintPackageId() | pkg-<n> |
| Plugin Run ID | mintPluginRunId() | run-<n> |
| Approval ID | mintApprovalRequestId() | approval-<n> |
一个 DynamicCordisPlugin 持有不可变 packages、currentPackageId(已提交的运行版本)、nextPackageId(目标版本)、当前 run 和最近一次 latestRun 尝试记录。待批准请求单独放在 pendingRequests,由 armRequest / peekRequest / claimRequest / disarmRequest / pendingRequestFor 管理,首个回答者获胜。
Host 半部沙箱
Host 半部不是普通 import,而是在 createSandbox(pluginId, { handle }) 建出的 node:vm 新 realm 里,由 evaluateHostCode(sandbox, code, id, vmTimeoutMs) 当作 async 函数体求值。定义阶段 precheckCode(code, 'code.host' | 'code.client') 只编译不执行,提前暴露语法错误。
沙箱暴露的符号是 HOST_BUILTIN_INSPECTION 的固定清单:
| 符号 | 用途 |
|---|---|
ctx | 受限 Cordis Context:ctx.get / ctx.on / ctx.provide / ctx.effect |
harness | handle(method, fn)、defineTool(definition)、registerTool(ctx, tool) |
console | 带 [cordis:<id>] 前缀的直写日志 |
btoa / atob | UTF-8 base64 编解码 |
TextEncoder / TextDecoder | 标准编解码构造器 |
Node 全局被刻意关掉,调用即抛教学式重定向(NODE_API_REDIRECTS):require → 用 ctx 上的 cordis 服务;setTimeout / setInterval / setImmediate / clearTimeout / clearInterval → 声明 inject: ['timer'] 用 ctx.timeout / ctx.interval;fetch → 声明 inject: ['web']。另外 DUAL_REALM_INSTANCEOF_PRELUDE 只给 vm 里的 Object / Array / Error 等构造器打 Symbol.hasInstance 补丁,让跨 realm 传入的参数 instanceof 仍然成立。
沙箱隔离全局,但不是安全边界:Node 全局缺失或重定向到 Cordis 服务,Host 半部拿到的门面不含框架内部件,但它声明的服务直达真实运行时。按 README 的说法——把动态包当 bash 权限看待。
注册边界(guard)
guard.ts 是"沙箱产物 → 真实运行时"的唯一闸门:
isPlugin(value)/pluginName(plugin):插件形状校验。normalizeHandler(method, fn):把harness.handle的入参规范化成{ method, handler },写进本次 run 的 handler 表。sandboxDefineTool/sandboxRegisterTool:harness.defineTool走真实 schema DSL 并打DYNAMIC_TOOL标记,harness.registerTool只接受该标记过的定义。sandboxContext(ctx, reportFailure):给 Host 半部的ctx门面——CTX_VERBS(effect/on/once/provide/ 定时器族)放行,ctx.get(name)可选查询,ctx.serviceName直接访问必须由 fiber 的inject声明(declaredInjects读ctx.fiber.inject的键),否则抛带修复提示的守卫错误。guardedPlugin(plugin, reportFailure):把注册动作包一层,post-activation 的拒绝经reportClientGuardFailure同类路径上报。
fiber 生命周期
startHostHalf(group, plugin, reportGuardFailure) 把沙箱返回的插件挂进 cordis-dynamic 组 fiber(requireGroup() 懒创建 rootCtx.plugin({ name: 'cordis-dynamic', apply: () => {} })),await fiber.await() 后返回;启动失败会先 fiber.dispose() 再重抛,绝不留半挂载的 fiber。missingServices(ctx, fiber) 用 fiber.inject 的键对照 ctx.get(name),得出"已 settle 但仍在等哪些服务"的 waitingFor 列表——这是合法的 cordis 语义(服务出现即激活),不是错误。
停止不需要专门 helper:Host 半部注册的一切都是它自己 fiber 上的 effect,fiber.dispose() 即可全部回滚。retract(plugin) 的顺序是——删除 plugin.run、逐个执行 handlerDisposers、await fiber.dispose()、最后 emit cordis/dynamic-retract。
invoke handler 表
Host 半部通过 harness.handle(method, fn) 注册的 handler 存在 run.handlers: Map<string, DynamicCordisHandler> 里,handlerDisposers 记录每个的撤销函数。浏览器侧 host.call(method, args) 经 Typert Remote 打到 @Remote('invoke'):
@Remote('invoke')
async invoke(pluginId, pluginRunId, method, args): Promise<DynamicCordisInvokeResult>
返回四类失败码(DynamicCordisInvokeResult):
| code | 含义 |
|---|---|
plugin-not-running | 插件没有活动运行 |
stale-run | 调用属于已被替换的旧激活 |
method-not-found | 该 method 没注册过 |
handler-error | handler 内部抛错(同时 steer 拥有者 Session) |
事件与 Remote 动词
Host 半部发出的事件(types.ts 的 interface Events 声明合并):
| 事件 | 时机 |
|---|---|
cordis/request-run | 一个带 Client 半部的激活需要页面(可能需要人批准) |
cordis/request-run-resolved | 待批准请求离开可回答状态 |
cordis/dynamic-package | 一次精确激活已在 Host 生效 |
cordis/dynamic-retract | 一次精确激活被撤回 |
cordis/inspect-query | 请求浏览器上的 Client inspect provider 做一次只读查询 |
cordis/inspect-query-resolved | 查询已结算或取消 |
对浏览器暴露的 @Remote 动词:
| 动词 | 作用 |
|---|---|
runHostHalf | 批准后(或面板直接触发时)先起 Host 半部 |
getClientCode | 把某个精确 run 的 Client 源码交给页面 |
resolveRequestRun | 结算一次模型驱动的 Client 激活(首个回答者获胜) |
settleUserRun | 结算面板直接触发的运行 |
stopFromPanel / undefineFromPanel | 面板停止/删除,并把结果注入拥有者 Session |
inventory | 全帧 inventory(不含源码) |
syncInspectManifest | 替换 Host 侧的 Client provider 目录镜像 |
resolveInspectQuery | 用页面结果结算一次跨面 inspect 查询 |
reportRenderFailure | 记录 post-load 渲染崩溃 |
reportClientGuardFailure | 记录 post-activation 守卫拒绝 |
invoke | 路由 Client → Host handler |
工具直接调用的非 Remote 方法有 define、run、stop、undefine、snapshot、reference、listPlugins、inspectPlugin、inspectPackage。
Host 侧的 inspect 目录由 CordisInspectRegistryService 提供,挂成 ctx.cordisInspect:register 收 Host provider、syncClientManifest 镜像页面来的 Client 目录、list 合并两端、query 本地执行或转成 cordis/inspect-query。第一方 Host provider 不在本包,而在 tool-cordis/src/providers.ts——见 运行时自省。
三、cordis-client-runner:浏览器半部
插件名 cordis-client-runner,inject = ['loader', 'modules', 'slots', 'remote', 'remote.dynamicCordisRunner']。声明 remote.dynamicCordisRunner 会让它停在未激活状态直到 Host 命名空间存在,这样页面永远不会加载一个够不到 Host 半部的 Client 半部。
它向浏览器 ctx 提供 ctx.dynamicCordisRunner: CordisRunnerFace:
| 成员 | 作用 |
|---|---|
activeRuns | 每个 Plugin 的在途活动(批准或激活),可观察 |
lastRunError | 本页最近一次运行失败 |
renderFailures | 本页每个包最近一次渲染崩溃 |
reconcileApprovals(rows) | 用 Host inventory 校准待批准行 |
approve(requestId, approveFutureVersions) / decline(requestId) | 回答模型请求 |
startUserRun(request) | 用户手势直接运行(手势本身即授权) |
subscribe / getSnapshot / isLoaded | 本页已加载集合的观察接口 |
事件订阅
apply 里用 ctx.remote.$on(...) 订阅 Host 事件($on 直接给监听器 Host 的原参数列表):
| 事件 | 页面动作 |
|---|---|
cordis/request-run | orchestrator.open(request) 记录待批准活动 |
cordis/request-run-resolved | orchestrator.close(requestId) |
cordis/dynamic-retract | runner.retract(pluginId, pluginRunId) 卸载 |
cordis/inspect-query | 在本页 provider 上执行查询并回送 |
cordis/inspect-query-resolved | 关闭待回答查询 |
闭包求值
evaluateClientHalf(pluginId, clientCode, env, styles) 把源码当作 async 函数体,参数是固定符号面:
| 参数 | 内容 |
|---|---|
React | React 运行时(无 JSX 转换,用 React.createElement) |
console | 带 [cordis:<id>] 前缀的 console,console.error 额外抄进加载报告 |
styles | DynamicCordisStyles,insert(css) 返回撤销函数,卸载自动清理 |
host | { call(method, args = null) },路由到本包 Host 半部的 handler |
harness | 教学陷阱:提示 harness.* 属于 Host 半部 |
setTimeout / setInterval / clearTimeout / clearInterval | 教学陷阱 → 声明 inject: ['timer'] |
fetch / require | 教学陷阱 → 走 Host handler 或 ctx 服务 |
process / Buffer | undefined(让 typeof process 探测保持安全) |
DYNAMIC_CLIENT_REDIRECTS 是这些重定向文案的唯一出处;闭包必须 return 一个插件——函数形式 (ctx) => {…},或对象形式 { name, inject, apply(ctx) },否则抛教学错误。isDynamicCordisPlugin 做与 Host 侧镜像的形状校验。
挂载:loader + 模块表
DynamicCordisPackageRunner 是页面侧加载引擎:
- 先求值闭包,失败 →
cause: 'evaluate'。 moduleId = 'dyn/<pluginId>'(同时是 loader entry 名和 fiber 名)。modules.invalidate(moduleId)后把工厂写进window.__ModuleLoader__的注册槽(缺这个槽说明不在 web shell 里启动)。loader.create({ name: moduleId });解析不到 fiber →cause: 'module-import'。await fiber.await();抛错 →cause: 'activate'。- 记录
waitingFor = Object.keys(fiber.inject).filter(name => ctx.get(name) === undefined)——已 settle 但在等本页没有的服务,算成功(parked)。
卸载 teardown 是 loader.remove(entryId) + modules.invalidate(moduleId) + styles.dispose():entry 移除会级联 dispose fiber,所以 slot 条目和门面 effect 一起消失。每个包 id 有一条串行队列(enqueue),慢加载不会被后来的请求超车;同一 revision 重复加载是幂等的,新 revision 替换旧的,retract 之后同一 revision 可重新加载。
守卫门面
dynamicCordisContext(ctx, env) 是 Host sandboxContext 的浏览器孪生:
ctx.get(name):可选查询,返回真实服务(拒绝返回 Context)。ctx.<service>:只有当 fiber 的inject声明过才放行,否则抛"declare it in inject"的教学错误。ctx.<verb>:CTX_VERBS(effect/on/once/provide/ 定时器族)放行;定时器族还要求声明inject: ['timer']。slots座位:register自动分配页面内遮蔽优先级并把行记进 ledger,同时登记组件归属(渲染崩溃要靠它归因)。theme座位:overrideTokens的 source 被强制替换成<pluginId>.<packageId>,动态包无法冒充或挤掉别人的主题层。
渲染崩溃经 slots.onEntryError 观察:只有本 runner 挂载的组件才算数,报告带 slot、abdicated(崩溃是否让条目退场)和面向作者的 message;一份观察两个出口——reportRenderFailure 上报 Host(给模型和 cordis_inspect_self),本页 renderFailures 给面板。
编排:先 Host 后 Client
CordisRunOrchestrator.drive(plan) 的顺序是固定的:
Host 半部先起,所以 Host 失败会在浏览器还没动之前短路;随后取源码、加载 Client 半部、只发一次结算。成功携带已加载的 pluginRunId 和 parked 服务名;失败只带一个 reason——rejected(用户拒绝)、host-half-failed、client-half-failed,后两者附带失败阶段(evaluate / module-import / activate)和具体 message。同一 Plugin 同时只有一次在途编排(inFlight 去重)。
四、ui-cordis:浏览器表面
inject = ['slots', 'locale', 'inputTriggers', 'remote', 'remote.dynamicCordisRunner', 'dynamicCordisRunner'],注册五处表面:
| 座位 | key | 内容 |
|---|---|---|
sidebar.footer.action | id cordis-panel | 全帧面板:徽标计数 + 每个定义一行 |
tool.call.toolview | cordis_define | 只读定义卡(Host/Client 源码页签) |
tool.call.toolview | cordis_run | 运行卡 + 子座位 tool.view.cordis(kind: 'keyed', scope: 'session') |
tool.call.toolview | cordis_stop / cordis_undefine | 紧凑动作行 |
inputTriggers source | trigger @,name cordis | @pluginId 补全 |
运行/停止开关在面板行上,不在定义卡上。 CordisDefineRow 是纯记录:模型写的 name、purpose、源码和当前状态,没有开关、没有批准按钮,只指向面板。面板行的 onRun / onStop / onRemove / onApprove / onDecline 才是生命周期操作,经 CordisDynamicPort 打到 stopFromPanel / undefineFromPanel / inventory,运行/批准走浏览器 runner。
每行显示两个独立事实——Host 在跑什么、本页加载了什么。cordisVisibleStatus(row, packageId, loaded) 只产出三种读数:idle(没有该 package 的活动 run)、client-pending(Host 已起、本页还没加载)、running(无 Client 半部,或本页已加载该 pluginRunId)。所以刷新后的页面会先给"重新加载到本页",而纯 Host 定义直接读作 running 并只提供停止。
面板数据来自 createCordisInventory:单个全局 inventory 读、single-flight、按公告重读而非就地打补丁。cordis/dynamic-package / cordis/dynamic-retract / cordis/request-run / cordis/request-run-resolved 都会触发刷新;connection/reset 会 reset() 丢弃在途读取,避免重连后发布旧 Host 的行。卡片视图模型(cordisDefineCard / cordisRunCard / cordisActionCard)从冻结的调用/结果切片派生,所以回放渲染同一张卡。
五、双半部契约:一个 Package 必须导出什么
Host 半部(code.host)
- 普通 JavaScript async 函数体,必须
return插件:(ctx) => {…}或{ name, inject, apply(ctx) {…} }。 - 只能用沙箱符号:
ctx、harness、console、btoa/atob、TextEncoder/TextDecoder。 - 对外暴露调用面:
harness.handle(method, fn),供 Client 半部host.call使用。 - 需要模型可见工具时:
harness.defineTool(definition)+harness.registerTool(ctx, tool)。 - 需要宿主能力时:在返回的插件上声明
inject: ['fs' | 'web' | 'bash' | 'timer' | …],再经ctx.get/ctx.<service>使用;未声明的服务读取被门面拒绝。
Client 半部(code.client)
- 同样必须
return插件;对象形式的inject是服务门禁的唯一声明点,纯函数形式因此够不到任何服务。 - 闭包符号固定:
React、console、styles、host;harness/ 浏览器定时器 /fetch/require是教学陷阱,process/Buffer是undefined。 - 调 Host 半部:
host.call(method, args),省略参数时 handler 收到null(线上只传 JSON,undefined不是 JSON)。 - 贡献 UI:注册进 slots;样式用
styles.insert(css);主题用theme.overrideTokens(source 被强制替换)。
和静态 cordis 插件的差别
| 静态 cordis 插件 | 动态 Package | |
|---|---|---|
| 代码位置 | 包里的 src/index.ts,编译进 lib/ | cordis_define 提交的函数体,只在进程内存 |
| 装载方式 | cordis.yml / patch 行,启动时装载 | cordis_run 按 Package 版本激活 |
| 生命周期 | 随组合装载/卸载,重启恢复 | stop 只停运行、undefine 永久删除;重启全部丢失 |
| 权限边界 | 完整 ctx,可 import 任意模块 | Host 半部 vm 沙箱 + Node API 重定向;Client 半部闭包 + 白名单门面 |
| 作用域 | 进程/组合作用域 | 归属定义它的 Session,其他 Session 读作不存在 |
| 版本 | npm 包版本 | 不可变 Package + currentPackageId / nextPackageId |
| 信任 | 与部署同权 | 视同 bash 权限 |
两者最终汇入同一条 Cordis 生命周期:静态插件走 ctx.plugin(...),动态 Host 半部走 group.ctx.plugin(guardedPlugin(...)) 挂进 cordis-dynamic 组,Client 半部经 loader.create 挂进模块表——所以 activation gating、fiber effect 清理和状态投影的语义是同一套。插件形态与 patch 应用序见 插件解剖,手写一个静态插件见 写第一个插件,双面边界的传输层见 Web UI 架构。
六、源码佐证
| 位置 | 符号 / 事实 |
|---|---|
packages/extensions/cordis-host-runner/src/index.ts | DynamicCordisRunnerService(static inject = ['tools'])、Config.vmTimeoutMs 默认 5000、ctx.dynamicCordisRunner 声明合并、define / run / stop / undefine / snapshot / reference / listPlugins / inspectPlugin / inspectPackage、requireGroup() 的 cordis-dynamic、retract 的 handlerDisposers → fiber.dispose → cordis/dynamic-retract |
packages/extensions/cordis-host-runner/src/index.ts:226,324,383,412,437,479,497,510,524,683,717,740 | 十二个 @Remote 动词:undefineFromPanel、runHostHalf、getClientCode、resolveRequestRun、settleUserRun、stopFromPanel、syncInspectManifest、resolveInspectQuery、inventory、reportRenderFailure、reportClientGuardFailure、invoke |
packages/extensions/cordis-host-runner/src/types.ts | CordisDynamicPluginId / CordisDynamicPackageId / CordisDynamicPluginRunId / ApprovalRequestId / CordisInspectRequestId、CordisDynamicRunMode、CordisRunStatus、DynamicCordisInvokeResult 四类 code、interface Events 的六个 cordis/* 事件 |
packages/extensions/cordis-host-runner/src/registry.ts | DynamicCordisRegistry、mintPluginId(<prefix>-<n>)、mintPackageId(pkg-<n>)、mintPluginRunId(run-<n>)、mintApprovalRequestId(approval-<n>)、armRequest / peekRequest / claimRequest / disarmRequest / pendingRequestFor、DynamicCordisHandler |
packages/extensions/cordis-host-runner/src/sandbox.ts | HOST_BUILTIN_INSPECTION(ctx / harness / console / btoa / atob / TextEncoder / TextDecoder)、createSandbox、evaluateHostCode、precheckCode、NODE_API_REDIRECTS、DUAL_REALM_INSTANCEOF_PRELUDE |
packages/extensions/cordis-host-runner/src/guard.ts | normalizeHandler、sandboxDefineTool、sandboxRegisterTool、sandboxContext、declaredInjects、guardedPlugin、isPlugin、pluginName、CTX_VERBS / TIMER_VERBS、DYNAMIC_TOOL |
packages/extensions/cordis-host-runner/src/lifecycle.ts | startHostHalf(组内 guardedPlugin 子 fiber,失败先 dispose)、missingServices |
packages/extensions/cordis-host-runner/src/inspect-registry.ts | CordisInspectRegistryService、ctx.cordisInspect 声明合并、register / syncClientManifest / list / query / resolveClientQuery |
packages/extensions/cordis-client-runner/src/client/index.ts | name = 'cordis-client-runner'、inject = ['loader','modules','slots','remote','remote.dynamicCordisRunner']、CordisRunnerFace、浏览器 ctx.dynamicCordisRunner、五个 ctx.remote.$on('cordis/…') |
packages/extensions/cordis-client-runner/src/client/evaluator.ts | evaluateClientHalf、闭包参数 ['React','console','styles','host','harness', …traps, 'process','Buffer']、DYNAMIC_CLIENT_REDIRECTS、DynamicCordisStyles.insert、isDynamicCordisPlugin |
packages/extensions/cordis-client-runner/src/client/runtime.ts | DynamicCordisPackageRunner、moduleIdOf → dyn/<id>、ModuleLoaderSink.__ModuleLoader__、loader.create / loader.remove、modules.invalidate、DynamicCordisLoadErrorCause(evaluate / module-import / activate)、waitingFor |
packages/extensions/cordis-client-runner/src/client/guard.ts | dynamicCordisContext、CTX_VERBS / TIMER_VERBS、guardedSlots(优先级 + ledger + claim)、guardedTheme(source 强制为 <pluginId>.<packageId>) |
packages/extensions/cordis-client-runner/src/client/orchestrator.ts | CordisRunOrchestrator、activeRuns / lastRunError、approve / decline / startUserRun / reconcileApprovals、drive() 的 Host→源码→Client→单次结算顺序 |
packages/extensions/cordis-client-runner/src/client/providers.ts | clientInspectProviders 的五个 provider id:Service / Event / Builtin / Slots / Theme、CLIENT_BUILTIN_INSPECTION |
packages/extensions/cordis-client-runner/src/client/timer.ts | ClientTimerService、provideClientTimer、timeout / interval / throttle / debounce / setTimeout / setInterval |
packages/extensions/ui-cordis/src/client/index.ts | inject 六项、sidebar.footer.action id cordis-panel、tool.call.toolview 的 cordis_define / cordis_run / cordis_stop / cordis_undefine、children: { 'tool.view.cordis': { kind: 'keyed', scope: 'session' } }、@ input source |
packages/extensions/ui-cordis/src/client/slots.ts | SlotMap['tool.view.cordis']、CordisPanelFace、CordisRunCardFace、CordisToolViewOwnerProps |
packages/extensions/ui-cordis/src/client/CordisDefineRow.tsx | 只读定义卡、SourceTab = 'client' | 'host'、无运行开关 |
packages/extensions/ui-cordis/src/client/status.ts | cordisVisibleStatus → 'idle' | 'client-pending' | 'running'、packageOf |
packages/extensions/ui-cordis/src/client/dynamic-port.ts | CordisDynamicPort.stop / remove / inventory |
packages/extensions/ui-cordis/src/client/inventory.ts | createCordisInventory、CordisInventorySnapshot、single-flight refresh、reset、retire |
packages/extensions/ui-cordis/src/client/card-model.ts | cordisDefineCard / cordisRunCard / cordisActionCard、CordisToolState |
packages/bundle/web-app/cordis.patch.yml:122,198,269 | 三行装载:@deepseek-ai/dsh-cordis-host-runner、@deepseek-ai/dsh-cordis-client-runner、@deepseek-ai/dsh-client-ui-cordis |
packages/preset/agent-presets/presets/cordis/agent.cordis.yml:246 | tool-cordis 的显式 opt-in 行 |
七、验证
# 1. 三个 runner/UI 包是否在 Web 组合里(tool-cordis 不在出厂组合,需 opt-in)
dsh web --dump-config | grep -E "cordis-(host|client)-runner|client-ui-cordis"
# 2. Host 侧的 Remote 动词与事件名
grep -n "@Remote(" packages/extensions/cordis-host-runner/src/index.ts
grep -rn "'cordis/" packages/extensions/cordis-host-runner/src
# 3. 沙箱符号与 Node API 重定向
grep -n "name: '" packages/extensions/cordis-host-runner/src/sandbox.ts
grep -n "NODE_API_REDIRECTS" -A 12 packages/extensions/cordis-host-runner/src/sandbox.ts
# 4. 浏览器侧的闭包符号面与守卫白名单
grep -n "const parameters" packages/extensions/cordis-client-runner/src/client/evaluator.ts
grep -n "DYNAMIC_CLIENT_REDIRECTS" -A 10 packages/extensions/cordis-client-runner/src/client/evaluator.ts
grep -n "CTX_VERBS" packages/extensions/cordis-client-runner/src/client/guard.ts
# 5. 运行时观察:打开 Web UI,侧边栏底部 Cordis 徽标计数 = 运行中 + 待批准;
# 面板里每个定义一行,行上才是 run / stop / remove 开关。
# 刷新页面后,纯 Host 定义仍读作 running,带 Client 半部的定义先显示"加载回本页"。
下一步
- 运行时自省与动态 Cordis 插件:七个模型工具的目录与
define → run/update/stop/undefine语义 - 插件解剖:静态插件的形态、dual-face 清单与 patch 应用序
- 写第一个插件:手写一个静态 cordis 插件
- Web UI 架构:Host/Client 双进程、slot 体系与 client 插件装载
- 运行时不变量:给插件加运行时健康断言