dsh-ptc-plus 0.2.3 → 0.2.4

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.
@@ -0,0 +1,31 @@
1
+ # Stage npm Releases With OIDC
2
+
3
+ ## Problem
4
+
5
+ The npm registry is retiring long-lived bypass-2FA credentials as a direct publication mechanism and now supports workflow-bound OIDC identities plus staged publication. PTC Plus needs a release path that proves the package came from an already verified Git commit, does not place reusable npm write authority in GitHub secrets, preserves maintainer proof-of-presence, and publishes the exact artifact that was inspected. Running the complete platform matrix again for a tag wastes CI capacity without strengthening identity when the same commit already has a successful required CI run.
6
+
7
+ ## Decision
8
+
9
+ The `Release` GitHub Actions workflow is the only automated npm publication path. A maintainer explicitly dispatches it from an annotated `v<package.version>` tag ref; there is no independent tag input. Before obtaining npm authority, the workflow proves that its `GITHUB_REF`, provenance-bearing `GITHUB_SHA`, annotated tag target, checked-out commit, manifest version, and the successful `main` push run of `.github/workflows/ci.yml` all identify the same commit. Normal CI does not run for tag pushes.
10
+
11
+ The npm Trusted Publisher is bound to the exact repository, `release.yml` workflow filename, and `npm-release` GitHub environment. Its allowed action is only `npm stage publish`; direct `npm publish` and reusable npm write tokens are absent from the workflow. The package uses npm's most restrictive recommended publishing-access policy even when npm documentation and the live UI use different labels for it. After the OIDC staging path is proven, maintainers revoke every remaining reusable token capable of publishing or staging this package rather than treating the selected UI label as evidence that those credentials no longer exist. The release job receives only `contents: read` and `id-token: write`, uses a GitHub-hosted runner without a package-manager cache, revalidates the dispatch ref and SHA after the environment boundary, builds and smoke-tests one tarball, and sends that same file to npm staging with provenance.
12
+
13
+ Promotion is separate from staging. A maintainer reviews the staged metadata and downloadable tarball, then approves or rejects it through npm with interactive 2FA. A successful approval is not treated as immediate availability because npm publish-time scanning may delay or block registry visibility. GitHub Release publication follows observed availability of the exact npm version.
14
+
15
+ npm 12 install-time restrictions remain enabled for the release job. The manifest explicitly denies the unnecessary `esbuild` install script, and release installation replaces registry hosts with the explicitly selected npm registry without enabling Git dependencies, arbitrary remote tarball dependencies, or all install scripts.
16
+
17
+ ## Alternatives considered
18
+
19
+ **Store a granular bypass-2FA npm token in GitHub secrets.** This leaves reusable write authority available to workflow code and depends on a credential class whose direct-publish capability is being retired. It also provides weaker provenance and audit boundaries than a workflow-specific OIDC exchange.
20
+
21
+ **Publish directly with OIDC after a GitHub Environment approval.** This removes long-lived credentials but substitutes a GitHub approval for npm proof-of-presence. Stage-only authority keeps CI compromise from making a version public and remains compatible with npm's dual-use publication requirements if they apply.
22
+
23
+ **Run the full platform matrix again inside the release workflow.** Revalidation of an immutable SHA repeats expensive deterministic work without changing the artifact identity. The release workflow instead verifies the successful CI run for that exact SHA, then performs npm 12 installation, build, package lint, audit, pack, checksum, and clean-consumer smoke on the release artifact.
24
+
25
+ **Publish a locally generated tarball after CI.** Interactive local publishing provides 2FA but separates the published bytes from the workflow identity and makes provenance, repeatability, and artifact review depend on workstation state.
26
+
27
+ **Automatically stage every unpublished manifest version pushed to `main`.** This removes one maintainer action but turns an ordinary merge into a release request and can reserve a semver version in npm staging before its annotated release identity exists.
28
+
29
+ ## Consequences
30
+
31
+ An npm release now requires a successful immutable CI identity, an explicit workflow dispatch, a narrowly scoped OIDC exchange, and a separate npm 2FA promotion. Once the proven migration removes legacy package-capable tokens, no npm write secret needs rotation, and a compromised release job can at most create a staged candidate under the configured package and workflow relationship. The trade-off is an intentional manual promotion step and a possible registry scanning delay. Maintainers must configure the npm Trusted Publisher, package publishing access, GitHub environment, tag protection, and post-verification token revocation outside the repository; those deployment facts cannot be inferred or silently created by source code.
@@ -15,7 +15,7 @@ PTC Plus 把 DSH PTC 模式的顶层 `run_code` 变成与 session 绑定的连
15
15
  主入口接管模型直接发起的顶层 `run_code`,并通过 DSH tool registry 真实注册 `edit_run_code`。
16
16
  使用 code-only direct-tool projection 的 PTC request 始终按固定顺序暴露 `[run_code, edit_run_code]`;插件用 agent scope 的
17
17
  `tools.register()` 真实注册 edit transport,并用同一 scope 的 `tools.presentAs('both')` 放行这两个 provider tool call,再把其他 native tool 保留在 program SDK。首次可判定的 prompt assembly 在安装这项 presentation effect 前建立 agent composition owner;注册与 mode 记录都在所属 session、agent 或插件释放时撤销,native-mode 与无关 agent scope 不继承 `edit_run_code`。
