feat(runtime-host): apply verified managed updates - #3687
Conversation
Download the exact npm package selected by update discovery, verify its registry integrity and packaged compatibility evidence, then hand it to the existing managed Host update transaction. Fence stale candidates so concurrent updates cannot overwrite a newer deployment. Generated-by: Codex
Bind registry artifacts to their verified integrity across deployment and route current selections through the existing readiness and repair transaction. Scope temporary acquisition to the update operation and prune inactive managed packages only after a healthy cutover. Generated-by: Codex
0024c4e to
048e9b4
Compare
Fence selected updates by the current and target deployment paths so verified same-version packages cannot be skipped or admitted from stale compatibility evidence. Avoid deployment-lock reentry during forced recovery, and atomically detach package directories before cleanup so exact retries remain recoverable. Generated-by: Codex
Treat exact package identity, not semver alone, as current so same-version registry artifacts still pass compatibility admission. Keep repeated setup on the active package while idempotently restoring its operator launcher, preserving exact recovery after interrupted setup or cutover. Generated-by: Codex
910181c to
bfd82dd
Compare
|
I reviewed #3687 as the reference transaction for #3228 and would like to confirm one boundary in the recovery machine contract. The registry target is identified by Should the post-cutover recovery hint also carry source-appropriate exact evidence—at least This does not require a universal 简体中文我在把 #3687 作为 #3228 的 reference transaction 核对时,有一个 recovery machine contract 的边界想确认。 当前 registry target 实际由 是否应该让 post-cutover recovery hint 也携带 source-appropriate exact evidence(registry 场景至少是 这不需要引入 universal |
|
@me2seeks Thank you — the underlying observation is correct: a version-only value cannot identify the exact registry package selected by the transaction. I considered extending the hint with integrity, but the cleaner boundary is to remove the retry hint entirely. Nothing consumes Commit f576d4f therefore removes 简体中文谢谢,这个观察的核心成立:只有版本号无法标识事务选中的精确 registry package。 我考虑过给提示补充 integrity,但更干净的边界是彻底删除这项重试提示。当前没有任何使用方消费 因此 commit f576d4f 从机器契约中删除了 |
Astro-Han
left a comment
There was a problem hiding this comment.
The acquisition path is carefully built. npm pack and npm install both run with --ignore-scripts, the install happens offline from the already-verified archive against an unreachable registry and an empty cache so nothing can be refetched, the temporary root is created with mode 0o700, and the candidate is rejected before any download unless its version, integrity and compatibility all parse. Those are the parts that usually go wrong, and they are right here.
Three things to fix.
[P2] Repairing an existing deployment drops the integrity evidence that acquiring it established
When a target derived from a verified archive already exists, the healthy and repair paths open it through openRuntimeHostManagedPackageDeployment(), and validatePackage() in packages/cli/src/runtime-host-managed-deployment.ts:286 is the whole check: it parses package.json, compares name and version, and calls stat on dist/cli.js and on node_modules/@maka/runtime-host/package.json to confirm they are files. Nothing compares content against the SHA-512 that admitted the package.
So a dist/cli.js modified after installation is accepted as current and executed. That is the one place where the integrity check earned something durable and the code declines to use it. The verified archive or a recorded tree hash should stay the authority for an existing deployment, not the presence of two paths and a matching version string.
[P2] The size budget covers only the compressed archive
ARCHIVE_MAX_BYTES bounds the .tgz at 256 MiB, and MANIFEST_MAX_BYTES bounds the metadata. There is no bound on the unpacked total, the file count, any single file, directory depth, or the number of links — and npm install extracts before anything walks the entries, so the first code that could object runs after the disk is already written. A payload whose logical size is orders of magnitude beyond the compressed cap reaches the acquisition callback.
Please bound the unpacked side explicitly and enforce it during extraction rather than after.
[P2] A failed backend replacement still reports the target version as installed
The config target is committed first. If the backend replacement then fails, ordinary status reads the config and reports the target version and path as installed without checking the backend deployment, while the backend is still running the old CLI and deployment verification returns target_mismatch. The user is told they are on the new version when they are not, which is worse than a plain failure because it removes the reason to retry.
Please have status report the backend deployment, or make the config commit conditional on the replacement succeeding.
One note on what the integrity check proves
candidate.integrity comes from dist.integrity in the registry metadata (runtime-host-update-discovery.ts:266), and the tarball comes from the same registry. Verifying one against the other is a real defence against a corrupted or truncated download and against a CDN serving something other than what the metadata describes. It is not evidence of publisher identity: metadata and artifact share a trust root, so a registry compromise satisfies the check. That is the ordinary npm trust model and I am not asking you to change it, but the wording around "trusted" updates reads stronger than what is actually established, and it is worth being precise in the description so nobody later builds on a guarantee that is not there.
简体中文
获取路径本身构建得很细致:npm pack 与 npm install 都带 --ignore-scripts;安装是从已校验过的归档离线进行,指向一个不可达的 registry 并使用空缓存,因此不可能二次取网;临时根目录以 0o700 创建;候选在任何下载之前就会因版本、完整性、兼容性任一解析失败而被拒。这些通常最容易出问题的地方,这里是对的。
三处需要修。
[P2] 修复既有部署时,丢掉了获取阶段建立起来的完整性证据
当一个由已校验归档派生出的目标已经存在时,healthy 与 repair 路径通过 openRuntimeHostManagedPackageDeployment() 打开它,而 packages/cli/src/runtime-host-managed-deployment.ts:286 的 validatePackage() 就是全部检查:解析 package.json、比对 name 与 version、再对 dist/cli.js 和 node_modules/@maka/runtime-host/package.json 做 stat 确认它们是文件。没有任何一处把内容与当初准入该包的那个 SHA-512 做比对。
于是安装之后被改动过的 dist/cli.js 会被当作当前版本接受并执行。这恰恰是完整性校验唯一换来了持久价值的地方,而代码在此选择不使用它。对既有部署而言,权威应当仍是那份已校验的归档或记录下来的树哈希,而不是"两个路径存在 + 版本号相符"。
[P2] 体积预算只覆盖了压缩后的归档
ARCHIVE_MAX_BYTES 把 .tgz 限制在 256 MiB,MANIFEST_MAX_BYTES 限制元数据。但解压后的总量、文件数、单个文件大小、目录深度、link 数量都没有任何上限——而且 npm install 会先解压,之后才轮到遍历条目的代码,也就是说第一处可能提出异议的代码运行时,磁盘已经写完了。一个逻辑体积远超压缩上限若干数量级的载荷可以一路到达获取回调。
请显式限制解压侧,并且在解压过程中执行,而不是解压之后。
[P2] backend 替换失败后,仍然把目标版本报告成已安装
config 目标是先提交的。若随后 backend 替换失败,普通的 status 会读取 config,把目标版本与路径报成已安装,而不去核对 backend 部署;此时 backend 仍在运行旧 CLI,部署校验返回 target_mismatch。用户被告知自己已在新版本上,而事实并非如此——这比直接失败更糟,因为它消解了用户重试的理由。
请让 status 报告 backend 部署的实际状态,或者让 config 的提交以替换成功为前提。
关于完整性校验究竟证明了什么
candidate.integrity 取自 registry 元数据中的 dist.integrity(runtime-host-update-discovery.ts:266),而 tarball 来自同一个 registry。用其中之一校验另一个,确实能有效防住下载损坏或截断,也能防住 CDN 返回与元数据描述不符的内容。但它不是发布者身份的证据:元数据与产物共享同一个信任根,registry 一旦被攻陷,这项校验照样通过。这就是 npm 通常的信任模型,我不要求你改变它;只是"trusted updates"这个说法读起来比实际建立的保证更强,建议在正文中把话说准,以免以后有人在一个并不存在的保证之上继续搭建。
|
The branch moved to
The new commit changes the Nothing here asks you to re-explain the change. I mention it only so the moved head is not mistaken for a response to the findings. 简体中文分支在我 review 之后动到了
新增的这条 commit 把 这里不需要你再解释一遍这次改动,我写出来只是为了避免把"head 变了"误读成对上述发现的回应。 |
Astro-Han
left a comment
There was a problem hiding this comment.
Two more blocking findings on f576d4fca045e0282138f0a464f9a9a70f40a058, both about the update transaction rather than the acquisition path.
[P2] The package is downloaded before the transaction is allowed to decide it does not need one
In packages/cli/src/runtime-host-update-command.ts:505, only a manual_action outcome short-circuits. Everything else — including outcome.kind === 'current' — goes through deps.withPackage(...) at :521, which packs or downloads before deps.update is ever called. But the transaction's own first decision, already_current at :239, is reached without reading sourcePackageRoot at all.
The consequence is deterministic, not a race: the installed deployment is already current and healthy, discovery succeeds, then npm pack or the download fails, or the registry is briefly unreachable. The user is told the update failed to acquire a package, when the correct answer was that they were already on the target and nothing needed to be fetched. The same eager acquisition is what stops the repair path from opening an already-installed current deployment while the registry is unavailable — which is exactly when repair matters most.
Please defer acquisition until the transaction has established that it must prepare a different deployment. The current-healthy and repair paths should be able to run off the installed deployment alone.
[P2] The forced-stop repair is unreachable in the case it exists for
When verifyReady fails, :222 sets activeTargetNeedsRepair. A few lines later, :246 unconditionally awaits runOperator(currentOperatorPath, ['status', '--framed', ...]) to decide the lifetime-lock capability. If that operator is missing, corrupt, fails to spawn, or times out, the rejection propagates straight to the outer handler.
The only --allow-interrupt-active-tasks forced-stop recovery is later, at :307, and it triggers on exactly one condition: a returned frame with error.code === 'retirement_failed'. An operator that cannot be executed never produces that frame.
So update --allow-interrupt-active-tasks cannot repair a Host whose operator is itself broken — which is a large share of the stopped/unhealthy states this recovery is meant to cover, and arguably the most likely reason a deployment is unhealthy in the first place. Please catch the probe failure in the repair case and fall through to the conservative forced-stop path instead of aborting ahead of it.
One process note (not blocking)
Commit a242e40fd8 changes seven code and test files (+239/−157) with no Generated-by: trailer, while the neighbouring commits including f576d4fc carry Generated-by: Codex. CONTRIBUTING asks for the trailer on each affected commit and for it to survive into the final commit. Since this repository squash-merges and the other commits carry it, the squashed result will most likely be fine — I mention it only so the trailer is checked on the final message rather than assumed.
Together with the three findings above, that is five open P2s. I am not approving while they are open, and the exact-head test run is still in progress.
简体中文
在 f576d4fca045e0282138f0a464f9a9a70f40a058 上再提两条阻塞项,都关于更新事务本身,而非获取路径。
[P2] 在事务被允许判断「不需要包」之前,包已经被下载了
packages/cli/src/runtime-host-update-command.ts:505 只对 manual_action 做了短路,其余分支——包括 outcome.kind === 'current'——都会走 :521 的 deps.withPackage(...),也就是在 deps.update 被调用之前就已经 pack 或下载完毕。但事务自己的第一个判断 :239 的 already_current,根本没有读取 sourcePackageRoot。
后果是确定性的,不是竞态:已安装的部署本就是当前版本且健康,discovery 成功,随后 npm pack 或下载失败、或 registry 短暂不可达。用户被告知「更新未能获取包」,而正确的答复本应是「你已经在目标版本上,什么都不需要取」。同样是这处提前获取,导致 registry 不可用时 repair 路径无法打开一个已经装好的当前部署——而那恰恰是 repair 最有价值的时刻。
请把获取推迟到事务确认「必须准备一个不同的部署」之后。current-healthy 与 repair 路径应当仅凭已安装的部署就能跑完。
[P2] 强制停止的修复路径,在它本该生效的场景下不可达
verifyReady 失败时,:222 置上 activeTargetNeedsRepair。紧接着 :246 无条件 await runOperator(currentOperatorPath, ['status', '--framed', ...]) 来判定 lifetime-lock 能力。如果这个 operator 缺失、损坏、拉起失败或超时,异常会直接抛到外层处理器。
而唯一的 --allow-interrupt-active-tasks 强制停止恢复在更后面的 :307,触发条件只有一个:返回一个 error.code === 'retirement_failed' 的帧。一个根本无法执行的 operator,永远产生不了这个帧。
于是 update --allow-interrupt-active-tasks 无法修复「operator 自身已坏」的 Host——而这在该恢复路径本应覆盖的 stopped/unhealthy 状态中占很大比例,甚至可以说是部署变得不健康的最常见原因。请在 repair 情形下捕获这次探测失败,落到保守的强制停止路径,而不是在它之前就中止。
一条流程性说明(不阻塞)
commit a242e40fd8 改动七个代码与测试文件(+239/−157),没有 Generated-by: trailer;而相邻的 commit(包括 f576d4fc)都带 Generated-by: Codex。CONTRIBUTING 要求每个相关 commit 都带该 trailer,并保证它保留到最终 commit。鉴于本仓库是 squash 合并、且其他 commit 带了该 trailer,最终结果大概率没问题——我写出来只是希望最终 commit message 上真的核一眼,而不是默认没事。
连同前面三条,目前共有五条未决 P2。这些未决期间我不会 approve;exact head 的 test 也仍在运行中。
EnglishThank you for the detailed review. I reproduced the replacement-state problem and fixed it in d08da5a. The service backends now distinguish two boundaries:
I did not add persistent tree attestation. The registry SHA-512 identifies the npm archive, not the extracted filesystem tree, and the managed package, any locally stored tree hash, the operator, and the updater all share the same per-user write boundary. A same-principal attacker could modify the verifier as readily as the package, while atomic staging and rename already prevent an incomplete copy from becoming current. I instead tightened the wording from different-content to different registry-integrity candidates. I also did not add a second tar parser or extraction engine for an unpacked quota. This command accepts only the official maka-agent package from the npm registry; metadata and artifact intentionally share that trust root, and a compromised publisher could already deliver arbitrary same-user code. Accidental release growth is bounded by the existing release gate at 18 MiB compressed, 90 MiB unpacked, and 9,000 entries. A portable consumer-side streaming quota would duplicate npm extraction without closing a distinct current boundary. We should revisit that if the updater later accepts arbitrary registries or packages. Finally, I renamed the PR from trusted to verified managed updates so the claim matches the actual archive-integrity and manifest checks rather than implying publisher-identity verification. 简体中文感谢这次细致审查。我复现了 replacement 状态问题,并在 d08da5a 中完成修复。Service backend 现在明确区分两个边界:
我没有增加持久 tree attestation。Registry SHA-512 标识的是 npm archive,不是解压后的 filesystem tree;managed package、本地 tree hash、operator 与 updater 都处于同一用户可写边界。同一 principal 的攻击者既能修改 package,也能修改 verifier;而现有原子 staging 与 rename 已经防止不完整复制成为 current。我改为收紧表述,只承诺区分 registry integrity 不同的候选。 我也没有为了 unpacked quota 引入第二套 tar parser 或 extraction engine。当前命令只接受 npm registry 上官方的 maka-agent package;metadata 与 artifact 本来就共享该信任根,publisher 被攻陷时已经可以投递任意同用户权限代码。意外的 release 体积增长则由现有发布门禁限制为 18 MiB compressed、90 MiB unpacked、9,000 entries。跨平台 consumer-side streaming quota 会重复 npm extraction,却不能闭合一个独立的当前边界。如果未来 updater 接受任意 registry 或 package,应重新评估该约束。 最后,我已将 PR 标题从 trusted managed updates 改为 verified managed updates,使表述准确对应 archive integrity 与 manifest 校验,不暗示 publisher identity verification。 |
Astro-Han
left a comment
There was a problem hiding this comment.
Re-reviewed on d08da5ab4fc24dd987f3b5b61909fe5429674bd5.
The status-reports-installed finding is closed. replaceRuntimeHostManagedServiceLocked now separates a replacement that never committed from a target that committed but never became ready. On any non-update_incomplete failure from backend.replace, the previous config JSON is written back before the error is raised, so status no longer reads a target version the backend is not running; when the config restore itself fails, the error is raised as an AggregateError naming both failures rather than a single misleading one. The distinguishing signal is the in-memory exception type rather than anything read back from disk, so there is no loop of judging contamination by a source the contamination may have rewritten. The tests pin the actual file contents in both outcomes, not just the thrown code.
I also checked the two platform backends against each other: capture → apply → restore is symmetric between systemd and LaunchAgent, and both terminate in the stopped state that was captured. Recovery after the readiness failure is real — the target config, package and operator are all retained, so service start or another update can proceed; the deliberate refusal to auto-restart the old service across an unknown storage boundary is a restriction, not a dead end.
The other four findings are unchanged on this head. validatePackage() still compares only name, version and the presence of two paths, with no content comparison against the admitting SHA-512. runtime-host-update-package.ts still bounds only the compressed archive and the manifest. runtime-host-update-command.ts still routes the current outcome through withPackage, and still awaits the operator status probe unconditionally after flagging repair, leaving the forced-stop recovery reachable only via a returned retirement_failed frame.
[P2] The readiness-failure message asserts a stopped service that may still be running
At packages/cli/src/runtime-host-service-manager.ts:579, a waitForReady failure is handled as:
await backend.stop().catch(() => undefined);
throw new RuntimeHostServiceManagerError(
'update_incomplete',
'The replacement Runtime Host did not become ready; the selected deployment was retained but stopped ...',
);The stop() failure is swallowed, and the message that reaches the user states as fact that the deployment is stopped. If the stop did not take, the operator is told a service is down while it is still running the binary that just failed its own readiness check — and the message removes their reason to check. This is the same shape as the finding closed above: reporting a state that was attempted rather than a state that was confirmed.
Please fold a stop failure into the raised error, the way the config-restore failure already is, so the message distinguishes "stopped" from "could not be stopped".
For clarity on where this does not apply: the neighbouring :574 path makes the same claim but issues no stop() at all — its claim rests on the backend having restored the captured stopped state, which the platform backends do.
Four blocking findings remain open — the two above from the earlier review plus the two from the follow-up, and this one. The exact-head test run is still queued, so there is no green gate either. Not approving.
简体中文
已在 d08da5ab4fc24dd987f3b5b61909fe5429674bd5 上重审。
「status 把目标版本报成已安装」这条已闭合。 replaceRuntimeHostManagedServiceLocked 现在把「替换从未提交」与「提交了但从未 ready」区分开:backend.replace 抛出任何非 update_incomplete 的异常时,会先把旧的 config JSON 写回再抛错,因此 status 不会再读到一个 backend 并未运行的目标版本;若 config 恢复本身失败,则以点名两个失败的 AggregateError 抛出,而不是给出一条会误导的错误。用来区分的信号是内存中的异常类型,而不是从磁盘读回的任何东西,因此不存在"用一个可能已被污染改写的来源去判断污染"的闭环。测试锁住的是两种结局下文件的真实内容,而不只是抛出的错误码。
我也把两个平台后端互相对照过:systemd 与 LaunchAgent 的 capture → apply → restore 结构对称,且都终止在捕获时的停止态。readiness 失败后的恢复是真实可用的——target 的 config、包与 operator 全部保留,service start 或再跑一次 update 都能继续;至于"跨未知存储边界不自动拉回旧服务"是有意的限制,不是死路。
其余四条在这个 head 上未变。 validatePackage() 仍然只比对 name、version 与两个路径是否存在,没有与准入时的 SHA-512 做内容比对。runtime-host-update-package.ts 仍然只限制压缩包与清单。runtime-host-update-command.ts 仍然让 current 结果走 withPackage,也仍然在标记 repair 之后无条件 await operator 状态探测,使强制停止恢复只能经由返回的 retirement_failed 帧到达。
[P2] readiness 失败的错误文案断言了一个可能仍在运行的服务已停止
packages/cli/src/runtime-host-service-manager.ts:579 对 waitForReady 失败的处理是:
await backend.stop().catch(() => undefined);
throw new RuntimeHostServiceManagerError(
'update_incomplete',
'The replacement Runtime Host did not become ready; the selected deployment was retained but stopped ...',
);stop() 的失败被吞掉,而送到用户面前的文案把"已停止"当作事实陈述。如果这次 stop 没有生效,运维看到的是"服务已停",实际上它仍在运行那个刚刚没能通过自身 readiness 检查的二进制——而这句话恰好消解了他去核实的理由。这与上面那条刚闭合的发现是同一种形状:报告的是尝试过的状态,而不是确认过的状态。
请把 stop 的失败并入抛出的错误,就像 config 恢复失败已经做的那样,让文案能区分"已停止"与"停不下来"。
为免误伤,说明一下不适用的地方:相邻的 :574 那条路径做了同样的陈述,但它完全没有调用 stop()——它的依据是 backend 已把服务恢复到捕获时的停止态,而两个平台后端确实做到了这一点。
目前仍有四条阻塞项未决——前一轮的两条、后续一轮的两条,加上本条。exact head 的 test 也仍在排队,没有绿灯。不 approve。
|
Correcting the count in my review just above: five blocking findings are open, not four — the two from 简体中文更正上一条 review 里的数字:未决的阻塞项是五条,不是四条—— |
Route both staged and existing packages through one deployment lifecycle so operator repair, cleanup, rollback, and exact-target recovery share the same authority. Keep npm acquisition state inside the update workspace while preserving the separate offline extraction cache. Generated-by: Codex
Let each update invocation observe the managed installation and reconcile its current desired deployment instead of exposing a version-only recovery authority that cannot identify an exact package. Generated-by: Codex
Distinguish a service-manager replacement that never committed from a target that committed but did not become ready. Restore the previous backend definition and config only in the former case; retain and stop the selected deployment when readiness leaves storage compatibility unknown. Generated-by: Codex
Reuse an already-selected current deployment without package acquisition, and let explicitly authorized repair stop an unhealthy Host when its operator is unavailable. Preserve truthful failure reporting when a replacement cannot be stopped after readiness fails. Generated-by: Codex
d08da5a to
50a0b5b
Compare
EnglishThanks for the careful follow-up. I re-adjudicated the findings and updated the PR at Accepted and fixed:
I did not add the proposed persistent tree hash or a second extraction implementation. The registry SHA-512 authenticates the acquired archive within the npm trust model; it does not define a canonical post-install tree, and a digest recorded beside a same-user-writable deployment would be mutable under the same trust boundary. Making that an actual tamper boundary would require a signed tree manifest or an OS-protected deployment authority, not a local hash comparison in this transaction. Likewise, the official release gate already bounds the published archive, unpacked bytes, and entry count. Reimplementing npm extraction solely to enforce a second consumer-side budget would add a second package installer without closing a distinct trust boundary in this PR. Verified on the rewritten head: all 443 CLI tests, repository typecheck, lint, format check, and ASF header audit pass locally. 简体中文感谢细致的复审。我重新裁决了这些发现,并已在 已采纳并修复:
我没有加入持久 tree hash,也没有实现第二套解包器。Registry SHA-512 在 npm 信任模型内验证的是获取到的 archive,并没有定义 canonical post-install tree;与 deployment 一样可由同一用户写入的本地 digest,也处在同一个可变信任边界内。若要形成真正的防篡改边界,需要签名 tree manifest 或受 OS 保护的 deployment authority,而不是在当前事务旁增加本地 hash 比对。同样,官方发布门禁已经限制发布产物的压缩体积、解包体积与条目数;仅为重复一层 consumer-side budget 而重写 npm extraction,会增加第二套 package installer,却没有在本 PR 的信任边界内关闭新的风险。 重写后的 head 已在本地通过全部 443 项 CLI 测试、仓库 typecheck、lint、format check 与 ASF header audit。 |
Astro-Han
left a comment
There was a problem hiding this comment.
Update after freshness on 50a0b5bb23:
Previously published 5×P2: 2 remain, 3 are now closed.
- Still open: (1)
managed-deploymentreuses existing package without SHA-512 content check; (2)update-packageunpacks without decompressed size/count/depth/link limits. - Closed in this head: (3) unsubmitted replace now restores previous config (previously reported incomplete); (4)
currentno longer goes throughwithPackage; (5)verifyReadyfailure handling now correctly absorbs operator status when active target is already not ready.
No new blocking issues beyond the 2 above. Checks on 50a0b5bb23 are test: success.
简体中文
原 5 项中 2 项仍成立,3 项已闭合。
Astro-Han
left a comment
There was a problem hiding this comment.
Follow-up on the author's explanation: these findings do not require a local tree hash to act as an adversarial tamper boundary, nor do they require a second npm installer. The first finding remains because validatePackage() checks path/type/name/version but does not revalidate that the bytes at an existing reused/repair target still match the admitting registry integrity. A same-principal attacker being able to modify a local verifier does not remove the operational consistency invariant; to close this point, either establish and test an immutable source/path invariant for every reuse path, or validate content at that boundary. The second finding is about the consumer contract: producer-side release limits are not an executable runtime guarantee for every registry/cache input this code can consume. The current path bounds the compressed archive and manifest, then hands the package to npm install without enforcing decompressed size, entry-count, depth, or link budgets. Either prove the source is closed to artifacts with those bounds, or add a pre-install archive preflight; a second package installer is not required.
简体中文
上述解释不改变两条 P2 的根因,无需引入 tree hash 或重写 installer 即可闭合。
EnglishThank you for reframing the two remaining findings. I re-adjudicated them against the actual transaction and want to make the intended boundary explicit. Existing deployment integrityYour operational-consistency distinction is valid. My previous reply focused on adversarial tampering within the same user boundary, which did not directly answer the non-adversarial case where an installed package tree is damaged after admission. The contract implemented here is narrower:
This also means the current wording around repairing a stopped or unhealthy Host is too broad unless the boundary is stated. The repair path restores service/operator lifecycle state over an intact managed package. If the package bytes themselves have been modified outside the deployment authority, this transaction does not reconstruct them. Same-version package re-materialization could be added as a separate recovery capability, but it requires a real source-of-truth design; placing a mutable tree hash beside the deployment would not create that authority. I will tighten the PR/issue wording so it does not imply package-corruption recovery. I do not think persistent local tree attestation belongs in this transaction. Unpacked archive budgetI agree with the literal observation that a producer-side release gate is not a cryptographic consumer-side guarantee. The merge decision, however, depends on the accepted source and threat model. This command accepts only the official That trade-off changes if Maka later accepts custom registries, mirrors, uploaded packages without the release gate, or another artifact trust root. At that point, consumer-enforced unpacked size, entry, depth, and link budgets should be a prerequisite. Under the current closed official-package source, I consider it useful future hardening rather than a P2 merge blocker. So the resulting adjudication is:
If you believe #3687 must include recovery from externally modified package trees, or must treat the official registry artifact as untrusted before execution, that is a different product requirement from the one currently stated. Calling out that requirement explicitly would help us decide it directly rather than encoding it indirectly through implementation findings. 简体中文感谢你重新界定剩余两条问题。我结合实际 transaction 重新裁决了一次,下面明确说明本 PR 的设计边界。 既有 deployment 的完整性你对“运行一致性”的区分是成立的。我之前的回复主要讨论同一用户边界内的对抗性篡改,没有直接回答 package 通过准入后发生非对抗性损坏的情况。 当前实现的契约更窄:
这也意味着当前“修复 stopped 或 unhealthy Host”的措辞如果不说明边界,确实过宽。Repair 的含义是:在 managed package 仍然完整的前提下,恢复 service/operator lifecycle 状态。如果 package bytes 已在 deployment authority 之外被修改,本事务不会重新构造它。未来可以单独增加同版本 package re-materialization,但那需要真正的 source-of-truth 设计;把一个同样可写的 tree hash 放在 deployment 旁边并不能形成这样的 authority。 我会收紧 PR/issue 的表述,避免暗示它能修复 package corruption。持久化的本地 tree attestation 不应进入当前 transaction。 Archive 解包预算我同意字面事实:producer-side release gate 并不是 consumer-side 的密码学保证。但它是否阻塞合并,取决于当前接受的 source 与 threat model。 这个命令只接受固定 npm registry 上的官方 如果未来 Maka 接受自定义 registry、mirror、未经 release gate 的上传 package,或另一种 artifact trust root,这个权衡就会变化。届时 consumer 强制执行 unpacked size、entry、depth 与 link budget 应当成为前置条件。在当前封闭的官方 package source 下,我认为它是有价值的未来 hardening,而不是 P2 merge blocker。 因此最终裁决是:
如果你的要求是 #3687 必须恢复被外部修改的 package tree,或者必须在执行前把官方 registry artifact 视为不可信输入,那属于不同于当前 issue 的产品需求。希望能把这项要求直接明确出来,以便我们讨论需求本身,而不是通过实现 finding 间接引入。 |
Astro-Han
left a comment
There was a problem hiding this comment.
Follow-up: after re-reviewing with the author's invariant, I consider the two remaining findings closed as blocking for this PR.
-
Content consistency: all operations on published packages are whole-package atomic copies/renames, and the package directory is derived from SHA-512 integrity. The author's stated invariant — published package directories are immutable under Maka's authority, excluding external tampering/disk corruption — closes the reuse-hash gap as a current lifecycle concern. Track as threat-model note.
-
Archive budgets: the current producer path is closed (registry.npmjs.org → SHA-512 → offline install) and release artifacts are bounded by
release-cli-artifact-policy(18 MiB/90 MiB/9000 entries). No custom registry input exists; consumer quotas would be for future trust-root expansion, not a correctness gate now.
No remaining P0-P2 in this head.
English
Summary
maka-agentarchive from the official npm registry and verify its SHA-512 integrityPublished package directories are immutable under Maka's deployment authority. Lifecycle repair assumes that the managed package tree remains intact; local tree attestation and recovery from external package modification are outside this PR.
Fixes #3679
Verification
npm --workspace maka-agent test— 443 tests passednpm run typecheck,npm run lint,npm run format:check, andnpm run buildnpm run release:cli:pack— built the self-containedmaka-agent@0.2.0tarball from the clean headnode scripts/smoke-release-cli-package.mjs packages/cli/release/maka-agent-0.2.0.tgz— offline installation, TUI setup, managed Runtime Host lifecycle, and controlled Turn passednpm run check:asf-headersandgit diff --checkRelease gate
The first ASF npm convenience release must contain this update entry point together with the compatibility evidence introduced by #3667. Before publishing that release, the same release-shaped successor exercise should be repeated against the immutable release candidate artifact.
AI use
Select exactly one:
Tool(s) and scope: Codex authored the implementation, focused tests, and documentation under maintainer direction
Checklist
Does this PR entail a change in behavior?
简体中文
摘要
maka-agentarchive,并校验其 SHA-512 integrity已发布的 package directory 在 Maka deployment authority 内按不可变对象处理。Lifecycle repair 假定 managed package tree 仍然完整;本 PR 不增加本地 tree attestation,也不负责从外部 package 修改中恢复。
关联 #3679
验证
npm --workspace maka-agent test— 443 项测试通过npm run typecheck、npm run lint、npm run format:check与npm run buildnpm run release:cli:pack— 从干净 head 构建 self-containedmaka-agent@0.2.0tarballnode scripts/smoke-release-cli-package.mjs packages/cli/release/maka-agent-0.2.0.tgz— 离线安装、TUI setup、managed Runtime Host 生命周期与受控 Turn 均通过npm run check:asf-headers与git diff --check发布门槛
第一次 ASF npm convenience release 必须同时包含本 PR 的更新入口和 #3667 引入的 compatibility evidence。正式发布前,还应使用不可变 release candidate artifact 重复同一套发布形态的后续版本升级演练。
AI 使用
工具与范围:Codex 在维护者指导下完成实现、聚焦测试与文档
检查清单
本 PR 是否改变行为?