@dsh-enhanced/plugin-control-plane 0.1.32 → 0.1.34
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.
- package/README.md +352 -4
- package/bin/dsh-local-release-adapter.js +80 -16
- package/bin/dsh-npm-registry-adapter.js +613 -0
- package/bin/dsh-source-adoption-authority.js +12 -0
- package/bin/dsh-source-approval-authority.js +16 -0
- package/bin/dsh-source-release-authority.js +12 -0
- package/bin/dsh-systemd-host-attestor.js +707 -0
- package/bin/dsh-task-observation-authority.js +15 -0
- package/lib/adapter-process.d.ts +23 -0
- package/lib/adapter-process.d.ts.map +1 -0
- package/lib/adapter-process.js +305 -0
- package/lib/adapter-process.js.map +1 -0
- package/lib/adoption-coordinator.d.ts +63 -0
- package/lib/adoption-coordinator.d.ts.map +1 -0
- package/lib/adoption-coordinator.js +241 -0
- package/lib/adoption-coordinator.js.map +1 -0
- package/lib/adoption-handoff.d.ts +16 -0
- package/lib/adoption-handoff.d.ts.map +1 -0
- package/lib/adoption-handoff.js +11 -0
- package/lib/adoption-handoff.js.map +1 -0
- package/lib/attestation.d.ts.map +1 -1
- package/lib/attestation.js +49 -1
- package/lib/attestation.js.map +1 -1
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +29 -2
- package/lib/catalog.js.map +1 -1
- package/lib/cli.d.ts +33 -9
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +624 -178
- package/lib/cli.js.map +1 -1
- package/lib/deployment-readiness.d.ts +38 -0
- package/lib/deployment-readiness.d.ts.map +1 -0
- package/lib/deployment-readiness.js +154 -0
- package/lib/deployment-readiness.js.map +1 -0
- package/lib/effect-blocked-replay.d.ts +81 -0
- package/lib/effect-blocked-replay.d.ts.map +1 -0
- package/lib/effect-blocked-replay.js +259 -0
- package/lib/effect-blocked-replay.js.map +1 -0
- package/lib/errors.d.ts +6 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +9 -0
- package/lib/errors.js.map +1 -0
- package/lib/foreground-deployment-runtime.d.ts +20 -0
- package/lib/foreground-deployment-runtime.d.ts.map +1 -0
- package/lib/foreground-deployment-runtime.js +79 -0
- package/lib/foreground-deployment-runtime.js.map +1 -0
- package/lib/foreground-deployment.d.ts +17 -0
- package/lib/foreground-deployment.d.ts.map +1 -0
- package/lib/foreground-deployment.js +43 -0
- package/lib/foreground-deployment.js.map +1 -0
- package/lib/host-attestor.d.ts +1 -1
- package/lib/host-attestor.d.ts.map +1 -1
- package/lib/host-attestor.js +51 -57
- package/lib/host-attestor.js.map +1 -1
- package/lib/index.d.ts +25 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -2
- package/lib/index.js.map +1 -1
- package/lib/npm-publish.d.ts +25 -0
- package/lib/npm-publish.d.ts.map +1 -0
- package/lib/npm-publish.js +280 -0
- package/lib/npm-publish.js.map +1 -0
- package/lib/owner-task-gap-types.d.ts +21 -0
- package/lib/owner-task-gap-types.d.ts.map +1 -0
- package/lib/owner-task-gap-types.js +2 -0
- package/lib/owner-task-gap-types.js.map +1 -0
- package/lib/owner-task-gaps.d.ts +24 -0
- package/lib/owner-task-gaps.d.ts.map +1 -0
- package/lib/owner-task-gaps.js +74 -0
- package/lib/owner-task-gaps.js.map +1 -0
- package/lib/post-activation.d.ts +33 -0
- package/lib/post-activation.d.ts.map +1 -0
- package/lib/post-activation.js +219 -0
- package/lib/post-activation.js.map +1 -0
- package/lib/registry-fetch.d.ts +50 -0
- package/lib/registry-fetch.d.ts.map +1 -0
- package/lib/registry-fetch.js +267 -0
- package/lib/registry-fetch.js.map +1 -0
- package/lib/release.d.ts +4 -4
- package/lib/release.d.ts.map +1 -1
- package/lib/release.js +142 -56
- package/lib/release.js.map +1 -1
- package/lib/replay-endpoint-protocol.d.ts +65 -0
- package/lib/replay-endpoint-protocol.d.ts.map +1 -0
- package/lib/replay-endpoint-protocol.js +183 -0
- package/lib/replay-endpoint-protocol.js.map +1 -0
- package/lib/replay-endpoint.d.ts +7 -0
- package/lib/replay-endpoint.d.ts.map +1 -0
- package/lib/replay-endpoint.js +193 -0
- package/lib/replay-endpoint.js.map +1 -0
- package/lib/replay-grant.d.ts +62 -0
- package/lib/replay-grant.d.ts.map +1 -0
- package/lib/replay-grant.js +208 -0
- package/lib/replay-grant.js.map +1 -0
- package/lib/replay-journal.d.ts +25 -0
- package/lib/replay-journal.d.ts.map +1 -0
- package/lib/replay-journal.js +275 -0
- package/lib/replay-journal.js.map +1 -0
- package/lib/runtime-observer-protocol.d.ts +63 -0
- package/lib/runtime-observer-protocol.d.ts.map +1 -0
- package/lib/runtime-observer-protocol.js +280 -0
- package/lib/runtime-observer-protocol.js.map +1 -0
- package/lib/runtime-observer.d.ts +8 -0
- package/lib/runtime-observer.d.ts.map +1 -0
- package/lib/runtime-observer.js +182 -0
- package/lib/runtime-observer.js.map +1 -0
- package/lib/service.d.ts +145 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +734 -6
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.d.ts +65 -0
- package/lib/source-adoption-authority.d.ts.map +1 -0
- package/lib/source-adoption-authority.js +271 -0
- package/lib/source-adoption-authority.js.map +1 -0
- package/lib/source-adoption-runner.d.ts +28 -0
- package/lib/source-adoption-runner.d.ts.map +1 -0
- package/lib/source-adoption-runner.js +197 -0
- package/lib/source-adoption-runner.js.map +1 -0
- package/lib/source-approval-authority.d.ts +50 -0
- package/lib/source-approval-authority.d.ts.map +1 -0
- package/lib/source-approval-authority.js +412 -0
- package/lib/source-approval-authority.js.map +1 -0
- package/lib/source-approval-client.d.ts +32 -0
- package/lib/source-approval-client.d.ts.map +1 -0
- package/lib/source-approval-client.js +175 -0
- package/lib/source-approval-client.js.map +1 -0
- package/lib/source-build.d.ts +60 -0
- package/lib/source-build.d.ts.map +1 -0
- package/lib/source-build.js +311 -0
- package/lib/source-build.js.map +1 -0
- package/lib/source-context.d.ts +24 -0
- package/lib/source-context.d.ts.map +1 -0
- package/lib/source-context.js +99 -0
- package/lib/source-context.js.map +1 -0
- package/lib/source-job-types.d.ts +87 -0
- package/lib/source-job-types.d.ts.map +1 -0
- package/lib/source-job-types.js +2 -0
- package/lib/source-job-types.js.map +1 -0
- package/lib/source-jobs.d.ts +113 -0
- package/lib/source-jobs.d.ts.map +1 -0
- package/lib/source-jobs.js +535 -0
- package/lib/source-jobs.js.map +1 -0
- package/lib/source-release-authority.d.ts +35 -0
- package/lib/source-release-authority.d.ts.map +1 -0
- package/lib/source-release-authority.js +271 -0
- package/lib/source-release-authority.js.map +1 -0
- package/lib/source-release-client.d.ts +10 -0
- package/lib/source-release-client.d.ts.map +1 -0
- package/lib/source-release-client.js +24 -0
- package/lib/source-release-client.js.map +1 -0
- package/lib/source-release-runner.d.ts +31 -0
- package/lib/source-release-runner.d.ts.map +1 -0
- package/lib/source-release-runner.js +125 -0
- package/lib/source-release-runner.js.map +1 -0
- package/lib/source-versioning.d.ts +24 -0
- package/lib/source-versioning.d.ts.map +1 -0
- package/lib/source-versioning.js +213 -0
- package/lib/source-versioning.js.map +1 -0
- package/lib/source-workspace.d.ts +117 -0
- package/lib/source-workspace.d.ts.map +1 -0
- package/lib/source-workspace.js +458 -0
- package/lib/source-workspace.js.map +1 -0
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +509 -7
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +229 -6
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +2431 -383
- package/lib/store.js.map +1 -1
- package/lib/task-observation-authority.d.ts +29 -0
- package/lib/task-observation-authority.d.ts.map +1 -0
- package/lib/task-observation-authority.js +370 -0
- package/lib/task-observation-authority.js.map +1 -0
- package/lib/task-observation-client.d.ts +4 -0
- package/lib/task-observation-client.d.ts.map +1 -0
- package/lib/task-observation-client.js +20 -0
- package/lib/task-observation-client.js.map +1 -0
- package/lib/task-observation-runtime.d.ts +59 -0
- package/lib/task-observation-runtime.d.ts.map +1 -0
- package/lib/task-observation-runtime.js +331 -0
- package/lib/task-observation-runtime.js.map +1 -0
- package/lib/task-observation-store.d.ts +17 -0
- package/lib/task-observation-store.d.ts.map +1 -0
- package/lib/task-observation-store.js +115 -0
- package/lib/task-observation-store.js.map +1 -0
- package/lib/task-observation-types.d.ts +67 -0
- package/lib/task-observation-types.d.ts.map +1 -0
- package/lib/task-observation-types.js +2 -0
- package/lib/task-observation-types.js.map +1 -0
- package/lib/trust.d.ts +3 -0
- package/lib/trust.d.ts.map +1 -1
- package/lib/trust.js +0 -0
- package/lib/trust.js.map +1 -1
- package/lib/types.d.ts +275 -8
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +40 -5
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
记录能力缺口与 ROI、匹配 owner 固定的 catalog,并在权威审批后执行可恢复的隔离激活。Agent 只能发现候选、记录缺口和创建计划;签名审批、profile 变更和 Host attestation CLI 都不会注册成模型工具。
|
|
4
4
|
|
|
5
|
+
可选 `runtimeObserver` 提供 owner-only、HMAC 认证的本地 Unix socket,读取实际 Loader/Fiber 与服务归属;socket、连接和密钥缓冲区由 Cordis 注入 Fiber 管理。该配置会读取私有认证 key 并创建本地 socket,不新增模型工具或签名权限;调用方与 Host 共享受信 owner 身份,不构成同 UID/同进程隔离。详见[配置、权限与证据](../../docs/runtime-observer.md)。
|
|
6
|
+
|
|
5
7
|
## 安装
|
|
6
8
|
|
|
7
9
|
```sh
|
|
@@ -94,6 +96,27 @@ dsh-plugin-control activate \
|
|
|
94
96
|
|
|
95
97
|
`activate` 只完成 staging:固定 DSH executable 的 canonical path、owner/root ownership、不可被 group/other 写入、inode 和 SHA-256;用无 shell 的 argv 安装 dossier 中精确 `package@version`;结构化核对 lockfile integrity;保留原 profile backup;最后停在 `awaiting-reload`。
|
|
96
98
|
|
|
99
|
+
### 从 npm 下载已批准制品
|
|
100
|
+
|
|
101
|
+
trust schema v3/v4 的 `releaseRegistry` 可以显式选择 npm 读取协议:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"id": "npm-public",
|
|
106
|
+
"locator": "https://registry.npmjs.org",
|
|
107
|
+
"protocol": "npm",
|
|
108
|
+
"tokenEnvironment": null
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
catalog 中的 registry id/locator 必须与 owner trust 相同,并预先固定精确包版本及 SHA-512 integrity。CLI 读取 npm 的 `/{encoded-package}/{version}` 元数据,核对 name/version 与该 integrity,再从元数据的 `dist.tarball` 独立下载和计算摘要。元数据不能更换已批准摘要;不使用 tag、版本范围或 SHA-1 回退。校验后的字节进入既有 `0400` 缓存和打开的文件描述符,再由固定 DSH executor 安装并核对 lockfile。成功仍停在 `awaiting-reload`,后续使用原有 Host attestation、有限试用和观察流程。
|
|
113
|
+
|
|
114
|
+
下载只允许同一 HTTPS origin 且位于 locator 路径下的地址;重定向、跨域/CDN、查询参数、URL 凭据和路径混淆均拒绝。整个元数据与 tarball 下载共用 120 秒期限,元数据最多 2 MiB,tarball 最多 256 MiB。可选 `caPins` 配置可信 CA;省略时使用系统 TLS 信任。私有 registry 可绑定 `tokenEnvironment`,token 仅从 owner 进程该变量读取,不读取 `.npmrc`,不传给 DSH executor。需要其他 origin 的 registry 尚不支持此模式。
|
|
115
|
+
|
|
116
|
+
省略 `protocol` 或设为 `dsh` 保持原有 `/packages/<分段编码包名>/<version>/package.tgz` 布局;省略字段也保持旧 trust 的规范化摘要不变。协议不会自动探测或失败降级。
|
|
117
|
+
|
|
118
|
+
本能力支持现有 catalog 的 npm 制品下载,不签发 source-release receipt、不发布包,也不验证 npm/Sigstore provenance。后续远端 release adapter 必须分别绑定 owner 签名证据与 npm 实际观测,不能把本地预期签名冒充 registry 返回的签名。可复现的只读验证见 [npm readback](../../docs/npm-registry-readback.md)。
|
|
119
|
+
|
|
97
120
|
## 固定 Host attestor 执行契约
|
|
98
121
|
|
|
99
122
|
配置 `hostAttestor` 后,每个 awaited phase 由 owner CLI 单步推进。可执行 attestor 与已固定摘要的解释器会以 `O_NOFOLLOW` 打开,贯穿版本探测和实际 attestation 保持相同文件描述符,并通过 Linux `/proc/self/fd` 启动;结束后复核 inode 与摘要。缺少 Linux/procfs 时拒绝执行,不回退到可被替换的 pathname,其他平台可使用人工 attestation。描述符固定防止路径替换选中另一 inode,不能隔离同 UID 进程对文件内容或信任配置的修改,生产信任根仍需独立 owner/broker 权限边界。
|
|
@@ -115,7 +138,13 @@ phase 不能从命令行指定,而是从 durable plan 状态推导。Control P
|
|
|
115
138
|
|
|
116
139
|
request 固定:installation id、ledger id/path、plan id/digest、activation id/fence、profile name/path、attestor identity/path/digest/key、phase、phase requirements、receipt TTL,以及一个 durable operation id。外部 attestor 必须永久把 operation id 当幂等键:相同 id + 相同 request 重放同一 receipt;相同 id + 不同 request 必须拒绝。
|
|
117
140
|
|
|
118
|
-
|
|
141
|
+
新 Host request 使用 schema 2,并固定 `predecessor: { operationId, receiptId, phase, receiptDigest, hostGeneration }`(reload 为 `null`)。每个正常后续阶段必须承接同一 activation/fence 中已应用、通过的前一阶段凭证;摘要覆盖完整签名 receipt。账本在调用外部程序前和应用结果时重新核对这条关联及 installation 最新代次。readiness、回放、shadow、canary、soak、health 不允许自行提高或降低 Host generation;换代只能通过 reload/rollback 的显式合同。失败凭证也不能绕过这些检查。该关联不替代独立运行时身份和副作用观测。
|
|
142
|
+
|
|
143
|
+
Host attestor 与 release adapter 的每次命令共用受控进程组:超时、输出超限及主进程正常/异常退出都会清理同组后代,核对主进程退出,并有界排空 stdout。清理无法证实时返回失败,不以遗留管道的 `close` 无限等待。该机制依赖 runner 存活,不能包含主动 `setsid()` 脱组进程,也不撤回远端已接受的操作;具体期限、恢复边界与回归证据见 [adapter 生命周期](../../docs/control-plane-adapter-lifetime.md)。
|
|
144
|
+
|
|
145
|
+
随包的 `bin/dsh-systemd-host-attestor.js`(v5)提供 Linux/systemd 的 **reload、readiness 与物理 rollback** 适配:先用 `probe --prepare-only` 取得确切持久请求,再由主人私有配置授权其摘要;重启前记录操作,重复调用仅重放或观测对账。它核验 fresh InvocationID/MainPID 与稳定窗口,按 installation 共享 Host 代次,reload 推进至 `awaiting-readiness`;readiness 再绑定最新已签重启、实际 Loader/Fiber 与服务实例,多次稳定观测后签名,重复请求不重启 Host。稳定、认证通过且身份匹配的 inactive 候选签为 failed readiness;认证错误或状态漂移不签发回执。rollback 在 CLI 恢复原 profile 文件后证明原 Host 就绪,或原本不存在的 profile 已停服;缺少物理恢复凭证时继续保持 pending。部署配置、权限和未覆盖阶段见 [systemd Host attestor](../../docs/systemd-host-attestor.md)。
|
|
146
|
+
|
|
147
|
+
phase operation 和派发 claim 在子进程启动前持久化。外部执行期间不持有控制面 SQLite 写锁,其他任务可继续写入;同一 operation 的并发或重启调用由持久 claim 拒绝。退出、验签失败或回执丢失后保留 unknown,不自动再次调用通用 attestor。取得原请求的精确签名回执后,用下述 `attest` 对账;未结算的派发也会阻止同计划换代回退,避免旧外部动作与恢复并行。
|
|
119
148
|
|
|
120
149
|
## Phase proof,而不是命令标签
|
|
121
150
|
|
|
@@ -131,7 +160,7 @@ phase operation 在子进程启动前持久化。子进程执行期间持有 SQL
|
|
|
131
160
|
|
|
132
161
|
每类 evidence 还带有探针/回放/trace digest。Control Plane 验证签名、结构、请求绑定、TTL 和 policy 阈值;它不会假装自己能独立观察部署。真正的 reload、流量、effect interception 和健康观测由 owner/deployment-controlled attestor 实现,并由其私钥为声明负责。测试目录中的 fixture attestor 只用于真实子进程集成测试,不进入发布包,也不是生产探针。
|
|
133
162
|
|
|
134
|
-
任一有效签名 receipt 返回 `outcome: failed` 时,计划进入 fenced `rollback-pending` 并自动恢复原 profile
|
|
163
|
+
任一有效签名 receipt 返回 `outcome: failed` 时,计划进入 fenced `rollback-pending` 并自动恢复原 profile;一旦进入过 `awaiting-reload`,恢复文件后仍保持 pending,必须由绑定原始 baseline 的 `rollback` 签名回执证明旧 Host 就绪或原本不存在的 profile 已停服,才能结束回退。只有七个部署 phase 全部通过、保存已安装 profile 的核心文件摘要并完成终态 CAS 后才是 `activated`;上一版 backup 保留供使用后回退。格式错误、错误 key/digest/phase/evidence、过期 receipt 或未配置 attestor 都保持当前 awaiting 状态。
|
|
135
164
|
|
|
136
165
|
## 人工 Host attestation
|
|
137
166
|
|
|
@@ -153,11 +182,270 @@ dsh-plugin-control attest \
|
|
|
153
182
|
|
|
154
183
|
人工路径使用相同 operation、evidence validator、Ed25519 verifier 和 CAS,不是弱化旁路。旧的 schema-v1 `evidenceDigest`-only Host receipt 会被拒绝,因为它不能证明 phase 语义。
|
|
155
184
|
|
|
185
|
+
`attest` 也接受 configured attestor 已生成但未入账的精确回执,不再次执行外部程序。保存已验签事实本身不推进计划;owner 来源已撤回时仍可保存以解除 unknown、进入恢复,前向应用仍须通过当前 Host 来源校验。通用 CLI 不能替代 owner-task 的 live source fence。
|
|
186
|
+
|
|
187
|
+
## 使用后的退化与物理回退
|
|
188
|
+
|
|
189
|
+
`watch-observe --receipt <file>` 接受 Host 签名的精确版本观察;healthy 仅记账,regressed 会恢复上一版;同一运行中的 Host 可连续提交不同的签名观察,无需为每次观察重启。`watch-retract --receipt <file>` 接受 owner 签名的撤回并走同一恢复路径。两者输出保留 watch 操作回执,另外返回当前 `activation`。配置了 Host attestor 时,命令继续调用既有 `rollback` phase;只有签名回执证明旧 Host 恢复就绪(或原 profile 不存在时已停服),计划才成为 `rolled-back`。未配置签名器时保留 `rollback-pending`,可通过 `host-request`/`attest` 完成。
|
|
190
|
+
|
|
191
|
+
schema 17 在成功启用前保存 `package.json`、`pnpm-lock.yaml`、`cordis.patch.yml` 的摘要,恢复前同时核对当前版与保留的原版核心文件。核心文件漂移、较新的部署已生效、目标存在进行中的部署或旧计划缺少恢复检查点时,拒绝覆盖。摘要不是整个目录的不可变证明,也不替代进程、凭据与文件写权限隔离。回退按 rename 分步恢复;相同签名触发可在重启后继续,未知的 Host 外部操作仍须原有对账,不创建新 operation 绕过。
|
|
192
|
+
|
|
193
|
+
成功部署保留自己的上一版备份,并清理同一目标已被它取代的旧备份;新部署失败不会提前删除旧备份。最初的 `activated` 回执保留,后续回退另存终态记录。迁移不为历史部署捏造备份或摘要。此入口接收可信签名观察;普通用户任务的版本归因和自动观察签发仍需接线;已发布 owner 修复可通过下述有限采用配置接入。
|
|
194
|
+
|
|
156
195
|
## 源码能力 lane 和边界
|
|
157
196
|
|
|
158
197
|
`source-plan` / `scaffold` 只在 owner 审批的 linked、clean worktree 和固定 generator digest 上生成插件并运行 `pnpm check`。local checks 使用临时 Git index 对 exact scope 计算 staged tree/patch digest,不污染工作树的真实 index。owner 必须在 checks 之后为 exact source digests、scope 和 release policy 签发独立 authorization,随后才能执行 `release-start`。
|
|
159
198
|
|
|
160
|
-
|
|
199
|
+
`prepareModifySourcePlan` 是另一条待审批修改路径:它只接受现有非保护插件树内的有界文件集,并用临时 Git index 生成精确 tree 后以 `git archive` stdin 传入 owner 配置的 Docker image。容器没有 Host bind mount、网络、特权或调用者环境,使用只读根、非 root UID、`cap-drop=ALL`、`no-new-privileges`、固定 CPU/内存/PID/tmpfs 限制和离线 `pnpm install --ignore-scripts`、`pnpm check`、`pnpm pack`。镜像必须由 registry manifest digest 或本地 image content ID 固定,并预热离线 pnpm store。控制面在持久化前重算 tree/patch digest;任意漂移、取消、超时或容器失败都会删除 worktree 而不创建计划。`.git`、`.gitattributes` 与 `.gitmodules` 不能通过该路径修改。
|
|
200
|
+
|
|
201
|
+
`inspectSource` 只从当前 `HEAD` 的 Git objects 读取非保护插件的已跟踪普通文本文件;它不读取工作树。调用者可先请求空 `paths` 获得有界 manifest,再将返回的 `baseCommit` 用作读取和 `prepareModifySourcePlan.expectedBaseCommit` 的精确绑定。HEAD 已移动、二进制/生成/隐藏路径、symlink、submodule 或越界路径都会被拒绝。清单最多 1024 条,内容最多 64 个文件、单文件 64 KiB、合计 256 KiB;返回真实 blob 大小并支持普通可执行文本文件。整个读取请求从服务入口起受 15 秒截止时间控制;取消会终止正在运行的 Git 子进程,异步权限检查的迟到结果不再生效,Fiber 卸载等待请求收尾。
|
|
202
|
+
|
|
203
|
+
Host patch config 在启用修改准备前必须提供 `sourceBuild`,例如:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
sourceBuild:
|
|
207
|
+
dockerPath: /usr/bin/docker
|
|
208
|
+
image: sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
|
209
|
+
timeoutMs: 180000
|
|
210
|
+
memoryMiB: 512
|
|
211
|
+
cpus: 1
|
|
212
|
+
pidsLimit: 64
|
|
213
|
+
workspaceMiB: 512
|
|
214
|
+
outputBytes: 65536
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
可选 `sourceBuild.versioning: patch` 让 Host 在冻结检查树前,从基准 Git 提交生成当前插件的下一补丁版本,同时更新 `package.json` 与 `src/version.ts`。只支持稳定 `x.y.z`;模型不得提交这两个保留文件。生成的版本变化与源码一起接受离线检查、打包和摘要绑定,检查后再次核对文件及制品版本。持久作业冻结此配置,重启不会改用新的版本策略。
|
|
218
|
+
|
|
219
|
+
使用有限审批器时还须显式配置 `grant.versioning: "patch"`。审批器独立核对基准与候选,manifest 除版本外的名称、依赖、脚本等字段必须完全一致,运行时版本文件只允许规范常量;生成的两个文件计入文件数及字节预算。未启用该 grant 时仍拒绝 manifest 修改。变更已有不可变 grant 配置须使用新 grant id;变更持久作业授权须使用新的 `sourceJobs.authorityId`。
|
|
220
|
+
|
|
221
|
+
同一基准的多个候选会得到相同下一版本,不提供覆盖已发布版本的权限。后续采用流程须串行推进获准源码基准;现有 registry/catalog 冲突检查继续拒绝版本复用。此选项只准备可区分版本的候选,自动发布和采用需显式启用下述有限授权与执行配置,不会为每次修复发布公共 npm 包。
|
|
222
|
+
|
|
223
|
+
`sourceBuild.profile` defaults to `standard`, whose existing maximum build timeout is 4 minutes and whose `/tmp` tmpfs is fixed at 32 MiB. An owner may explicitly set `profile: repository` for a full repository `pnpm check`; only that profile permits a timeout up to 30 minutes, memory up to 16 GiB, 16 CPUs, 1024 PIDs, an 8 GiB workspace tmpfs and a 4 GiB `/tmp` tmpfs (default 2 GiB). The repository profile explicitly permits execution from both bounded tmpfs mounts, needed by native build tools and temporary executable test fixtures; the standard profile retains Docker’s default no-exec mounts. The repository profile also fixes `CI=true` and `VITEST_MAX_WORKERS=1` inside the container. The caller cannot select a profile or increase these limits: its timeout is capped by the owner configuration. Cancellation, deadline expiry, output overflow, or Fiber disposal kills the preparation client, waits for archive/build processes, and proves named-container absence before any pending plan is stored. Interrupted builds are never automatically replayed; the optional durable Host lane below records their status and resource identity.
|
|
224
|
+
|
|
225
|
+
`repositorySandbox: { seccompPath: /absolute/owner/path/source-builder-seccomp.json }` is a separate repository-only opt-in for the existing nested Bubblewrap integration tests. It requires the approved profile digest and Docker Server `29.4.1/linux/amd64`; other bytes or runtimes fail before candidate execution. It permits additional namespace/mount syscalls, removes Docker's masked/read-only system-path lists, and hides `/sys` behind an empty read-only tmpfs. This expands the outer container's `/proc` visibility and kernel surface; it is not equivalent to Docker's default policy. UID 65534, zero capabilities, no-new-privileges, offline execution, read-only root, and no Host bind mounts remain mandatory. See the [profile provenance and limits](../../scripts/isolation/README.md#nested-sandbox-profile). Omitting this option retains Docker's default system-path restrictions, including in repository mode.
|
|
226
|
+
|
|
227
|
+
`dockerPath` 必须是 canonical、owner/root-owned 且不可被 group/world 写的可执行文件。镜像要包含 UID 65534 可执行的 Node、pnpm 和可用的离线 store;pnpm 的 cache 必须定位到可写 tmpfs;离线 store 由镜像中的只读种子复制到 workspace tmpfs,供 pnpm 11 写入其 SQLite 索引;未配置时 `canPrepareSource()` 返回 false,修改准备请求 fail closed。
|
|
228
|
+
|
|
229
|
+
完整仓库镜像的 owner 构建脚本见 [source builder](../../scripts/isolation/README.md)。它只传入依赖清单和 lockfile,在联网构建阶段预取依赖;候选源码进入容器时仍禁网、无 Host 挂载。`repository` profile 仅扩大 owner 配置的检查预算,不延长 Growth Driver 的 5 分钟授权。长检查可通过下述持久 Host 任务提交;入队不代表检查通过或源码已改进。
|
|
230
|
+
|
|
231
|
+
### 持久源码检查任务(可选)
|
|
232
|
+
|
|
233
|
+
Growth Driver 设置 `pluginSourceProposals.preparationMode: durable` 后,`plugin_source_prepare` 只提交任务,`plugin_source_job_status` 回读当前 owner 的状态。控制面还必须配置独立于模型回合的有限 Host 授权:
|
|
234
|
+
|
|
235
|
+
```yaml
|
|
236
|
+
sourceJobs:
|
|
237
|
+
authorityId: repo-source-checks-2026-09
|
|
238
|
+
expiresAt: 1790000000000 # 示例:替换为 owner 批准的绝对 Unix 毫秒期限
|
|
239
|
+
maxSubmissions: 3
|
|
240
|
+
repository: /absolute/canonical/repository
|
|
241
|
+
ownerRouteId: configured-delivery-owner-route
|
|
242
|
+
principalId: configured-principal
|
|
243
|
+
workspace: /absolute/owner/workspace
|
|
244
|
+
preset: primary
|
|
245
|
+
budgetId: source-check-runs
|
|
246
|
+
budgetAmount: 1
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
还需安装兼容的 `assistant-delivery` 和 `assistant-automations` Host peers;后者显式开启现有 scheduler,Policy 允许该 scope 下的 `plugin-control-plane-source` reconcile 及对应后台任务 execute,并配置 `source-check-runs` 的 `automation-runs` 预算。此预算计源码检查和后续恢复轮次,不代表模型 token/费用预算;希望所有作业共享额度时用 Policy 的 `global` 或 `workspace` scope,`subject` 按 automation 分开计费。缺少 peer 或授权时不执行。`sourceBuild` 决定镜像和检查上限,模型不能改写队列权限、owner、路径或构建限制。
|
|
250
|
+
|
|
251
|
+
任务先冻结完整 Delivery v2 回执、trust 摘要、gap revision/digest、read base、文件内容及构建配置,再以 **paused → 绑定规范化 definition hash → active** 注册到 Automations 的一次性 Host executor。源码只存控制面私有 SQLite;Automation definition 和模型状态投影不包含文件内容。相同 authority 的期限、配置和累计提交上限不可重置;同 key 不同内容拒绝。全账本同时最多一个 `queued/running/unknown` 任务。
|
|
252
|
+
|
|
253
|
+
模型回合结束不会取消已接受的 Host 任务。Host 自己受授权绝对期限、构建时限、Automations lease、取消和 Cordis provider 生命周期约束。成功时,job `prepared`、gap claim 和已有 `pending-approval` plan 在同一 SQLite 事务提交。原模型授权仍最多 300 秒;不新增模型循环或调度器。
|
|
254
|
+
|
|
255
|
+
配置审批、发布或采用后,临时失败的 `prepared` 作业由原生每分钟 cron 接续,无须重启 Host。独立 Host executor `plugin-control-plane-source-continuations-v1` 使用同一 system owner,持久 automation ID 为 `source-job-prepared-continuations`;如 Policy 采用精确 ID 规则,须覆盖它。每轮最多处理一个当前可推进作业,仍走生产 Policy/预算准入、原 owner/反馈/trust 校验和既有外部操作 claim。重启只注册恢复任务,不在启动时直接调用授权器;unknown 外部动作不重新派发。无可推进作业时暂停,新的已检查候选可重新激活。卸载暂停当前代次、取消并等待已有调用;旧代次不能暂停新实例。
|
|
256
|
+
|
|
257
|
+
每次恢复轮次消耗 `sourceJobs.budgetAmount`。Policy 预算按配置的时间窗口计算,窗口耗尽会拒绝执行;它不是终身重试次数上限。前向推进还受原授权与计划期限约束;已暴露版本的恢复义务可在旧来源失效后继续,但仍需预算和原部署权限。调用结果不明会留在原生执行记录中,不能算成功。独立模型审查仍使用原有冻结模型及预算;此接续不建立新的模型循环。
|
|
258
|
+
|
|
259
|
+
重启重接尚未 claim 的任务;已 claim 的任务转为 `unknown`,保留资源槽且不自动重跑。状态回读、入队和启动时核对 Automations 的精确生产终态,将预算/Policy 等在 executor 前发生的终结写回 `failed`。Host-only `reconcileSourceJob({id, owner})` 可对 `unknown` 进行资源核对:按容器标签、镜像、ID 删除并证明不存在,验证 worktree 的 Git 注册、base 和仓库归属后删除。归属不明、daemon 不可达或残留路径未注册时保留 `unknown`,需要 operator 检查;同一 route/principal record/version/workspace/preset 的新会话绑定仍可查看和清理旧任务;执行继续要求原完整回执精确匹配。该方法不暴露给模型,也不重跑候选。每个 statePath 使用单一控制面 Host 实例。
|
|
260
|
+
|
|
261
|
+
默认只准备待审批提案;配置下述有限审批后,真实 owner 失败来源的持久作业可继续审批。工程层 native scheduler/Policy/SQLite 集成测试不等于真实模型执行整仓修复或生产发布验收。
|
|
262
|
+
|
|
263
|
+
成功准备返回 `pending-approval`,不会自动发布。普通 gap 可用已有签名审批流程;owner 任务来源必须通过 Host 当前来源 fence 审批,离线 CLI 签名本身不能代替该校验。审批后,普通 gap 用 `dsh-plugin-control source verify-prepared --plan-id <id> --expected-revision <revision>` 重读同一 worktree 并核对 digest;owner 来源由下述 Host 发布接续入口完成复核,才能进入 review/release。修改 worktree 会使复核失败;旧 `create` 计划仍走 `scaffold`。`dsh-plugin-control source gc` 将已过 TTL、仍 pending/approved 的计划以版本 CAS 转为 `expired`,释放该计划的 gap 占用,再清理控制面登记的 modify worktree;已经 `expired` 的计划可重试物理清理,已经进入 review/release 的 worktree 保留。
|
|
264
|
+
|
|
265
|
+
### 普通任务修复的有限审批
|
|
266
|
+
|
|
267
|
+
可选 `sourceApprovals` 将持久 `sourceJobs` 的准备结果接到 owner 配置的有限签名器:
|
|
268
|
+
|
|
269
|
+
```yaml
|
|
270
|
+
sourceApprovals:
|
|
271
|
+
executable:
|
|
272
|
+
path: /opt/dsh/control-plane/bin/dsh-source-approval-authority.js
|
|
273
|
+
sha256: <wrapper-sha256>
|
|
274
|
+
interpreter:
|
|
275
|
+
path: /opt/node/bin/node
|
|
276
|
+
sha256: <node-sha256>
|
|
277
|
+
configPath: /private/owner/source-approval.json
|
|
278
|
+
timeoutMs: 10000
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
签名器配置示例(所有占位符须替换;配置、私钥、账本及其父目录由 owner 私有持有;仓库可为不可被其他用户写入的 0755 目录):
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"schemaVersion": 1,
|
|
286
|
+
"authority": "owner-source",
|
|
287
|
+
"keyId": "source-key",
|
|
288
|
+
"keyPath": "/private/owner/source-key.pem",
|
|
289
|
+
"statePath": "/private/owner/source-approval.sqlite",
|
|
290
|
+
"controlDatabasePath": "/private/dsh/control/control.sqlite",
|
|
291
|
+
"grant": {
|
|
292
|
+
"id": "tool-repair-1",
|
|
293
|
+
"expiresAt": 1800000000000,
|
|
294
|
+
"maxApprovals": 5,
|
|
295
|
+
"repository": "/work/dsh-enhanced",
|
|
296
|
+
"worktreeRoot": "/private/dsh/control/source-worktrees",
|
|
297
|
+
"owner": {
|
|
298
|
+
"authorityId": "<delivery-route>",
|
|
299
|
+
"authorityHash": "<route-sha256>",
|
|
300
|
+
"principalId": "<owner>",
|
|
301
|
+
"principalRecordId": "<principal-record>",
|
|
302
|
+
"principalVersion": 1,
|
|
303
|
+
"workspace": "/work/user",
|
|
304
|
+
"agentPreset": "primary"
|
|
305
|
+
},
|
|
306
|
+
"plugins": ["personal-memory"],
|
|
307
|
+
"maxChangedFiles": 4,
|
|
308
|
+
"maxChangedBytes": 65536,
|
|
309
|
+
"receiptTtlMs": 60000
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
在 Control Plane trust 的 `approvalKeys` 登记对应 Ed25519 公钥;该 key 不用于 release authorization、发布或 Host attestation。owner 字段取实际 Delivery 回执,不由模型生成。grant id 的配置与 key 指纹不可变,额度和已签回执落入独立 SQLite;重启及同一请求重试不重置额度、不延长签名期限。
|
|
315
|
+
|
|
316
|
+
签名器只读当前 schema 20 控制面库,重新验证完整来源摘要、owner、期限、仓库及 worktree 归属、检查证据和当前 tree/patch。默认只允许白名单非保护插件 `src/` 下的普通 `.ts/.js/.mts/.mjs` 源文件修改;`grant.versioning: "patch"` 仅额外允许上述 Host 管理的 manifest 版本变化。测试目录、其他 manifest 字段、脚本、lockfile 和保护插件不在授权范围。工程检查证据不构成业务目标达成证明。
|
|
317
|
+
|
|
318
|
+
持久作业在 `prepared` 落账后调用审批,Host 在验签后以 Delivery/Evaluation 当前来源 writer fence 提交 `approved`。纠正、撤回、身份/会话换代、取消或 trust 变化均阻止提交。审批失败保留 pending 计划和已完成构建;重启恢复最多 1000 个 pending 的 owner 作业,仍核对原 sourceJobs 授权和 owner,只重试审批。单次 helper 至多 10 秒;卸载会等待子进程清理并丢弃迟到结果。Host 可调用 `requestOwnerSourceApproval({planId, signal?})` 显式重试;该方法不暴露为模型工具。inline 准备仍只返回 pending。
|
|
319
|
+
|
|
320
|
+
此客户端要求 Linux `/proc`;wrapper 和 Node 均须是 canonical 路径、可执行且只有一个硬链接的 owner/root 文件,不能由其他用户写入。按文件描述符固定两者字节;完整已安装库仍须由 owner 控制,wrapper hash 不覆盖导入库。子进程是执行和清理边界,同 UID 运行不构成私钥隔离;若模型可任意执行 Host 代码,应将签名器置于独立身份边界,通过 owner 固定的代理执行文件调用。签名器不执行候选源码。仅启用 `sourceApprovals` 时停在 `approved`;可配置下面的有限发布授权接续,精确制品的验证、采用和观察/回滚仍走各自授权链路。
|
|
321
|
+
|
|
322
|
+
### 获批源码的有限本地发布授权
|
|
323
|
+
|
|
324
|
+
同时启用 `sourceApprovals`、`sourceBuild.versioning: patch` 和可选 `sourceReleases` 后,持久源码作业会在获批后自动复核同一 worktree,再请求独立发布授权,进入既有 `awaiting-pr` 状态。继续使用原生 Automations executor 和 `prepared` 作业恢复;不会重复构建。Host 方法 `requestOwnerSourceRelease({ planId, signal?, expectedTrustDigest? })` 也可显式接续已批准的 owner 来源计划,未注册为模型工具。普通 owner 来源的 `verify-prepared` 和 `release-start` 必须通过 Host 当前反馈校验,离线 CLI 不能代替该校验。
|
|
325
|
+
|
|
326
|
+
```yaml
|
|
327
|
+
sourceReleases:
|
|
328
|
+
executable:
|
|
329
|
+
path: /opt/dsh/control-plane/bin/dsh-source-release-authority.js
|
|
330
|
+
sha256: <installed-wrapper-sha256>
|
|
331
|
+
interpreter:
|
|
332
|
+
path: /opt/dsh/node
|
|
333
|
+
sha256: <installed-node-sha256>
|
|
334
|
+
configPath: /private/owner/source-release.json
|
|
335
|
+
timeoutMs: 10000
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
`source-release.json` 沿用上方源码审批配置的 `schemaVersion`、authority/key、私有状态库、控制面库和 owner/repository/worktree/文件预算结构,但须使用独立发布授权私钥和状态库:将 `grant.maxApprovals` 替换为 `grant.maxReleases`,设置 `grant.versioning: "patch"`,并给每个白名单插件配置一条 `grant.policies`。在 trust 的 `releaseAuthorizationKeys` 注册公钥;该 key 与 source approval、adapter receipt keys 分开。策略示例:
|
|
339
|
+
|
|
340
|
+
```json
|
|
341
|
+
{
|
|
342
|
+
"targetBranch": "rsi/repairs",
|
|
343
|
+
"candidateId": "personal-memory",
|
|
344
|
+
"packageName": "@dsh-enhanced/personal-memory",
|
|
345
|
+
"packagePath": "plugins/personal-memory",
|
|
346
|
+
"dshBaseline": "0.1.5-rc.2",
|
|
347
|
+
"capabilities": ["memory"],
|
|
348
|
+
"authorities": ["filesystem"],
|
|
349
|
+
"requires": [],
|
|
350
|
+
"registryId": "local-repairs",
|
|
351
|
+
"registryLocator": "file:///private/owner/registry",
|
|
352
|
+
"catalogId": "owner-catalog",
|
|
353
|
+
"catalogPath": "/private/owner/catalog.json",
|
|
354
|
+
"minimumReproducibleBuilds": 2
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
能力、权限和依赖应填写目标部署的精确授权值,数组按既有 release policy 规则排序。registry 目录和 catalog 文件必须已存在且归 owner 私有控制;此授权器仅支持现有本地 file registry。包版本从已检查的 Host 补丁版本推导,制品路径固定为 registry 的 `packages/<编码包名>/<版本>/package.tgz`;调用者不能指定版本、路径或扩大策略。
|
|
359
|
+
|
|
360
|
+
签名器独立重读当前控制面库、owner 来源、源码范围、构建证据、Git 摘要及版本。grant/config/key 指纹和累计签发数持久保存;重试和进程重启只返回同一回执,不重扣额度、不延长有效期。Host 在最终提交及重放时再检查当前反馈,纠正、撤回、身份变化、取消、trust 漂移均阻止接续。重启可从 `pending-approval`、`approved` 或 `ready-for-human-review` 接续;已进入 release 的计划不重新签发。单次 Host 接续最多 30 秒,卸载等待受控进程清理并丢弃迟到结果。
|
|
361
|
+
|
|
362
|
+
此授权配置只负责开始既有 release 状态机;自动推进须另行启用下述执行配置。
|
|
363
|
+
|
|
364
|
+
### 已授权本地发布的自动推进
|
|
365
|
+
|
|
366
|
+
```yaml
|
|
367
|
+
sourceReleaseExecution:
|
|
368
|
+
reviewDecisionRoot: /private/owner/review-decisions
|
|
369
|
+
timeoutMs: 900000
|
|
370
|
+
independentReview: true # 可选;需另配 assistant-verifier.sourceReviews
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
此配置依赖 `sourceReleases`,沿用 trust schema v4 中八个固定 `releaseAdapters` 和本地 `file:` registry。原生 Automations 作业获批后,Host 依次接续 PR、review、merge、build、sign、publish、registry verify 与 catalog admission;最多推进八个既有阶段,不创建额外定时器。`timeoutMs` 为本次接续总上限(1 秒至 30 分钟),各 adapter 保留自身时限。源码检查和 release 构建是两个原有检查阶段,恢复不会重做已完成阶段。
|
|
374
|
+
|
|
375
|
+
review decision 仍由独立审查方产生:Host 只读 canonical、无 symlink、owner 私有目录下的 `<prId>.json`,其内容须符合现有 local adapter 的 `dsh-local-review-decision` 格式并精确绑定 PR id、base/head commit 和 PR evidence digest。配置的 `reviewDecisionRoot` 须与 review adapter 读取目录一致。缺失时保持 `awaiting-review`,不派发 review;格式错误或绑定变化则拒绝。独立审查方完成后调用 Host-only `advanceOwnerSourceRelease({ planId, signal?, expectedTrustDigest? })` 即可在当前进程继续,不要求重启。该入口不是模型工具;decision 由独立 Verifier 或外部审查方生成。部署须把 decision 写权限、审查输入和审查执行环境与候选写权限分开;同 UID 的目录权限本身不证明进程隔离。
|
|
376
|
+
|
|
377
|
+
启用 `independentReview: true` 后,现有持久作业会在缺少 decision 时调用另行有限授权的 [Verifier 源码审查](../assistant-verifier/README.md#独立源码审查)。它使用固定 bare Git 仓库和新的无工具原生 Agent,默认继承来源任务的确切模型,也可固定审查模型。Verifier 的 decision root 必须与这里相同;服务或模型不可用时在 PR 前等待,依赖移除会停止对应作业。审查完成后仍通过原有签名 review adapter 和八阶段检查。后续精确制品采用与普通任务版本观察已有下述可选配置,完整部署仍待验收;这里的 publish 仅面向获准本地 registry,不上传公共 npm。
|
|
378
|
+
|
|
379
|
+
schema 18 在 adapter 派发前持久登记 operation claim;验签和子进程执行期间不持有 SQLite 写事务。当前 owner 来源、取消、trust 和阶段 CAS 在执行边界及回执应用前重查。超时、崩溃或响应丢失后,已 claim 且无回执的 operation 保持 unknown,重启不重新执行。已完成回执直接接续应用,catalog 已写而账本未确认时也不重新计算旧 preview。独立取得精确签名回执后,可通过 `advanceOwnerSourceRelease({ planId, receipt, ... })` 对账并继续;它只验签回执,不重新运行丢失响应的动作。无法取得可信回执时保留 unknown。
|
|
380
|
+
|
|
381
|
+
升级到 schema 18 会将历史 pending release operation 保守视为可能已派发,要求回执对账;不会把旧 pending 当作新动作。源码作业的 prepared 恢复覆盖等待中的 release 阶段,并继续检查原 owner 和冻结 trust。Cordis 卸载取消接续、终止并回收 adapter 进程组、等待在途工作后关闭数据库;脱离进程组的进程仍须由部署的 OS 隔离边界管理。
|
|
382
|
+
|
|
383
|
+
### 已发布 owner 修复的有限采用
|
|
384
|
+
|
|
385
|
+
在 `sourceReleaseExecution` 之上可选配置:
|
|
386
|
+
|
|
387
|
+
```yaml
|
|
388
|
+
sourceAdoptions:
|
|
389
|
+
profile: assistant
|
|
390
|
+
planTtlMs: 900000
|
|
391
|
+
timeoutMs: 600000
|
|
392
|
+
authority:
|
|
393
|
+
executable: { path: /opt/dsh/bin/dsh-source-adoption-authority.js, sha256: <sha256> }
|
|
394
|
+
interpreter: { path: /usr/bin/node, sha256: <sha256> }
|
|
395
|
+
configPath: /srv/dsh-owner/adoption-authority.json
|
|
396
|
+
timeoutMs: 10000
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
`dsh-source-adoption-authority` 使用独立 owner 私有配置和 Ed25519 key;公钥进入 trust 的 `approvalKeys`。配置为 `schemaVersion: 1`、`authority`、`keyId`、`keyPath`、`statePath`、`controlDatabasePath` 和 `grant`。grant 固定 `id`、`expiresAt`、`maxAdoptions`、稳定 owner 身份(与源码发布授权相同)、`installationId`、完整 `ledger/target/executor`、`catalogPath`、`receiptTtlMs` 和 `policies`。每条 policy 固定 `candidateId/packageName/dshBaseline/capabilities/authorities/requires/registryId/registryLocator`;版本、完整性与 registry reference 从精确完成的 release 读取。保护插件不在此授权范围。相同 grant 的配置/key 不可替换,重试返回同一回执且不延长期限。
|
|
400
|
+
|
|
401
|
+
原生源码作业在 `release-complete` 后自动接续:绑定原 owner 任务、精确 release 制品与 profile,申请有限采用回执,再调用与 CLI 相同的安装、Host 签名检查和物理回退实现。schema 19 的 `source_adoptions` 保证一条源码计划只对应一条激活计划;普通任务失败无需给插件添加虚假的通用修复 capability。Host 入口 `adoptOwnerSourceRelease({ sourcePlanId, signal?, expectedTrustDigest? })` 可继续已有记录,不暴露为模型工具。恢复不重建已发布制品、不重新审批已批准的激活。
|
|
402
|
+
|
|
403
|
+
使用 systemd 重启目标 Host 时,控制面应运行在目标 Host 之外,避免目标重启中断自己的部署作业。采用使用独立 SQLite 连接,并复用现有 profile 锁和跨进程文件操作互斥;同一 Service 一次执行一个采用任务。任务来源在异步操作前后重验,取消或失败后保留恢复义务。物理恢复不依赖已撤回的来源授权;已暴露 profile 仍须取得原 Host 的独立 rollback 回执才结算。trust 绑定变化、无法确认资源释放或签名服务不可用时保留待恢复状态。Cordis 卸载取消并等待采用工作,随后关闭其连接。
|
|
404
|
+
|
|
405
|
+
需要重启目标 Host 的部署,应在目标的 `sourceAdoptions` 中增加以下 `handoff`,并在采用签名器的 `grant.handoff` 配置完全相同的值:
|
|
406
|
+
|
|
407
|
+
```yaml
|
|
408
|
+
handoff:
|
|
409
|
+
schemaVersion: 1
|
|
410
|
+
coordinatorId: assistant-deployer
|
|
411
|
+
maximumWindowMs: 900000
|
|
412
|
+
commit: target-host
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
这些条款进入不可变计划摘要,由现有采用审批签名覆盖。目标在当前 owner 反馈写入屏障内创建持久交接;期限取计划、审批回执和窗口上限的最小值,恢复或重复调用不会续期。外部 Host 共享精确 ledger/trust,单独配置:
|
|
416
|
+
|
|
417
|
+
```yaml
|
|
418
|
+
adoptionCoordinator:
|
|
419
|
+
coordinatorId: assistant-deployer
|
|
420
|
+
budgetId: adoption-coordinator-runs
|
|
421
|
+
budgetAmount: 1
|
|
422
|
+
scope:
|
|
423
|
+
workspace: /srv/dsh/workspace
|
|
424
|
+
preset: assistant
|
|
425
|
+
principalId: owner
|
|
426
|
+
ownerRouteId: owner-route
|
|
427
|
+
timeoutMs: 300000
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
协调器必须运行在另一进程和独立 systemd unit;它不能同时配置目标的 `sourceJobs`、`sourceAdoptions`、`runtimeObserver` 或 `replayEndpoint`。只需连接所在 Host 的 Automations,复用原生每分钟调度、owner/Policy 和预算,每轮至多推进一个交接;空队列仍消耗一次配置的调度预算。`budgetId/budgetAmount` 必填,对应 Policy budget 使用 `automation-runs` metric 与有限周期额度,建议 `subject` scope,以稳定的协调器 automation id 聚合。Policy 账本按 scope、metric 和周期计量,仅换 budget id 不会分池;同一 Host 的其他任务应明确选择共用或分开额度。旧配置升级时须补齐,不能打开 `allowUnbudgetedExecution` 代替。`coordinatorId` 是绑定标识,不提供 OS 隔离或替代 ledger 文件权限。停用调度或耗尽原生预算也会暂停自动恢复,恢复前需先核对未知外部操作。
|
|
431
|
+
|
|
432
|
+
协调器沿现有安装和签名检查路径推进,到 `commit-pending` 停止。目标恢复后由其原生源码 continuation 重验当前 owner、反馈和信任,再完成启用;已撤销或过期交接只能进入既有回退。目标卸载不会撤销已交出的任务;未知 Host 派发仍须精确签名对账,不能自动重派。目标离线期间无法即时获知反馈纠正,允许暴露的最长授权窗口由上述期限限制;回退完成时间仍取决于协调器可用性和签名服务。
|
|
433
|
+
|
|
434
|
+
当前安装器尚未提供这个双 Host 配置。现有 systemd attestor 只签发 reload/readiness/rollback;其余行为验收阶段仍需要真实独立观测,不能用进程就绪代替。组件接线不等于生产自动采用闭环,npm 发布仍须等待实际部署验收。
|
|
435
|
+
|
|
436
|
+
这提供自动采用的执行链。目标 Host 可另配 `foregroundDeployments: { attestorJournalPath: /srv/dsh-owner/supervisor/reload.sqlite }`,同时启用 `runtimeObserver`,并挂载 Delivery。路径须指向 systemd attestor 实际的私有 SQLite journal;配置只授予读取已签名 readiness 记录的权限,不包含签名密钥。目标 Host 与外部部署协调器需使用同一精确控制面账本和 trust 绑定;当前实现要求同 UID 的私有文件读取,不能据此声称独立 UID 隔离。
|
|
437
|
+
|
|
438
|
+
schema 20 的 `foreground_deployments` 在真实 owner 前台任务开始和完成时保存身份及运行实例。采样复用认证 observer 的同一实例,核对已应用 readiness 的签名、保留观测摘要、PID/InvocationID、profile、Fiber/依赖/服务代次,并关联精确 package/version/integrity。只接受当前最近一次成功部署、已完成 owner 源码采用、且属于同一 owner/workspace/preset 的新任务;未结算部署、重载、实例变化或非静止完成均不产生有效观察。注销/重启中断的记录保持 pending,历史任务不回填。trust 文件改变后需重载配置;观察失败不会中断正常对话。
|
|
439
|
+
|
|
440
|
+
Host 可用 `inspectOwnerForegroundDeployment()` 按与 Delivery 学习来源查询相同的参数读取归因;它重验当前可信任务,保持原学习来源摘要不变。记录证明该任务处于这一部署实例下,不证明调用过某个工具或该版本导致了结果,也不会把正常结束计作质量成功。读取结果作为质量依据仍需消费当前 canonical 反馈与撤回,并在最终写入时使用 writer fence。可选 `taskObservations` 已接通有限批次的当前反馈、签名观察与物理回退;可安装日常使用配置及端到端部署验收仍待完成,此能力不代表 npm 发布验收通过。
|
|
441
|
+
|
|
442
|
+
单个 Service 最多准备一条提案。Cordis 卸载先取消并等待所有准备步骤和容器/worktree 清理,再关闭 SQLite。数据库 schema 23 保留旧 create 摘要和 release 外键;modify 的审批摘要另外绑定 mode、检查结果及构建证据。构建证据证明配置镜像中的检查过程,不证明候选业务质量或独立隐藏评测通过;正式 release 仍需要原有审批、独立 review、构建和签名。
|
|
443
|
+
|
|
444
|
+
非 owner-task 来源的计划可用 `release-request` 导出当前 durable phase request、用 `release-step` 调用已固定 adapter 并应用 receipt,或用 `release-attest` 应用 owner-controlled 外部系统生成的同协议 receipt。phase 不能由调用者选择,而由 durable source plan 状态决定。adapter 返回签名 publish 歧义回执后进入 `publish-ambiguous`,再由独立 registry verifier 的签名 reconciliation receipt 决定继续验证、以新 fence 重试,或 fail closed。派发后没有签名回执则保持 unknown,不自动重跑;普通 owner-task 来源的全部阶段必须通过 Host 当前来源校验。
|
|
445
|
+
|
|
446
|
+
`bin/dsh-npm-registry-adapter.js` 的独立 verifier 部署提供匿名 npm `registry-verify` / `reconcile`,沿用上述命令与状态机。它在 Linux Host 的固定 adapter/Node 进程中,从已校验字节加载固定下载 helper,读取 owner 私有配置、验签公钥和独立 verifier 私钥,写入私有操作记录;该角色网络权限仅为配置的 HTTPS origin/path 下的 GET,不读取 `.npmrc` 或环境凭据。配置格式、文件权限、预算与退出清理见 [npm verifier 指南](../../docs/npm-release-verifier.md)。npm 对账使用 v2 签名回执,分别记录 owner 预期和实际 metadata/tarball 观测;404 或不完整读取保持 `unknown`,不会据此自动重发发布。旧 v1 回执继续使用原有语义。
|
|
447
|
+
|
|
448
|
+
同一 npm adapter 可部署为独立 `publish` 角色:固定自己的 executable、receipt key、私有 state/config 与 `lib/npm-publish.js` helper,通过 `DSH_RELEASE_PUBLISH_CONFIG` 指定 owner token 文件和 tag。它重验继承 FD 中的已签 tarball,构造 npm metadata/attachment,再发送一次有界 HTTPS PUT;不执行 npm CLI 或 lifecycle scripts,也不注册模型工具。发送前持久化 dispatch marker;响应丢失、进程取消或重启后,同一操作只返回原 receipt 或发布歧义,不重复上传,由独立 verifier 对账。该角色新增的凭据读取、网络写入、配置与预算见 [npm publish 指南](../../docs/npm-publish-adapter.md)。本地 TLS 验证不代表已完成真实 npm 发布或生产 Host 启用。
|
|
161
449
|
|
|
162
450
|
随包发布的 `bin/dsh-local-release-adapter.js` 是 local-only 的通用参考 adapter;trust 中每个 phase 必须安装为不同 canonical 文件/inode,并使用不同 adapter id、authority 与 receipt key;脚本副本可以共享同一个固定、只读的 Node interpreter。各副本的 owner-private config 还应给出不同 state directory。它实现:
|
|
163
451
|
|
|
@@ -168,11 +456,21 @@ owner 可以用 `release-request` 导出当前 durable phase request、用 `rele
|
|
|
168
456
|
- 在独立 download root 复制并重新验证 registry bytes;
|
|
169
457
|
- 复用 Control Plane catalog helper 执行 request-bound before/after digest CAS admission。
|
|
170
458
|
|
|
459
|
+
catalog-admission 可显式配置 `registry.protocol: "npm"`,接收独立 npm verifier 的 v1 签名回执并准入其精确 HTTPS tarball 地址。该分支仍只写 owner 本地 catalog,不联网;以继承 FD 重验已签 artifact,固定 `catalog.js` 和 `catalog-interpreter.js` 两份模块字节,在 adapter 进程内调用既有 CAS helper。catalog、verifier、signer 和 owner 使用独立权限配置;catalog receipt key 必须与其他三个角色不同。随后正式 activation 再次下载时,地址和完整性摘要都必须与已批准 catalog 一致。旧 `package@version` 逻辑引用保持兼容。配置与边界见 [npm catalog 指南](../../docs/npm-catalog-admission.md)。
|
|
460
|
+
|
|
171
461
|
adapter 的 stdout 只有一个签名 JSON receipt,stderr 不打印 request 或 secret;它还会用 config 中固定的 release-authorization 公钥重新验签。每个 phase 在 owner-private state directory 永久绑定 operationId + requestDigest:完全相同请求重放同一 receipt,同 id 不同 payload 拒绝。`registry-verify` 副本还实现 `reconcile`,同时核对 immutable tarball 和 publication record,并用自己的独立 key 签发 `exists-match` / `absent` / `unknown` / `digest-conflict` evidence。该参考实现不访问网络,也不等同于 GitHub/npm adapter;需要远端 PR/registry 的部署应提供遵循相同 request/receipt 与幂等协议的 owner adapter。
|
|
172
462
|
|
|
463
|
+
### 真实任务失败来源
|
|
464
|
+
|
|
465
|
+
Host-only `recordOwnerTaskFailureGap(source)` 将经 Delivery 再验证的 foreground `not-achieved` 结果,在 Evaluation canonical writer fence 中原子写入 gap 和私有来源引用。schema 16 从 v15 保留原有计划并增加 sidecar;引用只保存完整 owner receipt、outcome/canonical 修订和 source digest,gap 使用固定说明且 ROI 为未知占位 0。模型自报、调用方文案、未结束或截断来源均不能登记,接口不注册为 CLI/模型工具。
|
|
466
|
+
|
|
467
|
+
这些 gap 不出现在全局 `gaps()` 中。Growth 自动复盘仅看本次来源的 exact gap;同步准备必须提供当前 owner,durable source job 绑定相同 owner。每个检查边界与最终计划提交重新验证来源,最终写入同时持有 Evaluation writer fence 和同步 Store admission;普通 CLI 不能凭 gap id 绕过来源检查。`/new` 的 binding/generation 改变、纠正、撤回或依赖服务不可用会拒绝旧任务,已派发而中断的 job 保留 unknown。历史引用保留以供审计,不自动重放。
|
|
468
|
+
|
|
469
|
+
该 Host API 使用同批 `assistant-delivery` 与可选 `assistant-evaluation >=0.1.33` 的精确接口;不要求手工 gap 部署安装 Evaluation。它只授权来源绑定的候选准备,pending plan 的后续授权采用、发布与观测仍走独立控制链。
|
|
470
|
+
|
|
173
471
|
## 权限
|
|
174
472
|
|
|
175
|
-
- 插件 Host service
|
|
473
|
+
- 插件 Host service:启用 `sourceApprovals` 时执行固定的 owner helper(可读其配置、私钥和私有审批账本),Host 只消费签名回执;读取 catalog/trust,写 owner-private SQLite/WAL;启用源码读取或准备时执行受限 Git,启用 `sourceBuild`/`sourceJobs` 时创建隔离 worktree 并调用 owner 配置的离线容器构建。模型不能选择可执行文件、网络或凭据;Host 不使用浏览器。
|
|
176
474
|
- owner CLI `activate`:读取/复制/rename/恢复 DSH profile,并执行固定 DSH executable。
|
|
177
475
|
- owner CLI `probe`:执行固定 Host attestor,只有严格 allowlist 环境;不读取 attestation 私钥,不使用 shell或网络客户端。
|
|
178
476
|
- owner CLI `scaffold`:仅在审批绑定的 linked worktree 中运行固定边界内的 `git` / `pnpm`。
|
|
@@ -184,3 +482,53 @@ adapter 的 stdout 只有一个签名 JSON receipt,stderr 不打印 request
|
|
|
184
482
|
- local artifact activation 在安装期间持续持有已验证 cache inode,并把 `/proc/<control-plane-pid>/fd/<n>` reference 交给 DSH;registered DSH executor 及其解释器也从已验证 descriptor 启动。该路径是 Linux-only,且不会把 `release-complete` 视为 activation。
|
|
185
483
|
|
|
186
484
|
兼容性见仓库 [compatibility baseline](../../docs/compatibility.md)。Node.js 要求 `^22.19.0 || >=24.0.0`(使用 `node:sqlite`)。
|
|
485
|
+
|
|
486
|
+
物理 Host 回退使用随包 systemd attestor v5 的 schema-3 配置与 schema-2 请求,复用 `probe --prepare-only` / `probe`。回退前固定原始三份 core 文件摘要;文件恢复后固定 fence,重试同一个持久化操作只重新观察,不再次 restart/stop。原 profile 不存在时只允许 stop。详情与环境约束见[操作文档](../../docs/systemd-host-attestor.md#physical-rollback)。
|
|
487
|
+
|
|
488
|
+
## 原生阻断回放组件
|
|
489
|
+
|
|
490
|
+
Host 可显式使用 `EffectBlockedReplayRuntime`,复用当前 Loader、ToolRuntime 和 Delivery,消费专用原生 AgentHandle。固定有限 case 经原生单向 guard 阻止工具 body,或经真实 Delivery reply 准入阻止 Outbox 写入;逐次记录调用摘要,持续核对候选和服务 Fiber 代次,回收 Agent 后才返回。它不调用模型,不自动启用,也不增加模型工具。
|
|
491
|
+
|
|
492
|
+
可选 `replayEndpoint` 将 owner 固定请求和用例接入独立 HMAC Unix socket;客户端只可执行或查询该操作。SQLite 在创建原生 Agent 前持久准入,未知结果不重跑,缓存结果重验当前 Loader/Fiber;实例重建后返回 stale。配置、状态目录、socket 与 32 字节密钥都由 owner 管理,观测通道与执行通道使用不同密钥。
|
|
493
|
+
|
|
494
|
+
`replayEndpoint.authority.mode: 'signed'` 支持晚到授权:启动时固定 Ed25519 公钥、installation/ledger/plan/activation/profile 作用域和 cases;readiness 落账后由外部 owner 签署完整 schema-2 请求、端点/用例摘要、PID/InvocationID 与最多 60 秒时窗,再随 execute/query 提交。无须修改部署配置或重启 Host。journal schema 2 在创建 Agent 前原子绑定 scope、operation 和 grant;同一 scope 换 operation 或重新签名均不能绕过 unknown。旧 journal 自动保留 fixed 行,不能转成 signed 准入。私钥不进入 Host;签发方负责核对外部账本和 readiness,端点签名校验不代替这一核对。配置与签名示例见[阻断回放](../../docs/effect-blocked-replay.md)。
|
|
495
|
+
|
|
496
|
+
回放依赖与 Host AgentLoop 同一模块实例的 `@deepseek-ai/dsh-scope` peer;使用原生 scope 验证 Host/Agent 身份,不能混用工作区和实际 Host 的副本。`agent.preset` 仅写入会话元数据,工具需由 Host 钩子注册。真实 DSH CLI `0.1.5-rc.2` 已验证完成态重启 stale 和 SIGKILL 后 unknown、不重复派发;硬退出留下的 socket 由 supervisor 确认旧进程已停止后清理,journal 保留。
|
|
497
|
+
|
|
498
|
+
这是未签名的观察组件,不能单独推进启用状态。外部签名器、未知操作的外部对账和独立副作用读回仍需后续接线。权限包括 owner 配置路径/密钥读取、私有 journal 写入、Unix socket、当前 Loader 状态、原生 Agent 创建/回收、工具管线与 Delivery 方法;原生钩子仍有 Host 权限,组件不提供 OS/网络隔离。完整生命周期、边界及示例见[组件契约](../../docs/effect-blocked-replay.md)。
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
### 真实使用中的部署观察
|
|
502
|
+
|
|
503
|
+
目标 Host 可启用下面的可选项,依赖同一 profile 的 `foregroundDeployments`、Delivery、Evaluation 和 Automations。调度由原生 Automations 的每分钟 cron 执行;配置期间扫描至多最近 1,000 条有效部署任务,不调用模型,不产生新的 AgentLoop。该窗口之外的任务不参与观察。`budgetId/budgetAmount` 必填,对应有限周期的 Policy `automation-runs` 预算,建议 `subject` scope,以稳定观察任务 id 计量,避免与模型复盘共用 scope/metric/周期;仅换 budget id 不会分池。空队列也消耗一次调度额度。此预算与签名器的 `maximumObservations` 是两种独立限制。预算耗尽会暂停观察签发及其自动回退接续,原恢复义务仍保留;升级旧配置须补齐预算。
|
|
504
|
+
|
|
505
|
+
```yaml
|
|
506
|
+
taskObservations:
|
|
507
|
+
budgetId: plugin-task-observation-runs
|
|
508
|
+
budgetAmount: 1
|
|
509
|
+
policy:
|
|
510
|
+
id: owner-plugin-watch-1
|
|
511
|
+
expiresAt: 1790000000000 # 替换为有限授权截止时间(毫秒)
|
|
512
|
+
maximumObservations: 20
|
|
513
|
+
minimumChecks: 2
|
|
514
|
+
maximumChecks: 8
|
|
515
|
+
lookbackMs: 86400000
|
|
516
|
+
scope:
|
|
517
|
+
ownerRouteId: owner-route
|
|
518
|
+
principalId: owner
|
|
519
|
+
workspace: /srv/dsh-owner/workspace
|
|
520
|
+
preset: primary
|
|
521
|
+
profilePath: /srv/dsh-owner/profiles/web
|
|
522
|
+
timeoutMs: 60000
|
|
523
|
+
authority:
|
|
524
|
+
executable: { path: /srv/dsh-owner/bin/dsh-task-observation-authority.js, sha256: '<固定文件摘要>' }
|
|
525
|
+
interpreter: { path: /usr/bin/node, sha256: '<固定解释器摘要>' }
|
|
526
|
+
configPath: /srv/dsh-owner/private/task-observation.json
|
|
527
|
+
timeoutMs: 10000
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
`dsh-task-observation-authority --config <private-file>` 从 stdin 接收 `{ protocol: "dsh-task-observation/v1", observationId, observationDigest }`,只读控制面账本中的批次。独立私有配置字段为 `schemaVersion: 1`、`authority`、`keyId`、`keyPath`、`statePath`、`controlDatabasePath` 与 `grant`;grant 包含与 Host 完全相同的 `policy`、稳定 `owner`(authorityId/authorityHash/principalId/principalRecordId/principalVersion/workspace/agentPreset)、`installationId`、`ledger: { id, path }`、`profilePath`、允许的 `packages` 数组和 `receiptTtlMs`(1,000–300,000)。Ed25519 公钥进入 trust 的 `hostAttestationKeys`。配置、密钥、数据库路径须为 owner 私有本地文件;固定执行器通过受限环境和描述符执行,无网络或模型凭据。其签名授权不授予候选插件修改控制面或验收器的权限。
|
|
531
|
+
|
|
532
|
+
每个批次包含同一实际部署下、不同真实任务的当前 `achieved/not-achieved` owner 反馈或独立 verifier 结果;unknown、撤回、截断或非静止任务不计票。批次内任一失败触发 `regressed`,否则为 `healthy`。这是 owner 明确选择的部署任务组策略,不能证明某个工具被调用或该版本造成结果。相同任务的相同 canonical revision 只落账一次;后续纠正可形成新观察。批次身份、grant/key 指纹、签名次数和回执持久化,重启重试复用原回执,不延长期限。政策最多 1,000 次观察、每批 1–32 个任务、回看 1 秒至 30 天。
|
|
533
|
+
|
|
534
|
+
Host 在冻结批次、签名前后与最终落账时重读来源;最终 Evaluation writer fence 包裹控制面 SQLite 事务,同步写入 watch 和 applied 状态。签名器验证有限策略及已记录部署事实,可信任务质量来自 Host 当前 canonical 读取;它不是另一次独立模型评分,也不构成同 UID 文件隔离。签名期间纠正、撤回或部署变化阻止旧观察落账。已落账的回滚决定保留审计,后续反馈变更不取消恢复义务;cron 复用现有物理恢复/Host attestor 回执链,失败保留待恢复状态。进程重启可继续,主动卸载会暂停对应 cron、取消并等待在途执行,再关闭连接。owner/trust 被撤销、目标 Host 自身重启或资源无法释放时仍需外部控制面/CLI 完成恢复,不能承诺目标 Host 单独完成自重启恢复。
|
|
@@ -375,10 +375,36 @@ function registryConfig(value) {
|
|
|
375
375
|
function registryPublicationPath(registry, packageName, packageVersion) {
|
|
376
376
|
return join(registry.root, 'packages', encodeURIComponent(packageName), packageVersion, 'publication.json')
|
|
377
377
|
}
|
|
378
|
-
function
|
|
379
|
-
const
|
|
378
|
+
function catalogHttps(value, label) {
|
|
379
|
+
const raw = text(value, label)
|
|
380
|
+
let url
|
|
381
|
+
try { url = new URL(raw) } catch { fail(`${label} is invalid`) }
|
|
382
|
+
if (url.protocol !== 'https:' || url.href !== raw || url.username || url.password || /[\\?#\s]/u.test(raw)
|
|
383
|
+
|| /%(?:2e|2f|5c|25)|%(?![a-f0-9]{2})/iu.test(raw) || url.pathname.includes('//')) fail(`${label} must be canonical bare HTTPS`)
|
|
384
|
+
return url
|
|
385
|
+
}
|
|
386
|
+
function catalogRegistryConfig(config) {
|
|
387
|
+
if (config.registry?.protocol !== 'npm') return registryConfig(config.registry)
|
|
388
|
+
if (config.phase !== 'catalog-admission') fail('npm registry is only valid for catalog admission in the local adapter')
|
|
389
|
+
const item = object(config.registry, 'npm catalog registry')
|
|
390
|
+
exactKeys(item, ['protocol', 'id', 'locator', 'signer'], 'npm catalog registry')
|
|
391
|
+
const id = text(item.id, 'registry id', ID); const locator = catalogHttps(item.locator, 'registry locator').href
|
|
392
|
+
const signer = loadPublicIdentity(item.signer, 'artifact signer'); const ownKey = createPublicKey(config.privateKey)
|
|
393
|
+
for (const identity of [signer, config.authorizationAuthority, config.registryVerifier]) {
|
|
394
|
+
if (identity === undefined || identity.publicKey.equals(ownKey)
|
|
395
|
+
|| (identity.authority === config.authority && identity.keyId === config.keyId)) fail('npm catalog authority must be independent')
|
|
396
|
+
}
|
|
397
|
+
return { protocol: 'npm', id, locator, signer }
|
|
398
|
+
}
|
|
399
|
+
function catalogConfig(value, npm = false) {
|
|
400
|
+
const item = object(value, 'catalog config'); exactKeys(item, ['id', 'path', 'helper', npm ? 'interpreterModule' : 'interpreter'], 'catalog config')
|
|
380
401
|
const id = text(item.id, 'catalog id', ID); const path = canonicalPath(item.path, 'catalog path')
|
|
381
402
|
const helper = object(item.helper, 'catalog admission helper'); exactKeys(helper, ['path', 'sha256'], 'catalog admission helper')
|
|
403
|
+
if (npm) {
|
|
404
|
+
const interpreterModule = object(item.interpreterModule, 'catalog interpreter module')
|
|
405
|
+
exactKeys(interpreterModule, ['path', 'sha256'], 'catalog interpreter module')
|
|
406
|
+
return { id, path, helper, interpreterModule }
|
|
407
|
+
}
|
|
382
408
|
const interpreter = object(item.interpreter, 'catalog helper interpreter'); exactKeys(interpreter, ['path', 'sha256'], 'catalog helper interpreter')
|
|
383
409
|
inspectExecutable(interpreter, 'catalog helper interpreter')
|
|
384
410
|
return { id, path, helper, interpreter }
|
|
@@ -679,7 +705,8 @@ function validateCatalogRegistryVerificationReceipt(request, config) {
|
|
|
679
705
|
|| !ID.test(verificationRegistry.id) || typeof verificationRegistry.locator !== 'string'
|
|
680
706
|
|| !ID.test(verificationCatalog.id) || typeof verificationCatalog.path !== 'string' || !isAbsolute(verificationCatalog.path)
|
|
681
707
|
|| !ID.test(verificationAdapter.id) || !ID.test(verificationAdapter.authority) || !ID.test(verificationAdapter.keyId)
|
|
682
|
-
|| verificationAdapter.version !==
|
|
708
|
+
|| verificationAdapter.version !== (config.registry?.protocol === 'npm' ? 'dsh-npm-registry-adapter-1' : LOCAL_RELEASE_ADAPTER_VERSION)
|
|
709
|
+
|| typeof verificationAdapter.path !== 'string'
|
|
683
710
|
|| !isAbsolute(verificationAdapter.path) || !DIGEST.test(verificationAdapter.sha256)
|
|
684
711
|
|| (verificationInterpreter !== null && (!isAbsolute(verificationInterpreter.path) || !DIGEST.test(verificationInterpreter.sha256)))
|
|
685
712
|
|| receipt.schemaVersion !== 1 || receipt.phase !== 'registry-verify' || receipt.outcome !== 'passed'
|
|
@@ -744,7 +771,7 @@ function validatePhasePolicy(request, config) {
|
|
|
744
771
|
buildConfig(config.build)
|
|
745
772
|
}
|
|
746
773
|
if (request.phase === 'publish' || request.phase === 'registry-verify' || request.phase === 'catalog-admission') {
|
|
747
|
-
const registry = registryConfig(config.registry)
|
|
774
|
+
const registry = request.phase === 'catalog-admission' ? catalogRegistryConfig(config) : registryConfig(config.registry)
|
|
748
775
|
if (registry.id !== policy.registryId || registry.locator !== request.registry.locator) fail('registry adapter is not bound to the authorized registry')
|
|
749
776
|
}
|
|
750
777
|
}
|
|
@@ -1153,11 +1180,36 @@ function registryVerifyPhase(request, config, context) {
|
|
|
1153
1180
|
publishEvidenceDigest: input.publishEvidenceDigest }
|
|
1154
1181
|
}
|
|
1155
1182
|
|
|
1183
|
+
async function runNpmCatalogHelper(catalog, input) {
|
|
1184
|
+
// Both shipped modules are pinned. Importing verified bytes also avoids
|
|
1185
|
+
// resolving an unpinned relative module through a filesystem URL.
|
|
1186
|
+
const pinned = []
|
|
1187
|
+
try {
|
|
1188
|
+
const helper = openPinnedFile(catalog.helper, 'catalog helper', 1_048_576, false); pinned.push(helper)
|
|
1189
|
+
const dependency = openPinnedFile(catalog.interpreterModule, 'catalog interpreter module', 1_048_576, false); pinned.push(dependency)
|
|
1190
|
+
const helperBytes = inheritedDescriptorBytes(helper.descriptor, 'catalog helper', 1_048_576)
|
|
1191
|
+
const dependencyBytes = inheritedDescriptorBytes(dependency.descriptor, 'catalog interpreter module', 1_048_576)
|
|
1192
|
+
if (sha256Bytes(helperBytes) !== helper.sha256 || sha256Bytes(dependencyBytes) !== dependency.sha256) fail('catalog module changed before import')
|
|
1193
|
+
const source = new TextDecoder('utf-8', { fatal: true }).decode(helperBytes)
|
|
1194
|
+
const importPattern = /from\s+(['"])\.\/catalog-interpreter\.js\1/gu
|
|
1195
|
+
if ([...source.matchAll(importPattern)].length !== 1) fail('catalog helper dependency contract changed')
|
|
1196
|
+
const dependencyUrl = `data:text/javascript;base64,${dependencyBytes.toString('base64')}`
|
|
1197
|
+
const boundSource = source.replace(importPattern, `from '${dependencyUrl}'`)
|
|
1198
|
+
const module = await import(`data:text/javascript;base64,${Buffer.from(boundSource).toString('base64')}`)
|
|
1199
|
+
if (typeof module.admitCatalogCandidate !== 'function') fail('catalog admission helper is unavailable')
|
|
1200
|
+
return object(await module.admitCatalogCandidate(input), 'catalog helper result')
|
|
1201
|
+
} finally {
|
|
1202
|
+
try { for (const entry of pinned) verifyPinnedFile(entry, 'catalog pinned module') }
|
|
1203
|
+
finally { for (const entry of pinned) closePinnedFile(entry) }
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1156
1207
|
async function catalogAdmissionPhase(request, config, registryVerificationReceipt, hooks = {}) {
|
|
1157
|
-
const
|
|
1208
|
+
const registry = catalogRegistryConfig(config)
|
|
1209
|
+
const input = object(request.input, 'catalog admission input'); const catalog = catalogConfig(config.catalog, registry.protocol === 'npm')
|
|
1158
1210
|
const { id, path } = catalog
|
|
1159
1211
|
if (request.catalog.id !== id || request.catalog.path !== path) fail('catalog request targets a different owner catalog')
|
|
1160
|
-
const
|
|
1212
|
+
const signed = verifySignedArtifact(input, registry, true)
|
|
1161
1213
|
const expectedBeforeDigest = text(input.expectedBeforeCatalogDigest, 'expected catalog digest', DIGEST)
|
|
1162
1214
|
const expectedAfterDigest = text(input.expectedAfterCatalogDigest, 'expected after catalog digest', DIGEST)
|
|
1163
1215
|
if (registryVerificationReceipt === undefined) fail('catalog admission has no verified registry receipt')
|
|
@@ -1170,12 +1222,21 @@ async function catalogAdmissionPhase(request, config, registryVerificationReceip
|
|
|
1170
1222
|
if (digest(candidate) !== digest(expectedCandidate)) fail('catalog candidate does not match the signed artifact')
|
|
1171
1223
|
const reference = text(input.registryReference, 'registry reference')
|
|
1172
1224
|
if (request.authorization.releasePolicy.registryReference !== reference) fail('catalog registry reference is not owner-authorized')
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1225
|
+
if (registry.protocol === 'npm') {
|
|
1226
|
+
const base = catalogHttps(registry.locator, 'registry locator'); const target = catalogHttps(reference, 'registry reference')
|
|
1227
|
+
if (base.origin !== target.origin || !target.pathname.startsWith(`${base.pathname.replace(/\/+$/u, '')}/`)) fail('npm catalog reference escapes its registry')
|
|
1228
|
+
const policy = request.authorization.releasePolicy
|
|
1229
|
+
for (const key of ['candidateId', 'packageName', 'packageVersion', 'packagePath', 'dshBaseline', 'capabilities', 'authorities', 'requires']) {
|
|
1230
|
+
if (digest(input.artifact[key]) !== digest(policy[key])) fail('npm catalog artifact differs from owner release policy')
|
|
1231
|
+
}
|
|
1232
|
+
} else {
|
|
1233
|
+
let registryPath
|
|
1234
|
+
try { registryPath = fileURLToPath(reference) } catch { fail('catalog registry reference is not a local file URL') }
|
|
1235
|
+
const registryBytes = readBounded(registryPath, 'catalog registry object')
|
|
1236
|
+
if (!realpathSync(registryPath).startsWith(`${join(registry.root, 'packages')}${sep}`)
|
|
1237
|
+
|| sha256Bytes(registryBytes) !== input.artifact.tarballSha256 || sha512Integrity(registryBytes) !== input.artifact.tarballIntegrity) {
|
|
1238
|
+
fail('catalog registry reference does not contain the signed artifact')
|
|
1239
|
+
}
|
|
1179
1240
|
}
|
|
1180
1241
|
assertUnexpired(request, 'catalog admission')
|
|
1181
1242
|
const helperInput = { catalog: { id, path }, registry: { id: registry.id, locator: registry.locator },
|
|
@@ -1187,10 +1248,13 @@ const helper=await import('file:///proc/self/fd/3');const input=JSON.parse(readF
|
|
|
1187
1248
|
if(typeof helper.admitCatalogCandidate!=='function')throw new Error('catalog admission helper is unavailable');
|
|
1188
1249
|
process.stdout.write(JSON.stringify(await helper.admitCatalogCandidate(input))+'\\n');`
|
|
1189
1250
|
let result
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1251
|
+
if (registry.protocol === 'npm') result = await runNpmCatalogHelper(catalog, helperInput)
|
|
1252
|
+
else {
|
|
1253
|
+
const stdout = runPinnedNodeModule(catalog.interpreter, catalog.helper, helperSource, dirname(path), Buffer.from(JSON.stringify(helperInput)), {
|
|
1254
|
+
beforeSpawn: hooks.beforeCatalogHelperSpawn, afterSpawn: hooks.afterCatalogHelperSpawn, afterFinally: hooks.afterCatalogHelperFinally,
|
|
1255
|
+
})
|
|
1256
|
+
result = object(JSON.parse(stdout.toString('utf8')), 'catalog helper result')
|
|
1257
|
+
}
|
|
1194
1258
|
if (result.evidence.afterCatalogDigest !== expectedAfterDigest) fail('catalog admission produced an unauthorized after digest')
|
|
1195
1259
|
return { ...result.evidence, registryReference: reference, artifactStatementDigest: signed.statementDigest,
|
|
1196
1260
|
artifactSignatureDigest: signed.signatureDigest, verificationEvidenceDigest, candidate }
|