18
- DSH 为一个 agent composition 固定选择 `native`、`code` 或 `both`。首次 assembly 的非空 `tools:code-only` section text 证明 `code`,同名空 section 证明 `both`;只有缺少该 owner signal 时才能从 tool shape 推断。后续 assembly 不重新解释插件自己的 `tools.presentAs('both')` 效果,而是从 agent composition owner 派生 presentation。`code` 独占 direct-tool collapse、顶层 native-call normalization 和 native dispatch rejection;`both` 保留 native direct tools。`code` 与 `both` 都适配 PTC `run_code` schema、program SDK 和 session runtime contexts,只有 `code` 安装并直接执行 `edit_run_code`。model stream 将完整 call 的 session 与 call ID 绑定到由 composition mode 派生的 request policy,dispatch wrapper 替换 cancellation signal 不改变执行判定;result settlement 和 session、agent 或插件释放分别撤销单次 call 与整个 lifecycle。
18
+ DSH 为一个 agent composition 固定选择 `native`、`ptc` 或 `both`。首次 assembly 的非空 `tools:ptc-only` section text 证明 `ptc`,同名空 section 证明 `both`;旧 Host 的 `tools:code-only` 仅在当前 section 缺失时作为边界别名,两者同时存在时当前 section 优先。只有缺少 owner signal 时才能从 tool shape 推断。后续 assembly 不重新解释插件自己的 `tools.presentAs('both')` 效果,而是从 agent composition owner 派生 presentation。`ptc` 独占 direct-tool collapse、顶层 native-call normalization 和 native dispatch rejection;`both` 保留 native direct tools。`ptc` 与 `both` 都适配 PTC `run_code` schema、program SDK 和 session runtime contexts,只有 `ptc` 安装并直接执行 `edit_run_code`。model stream 将完整 call 的 session 与 call ID 绑定到由 composition mode 派生的 request policy,dispatch wrapper 替换 cancellation signal 不改变执行判定;result settlement 和 session、agent 或插件释放分别撤销单次 call 与整个 lifecycle。
19
19
  若模型误发 live schema 可证明的顶层 native call,stream canonicalizer 会把这个 out-of-surface 调用规范为与模型本应生成的同一 member `run_code`;未知、畸形或不一致输入原样透传。派生 cell 不携带 provenance、纠错提示或其他模型可见标记;普通参数保留原始 JSON,含 own `__proto__` 的参数使用安全字面量,超过 TypeScript parser 安全深度的参数才使用浅层 `JSON.parse` 表达式。该历史修改只修复无效 direct call。已声明的 `run_code` 与 `edit_run_code` 原样通过,因此 session history、UI 与下一轮 model context 保留合法调用的实际 tool name 和参数。
20
20
 
21
21
  `edit_run_code` 对其持久化 `tool/call` 事件出现时当前 open turn 最近的可编辑 cell 生效,无论该 cell 被拒绝、运行失败,还是成功执行但结果需要微调。执行通过 call event sequence 读取该快照,后续 settlement 或 handler 调度不改变目标;派生 cell 则按外层 `tool/result` event sequence 进入恢复历史,使 cold replay 保持 live kernel 的实际结算顺序。
@@ -50,9 +50,9 @@ system sections、tool schema 或 tool order,也不假设 Windows、WSL、POSI
50
50
 
51
51
  ## 设置与启用开关
52
52
 
53
- Host half 通过 DSH 公共 settings 服务注册 `ptc-plus` 命名空间,字段清单、默认值与校验来自 `internal/config-spec.js`。
53
+ Host half 通过 DSH 公共 settings 服务注册 `ptc-plus` 命名空间,优先使用 provider-owned `installSection()`,并在旧 Host 上适配根模块的 `installSettingsSection()`;两条路径共享同一 fallback、watch、rollback 与 disposal 语义。字段清单、默认值与校验来自 `internal/config-spec.js`。
54
54
  Client half 通过 `settings.plugin.item` 卡片呈现全部配置。`enabled` 是 kill switch:关闭时只保留 settings 注册和设置卡片,
55
- 不注册 runtime、hook、tool surface 或 system prompt section;设置卡片显示“已启用/已停用”,`code` preset 会话在头部单独显示 `PTC Plus` 指示器。稳定指引不包含 UI 品牌名。
55
+ 不注册 runtime、hook、tool surface 或 system prompt section;设置卡片显示“已启用/已停用”,当前 `ptc` 与旧 Host `code` preset 会话在头部单独显示 `PTC Plus` 指示器。稳定指引不包含 UI 品牌名。
56
56
  所有字段都由 settings watch 即时交给各自 owner,且不替换已有 session-bound binding。每个已提交 cell 固定其提交时的配置代际,timer、worker 消息、program binding bridge、结果校验和诊断共同消费该快照;重配置更新随后提交 cell 的默认值。`maxOldGenerationSizeMb` 在活动 worker 存在时因 Node 的创建期限制而拒绝并回滚。settings 服务缺失时 Host 回退到 composition config,原有行为不变。
57
57
 
58
58
  `cordisToolsEnabled` 默认关闭且即时生效。Host 只在可见 `run_code` 的 agent scope 中挂载官方 `@deepseek-ai/dsh-tool-cordis`,并通过公共 `agentPresets` service 定位 shipped `cordis` preset,再用维护中的 Skill filesystem plugin 把其 companion Skill 目录发布到同一 scope。两个 child fiber、tool guidance 与 `cordis-plugin-development` Skill 是一个 mount;首轮 request 等待完整发布和 scoped Skill load 验证,关闭、agent/runtime 释放或任一激活失败时逆序卸载。工具名、数量、schema 和 guidance 直接来自 official tool fiber,Skill 内容直接来自 shipped preset,Host 均不复制。官方插件同时向 process-global `cordisInspect` 注册 Host provider;owner 将 manifest 相同的 per-agent 注册合并为引用计数 lease,查询委托给当前仍存活的官方 registration,最后一份 lease 释放后才注销 provider。manifest 不一致时启用失败。该开关不切换 preset,也不改变 code-only direct-tool projection。
package/docs/client-ui.md CHANGED
@@ -27,7 +27,7 @@ Host half 通过 DSH 公共 `settings` 服务注册命名空间 `ptc-plus`。字
27
27
 
28
28
  `cordisToolsEnabled` 默认关闭并即时生效。它不切换 preset,而是把官方 Cordis 工具、owner guidance 与 `cordis-plugin-development` Skill 作为一个 agent-scoped mount 加入或移出 PTC agent;顶层仍为 `run_code` / `edit_run_code`,普通 agent 不继承。Host 缺少 official preset、Skill/Cordis service 或任一 contribution 加载失败时,启用会完整回滚。完整运行要求见 [运行时参考](runtime-reference.md)。
29
29
 
30
- 设置卡片折叠头部显示 `PTC Plus`、`PTC 模式的会话级 TypeScript REPL。`、启用状态和可访问的展开/收起名称;稳定 REPL 指引不承载 UI 品牌名。启用且会话选择 `code` preset 时,`conversation.session.header.actions` 额外显示 `PTC Plus` 指示器。关闭时不注入任何 PTC 指引或工具 surface。
30
+ 设置卡片的全部文案(字段名称与说明、折叠头部副标题、启用状态、展开/收起名称、同步与错误提示)都注册到 DSH client locale 的 `settings.ptcPlus` 命名空间,随当前界面语言在中文与 English 之间切换;字段名称与说明的两种语言文本同样来自 `internal/config-spec.js`(`label`/`labelEn`、`description`/`descriptionEn`,某一字段的说明要么两种语言都有,要么都没有),卡片 chrome 文案由 client half 拥有。稳定 REPL 指引不承载 UI 品牌名。启用且会话选择当前 `ptc` 或旧 Host `code` preset 时,`conversation.session.header.actions` 额外显示 `PTC Plus` 指示器,其 tooltip 也跟随界面语言;当前 session projection 与旧顶层字段冲突时以前者为准。关闭时不注入任何 PTC 指引或工具 surface。
31
31
 
32
32
  live 配置若因宿主能力缺失或 runtime 安装/重配置失败,会先回滚所有已创建或更新的 owner,再把持久设置回写为上一次已应用值;回滚写入失败时 Host 记录 activation diagnostic,避免静默把配置显示成不存在的 runtime。
33
33
 
@@ -39,7 +39,7 @@ live 配置若因宿主能力缺失或 runtime 安装/重配置失败,会先
39
39
  npm run build
40
40
  ```
41
41
 
42
- `package.json` 声明 `dsh.client` 与 `./client` export。`npm run build` 从 `src/client.js` 生成 `client.js`,`npm run build:check` 比较确定性产物;`prepack` 会阻止陈旧 bundle 发布。Client half 只依赖 DSH 注入面提供的 settings、session、slot 与 UI primitive 模块。
42
+ `package.json` 声明 `dsh.client` 与 `./client` export。`npm run build` 从 `src/client.js` 生成 `client.js`,`npm run build:check` 比较确定性产物;`prepack` 会阻止陈旧 bundle 发布。Client half 只依赖 DSH 注入面提供的 settings、locale、session、slot 与 UI primitive 模块。
43
43
 
44
44
  ## 回退
45
45
 
@@ -1,6 +1,6 @@
1
1
  # Installation
2
2
 
3
- PTC Plus requires Node.js `^22.19.0 || >=24.0.0` and the latest available DSH release with TypeScript PTC mode. Compatibility follows DSH's public extension surfaces rather than a version allowlist.
3
+ PTC Plus requires Node.js `^22.19.0 || >=24.0.0` and targets the latest available DSH release with TypeScript PTC mode. Compatibility with the preceding Host contract is retained through public capability detection; when both generations expose competing presentation evidence, the current DSH contract wins. Compatibility follows those live extension surfaces rather than a version allowlist.
4
4
 
5
5
  Install the plugin into the profile that actually runs the target DSH surface. Do not assume a profile named `default` is active.
6
6
 
@@ -16,14 +16,14 @@ Install the plugin into the profile that actually runs the target DSH surface. D
16
16
 
17
17
  `danger-full-access` is the primary supported experience. The worker isolates the REPL lifecycle; it is not a malicious-code sandbox. DSH continues to own native-tool scope, policy, approval, cancellation, sandboxing, and scheduling. Narrower profiles expose only their available capabilities; PTC Plus does not simulate missing authority or add another permission system.
18
18
 
19
- The optional `cordisToolsEnabled` integration requires the current DSH installation to provide its shipped `cordis` preset plus the public preset, Skill, Cordis, settings, and tool-runtime packages. PTC Plus declares those host-owned DSH packages as required peers instead of installing private runtime copies; DSH's profile module fallback must resolve them from the active installation. Do not copy `SKILL.md` or add the Cordis preset's Skill directory to global roots. If the host surface is incomplete, plugin activation or enabling the setting fails instead of loading a second DSH core.
19
+ The optional `cordisToolsEnabled` integration requires the current DSH installation to provide its shipped `cordis` preset plus the public preset, Skill, Cordis, settings, and tool-runtime packages. PTC Plus declares those host-owned DSH packages as unrestricted required peers instead of installing private runtime copies; runtime capability validation owns compatibility, and CI imports the packed plugin against both the current and preceding release channels. DSH's profile module fallback must resolve the peers from the active installation. Do not copy `SKILL.md` or add the Cordis preset's Skill directory to global roots. If the host surface is incomplete, plugin activation or enabling the setting fails instead of loading a second DSH core.
20
20
 
21
21
  ## npm Release
22
22
 
23
23
  Use this form after the selected version is available from the npm registry:
24
24
 
25
25
  ```sh
26
- dsh plugin --profile <profile> add dsh-ptc-plus@0.2.3
26
+ dsh plugin --profile <profile> add dsh-ptc-plus@0.2.4
27
27
  dsh --profile <profile> --dump-config
28
28
  ```
29
29
 
@@ -60,7 +60,7 @@ pnpm dsh --profile <profile> --dump-config
60
60
 
61
61
  ```sh
62
62
  npm pack
63
- dsh plugin --profile <profile> add /absolute/path/to/dsh-ptc-plus-0.2.3.tgz
63
+ dsh plugin --profile <profile> add /absolute/path/to/dsh-ptc-plus-0.2.4.tgz
64
64
  dsh --profile <profile> --dump-config
65
65
  ```
66
66
 
@@ -79,4 +79,4 @@ dsh plugin add github:muyuanjin/dsh-ptc-plus#main
79
79
  dsh --dump-config
80
80
  ```
81
81
 
82
- After an npm release, the package spec may instead be `dsh-ptc-plus@0.2.3`. For a local package, use its absolute tarball path. Restart DSH Desktop after installation. Linux Desktop is not a current DSH Desktop release target; use DSH CLI/Web on Linux.
82
+ After an npm release, the package spec may instead be `dsh-ptc-plus@0.2.4`. For a local package, use its absolute tarball path. Restart DSH Desktop after installation. Linux Desktop is not a current DSH Desktop release target; use DSH CLI/Web on Linux.
@@ -14,6 +14,30 @@
14
14
 
15
15
  在 `cordis.patch.yml` 和解析后的 DSH 配置中,`id: ptc-plus` 标识插件配置项,`name: dsh-ptc-plus` 标识待加载的包。内部错误前缀、runtime context 名称和 worker 临时路径会按所属子系统使用 `ptc-plus` 或 `dsh-ptc-plus`;它们不是另外的公开产品名。
16
16
 
17
+ ## 发布权限与流水线
18
+
19
+ npm 发布遵循 [ADR 0022](adr/0022-stage-npm-releases-with-oidc.md),使用 `.github/workflows/release.yml` 声明的 Trusted Publisher,不向 GitHub 保存 npm write token。npm package settings 必须把 publisher 精确绑定到 `muyuanjin/dsh-ptc-plus`、workflow filename `release.yml` 和 environment `npm-release`,且只允许 `npm stage publish`。Publishing access 选择页面中最严格且标记为 recommended 的 2FA 选项;npm 文档称其为 `Require two-factor authentication and disallow tokens`,当前 npm 页面也可能显示 `Require two-factor authentication and disallow bypass 2fa tokens (recommended)`。不要从滚动变化的标签推断账户中已不存在传统 token。OIDC 只把经过验证的 tarball 放入 npm staging,维护者仍须在 npm CLI 或 npmjs.com 检查 staged package 并以 2FA approve,包才会进入公开 registry。
20
+
21
+ 普通 `CI` 只响应 `main` push 与 pull request。发布前创建 annotated `v<package.version>` tag,原子推送 release commit 与 tag,等待该 commit 的 `CI` 成功,然后以该 tag 作为 ref 显式运行 `Release` workflow。workflow 不接受另一个 tag input,并会重新证明 dispatch 的 `GITHUB_REF`、`GITHUB_SHA`、annotated tag target、checkout HEAD、manifest 版本和成功 CI 的 SHA 完全一致;release job 使用 GitHub-hosted Node `24.15.0`、npm `12.0.2`、无 package-manager cache 和最小 `id-token: write` 权限,构建一个 tarball、执行 clean-consumer smoke,再对同一文件执行 `npm stage publish`。`package.json#allowScripts` 显式拒绝当前不需要的 `esbuild` install script;`replace-registry-host=always` 让 release install 将 lockfile 中的 registry tarball 路由到显式 npm 官方 registry,而不放宽 npm 12 的 Git/remote dependency 默认拒绝。
22
+
23
+ ```powershell
24
+ git tag -a "v$((Get-Content package.json | ConvertFrom-Json).version)" -m "v$((Get-Content package.json | ConvertFrom-Json).version)"
25
+ git push --atomic origin main "v$((Get-Content package.json | ConvertFrom-Json).version)"
26
+ gh workflow run release.yml --ref "v$((Get-Content package.json | ConvertFrom-Json).version)"
27
+ ```
28
+
29
+ workflow 成功只表示 package 已 staged,并非已经公开发布。维护者从 workflow 日志或 npmjs.com 的 Staged Packages 页面取得 stage id,检查 metadata 和下载的 tarball 后执行:
30
+
31
+ ```powershell
32
+ npm.cmd stage view '<stage-id>' --json --registry=https://registry.npmjs.org/
33
+ npm.cmd stage download '<stage-id>' --registry=https://registry.npmjs.org/
34
+ npm.cmd stage approve '<stage-id>' --registry=https://registry.npmjs.org/
35
+ ```
36
+
37
+ `npm stage approve` 必须完成 npm 2FA。approve 后 npm 会进行 publish-time malware scan;公开可安装通常延迟约 5 分钟,高峰或需进一步检查时可能超过 15 分钟。发布自动化必须轮询明确版本并允许该延迟,不能把 approve 返回等同于 registry 已可安装。扫描确认版本可用后再发布对应 GitHub Release。staged 内容错误时使用 `npm stage reject <stage-id>` 并完成 2FA,不能移动既有 tag 或用同一版本覆盖。
38
+
39
+ 首次 OIDC staging 成功后,检查 npm 账户的 Access Tokens,并撤销所有仍可对本包执行 publish 或 stage 的 granular/automation token,尤其是 bypass-2FA token;在 Trusted Publisher 尚未实测成功前保留恢复路径,不能把未验证的凭据切换当成完成。后续 workflow 不保存或读取 npm token,定期清点用于发现账户侧重新引入的发布凭据。
40
+
17
41
  ## Release checklist
18
42
 
19
43
  1. 在 Node `22.19.0` 与 Node 24.x 上运行 `npm ci`、`npm run build:check` 和 `npm run check`;Node 24.x 还应覆盖 Windows、Linux 与 macOS。仓库 CI 定义同一矩阵。
@@ -22,7 +46,7 @@
22
46
  4. 在 Windows 与 Linux CLI/Web 实机启动临时 profile。macOS 由原生 runner 验证 CLI/Web;Windows 与 macOS Desktop 从托盘打开当前 profile 的 DSH Terminal 安装,重启 Desktop 后做一次 PTC 模式 smoke。Desktop 当前发布平台不包括 Linux。
23
47
  5. 只有在明确授权模型消耗后才运行 `npm run test:expensive` 与 `npm run test:ab`。发布结论以结构化测试结果为准,不把一次模型样本推广为普遍保证。
24
48
  6. 确认 npm 包名可用、待发布版本高于 registry 版本、README 截图来自真实 DSH 会话,并核对 repository、homepage、bugs、license 与 `dsh-plugin` topic。
25
- 7. 最后检查 `npm pack --silent --json` 的名称、版本、大小与文件列表,再创建并推送 release commit/tag,随后发布同一 commit 产生的 tarball。
49
+ 7. 最后检查 `npm pack --silent --json` 的名称、版本、大小与文件列表,创建 annotated release tag,并确认 tag commit 已通过 `CI`。运行 `Release` workflow 将同一源码产生的 tarball送入 npm staging;人工检查并以 2FA approve 后,等待 registry 扫描完成,再创建 GitHub Release。
26
50
 
27
51
  远程仓库、registry、签名凭据、目标平台运行状态和模型额度都属于发布时事实,不能由本地 manifest 推断。
28
52
 
@@ -8,7 +8,7 @@ The default loose binding mode lets a complete top-level `const` or `let` declar
8
8
 
9
9
  Every capability namespace is leased to one cell. Captured `tools`, `capabilities`, `repl`, `code`, or member functions expire when that cell ends.
10
10
 
11
- For PTC mode requests using the code-only direct-tool projection, PTC Plus presents two tools in stable order: `run_code` and `edit_run_code`. The latter edits the most recent eligible cell visible when the edit call is dispatched, including a cell that completed successfully but needs a small adjustment. That target remains fixed while the call is in flight; later tool settlements cannot retarget it. The edit accepts exactly one atomic `edits` or `regex_edits` array and an optional `expected_target_call_seq` precondition. Invalid, unavailable, or mismatched-target edits return `{ edited: false, reason }` without executing source or consuming the target.
11
+ For PTC mode requests using the code-only direct-tool projection, PTC Plus presents two tools in stable order: `run_code` and `edit_run_code`. It recognizes the current Host's `tools:ptc-only` owner section and the preceding Host's `tools:code-only` alias, with the current section authoritative if both appear. The latter tool edits the most recent eligible cell visible when the edit call is dispatched, including a cell that completed successfully but needs a small adjustment. That target remains fixed while the call is in flight; later tool settlements cannot retarget it. The edit accepts exactly one atomic `edits` or `regex_edits` array and an optional `expected_target_call_seq` precondition. Invalid, unavailable, or mismatched-target edits return `{ edited: false, reason }` without executing source or consuming the target.
12
12
 
13
13
  `edit_run_code` is a real registered tool call. Session history and later model requests retain its original name and delta arguments. The plugin executes the materialized source as a host-derived `run_code`, returns only the edit status, value, and logs to the model, and keeps the complete source plus journal in private replay metadata. It does not rewrite assistant history or emit edit-specific runtime context.
14
14
 
package/index.js CHANGED
@@ -16,7 +16,8 @@ import {
16
16
  CONFIG_FIELDS,
17
17
  SETTINGS_NAMESPACE,
18
18
  } from './internal/config-spec.js'
19
- import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
19
+ import { installSettingsSectionCompat } from './internal/settings-compat.js'
20
+ import * as dshSettings from '@deepseek-ai/dsh-settings'
20
21
 
21
22
  const INSTALL_CLEANUP = Symbol('ptc-plus install cleanup')
22
23
 
@@ -473,7 +474,7 @@ export function apply(ctx, config = {}) {
473
474
  ? { enabled: false }
474
475
  : Object.fromEntries(CONFIG_FIELDS.map(field => [field.key, previousConfig[field.key]]))
475
476
  try {
476
- await settingsWriter.update(settingsNamespace(SETTINGS_NAMESPACE), patch)
477
+ await settingsWriter.update(SETTINGS_NAMESPACE, patch)
477
478
  } catch (rollbackError) {
478
479
  reportActivationFailure(new Error(
479
480
  `ptc-plus: failed to roll back runtime configuration: ${rollbackError.message}`,
@@ -512,21 +513,22 @@ export function apply(ctx, config = {}) {
512
513
  }
513
514
  }
514
515
  if (typeof ctx.inject === 'function') {
515
- ctx.inject(['settings'], settings => {
516
- settingsWriter = settings.settings
517
- })
518
- installSettingsSection(
516
+ installSettingsSectionCompat({
519
517
  ctx,
520
- settingsNamespace(SETTINGS_NAMESPACE),
521
- Config,
522
- resolvedConfig,
523
- {
518
+ settingsModule: dshSettings,
519
+ namespace: SETTINGS_NAMESPACE,
520
+ schema: Config,
521
+ entry: resolvedConfig,
522
+ hooks: {
524
523
  setSource(source) {
525
524
  configSource = source
526
525
  },
527
526
  onChange: reconcile,
528
527
  },
529
- )
528
+ onProvider(provider) {
529
+ settingsWriter = provider
530
+ },
531
+ })
530
532
  }
531
533
  if (configurationGeneration === 0) return reconcile(settingsWriter === undefined)
532
534
  }
@@ -4,21 +4,30 @@ export const MAX_TIMER_DELAY_MS = 2_147_483_647
4
4
  /** Stable settings namespace used by both Host and browser halves. */
5
5
  export const SETTINGS_NAMESPACE = 'ptc-plus'
6
6
 
7
- /** Ordered field definitions for the plugin configuration card. */
7
+ /**
8
+ * Ordered field definitions for the plugin configuration card. `label` and
9
+ * `description` are the canonical Host schema copy; `labelEn` and
10
+ * `descriptionEn` carry the English settings-card copy. A field's description
11
+ * exists in both locales or in neither.
12
+ */
8
13
  export const CONFIG_FIELDS = Object.freeze([
9
14
  {
10
15
  key: 'enabled',
11
16
  type: 'boolean',
12
17
  default: true,
13
18
  label: '启用 PTC Plus',
19
+ labelEn: 'Enable PTC Plus',
14
20
  description: '',
21
+ descriptionEn: '',
15
22
  },
16
23
  {
17
24
  key: 'cordisToolsEnabled',
18
25
  type: 'boolean',
19
26
  default: false,
20
27
  label: '在 PTC 模式中启用 Cordis 工具',
28
+ labelEn: 'Enable Cordis tools in PTC mode',
21
29
  description: '即时为 PTC agent 加入或移除官方 Cordis 工具。',
30
+ descriptionEn: 'Adds or removes the official Cordis tools for the PTC agent immediately.',
22
31
  },
23
32
  {
24
33
  key: 'computeMs',
@@ -27,7 +36,9 @@ export const CONFIG_FIELDS = Object.freeze([
27
36
  min: 1,
28
37
  max: Number.MAX_SAFE_INTEGER,
29
38
  label: '单 cell 最大 CPU 时间 (ms)',
39
+ labelEn: 'Max CPU time per cell (ms)',
30
40
  description: '同步计算超过该预算的 cell 会被中断。',
41
+ descriptionEn: 'Cells whose synchronous computation exceeds this budget are interrupted.',
31
42
  },
32
43
  {
33
44
  key: 'maxWallMs',
@@ -36,7 +47,9 @@ export const CONFIG_FIELDS = Object.freeze([
36
47
  min: 1,
37
48
  max: MAX_TIMER_DELAY_MS,
38
49
  label: '单 cell 最大墙钟时间 (ms)',
50
+ labelEn: 'Max wall-clock time per cell (ms)',
39
51
  description: '完整 cell 执行(含异步等待)的最长耗时。',
52
+ descriptionEn: 'The longest a full cell execution may take, including async waits.',
40
53
  },
41
54
  {
42
55
  key: 'maxOutputBytes',
@@ -45,7 +58,9 @@ export const CONFIG_FIELDS = Object.freeze([
45
58
  min: 1,
46
59
  max: Number.MAX_SAFE_INTEGER,
47
60
  label: '最大输出字节',
61
+ labelEn: 'Max output bytes',
48
62
  description: '限制单个 cell 的输出和结果数据总大小。',
63
+ descriptionEn: 'Caps the combined size of one cell output and result data.',
49
64
  },
50
65
  {
51
66
  key: 'maxOldGenerationSizeMb',
@@ -54,7 +69,9 @@ export const CONFIG_FIELDS = Object.freeze([
54
69
  min: 1,
55
70
  max: Number.MAX_SAFE_INTEGER,
56
71
  label: 'worker 旧生代内存上限 (MiB)',
72
+ labelEn: 'Worker old-generation memory cap (MiB)',
57
73
  description: '每个 worker 的 V8 旧生代上限;活动 worker 存在时修改会被拒绝。',
74
+ descriptionEn: 'Per-worker V8 old-generation limit; changes are rejected while an active worker exists.',
58
75
  },
59
76
  {
60
77
  key: 'maxValueNodes',
@@ -63,7 +80,9 @@ export const CONFIG_FIELDS = Object.freeze([
63
80
  min: 1,
64
81
  max: Number.MAX_SAFE_INTEGER,
65
82
  label: 'Value Graph 最大节点数',
83
+ labelEn: 'Max Value Graph nodes',
66
84
  description: '限制单次返回值的节点数。',
85
+ descriptionEn: 'Caps the node count of a single returned value.',
67
86
  },
68
87
  {
69
88
  key: 'maxValueEdges',
@@ -72,7 +91,9 @@ export const CONFIG_FIELDS = Object.freeze([
72
91
  min: 1,
73
92
  max: Number.MAX_SAFE_INTEGER,
74
93
  label: 'Value Graph 最大边数',
94
+ labelEn: 'Max Value Graph edges',
75
95
  description: '限制单次返回值的引用关系数。',
96
+ descriptionEn: 'Caps the reference-edge count of a single returned value.',
76
97
  },
77
98
  {
78
99
  key: 'maxValueArrayLength',
@@ -81,7 +102,9 @@ export const CONFIG_FIELDS = Object.freeze([
81
102
  min: 1,
82
103
  max: Number.MAX_SAFE_INTEGER,
83
104
  label: '数组最大声明长度',
105
+ labelEn: 'Max declared array length',
84
106
  description: '限制返回数组的最大长度。',
107
+ descriptionEn: 'Caps the maximum length of a returned array.',
85
108
  },
86
109
  {
87
110
  key: 'maxValueBigIntDigits',
@@ -90,7 +113,9 @@ export const CONFIG_FIELDS = Object.freeze([
90
113
  min: 1,
91
114
  max: Number.MAX_SAFE_INTEGER,
92
115
  label: 'BigInt 最大十进制位数',
116
+ labelEn: 'Max BigInt decimal digits',
93
117
  description: '限制返回值中 BigInt 的十进制位数。',
118
+ descriptionEn: 'Caps the decimal digits of BigInt values in a returned value.',
94
119
  },
95
120
  {
96
121
  key: 'maxNestedRunCodeDepth',
@@ -99,56 +124,72 @@ export const CONFIG_FIELDS = Object.freeze([
99
124
  min: 1,
100
125
  max: Number.MAX_SAFE_INTEGER,
101
126
  label: 'code.run 最大递归深度',
127
+ labelEn: 'Max code.run recursion depth',
102
128
  description: '限制 code.run 的嵌套层数。',
129
+ descriptionEn: 'Caps how deeply code.run may nest.',
103
130
  },
104
131
  {
105
132
  key: 'canonicalizeToolCalls',
106
133
  type: 'boolean',
107
134
  default: true,
108
135
  label: '规范顶层 native 误调',
136
+ labelEn: 'Canonicalize top-level native mis-calls',
109
137
  description: '修正可以明确识别的顶层 native 误调。',
138
+ descriptionEn: 'Repairs top-level native mis-calls that can be identified unambiguously.',
110
139
  },
111
140
  {
112
141
  key: 'looseTopLevelRedeclarations',
113
142
  type: 'boolean',
114
143
  default: true,
115
144
  label: '宽松顶层重声明',
145
+ labelEn: 'Loose top-level redeclarations',
116
146
  description: '允许顶层 const/let 重声明已有变量。',
147
+ descriptionEn: 'Allows top-level const/let to redeclare existing variables.',
117
148
  },
118
149
  {
119
150
  key: 'durableReplay',
120
151
  type: 'boolean',
121
152
  default: true,
122
153
  label: '持久重放',
154
+ labelEn: 'Durable replay',
123
155
  description: 'worker 重启后恢复可以重建的 REPL 状态。',
156
+ descriptionEn: 'Rebuilds recoverable REPL state after a worker restart.',
124
157
  },
125
158
  {
126
159
  key: 'autoRewriteImports',
127
160
  type: 'boolean',
128
161
  default: true,
129
162
  label: '自动改写 import',
163
+ labelEn: 'Auto-rewrite import',
130
164
  description: '允许在 run_code 中使用静态 import。',
165
+ descriptionEn: 'Allows static import declarations inside run_code.',
131
166
  },
132
167
  {
133
168
  key: 'autoStripExports',
134
169
  type: 'boolean',
135
170
  default: true,
136
171
  label: '自动剥离 export',
172
+ labelEn: 'Auto-strip export',
137
173
  description: '允许在 run_code 中使用顶层 export。',
174
+ descriptionEn: 'Allows top-level export modifiers inside run_code.',
138
175
  },
139
176
  {
140
177
  key: 'autoSplitRedeclarations',
141
178
  type: 'boolean',
142
179
  default: true,
143
180
  label: '自动拆分混合重声明',
181
+ labelEn: 'Auto-split mixed redeclarations',
144
182
  description: '允许顶层解构声明同时包含新旧变量。',
183
+ descriptionEn: 'Allows top-level destructuring declarations that mix new and existing variables.',
145
184
  },
146
185
  {
147
186
  key: 'tipsEnabled',
148
187
  type: 'boolean',
149
188
  default: true,
150
189
  label: '启用恢复提示',
190
+ labelEn: 'Enable recovery tips',
151
191
  description: '在符合条件的失败后显示恢复提示。',
192
+ descriptionEn: 'Shows recovery tips after qualifying failures.',
152
193
  },
153
194
  {
154
195
  key: 'tipCooldownMessages',
@@ -157,7 +198,9 @@ export const CONFIG_FIELDS = Object.freeze([
157
198
  min: 1,
158
199
  max: Number.MAX_SAFE_INTEGER,
159
200
  label: '恢复提示冷却步数',
201
+ labelEn: 'Recovery tip cooldown (messages)',
160
202
  description: '同类恢复提示之间的最小间隔。',
203
+ descriptionEn: 'The minimum gap between recovery tips of the same kind.',
161
204
  },
162
205
  {
163
206
  key: 'tipEscalationFailures',
@@ -166,7 +209,9 @@ export const CONFIG_FIELDS = Object.freeze([
166
209
  min: 1,
167
210
  max: Number.MAX_SAFE_INTEGER,
168
211
  label: '恢复提示升级失败次数',
212
+ labelEn: 'Recovery tip escalation failures',
169
213
  description: '连续失败达到此次数后显示更详细的恢复提示。',
214
+ descriptionEn: 'After this many consecutive failures, a more detailed recovery tip is shown.',
170
215
  },
171
216
  ])
172
217
 
@@ -11,6 +11,7 @@ const RUN_CODE_TOOL_DESCRIPTION = 'Evaluate the next TypeScript cell in this ses
11
11
  const RUN_CODE_CODE_DESCRIPTION = 'Code for the next REPL cell, parsed as the body of an async TypeScript function.'
12
12
  const RUN_CODE_DESCRIPTION_DESCRIPTION = 'Short active-voice summary of what this cell does, 5-10 words (shown in the UI).'
13
13
  const CODE_TRANSPORT_INSTRUCTION = '`run_code` and `edit_run_code` are the only tools callable directly. Call every native tool declared by the SDK from inside a program.'
14
+ const PTC_COLLAPSE_SECTION_NAMES = Object.freeze(['tools:ptc-only', 'tools:code-only'])
14
15
 
15
16
  function adaptRunCodeSchema(tool) {
16
17
  const parameters = tool.parameters
@@ -94,19 +95,21 @@ export function createCordisRecoveryPolicy(initiallyEnabled) {
94
95
  function presentationState(assembly) {
95
96
  const tools = Array.isArray(assembly?.tools) ? assembly.tools : []
96
97
  if (!tools.some(tool => tool?.name === RUN_CODE)) {
97
- return { presentation: 'native', ownerProven: false }
98
+ return { presentation: 'native', ownerProven: false, collapseSectionName: undefined }
98
99
  }
99
- const collapse = assembly?.sections?.find(section => section?.name === 'tools:code-only')
100
+ const collapse = PTC_COLLAPSE_SECTION_NAMES
101
+ .map(name => assembly?.sections?.find(section => section?.name === name))
102
+ .find(section => section !== undefined)
100
103
  if (collapse !== undefined) {
101
104
  const presentation = typeof collapse.text === 'string' && collapse.text.trim().length > 0
102
- ? 'code'
105
+ ? 'ptc'
103
106
  : 'both'
104
- return { presentation, ownerProven: true }
107
+ return { presentation, ownerProven: true, collapseSectionName: collapse.name }
105
108
  }
106
109
  const presentation = tools.every(tool => tool?.name === RUN_CODE || tool?.name === EDIT_RUN_CODE)
107
- ? 'code'
110
+ ? 'ptc'
108
111
  : 'both'
109
- return { presentation, ownerProven: false }
112
+ return { presentation, ownerProven: false, collapseSectionName: undefined }
110
113
  }
111
114
 
112
115
  async function* bindCallPolicies(source, policy, calls) {
@@ -219,10 +222,10 @@ export function createDirectSurfaceOwner({
219
222
  const initialState = presentationState(initialAssembly)
220
223
  let composition = compositions.get(agent)
221
224
  if (composition === undefined && id !== undefined
222
- && (initialState.ownerProven || initialState.presentation === 'code')) {
225
+ && (initialState.ownerProven || initialState.presentation === 'ptc')) {
223
226
  composition = captureComposition(agent, id, initialState.presentation)
224
227
  }
225
- if (composition?.presentation === 'code' && !editTransport.isInstalled(agent)) {
228
+ if (composition?.presentation === 'ptc' && !editTransport.isInstalled(agent)) {
226
229
  editTransport.ensureInstalled(agent)
227
230
  }
228
231
 
@@ -235,7 +238,7 @@ export function createDirectSurfaceOwner({
235
238
  if (composition === undefined && id !== undefined) {
236
239
  const state = initialState.ownerProven ? initialState : completedState
237
240
  composition = captureComposition(agent, id, state.presentation)
238
- if (composition.presentation === 'code' && !editTransport.isInstalled(agent)) {
241
+ if (composition.presentation === 'ptc' && !editTransport.isInstalled(agent)) {
239
242
  editTransport.ensureInstalled(agent)
240
243
  }
241
244
  }
@@ -257,10 +260,10 @@ export function createDirectSurfaceOwner({
257
260
  throw new Error('ptc-plus: native agent composition assembled with run_code')
258
261
  }
259
262
  const sessionPtc = presentation !== 'native' && id !== undefined
260
- const sessionCodeOnlyProjection = presentation === 'code' && id !== undefined
263
+ const sessionPtcProjection = presentation === 'ptc' && id !== undefined
261
264
  const runCode = tools.find(tool => tool?.name === RUN_CODE)
262
265
  let directTools = tools
263
- if (sessionCodeOnlyProjection) {
266
+ if (sessionPtcProjection) {
264
267
  editTransport.ensureInstalled(agent)
265
268
  directTools = [adaptRunCodeSchema(runCode), editRunCodeSchema()]
266
269
  }
@@ -277,13 +280,13 @@ export function createDirectSurfaceOwner({
277
280
  : assembly.contexts
278
281
  const projectsSections = presentation !== 'native' && Array.isArray(assembly.sections)
279
282
  && assembly.sections.some(section => section?.name === 'tools:sdk'
280
- || (sessionCodeOnlyProjection && section?.name === 'tools:code-only'))
283
+ || (sessionPtcProjection && section?.name === completedState.collapseSectionName))
281
284
  const sections = projectsSections
282
285
  ? assembly.sections.map(section => {
283
286
  if (section?.name === 'tools:sdk') {
284
287
  return { ...section, text: capabilitySdk(section.text) }
285
288
  }
286
- if (sessionCodeOnlyProjection && section?.name === 'tools:code-only') {
289
+ if (sessionPtcProjection && section?.name === completedState.collapseSectionName) {
287
290
  return { ...section, text: CODE_TRANSPORT_INSTRUCTION }
288
291
  }
289
292
  return section
@@ -291,7 +294,7 @@ export function createDirectSurfaceOwner({
291
294
  : assembly.sections
292
295
 
293
296
  if (id !== undefined) {
294
- const nativeSchemas = presentation === 'code'
297
+ const nativeSchemas = presentation === 'ptc'
295
298
  ? new Map(toolSchemasForAgent(agent)
296
299
  .filter(schema => typeof schema?.name === 'string'
297
300
  && schema.name !== RUN_CODE && schema.name !== EDIT_RUN_CODE)
@@ -302,7 +305,7 @@ export function createDirectSurfaceOwner({
302
305
 
303
306
  return {
304
307
  ...assembly,
305
- tools: sessionCodeOnlyProjection
308
+ tools: sessionPtcProjection
306
309
  ? directTools
307
310
  : directTools.map(tool => tool?.name === RUN_CODE ? adaptRunCodeSchema(tool) : tool),
308
311
  sections,
@@ -313,7 +316,7 @@ export function createDirectSurfaceOwner({
313
316
  const optionSessionId = options.sessionId === undefined ? undefined : String(options.sessionId)
314
317
  const policy = requestPolicy(options?.signal, optionSessionId)
315
318
  if (policy === undefined || optionSessionId === undefined) return next()
316
- const source = policy.presentation === 'code'
319
+ const source = policy.presentation === 'ptc'
317
320
  ? canonicalizeToolCallStream(next(), {
318
321
  tools: options.tools,
319
322
  nativeSchemas: currentCanonicalizeToolCalls ? policy.nativeSchemas : new Map(),
@@ -330,14 +333,14 @@ export function createDirectSurfaceOwner({
330
333
  const policy = executionPolicy(id, exec.callId)
331
334
  ?? requestPolicy(exec?.signal, id)
332
335
  if (exec.name === EDIT_RUN_CODE) {
333
- return policy?.presentation === 'code'
336
+ return policy?.presentation === 'ptc'
334
337
  ? undefined
335
338
  : rejection(`tool ${EDIT_RUN_CODE} is not declared for this request`)
336
339
  }
337
340
  if (exec.name === RUN_CODE || exec.parent !== undefined) {
338
341
  return undefined
339
342
  }
340
- if (policy?.presentation !== 'code') return undefined
343
+ if (policy?.presentation !== 'ptc') return undefined
341
344
  return rejection(`tool ${exec.name} is not a direct PTC tool; use run_code or edit_run_code directly, and call native tools from inside run_code`)
342
345
  },
343
346
  handleResult(exec) {
@@ -0,0 +1,31 @@
1
+ /** Install an optional settings section across current and legacy DSH settings APIs. */
2
+ export function installSettingsSectionCompat({
3
+ ctx,
4
+ settingsModule,
5
+ namespace,
6
+ schema,
7
+ entry,
8
+ hooks,
9
+ onProvider,
10
+ }) {
11
+ ctx.inject(['settings'], settingsContext => {
12
+ const provider = settingsContext.settings
13
+ onProvider(provider)
14
+ if (typeof provider?.installSection === 'function') {
15
+ provider.installSection(ctx, namespace, schema, entry, hooks)
16
+ return
17
+ }
18
+ if (typeof settingsModule?.installSettingsSection !== 'function') {
19
+ throw new Error('ptc-plus: incompatible settings service; expected installSection or installSettingsSection')
20
+ }
21
+ const mountedOwner = new Proxy(ctx, {
22
+ get(target, property) {
23
+ if (property === 'inject') {
24
+ return (_services, attach) => attach(settingsContext)
25
+ }
26
+ return Reflect.get(target, property, target)
27
+ },
28
+ })
29
+ settingsModule.installSettingsSection(mountedOwner, namespace, schema, entry, hooks)
30
+ })
31
+ }