chainlesschain 0.165.1 → 0.165.2

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.
Files changed (202) hide show
  1. package/README.md +248 -2
  2. package/package.json +4 -1
  3. package/src/assets/web-panel/assets/{AIOps-CYxFDrcm.js → AIOps-lS1R0Cza.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-CPBtdiU2.js → ActionButton-BmlgBPsj.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics-Dayq7Dp7.js → Analytics-CNKczv1S.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-6U7bPb2s.js → AppLayout-BuZGJARQ.js} +4 -4
  7. package/src/assets/web-panel/assets/{Artifacts-D2Iqg_3D.js → Artifacts-Dm73Sl-O.js} +1 -1
  8. package/src/assets/web-panel/assets/{Audit-CdoOB7WV.js → Audit-BGMW7A5e.js} +1 -1
  9. package/src/assets/web-panel/assets/{BackgroundAgents-DrnAVB8w.js → BackgroundAgents-BQ6M54JR.js} +1 -1
  10. package/src/assets/web-panel/assets/{Backup-CesMi5cy.js → Backup-DBzGP0bR.js} +1 -1
  11. package/src/assets/web-panel/assets/{BaseInput-DaX9ptax.js → BaseInput-D1mLCoOV.js} +1 -1
  12. package/src/assets/web-panel/assets/{Chat-OrwOPxDS.js → Chat-BUfcN7p8.js} +4 -4
  13. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CvPZFRxM.js → ChatBubbleRenderer-CY5m8ZHg.js} +1 -1
  14. package/src/assets/web-panel/assets/{Checkbox-DiHbcysC.js → Checkbox-Du0Yg5Pi.js} +1 -1
  15. package/src/assets/web-panel/assets/{Codegen-CbgCkaBJ.js → Codegen-B2RXziHW.js} +1 -1
  16. package/src/assets/web-panel/assets/{Col-Du5hgrh4.js → Col-DWhekOrR.js} +1 -1
  17. package/src/assets/web-panel/assets/{Community-DsPOBW-p.js → Community-HQhrkon4.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compact-BwHIFkNC.js → Compact-izHNb4uz.js} +1 -1
  19. package/src/assets/web-panel/assets/{Compliance-BvrDQSgd.js → Compliance-BDwLy41-.js} +1 -1
  20. package/src/assets/web-panel/assets/{Cowork-gwAE6arx.js → Cowork-DFJhLa7K.js} +3 -3
  21. package/src/assets/web-panel/assets/{Cron-DW-0l4uF.js → Cron-DYY9RG7-.js} +2 -2
  22. package/src/assets/web-panel/assets/{Crosschain-j-NsLesB.js → Crosschain-rr7tJTtv.js} +1 -1
  23. package/src/assets/web-panel/assets/{DID-CibVEBOI.js → DID-D2sfHRLi.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dashboard-W4z9vSfe.js → Dashboard-q_eGdhha.js} +2 -2
  25. package/src/assets/web-panel/assets/{Dropdown-DAcrkKxG.js → Dropdown-Cufrh2Bz.js} +1 -1
  26. package/src/assets/web-panel/assets/{EmailListRenderer-6GXah-89.js → EmailListRenderer-B-kdv10f.js} +1 -1
  27. package/src/assets/web-panel/assets/{FamilyGuardDashboard-TKDCq8Ku.js → FamilyGuardDashboard-D21XjgfG.js} +1 -1
  28. package/src/assets/web-panel/assets/{Federation-BXBLlcNm.js → Federation-CAwRTLZH.js} +1 -1
  29. package/src/assets/web-panel/assets/{FormItemContext-BoJJBkG3.js → FormItemContext-vx3daNDF.js} +1 -1
  30. package/src/assets/web-panel/assets/{GenericCardRenderer-okjsl46v.js → GenericCardRenderer-CRWY2wU4.js} +1 -1
  31. package/src/assets/web-panel/assets/{Git-Rvs7i6nn.js → Git-Q6SwauyD.js} +2 -2
  32. package/src/assets/web-panel/assets/{Governance-CHSUxXXS.js → Governance-BJ1q4IQZ.js} +1 -1
  33. package/src/assets/web-panel/assets/{Inference-V3BlfxsB.js → Inference-CSWjROgD.js} +1 -1
  34. package/src/assets/web-panel/assets/{KnowledgeGraph-C_NlD4aR.js → KnowledgeGraph-fzuQsWuY.js} +1 -1
  35. package/src/assets/web-panel/assets/{Logs-Nn4LNxhA.js → Logs-DVF8Mnns.js} +2 -2
  36. package/src/assets/web-panel/assets/MarkdownRenderer-BLif4QSg.js +1 -0
  37. package/src/assets/web-panel/assets/{Marketplace-DEmjFcg-.js → Marketplace-BQP7LOA4.js} +1 -1
  38. package/src/assets/web-panel/assets/{McpTools-DlopXipI.js → McpTools-BNlJCmNc.js} +4 -4
  39. package/src/assets/web-panel/assets/{Memory-BG3ynX60.js → Memory-DLLg5WQ4.js} +2 -2
  40. package/src/assets/web-panel/assets/{MobileBridge-DeRzWOfT.js → MobileBridge-CR2skNAF.js} +1 -1
  41. package/src/assets/web-panel/assets/{MobileProjects-Tyd3o2A0.js → MobileProjects-BxVavObl.js} +1 -1
  42. package/src/assets/web-panel/assets/{Mtc-BoPbOv1C.js → Mtc-B17pgqMI.js} +4 -4
  43. package/src/assets/web-panel/assets/{MtcAudit-un-rTHp7.js → MtcAudit-4UmV5cIL.js} +2 -2
  44. package/src/assets/web-panel/assets/{Multisig-Ds0oveEB.js → Multisig-DoMEXEcU.js} +3 -3
  45. package/src/assets/web-panel/assets/{NLProgramming-D09JI-yY.js → NLProgramming-h9DMXFX4.js} +1 -1
  46. package/src/assets/web-panel/assets/{Notes-DTRhiKJn.js → Notes-Cihn-xX8.js} +3 -3
  47. package/src/assets/web-panel/assets/{NotificationSettings-BAnnNCim.js → NotificationSettings-BE6ZGa3R.js} +1 -1
  48. package/src/assets/web-panel/assets/{OrderTableRenderer-Yr-1fSYi.js → OrderTableRenderer-CnwojoSI.js} +1 -1
  49. package/src/assets/web-panel/assets/{Organization-S9EAolpj.js → Organization-B2vpwmjb.js} +4 -4
  50. package/src/assets/web-panel/assets/{Overflow-CqcLKqJ8.js → Overflow-C4z28R0-.js} +1 -1
  51. package/src/assets/web-panel/assets/{P2P-Dw0F9EcU.js → P2P-BVlFkwhW.js} +2 -2
  52. package/src/assets/web-panel/assets/{PdhVaultBrowser-BDRwUevN.js → PdhVaultBrowser-BXbEdL5B.js} +3 -3
  53. package/src/assets/web-panel/assets/{Permissions-D3_c3nKB.js → Permissions-C28xs4lp.js} +4 -4
  54. package/src/assets/web-panel/assets/{PersonalDataHub-CxdMV98x.js → PersonalDataHub-Dgl_rBjM.js} +3 -3
  55. package/src/assets/web-panel/assets/{Pipeline-CgjNQFLI.js → Pipeline-3QUIhrKI.js} +1 -1
  56. package/src/assets/web-panel/assets/{Privacy-BHghIIht.js → Privacy-8Gh33OZ5.js} +1 -1
  57. package/src/assets/web-panel/assets/{ProjectInit-De6pqaKM.js → ProjectInit-BNmg4tZi.js} +2 -2
  58. package/src/assets/web-panel/assets/{ProjectSettings-Bg4ZkNYQ.js → ProjectSettings-vgMfeCf9.js} +2 -2
  59. package/src/assets/web-panel/assets/Projects-BhtAVilc.js +1 -0
  60. package/src/assets/web-panel/assets/{Providers-CUnHbNf-.js → Providers-CXoOa8eX.js} +1 -1
  61. package/src/assets/web-panel/assets/{QrScannerModal-DA2f3Wts.js → QrScannerModal-BhfTF0oS.js} +1 -1
  62. package/src/assets/web-panel/assets/{QuickAsk-C1FeHDqp.js → QuickAsk-C746HEAj.js} +1 -1
  63. package/src/assets/web-panel/assets/{Recommend-B5guaUMN.js → Recommend-BSej8Eo0.js} +1 -1
  64. package/src/assets/web-panel/assets/{RemoteSession-PDADSds3.js → RemoteSession-f3gmnlG3.js} +3 -3
  65. package/src/assets/web-panel/assets/{Reputation-eTlRhgvK.js → Reputation-CyvybpkN.js} +1 -1
  66. package/src/assets/web-panel/assets/{Row-D7AqSGY0.js → Row-DwV2PeYn.js} +1 -1
  67. package/src/assets/web-panel/assets/{RssFeed-JiuLm4Ja.js → RssFeed-11GrzjZy.js} +3 -3
  68. package/src/assets/web-panel/assets/{Search-CwruahW3.js → Search-BjKXkrAY.js} +1 -1
  69. package/src/assets/web-panel/assets/{Security-CdhqaR0b.js → Security-eWiFb9Yk.js} +4 -4
  70. package/src/assets/web-panel/assets/{Services-CU0ZIlcT.js → Services-CdCsMAtt.js} +2 -2
  71. package/src/assets/web-panel/assets/{Skeleton-CtduPfAb.js → Skeleton-4Q2UTKQ5.js} +1 -1
  72. package/src/assets/web-panel/assets/{Skills-CROXm6AY.js → Skills-v4rtYrRj.js} +1 -1
  73. package/src/assets/web-panel/assets/{Sla-DiZiABFD.js → Sla-CRhsFvd_.js} +1 -1
  74. package/src/assets/web-panel/assets/SpeechSettings-DLjKsDVu.js +1 -0
  75. package/src/assets/web-panel/assets/{SyncSettings-DBX6J1Oe.js → SyncSettings-D16zonl7.js} +2 -2
  76. package/src/assets/web-panel/assets/{Tasks-BLyRJd3-.js → Tasks-Xxozz0i_.js} +1 -1
  77. package/src/assets/web-panel/assets/{Templates-BKa5qzmk.js → Templates-CXJpPtCT.js} +1 -1
  78. package/src/assets/web-panel/assets/{Tenant-DnD3a4M6.js → Tenant-XRV0L1ZF.js} +1 -1
  79. package/src/assets/web-panel/assets/{Terminal-DEitQeTb.js → Terminal-CYuO1i_w.js} +2 -2
  80. package/src/assets/web-panel/assets/{TimelineRenderer-CjHggTDq.js → TimelineRenderer-BXqitWsu.js} +1 -1
  81. package/src/assets/web-panel/assets/{Tokens-CsGT7sfl.js → Tokens-BvPrDHIy.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trigger-yr4rFDrW.js → Trigger-DhCrSTf5.js} +1 -1
  83. package/src/assets/web-panel/assets/{Trust-CLvu1Sjp.js → Trust-DmsuZD6K.js} +1 -1
  84. package/src/assets/web-panel/assets/{UkeySign-B0m7yUP8.js → UkeySign-D_yrts5C.js} +1 -1
  85. package/src/assets/web-panel/assets/{VideoEditing-C0VvP4lc.js → VideoEditing-B01ihPrm.js} +1 -1
  86. package/src/assets/web-panel/assets/{Wallet-kllBAG1M.js → Wallet-DpUjKa-N.js} +3 -3
  87. package/src/assets/web-panel/assets/{WebAuthn-C54UQNLx.js → WebAuthn-CL07yON6.js} +3 -3
  88. package/src/assets/web-panel/assets/{WorkflowEditor-B7y8qSvU.js → WorkflowEditor-CLMflnRr.js} +1 -1
  89. package/src/assets/web-panel/assets/{chat-DRzTh8CJ.js → chat-DtUqe8h_.js} +1 -1
  90. package/src/assets/web-panel/assets/{colors-BSMwSnCx.js → colors-DPKrgVfy.js} +1 -1
  91. package/src/assets/web-panel/assets/{compact-item-BTg602H5.js → compact-item-BTt2GtOm.js} +1 -1
  92. package/src/assets/web-panel/assets/{createContext-B6EGsdvp.js → createContext-D_VeUc1J.js} +1 -1
  93. package/src/assets/web-panel/assets/devWarning-Bj5nUW2v.js +1 -0
  94. package/src/assets/web-panel/assets/{hasIn-DDKqJ8RD.js → hasIn-1kndiylj.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-CUEU48o1.js → index-2eyAXE3d.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-rjzk9z1Q.js → index-4bna2_SR.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-CmAubjDi.js → index-8nz_fmlj.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-Co7oM1qr.js → index-B9KvcS7R.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BMuGd2--.js → index-BAXfRKfG.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BVTAl5N2.js → index-BHpBMWCh.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DpTHIRfZ.js → index-BaZAbw9A.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-CWGax2QW.js → index-BepLZwgB.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-DiW0sDkz.js → index-Bhudc_Ez.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CGNklbP6.js → index-BlDNVgBt.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-u2YVld9W.js → index-By5n8up9.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-pqfux8vZ.js → index-CHG-KcS4.js} +1 -1
  107. package/src/assets/web-panel/assets/index-CPcM-eqo.js +1 -0
  108. package/src/assets/web-panel/assets/{index-BRqt4fO-.js → index-CX81qSM4.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BsR7Y2NG.js → index-CauoBQkH.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-C_JzIXUq.js → index-CcO04eDn.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CQlHFN_g.js → index-CtF7EN49.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CwPUZbMI.js → index-CzjkKUmR.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-th2ihDc0.js → index-D95OFht7.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CHfLYwcW.js → index-DFIZ_1Sf.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-C1GFfuqN.js → index-DG2MZQ3f.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-D1naNj4U.js → index-DJ8vXGOa.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-DLdrHhML.js → index-Dbbymxpj.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-DBT3YeQ9.js → index-DdkplL9R.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-BICn3Qh1.js → index-DfGl8CqG.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DqFXGzzC.js → index-DfKBRdSS.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-Bx044qDj.js → index-DsJCDUJG.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-XFe-1twF.js → index-Dwqmtk65.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-C66NpWy5.js → index-DxdTdBix.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CmAInUkX.js → index-FzdwcSy2.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-Cf23wVMs.js → index-GsoUQ2M7.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-D5CsBShd.js → index-brCInrzq.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-323Hhxxt.js → index-dVc-CXY4.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-p2HYn89m.js → index-fk2Rjacw.js} +3 -3
  129. package/src/assets/web-panel/assets/{index-Dfv2lKu3.js → index-l1BETrDb.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CNxg61YP.js → index-m31Ka8hZ.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-CDTvsX8t.js → index-ocf06EVQ.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-Dc77VR3H.js → index-sDoMHTNA.js} +1 -1
  133. package/src/assets/web-panel/assets/index-xeSyVoRe.js +1 -0
  134. package/src/assets/web-panel/assets/{initDefaultProps-C3Djh9fa.js → initDefaultProps-CvyYJd7z.js} +1 -1
  135. package/src/assets/web-panel/assets/{motion-BzqFmbSe.js → motion-DYZkhUdz.js} +1 -1
  136. package/src/assets/web-panel/assets/{move-DJo_FFaW.js → move-slnJRVT1.js} +1 -1
  137. package/src/assets/web-panel/assets/{mtc-parser-_tlTiBvk.js → mtc-parser-DVc0CKmy.js} +1 -1
  138. package/src/assets/web-panel/assets/{omit-CB2ZyxUg.js → omit-BCXAtHCq.js} +1 -1
  139. package/src/assets/web-panel/assets/{pickAttrs-DZwIFD4u.js → pickAttrs-DBsemz_K.js} +1 -1
  140. package/src/assets/web-panel/assets/{placementArrow-DxZY0dgk.js → placementArrow-BRtkJ-nv.js} +1 -1
  141. package/src/assets/web-panel/assets/{responsiveObserve-CzzGt7GL.js → responsiveObserve-Cebq7EZn.js} +1 -1
  142. package/src/assets/web-panel/assets/{slide-BaIK52yz.js → slide-DT7qvId2.js} +1 -1
  143. package/src/assets/web-panel/assets/{statusUtils-3-FHu9_M.js → statusUtils-pQiITLkG.js} +1 -1
  144. package/src/assets/web-panel/assets/{styleChecker-BkRT68fK.js → styleChecker-c-19BVKQ.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFlexGapSupport-DHYwKS-s.js → useFlexGapSupport-DsS5-0bE.js} +1 -1
  146. package/src/assets/web-panel/assets/{useFs-DVVUhV8p.js → useFs-CxGiMe5n.js} +1 -1
  147. package/src/assets/web-panel/assets/{usePersonalDataHub-CkrFKAzQ.js → usePersonalDataHub-D9btVID5.js} +1 -1
  148. package/src/assets/web-panel/assets/{vnode-BuBLFz0c.js → vnode-CUYFsFEN.js} +1 -1
  149. package/src/assets/web-panel/assets/{zoom-kpm6LtXU.js → zoom-CXmn_35j.js} +1 -1
  150. package/src/assets/web-panel/index.html +1 -1
  151. package/src/command-help-index.json +1 -1
  152. package/src/commands/cowork.js +280 -5
  153. package/src/commands/plugin.js +819 -64
  154. package/src/commands/session-location.js +318 -7
  155. package/src/constants.js +2 -2
  156. package/src/data/changelog.json +14 -1
  157. package/src/gateways/ws/action-protocol.js +1 -1
  158. package/src/harness/jsonl-session-store.js +638 -2
  159. package/src/harness/prompt-compressor.js +4 -0
  160. package/src/lib/background-agent-keeper-client.js +29 -0
  161. package/src/lib/background-agent-keeper-protocol.js +4 -0
  162. package/src/lib/background-agent-supervisor.js +59 -29
  163. package/src/lib/cowork-task-runner.js +70 -0
  164. package/src/lib/cowork-workflow.js +42 -9
  165. package/src/lib/doctor-checkup.js +34 -10
  166. package/src/lib/dynamic-workflow-draft.js +535 -0
  167. package/src/lib/dynamic-workflow-facade.js +11 -11
  168. package/src/lib/dynamic-workflow-runtime.js +2974 -0
  169. package/src/lib/execution-location-contract.js +111 -7
  170. package/src/lib/execution-location-runtime.js +3 -1
  171. package/src/lib/execution-location-target.js +910 -0
  172. package/src/lib/llm-providers.js +1 -0
  173. package/src/lib/mcp-call-ledger-store.js +35 -0
  174. package/src/lib/mcp-call-ledger.js +30 -0
  175. package/src/lib/plugin-runtime/__tests__/remote-source.test.js +34 -16
  176. package/src/lib/plugin-runtime/install.js +4956 -657
  177. package/src/lib/plugin-runtime/manifest.js +3 -1
  178. package/src/lib/plugin-runtime/marketplace-artifact-readback.js +671 -31
  179. package/src/lib/plugin-runtime/marketplace-catalog.js +68 -2
  180. package/src/lib/plugin-runtime/marketplace-impact.js +208 -15
  181. package/src/lib/plugin-runtime/marketplace-network.js +317 -0
  182. package/src/lib/plugin-runtime/marketplace-pac-worker.js +15 -0
  183. package/src/lib/plugin-runtime/marketplace-remote-artifacts.js +25 -2
  184. package/src/lib/plugin-runtime/marketplace-source-cache.js +359 -0
  185. package/src/lib/plugin-runtime/policy.js +21 -6
  186. package/src/lib/plugin-runtime/publisher-trust.js +332 -0
  187. package/src/lib/plugin-runtime/remote-source.js +323 -27
  188. package/src/lib/plugin-runtime/scopes.js +317 -41
  189. package/src/lib/plugin-runtime/transaction-journal.js +806 -0
  190. package/src/lib/public-ide-capability-manifest.js +1 -1
  191. package/src/lib/remote-membership-coordinator.js +8 -6
  192. package/src/lib/sub-agent-context.js +268 -2
  193. package/src/lib/task-model-selector.js +1 -1
  194. package/src/lib/with-file-lock.js +95 -5
  195. package/src/runtime/agent-core.js +595 -41
  196. package/src/workers/background-agent-keeper.js +40 -10
  197. package/src/assets/web-panel/assets/MarkdownRenderer-SVAgNnkt.js +0 -1
  198. package/src/assets/web-panel/assets/Projects-CP8vPvzs.js +0 -1
  199. package/src/assets/web-panel/assets/SpeechSettings-B5pLcXUw.js +0 -1
  200. package/src/assets/web-panel/assets/devWarning-Dol8jLEP.js +0 -1
  201. package/src/assets/web-panel/assets/index-C3QX5tOl.js +0 -1
  202. package/src/assets/web-panel/assets/index-xYybvEic.js +0 -1
@@ -5,9 +5,11 @@
5
5
  * only fields whose semantics are exact: manifest bytes, manifest license,
6
6
  * the cryptographically verified signing key, and the repository-defined
7
7
  * payload SBOM format. Persisted remote-artifact evidence is checked for
8
- * internal consistency and bound back to the installed signature lock;
9
- * external SBOM bytes are not persisted or re-fetched, so their install-time
10
- * digest record remains explicitly distinct from a current payload comparison.
8
+ * internal consistency and bound back to the installed signature lock. For
9
+ * the repository-defined payload SBOM, the installer also persists an exact
10
+ * install-time comparison record; readback validates its internal bindings and
11
+ * compares it with a freshly computed inventory of the installed payload. The
12
+ * record is not a publisher or organization trust anchor.
11
13
  */
12
14
 
13
15
  import crypto from "node:crypto";
@@ -19,22 +21,67 @@ import {
19
21
  readPluginLock,
20
22
  verifyInstalledSignature,
21
23
  } from "./signature.js";
24
+ import { normalizePublisherAuthority } from "./publisher-trust.js";
22
25
 
23
26
  export const PLUGIN_MARKETPLACE_ARTIFACT_READBACK_SCHEMA =
24
27
  "cc-plugin-marketplace-artifact-readback/v1";
25
28
  export const PLUGIN_MARKETPLACE_PAYLOAD_SBOM_SCHEMA =
26
29
  "cc-plugin-marketplace-payload-sbom/v1";
30
+ export const PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA =
31
+ "cc-plugin-marketplace-payload-sbom/v2";
32
+ export const PLUGIN_MARKETPLACE_REMOTE_SBOM_PAYLOAD_COMPARISON_SCHEMA =
33
+ "cc-plugin-marketplace-remote-sbom-payload-comparison/v1";
27
34
  export const MAX_MARKETPLACE_READBACK_FILES = 10_000;
28
35
  export const MAX_MARKETPLACE_READBACK_FILE_BYTES = 64 * 1024 * 1024;
29
36
  export const MAX_MARKETPLACE_READBACK_TOTAL_BYTES = 1024 * 1024 * 1024;
30
37
 
31
38
  const SOURCE_METADATA_FILENAME = ".plugin-source.json";
39
+ const PLUGIN_MARKETPLACE_PAYLOAD_V1_EXCLUSIONS = Object.freeze([
40
+ LOCK_FILENAME,
41
+ SOURCE_METADATA_FILENAME,
42
+ ]);
43
+ export const PLUGIN_MARKETPLACE_STAGED_SOURCE_EXCLUSIONS = Object.freeze([
44
+ ".git",
45
+ LOCK_FILENAME,
46
+ SOURCE_METADATA_FILENAME,
47
+ ]);
32
48
  const SHA256_RE = /^[a-f0-9]{64}$/;
33
49
  const COMPARABLE_SBOM_FORMATS = new Set([
34
- "cc-plugin-marketplace-payload-sbom/v1",
50
+ PLUGIN_MARKETPLACE_PAYLOAD_SBOM_SCHEMA,
51
+ PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA,
35
52
  "cc-plugin-payload-sbom/v1",
36
53
  ]);
37
54
 
55
+ export function assertSafeInstalledPluginStructure(root, current = root) {
56
+ if (current === root) {
57
+ const rootStat = fs.lstatSync(root);
58
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
59
+ throw new Error("EXISTING_VERSION_UNSAFE_ENTRY: .");
60
+ }
61
+ }
62
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
63
+ const absolute = path.join(current, entry.name);
64
+ const relative = path.relative(root, absolute).replace(/\\/g, "/");
65
+ if (relative === ".git" || relative.startsWith(".git/")) {
66
+ throw new Error(`EXISTING_VERSION_UNSAFE_ENTRY: ${relative}`);
67
+ }
68
+ if (entry.isSymbolicLink()) {
69
+ throw new Error(`EXISTING_VERSION_UNSAFE_ENTRY: ${relative}`);
70
+ }
71
+ if (
72
+ (relative === LOCK_FILENAME || relative === SOURCE_METADATA_FILENAME) &&
73
+ !entry.isFile()
74
+ ) {
75
+ throw new Error(`EXISTING_VERSION_UNSAFE_ENTRY: ${relative}`);
76
+ }
77
+ if (entry.isDirectory()) {
78
+ assertSafeInstalledPluginStructure(root, absolute);
79
+ } else if (!entry.isFile()) {
80
+ throw new Error(`EXISTING_VERSION_UNSAFE_ENTRY: ${relative}`);
81
+ }
82
+ }
83
+ }
84
+
38
85
  export function buildPluginMarketplaceArtifactReadback({
39
86
  root,
40
87
  scope = null,
@@ -45,6 +92,12 @@ export function buildPluginMarketplaceArtifactReadback({
45
92
  if (!root || !fs.existsSync(resolvedRoot)) {
46
93
  throw new Error("marketplace artifact readback root does not exist");
47
94
  }
95
+ const remotePayloadFormat =
96
+ source?.catalogAuthority?.remoteSbomPayloadComparison?.format ||
97
+ source?.catalogAuthority?.artifactExpectations?.sbom?.format;
98
+ if (isMarketplacePayloadSbomFormat(remotePayloadFormat)) {
99
+ assertSafeInstalledPluginStructure(resolvedRoot);
100
+ }
48
101
  const manifest = parsePluginManifest(resolvedRoot);
49
102
  if (!manifest.ok || !manifest.manifestPath) {
50
103
  throw new Error(
@@ -63,6 +116,26 @@ export function buildPluginMarketplaceArtifactReadback({
63
116
  const remoteArtifactEvidence = validateRemoteArtifactEvidence(
64
117
  source?.catalogAuthority?.remoteArtifactEvidence,
65
118
  );
119
+ const currentRemotePayloadSbom = isMarketplacePayloadSbomFormat(
120
+ remotePayloadFormat,
121
+ )
122
+ ? buildMarketplacePayloadSbom(resolvedRoot, {
123
+ schemaVersion: remotePayloadFormat,
124
+ })
125
+ : payloadSbom;
126
+ const remoteSbomPayloadComparison = validateRemoteSbomPayloadComparison(
127
+ source?.catalogAuthority?.remoteSbomPayloadComparison,
128
+ {
129
+ remoteArtifactEvidence: remoteArtifactEvidence.valid
130
+ ? {
131
+ ...remoteArtifactEvidence.authority,
132
+ evidenceDigest: remoteArtifactEvidence.evidenceDigest,
133
+ }
134
+ : null,
135
+ expectedPayloadSha256: expectations.sbom.payloadSha256,
136
+ currentPayloadSbom: currentRemotePayloadSbom,
137
+ },
138
+ );
66
139
  const actual = {
67
140
  manifest: {
68
141
  relativePath: path
@@ -90,10 +163,12 @@ export function buildPluginMarketplaceArtifactReadback({
90
163
  Boolean(lock?.sbom) &&
91
164
  cleanDigest(lock?.sbom?.digest) != null,
92
165
  },
93
- payloadSbom,
166
+ payloadSbom: currentRemotePayloadSbom,
94
167
  };
95
168
  actual.remoteArtifacts = summarizeRemoteArtifacts({
96
169
  evidence: remoteArtifactEvidence,
170
+ payloadComparison: remoteSbomPayloadComparison,
171
+ payloadSbom: currentRemotePayloadSbom,
97
172
  lock,
98
173
  signature: actual.signature,
99
174
  expectedRegistryOrigin,
@@ -119,6 +194,10 @@ export function buildPluginMarketplaceArtifactReadback({
119
194
  expectations.sbom,
120
195
  actual.remoteArtifacts.sbom,
121
196
  ),
197
+ remoteSbomPayload: compareRemoteSbomPayload(
198
+ expectations.sbom,
199
+ actual.remoteArtifacts.sbom,
200
+ ),
122
201
  };
123
202
  const blockers = [];
124
203
  if (comparisons.manifest.status === "mismatch") {
@@ -150,17 +229,51 @@ export function buildPluginMarketplaceArtifactReadback({
150
229
  if (comparisons.remoteSbom.status === "mismatch") {
151
230
  blockers.push(issue("REMOTE_SBOM_EVIDENCE_MISMATCH"));
152
231
  }
232
+ if (
233
+ remoteSbomPayloadComparison.present &&
234
+ !remoteSbomPayloadComparison.valid
235
+ ) {
236
+ blockers.push(
237
+ issue(
238
+ "REMOTE_SBOM_PAYLOAD_COMPARISON_INVALID",
239
+ remoteSbomPayloadComparison.reason,
240
+ ),
241
+ );
242
+ }
243
+ if (comparisons.remoteSbomPayload.status === "mismatch") {
244
+ blockers.push(
245
+ issue(
246
+ (expectations.sbom.format ===
247
+ PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA ||
248
+ (expectations.sbom.format ===
249
+ PLUGIN_MARKETPLACE_PAYLOAD_SBOM_SCHEMA &&
250
+ expectations.sbom.url &&
251
+ expectations.sbom.documentSha256)) &&
252
+ !remoteSbomPayloadComparison.present
253
+ ? "REMOTE_SBOM_PAYLOAD_COMPARISON_MISSING"
254
+ : "REMOTE_SBOM_PAYLOAD_MISMATCH",
255
+ ),
256
+ );
257
+ }
153
258
  const comparisonStatuses = [
154
259
  comparisons.manifest.status,
155
260
  comparisons.license.status,
156
261
  comparisons.signature.status,
157
- comparisons.sbom.status,
262
+ comparisons.sbom.status === "unbound" &&
263
+ comparisons.remoteSbomPayload.status === "matched"
264
+ ? "matched"
265
+ : comparisons.sbom.status,
158
266
  ...(expectations.signature.url || actual.remoteArtifacts.signature.fetched
159
267
  ? [comparisons.remoteSignature.status]
160
268
  : []),
161
269
  ...(expectations.sbom.url || actual.remoteArtifacts.sbom.fetched
162
270
  ? [comparisons.remoteSbom.status]
163
271
  : []),
272
+ ...((expectations.sbom.url &&
273
+ isMarketplacePayloadSbomFormat(expectations.sbom.format)) ||
274
+ remoteSbomPayloadComparison.present
275
+ ? [comparisons.remoteSbomPayload.status]
276
+ : []),
164
277
  ];
165
278
  const status = blockers.length
166
279
  ? "failed"
@@ -176,6 +289,7 @@ export function buildPluginMarketplaceArtifactReadback({
176
289
  },
177
290
  catalogAuthority: normalizeCatalogAuthority(source?.catalogAuthority),
178
291
  remoteArtifactEvidence,
292
+ remoteSbomPayloadComparison,
179
293
  expectations,
180
294
  actual,
181
295
  comparisons,
@@ -188,7 +302,9 @@ export function buildPluginMarketplaceArtifactReadback({
188
302
  status,
189
303
  ...authority,
190
304
  claims: {
191
- registryPublisherIdentityVerified: false,
305
+ registryPublisherIdentityVerified: Boolean(
306
+ source?.catalogAuthority?.publisherAuthority,
307
+ ),
192
308
  remoteSignatureFetched: actual.remoteArtifacts.signature.fetched === true,
193
309
  remoteSignatureBoundToInstalledLock:
194
310
  remoteArtifactEvidence.valid === true &&
@@ -199,24 +315,69 @@ export function buildPluginMarketplaceArtifactReadback({
199
315
  remoteSbomFetched: actual.remoteArtifacts.sbom.fetched === true,
200
316
  remoteArtifactEvidenceSelfConsistent:
201
317
  remoteArtifactEvidence.valid === true,
202
- manifestBytesReadFromImmutableInstall: true,
318
+ manifestBytesReadFromCurrentInstall: true,
203
319
  licenseReadFromInstalledManifest: true,
204
320
  signatureCryptographicallyReverified: actual.signature.verified,
205
- payloadSbomComputedFromImmutableInstall: true,
321
+ payloadSbomComputedFromCurrentInstall: true,
206
322
  externalSbomDigestComparable: comparisons.sbom.comparable,
207
323
  remoteSbomDigestVerifiedAtInstallRecorded:
208
324
  remoteArtifactEvidence.valid === true &&
209
325
  actual.remoteArtifacts.registryOriginMatches === true &&
210
326
  comparisons.remoteSbom.status === "matched" &&
211
327
  actual.remoteArtifacts.sbom.digestVerifiedAtInstallRecorded === true,
328
+ remoteSbomPayloadComparisonRecorded:
329
+ remoteSbomPayloadComparison.valid === true,
330
+ remoteSbomPayloadComparisonSelfConsistent:
331
+ remoteSbomPayloadComparison.valid === true,
332
+ remoteSbomRecordedPayloadMatchesCurrentInstall:
333
+ comparisons.remoteSbomPayload.status === "matched" &&
334
+ actual.remoteArtifacts.sbom.currentPayloadMatches === true,
212
335
  },
213
336
  };
214
337
  }
215
338
 
216
- export function buildMarketplacePayloadSbom(root) {
339
+ export function buildMarketplacePayloadSbom(
340
+ root,
341
+ { schemaVersion = PLUGIN_MARKETPLACE_PAYLOAD_SBOM_SCHEMA } = {},
342
+ ) {
343
+ const exclusions = marketplacePayloadExclusions(schemaVersion);
344
+ const typedPayload =
345
+ schemaVersion === PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA;
217
346
  const resolvedRoot = path.resolve(root);
218
347
  const files = [];
219
348
  let totalBytes = 0;
349
+ const appendFile = (relative, bytes, type = "file") => {
350
+ if (files.length >= MAX_MARKETPLACE_READBACK_FILES) {
351
+ throw new Error(
352
+ `marketplace artifact readback exceeds ${MAX_MARKETPLACE_READBACK_FILES} files`,
353
+ );
354
+ }
355
+ if (bytes.length > MAX_MARKETPLACE_READBACK_FILE_BYTES) {
356
+ throw new Error(
357
+ `marketplace artifact readback file exceeds ${MAX_MARKETPLACE_READBACK_FILE_BYTES} bytes: ${relative}`,
358
+ );
359
+ }
360
+ totalBytes += bytes.length;
361
+ if (totalBytes > MAX_MARKETPLACE_READBACK_TOTAL_BYTES) {
362
+ throw new Error(
363
+ `marketplace artifact readback exceeds ${MAX_MARKETPLACE_READBACK_TOTAL_BYTES} total bytes`,
364
+ );
365
+ }
366
+ files.push(
367
+ typedPayload
368
+ ? {
369
+ path: relative,
370
+ type,
371
+ bytes: bytes.length,
372
+ sha256: sha256(bytes),
373
+ }
374
+ : {
375
+ path: relative,
376
+ bytes: bytes.length,
377
+ sha256: sha256(bytes),
378
+ },
379
+ );
380
+ };
220
381
  const walk = (dir) => {
221
382
  const entries = fs
222
383
  .readdirSync(dir, { withFileTypes: true })
@@ -226,19 +387,30 @@ export function buildMarketplacePayloadSbom(root) {
226
387
  const relative = path
227
388
  .relative(resolvedRoot, absolute)
228
389
  .replace(/\\/g, "/");
229
- if (relative === LOCK_FILENAME || relative === SOURCE_METADATA_FILENAME) {
390
+ if (exclusions.includes(relative)) {
391
+ continue;
392
+ }
393
+ if (entry.isSymbolicLink()) {
394
+ if (typedPayload) {
395
+ appendFile(
396
+ relative,
397
+ fs.readlinkSync(absolute, { encoding: "buffer" }),
398
+ "symlink",
399
+ );
400
+ }
230
401
  continue;
231
402
  }
232
- if (entry.isSymbolicLink()) continue;
233
403
  if (entry.isDirectory()) {
234
404
  walk(absolute);
235
405
  continue;
236
406
  }
237
- if (!entry.isFile()) continue;
238
- if (files.length >= MAX_MARKETPLACE_READBACK_FILES) {
239
- throw new Error(
240
- `marketplace artifact readback exceeds ${MAX_MARKETPLACE_READBACK_FILES} files`,
241
- );
407
+ if (!entry.isFile()) {
408
+ if (typedPayload) {
409
+ throw new Error(
410
+ `marketplace payload SBOM does not support entry type: ${relative}`,
411
+ );
412
+ }
413
+ continue;
242
414
  }
243
415
  const stat = fs.statSync(absolute);
244
416
  if (stat.size > MAX_MARKETPLACE_READBACK_FILE_BYTES) {
@@ -246,36 +418,435 @@ export function buildMarketplacePayloadSbom(root) {
246
418
  `marketplace artifact readback file exceeds ${MAX_MARKETPLACE_READBACK_FILE_BYTES} bytes: ${relative}`,
247
419
  );
248
420
  }
249
- totalBytes += stat.size;
250
- if (totalBytes > MAX_MARKETPLACE_READBACK_TOTAL_BYTES) {
251
- throw new Error(
252
- `marketplace artifact readback exceeds ${MAX_MARKETPLACE_READBACK_TOTAL_BYTES} total bytes`,
253
- );
254
- }
255
421
  const bytes = fs.readFileSync(absolute);
256
- files.push({
257
- path: relative,
258
- bytes: bytes.length,
259
- sha256: sha256(bytes),
260
- });
422
+ appendFile(relative, bytes);
261
423
  }
262
424
  };
263
425
  walk(resolvedRoot);
264
426
  files.sort((a, b) => stableCompare(a.path, b.path));
265
427
  return {
266
- schemaVersion: PLUGIN_MARKETPLACE_PAYLOAD_SBOM_SCHEMA,
428
+ schemaVersion,
267
429
  digest: sha256(
268
430
  files
269
- .map((file) => `${file.path}\0${file.sha256}\0${file.bytes}\n`)
431
+ .map((file) => marketplacePayloadDigestLine(file, schemaVersion))
270
432
  .join(""),
271
433
  ),
272
434
  fileCount: files.length,
273
435
  totalBytes,
274
436
  files,
275
- exclusions: [LOCK_FILENAME, SOURCE_METADATA_FILENAME],
437
+ exclusions: [...exclusions],
276
438
  };
277
439
  }
278
440
 
441
+ export function isMarketplacePayloadSbomFormat(value) {
442
+ return (
443
+ value === PLUGIN_MARKETPLACE_PAYLOAD_SBOM_SCHEMA ||
444
+ value === PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA
445
+ );
446
+ }
447
+
448
+ function marketplacePayloadExclusions(schemaVersion) {
449
+ if (schemaVersion === PLUGIN_MARKETPLACE_PAYLOAD_SBOM_SCHEMA) {
450
+ return [...PLUGIN_MARKETPLACE_PAYLOAD_V1_EXCLUSIONS];
451
+ }
452
+ if (schemaVersion === PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA) {
453
+ return [...PLUGIN_MARKETPLACE_STAGED_SOURCE_EXCLUSIONS];
454
+ }
455
+ throw new Error(
456
+ `unsupported marketplace payload SBOM schema: ${schemaVersion}`,
457
+ );
458
+ }
459
+
460
+ function marketplacePayloadDigestLine(file, schemaVersion) {
461
+ return schemaVersion === PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA
462
+ ? `${file.type}\0${file.path}\0${file.sha256}\0${file.bytes}\n`
463
+ : `${file.path}\0${file.sha256}\0${file.bytes}\n`;
464
+ }
465
+
466
+ /**
467
+ * Parse the repository-defined remote payload SBOM without trusting any of
468
+ * its summary fields. The document is accepted only when its complete ordered
469
+ * file list reproduces the declared digest and bounded counters.
470
+ */
471
+ export function parseMarketplacePayloadSbomDocument(bytes, { format } = {}) {
472
+ if (!isMarketplacePayloadSbomFormat(format)) return null;
473
+ if (!Buffer.isBuffer(bytes)) {
474
+ throw new Error("remote marketplace payload SBOM bytes are missing");
475
+ }
476
+ const text = bytes.toString("utf8");
477
+ if (!Buffer.from(text, "utf8").equals(bytes)) {
478
+ throw new Error("remote marketplace payload SBOM must be valid UTF-8");
479
+ }
480
+ let value;
481
+ try {
482
+ value = JSON.parse(text);
483
+ } catch {
484
+ throw new Error("remote marketplace payload SBOM must be valid JSON");
485
+ }
486
+ if (
487
+ !exactObjectKeys(value, [
488
+ "schemaVersion",
489
+ "digest",
490
+ "fileCount",
491
+ "totalBytes",
492
+ "files",
493
+ "exclusions",
494
+ ])
495
+ ) {
496
+ throw new Error("remote marketplace payload SBOM shape is invalid");
497
+ }
498
+ if (value.schemaVersion !== format) {
499
+ throw new Error("remote marketplace payload SBOM schema is invalid");
500
+ }
501
+ if (!Array.isArray(value.files) || !Array.isArray(value.exclusions)) {
502
+ throw new Error("remote marketplace payload SBOM lists are invalid");
503
+ }
504
+ if (
505
+ value.files.length > MAX_MARKETPLACE_READBACK_FILES ||
506
+ value.fileCount !== value.files.length ||
507
+ !Number.isSafeInteger(value.fileCount) ||
508
+ value.fileCount < 0
509
+ ) {
510
+ throw new Error("remote marketplace payload SBOM file count is invalid");
511
+ }
512
+ const expectedExclusions = marketplacePayloadExclusions(format);
513
+ if (canonicalJson(value.exclusions) !== canonicalJson(expectedExclusions)) {
514
+ throw new Error("remote marketplace payload SBOM exclusions are invalid");
515
+ }
516
+
517
+ const files = [];
518
+ let totalBytes = 0;
519
+ let previousPath = null;
520
+ for (const file of value.files) {
521
+ const expectedFileKeys =
522
+ format === PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA
523
+ ? ["path", "type", "bytes", "sha256"]
524
+ : ["path", "bytes", "sha256"];
525
+ if (!exactObjectKeys(file, expectedFileKeys)) {
526
+ throw new Error("remote marketplace payload SBOM file shape is invalid");
527
+ }
528
+ if (
529
+ format === PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA &&
530
+ file.type !== "file" &&
531
+ file.type !== "symlink"
532
+ ) {
533
+ throw new Error("remote marketplace payload SBOM file type is invalid");
534
+ }
535
+ const relativePath = file.path;
536
+ if (
537
+ typeof relativePath !== "string" ||
538
+ !relativePath ||
539
+ relativePath.includes("\\") ||
540
+ path.posix.isAbsolute(relativePath) ||
541
+ path.posix.normalize(relativePath) !== relativePath ||
542
+ relativePath === "." ||
543
+ relativePath.startsWith("../") ||
544
+ expectedExclusions.includes(relativePath) ||
545
+ (previousPath != null && stableCompare(previousPath, relativePath) >= 0)
546
+ ) {
547
+ throw new Error("remote marketplace payload SBOM file path is invalid");
548
+ }
549
+ if (
550
+ !Number.isSafeInteger(file.bytes) ||
551
+ file.bytes < 0 ||
552
+ file.bytes > MAX_MARKETPLACE_READBACK_FILE_BYTES
553
+ ) {
554
+ throw new Error("remote marketplace payload SBOM file size is invalid");
555
+ }
556
+ const fileSha256 = cleanDigest(file.sha256);
557
+ if (!fileSha256 || fileSha256 !== file.sha256) {
558
+ throw new Error("remote marketplace payload SBOM file digest is invalid");
559
+ }
560
+ totalBytes += file.bytes;
561
+ if (totalBytes > MAX_MARKETPLACE_READBACK_TOTAL_BYTES) {
562
+ throw new Error("remote marketplace payload SBOM total size is invalid");
563
+ }
564
+ files.push(
565
+ format === PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA
566
+ ? {
567
+ path: relativePath,
568
+ type: file.type,
569
+ bytes: file.bytes,
570
+ sha256: fileSha256,
571
+ }
572
+ : { path: relativePath, bytes: file.bytes, sha256: fileSha256 },
573
+ );
574
+ previousPath = relativePath;
575
+ }
576
+ if (value.totalBytes !== totalBytes) {
577
+ throw new Error("remote marketplace payload SBOM total bytes mismatch");
578
+ }
579
+ const digest = sha256(
580
+ files.map((file) => marketplacePayloadDigestLine(file, format)).join(""),
581
+ );
582
+ if (cleanDigest(value.digest) !== digest || value.digest !== digest) {
583
+ throw new Error("remote marketplace payload SBOM digest mismatch");
584
+ }
585
+ return {
586
+ schemaVersion: format,
587
+ digest,
588
+ fileCount: files.length,
589
+ totalBytes,
590
+ files,
591
+ exclusions: expectedExclusions,
592
+ };
593
+ }
594
+
595
+ export function buildRemoteSbomPayloadComparison({
596
+ remoteArtifactEvidence,
597
+ remoteSbomBytes,
598
+ installedRoot,
599
+ expectedSbom = null,
600
+ expectedPayloadSha256 = null,
601
+ } = {}) {
602
+ const remoteSbom = remoteArtifactEvidence?.sbom;
603
+ const expectsCanonicalRemoteSbom =
604
+ expectedSbom?.format === PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA;
605
+ const expectsComparableRemoteSbom =
606
+ isMarketplacePayloadSbomFormat(expectedSbom?.format) &&
607
+ Boolean(expectedSbom?.url && expectedSbom?.documentSha256);
608
+ if (!remoteSbom) {
609
+ if (remoteSbomBytes != null) {
610
+ throw new Error("remote SBOM bytes are not bound to artifact evidence");
611
+ }
612
+ if (expectsCanonicalRemoteSbom) {
613
+ throw new Error(
614
+ "marketplace payload SBOM v2 requires complete bound remote evidence before plugin activation",
615
+ );
616
+ }
617
+ if (expectsComparableRemoteSbom) {
618
+ throw new Error(
619
+ "repository payload SBOM evidence is required before plugin activation",
620
+ );
621
+ }
622
+ return null;
623
+ }
624
+ if (!isMarketplacePayloadSbomFormat(remoteSbom.format)) {
625
+ if (expectsComparableRemoteSbom) {
626
+ throw new Error(
627
+ "repository payload SBOM evidence format changed before plugin activation",
628
+ );
629
+ }
630
+ return null;
631
+ }
632
+ if (!Buffer.isBuffer(remoteSbomBytes)) {
633
+ throw new Error(
634
+ "comparable remote SBOM bytes are required before plugin activation",
635
+ );
636
+ }
637
+ if (remoteSbomBytes.length !== remoteSbom.bytes) {
638
+ throw new Error("remote SBOM byte count changed before payload comparison");
639
+ }
640
+ const documentSha256 = sha256(remoteSbomBytes);
641
+ if (
642
+ documentSha256 !== remoteSbom.documentSha256 ||
643
+ documentSha256 !== remoteSbom.expectedDocumentSha256
644
+ ) {
645
+ throw new Error("remote SBOM bytes changed before payload comparison");
646
+ }
647
+ const remotePayload = parseMarketplacePayloadSbomDocument(remoteSbomBytes, {
648
+ format: remoteSbom.format,
649
+ });
650
+ if (
651
+ remoteSbom.format === PLUGIN_MARKETPLACE_PAYLOAD_SBOM_SCHEMA &&
652
+ remotePayload.files.some(
653
+ (file) => file.path === ".git" || file.path.startsWith(".git/"),
654
+ )
655
+ ) {
656
+ throw new Error(
657
+ "legacy marketplace payload SBOM v1 cannot bind Git VCS metadata; publish the sanitized v2 payload format",
658
+ );
659
+ }
660
+ const installedPayload = buildMarketplacePayloadSbom(installedRoot, {
661
+ schemaVersion: remoteSbom.format,
662
+ });
663
+ if (canonicalJson(remotePayload) !== canonicalJson(installedPayload)) {
664
+ throw new Error(
665
+ "remote marketplace payload SBOM does not match staged plugin bytes",
666
+ );
667
+ }
668
+ const expectedDigest = cleanDigest(expectedPayloadSha256);
669
+ if (expectedPayloadSha256 != null && !expectedDigest) {
670
+ throw new Error("catalog payload SBOM digest is invalid");
671
+ }
672
+ if (expectedDigest && expectedDigest !== remotePayload.digest) {
673
+ throw new Error(
674
+ "remote marketplace payload SBOM does not match catalog payload digest",
675
+ );
676
+ }
677
+ const summarize = (value) => ({
678
+ schemaVersion: value.schemaVersion,
679
+ digest: value.digest,
680
+ fileCount: value.fileCount,
681
+ totalBytes: value.totalBytes,
682
+ });
683
+ const authority = {
684
+ schemaVersion: PLUGIN_MARKETPLACE_REMOTE_SBOM_PAYLOAD_COMPARISON_SCHEMA,
685
+ status: "matched",
686
+ remoteArtifactEvidenceDigest: cleanDigest(
687
+ remoteArtifactEvidence.evidenceDigest,
688
+ ),
689
+ format: remoteSbom.format,
690
+ documentSha256,
691
+ remotePayload: summarize(remotePayload),
692
+ installedPayload: summarize(installedPayload),
693
+ };
694
+ if (!authority.remoteArtifactEvidenceDigest) {
695
+ throw new Error("remote artifact evidence digest is invalid");
696
+ }
697
+ return {
698
+ ...authority,
699
+ comparisonDigest: sha256Canonical(authority),
700
+ };
701
+ }
702
+
703
+ /**
704
+ * Validate the persisted install-time semantic comparison. This does not need
705
+ * the remote document bytes: its unkeyed comparison digest checks the record's
706
+ * internal bindings, and its installed summary is checked against a newly
707
+ * computed payload inventory when `currentPayloadSbom` is supplied. The record
708
+ * remains locally writable and is not an external trust anchor.
709
+ */
710
+ export function validateRemoteSbomPayloadComparison(
711
+ value,
712
+ {
713
+ remoteArtifactEvidence = null,
714
+ expectedPayloadSha256 = null,
715
+ currentPayloadSbom = null,
716
+ } = {},
717
+ ) {
718
+ if (value == null) {
719
+ return {
720
+ present: false,
721
+ valid: false,
722
+ authority: null,
723
+ currentPayloadMatches: false,
724
+ reason: null,
725
+ };
726
+ }
727
+ const invalid = (reason) => ({
728
+ present: true,
729
+ valid: false,
730
+ authority: null,
731
+ currentPayloadMatches: false,
732
+ reason,
733
+ });
734
+ const expectedPayloadSchema = isMarketplacePayloadSbomFormat(value?.format)
735
+ ? value.format
736
+ : null;
737
+ if (
738
+ !exactObjectKeys(value, [
739
+ "schemaVersion",
740
+ "status",
741
+ "remoteArtifactEvidenceDigest",
742
+ "format",
743
+ "documentSha256",
744
+ "remotePayload",
745
+ "installedPayload",
746
+ "comparisonDigest",
747
+ ])
748
+ ) {
749
+ return invalid("comparison shape is invalid");
750
+ }
751
+ const normalizeSummary = (candidate) => {
752
+ if (
753
+ !exactObjectKeys(candidate, [
754
+ "schemaVersion",
755
+ "digest",
756
+ "fileCount",
757
+ "totalBytes",
758
+ ]) ||
759
+ candidate.schemaVersion !== expectedPayloadSchema ||
760
+ !exactDigest(candidate.digest) ||
761
+ !Number.isSafeInteger(candidate.fileCount) ||
762
+ candidate.fileCount < 0 ||
763
+ candidate.fileCount > MAX_MARKETPLACE_READBACK_FILES ||
764
+ !Number.isSafeInteger(candidate.totalBytes) ||
765
+ candidate.totalBytes < 0 ||
766
+ candidate.totalBytes > MAX_MARKETPLACE_READBACK_TOTAL_BYTES
767
+ ) {
768
+ return null;
769
+ }
770
+ return {
771
+ schemaVersion: candidate.schemaVersion,
772
+ digest: candidate.digest,
773
+ fileCount: candidate.fileCount,
774
+ totalBytes: candidate.totalBytes,
775
+ };
776
+ };
777
+ const remotePayload = normalizeSummary(value.remotePayload);
778
+ const installedPayload = normalizeSummary(value.installedPayload);
779
+ if (!remotePayload || !installedPayload) {
780
+ return invalid("payload summary is invalid");
781
+ }
782
+ const authority = {
783
+ schemaVersion: value.schemaVersion,
784
+ status: value.status,
785
+ remoteArtifactEvidenceDigest: value.remoteArtifactEvidenceDigest,
786
+ format: value.format,
787
+ documentSha256: value.documentSha256,
788
+ remotePayload,
789
+ installedPayload,
790
+ };
791
+ if (
792
+ authority.schemaVersion !==
793
+ PLUGIN_MARKETPLACE_REMOTE_SBOM_PAYLOAD_COMPARISON_SCHEMA ||
794
+ authority.status !== "matched" ||
795
+ !exactDigest(authority.remoteArtifactEvidenceDigest) ||
796
+ !isMarketplacePayloadSbomFormat(authority.format) ||
797
+ !exactDigest(authority.documentSha256)
798
+ ) {
799
+ return invalid("comparison authority is invalid");
800
+ }
801
+ if (
802
+ !remoteArtifactEvidence?.sbom ||
803
+ authority.remoteArtifactEvidenceDigest !==
804
+ remoteArtifactEvidence.evidenceDigest ||
805
+ authority.documentSha256 !== remoteArtifactEvidence.sbom.documentSha256 ||
806
+ authority.format !== remoteArtifactEvidence.sbom.format
807
+ ) {
808
+ return invalid("comparison is not bound to remote artifact evidence");
809
+ }
810
+ if (canonicalJson(remotePayload) !== canonicalJson(installedPayload)) {
811
+ return invalid("remote and installed payload summaries differ");
812
+ }
813
+ const expectedDigest = cleanDigest(expectedPayloadSha256);
814
+ if (expectedPayloadSha256 != null && !expectedDigest) {
815
+ return invalid("catalog payload digest is invalid");
816
+ }
817
+ if (expectedDigest && expectedDigest !== remotePayload.digest) {
818
+ return invalid("comparison is not bound to the catalog payload digest");
819
+ }
820
+ if (
821
+ !exactDigest(value.comparisonDigest) ||
822
+ value.comparisonDigest !== sha256Canonical(authority)
823
+ ) {
824
+ return invalid("comparison digest is invalid");
825
+ }
826
+ const currentPayloadMatches = currentPayloadSbom
827
+ ? canonicalJson(summarizeSbom(currentPayloadSbom)) ===
828
+ canonicalJson(installedPayload)
829
+ : false;
830
+ return {
831
+ present: true,
832
+ valid: true,
833
+ authority: { ...authority, comparisonDigest: value.comparisonDigest },
834
+ currentPayloadMatches,
835
+ reason: null,
836
+ };
837
+ }
838
+
839
+ export function assertRemoteSbomPayloadComparison(value, options = {}) {
840
+ const validation = validateRemoteSbomPayloadComparison(value, options);
841
+ if (!validation.present) return null;
842
+ if (!validation.valid) {
843
+ throw new Error(
844
+ `catalogAuthority.remoteSbomPayloadComparison is invalid: ${validation.reason}`,
845
+ );
846
+ }
847
+ return validation.authority;
848
+ }
849
+
279
850
  function normalizeExpectations(value) {
280
851
  const raw = value && typeof value === "object" ? value : {};
281
852
  return {
@@ -467,6 +1038,8 @@ function normalizeRemoteArtifactClaims(value) {
467
1038
 
468
1039
  function summarizeRemoteArtifacts({
469
1040
  evidence,
1041
+ payloadComparison,
1042
+ payloadSbom,
470
1043
  lock,
471
1044
  signature,
472
1045
  expectedRegistryOrigin,
@@ -558,6 +1131,14 @@ function summarizeRemoteArtifacts({
558
1131
  documentSha256: cleanDigest(remoteSbom?.documentSha256),
559
1132
  expectedDocumentSha256: cleanDigest(remoteSbom?.expectedDocumentSha256),
560
1133
  digestVerifiedAtInstallRecorded: sbomDigestVerified,
1134
+ payloadComparisonPresent: payloadComparison.present,
1135
+ payloadComparisonValid: payloadComparison.valid,
1136
+ payloadComparisonDigest:
1137
+ payloadComparison.authority?.comparisonDigest || null,
1138
+ remotePayload: payloadComparison.authority?.remotePayload || null,
1139
+ installedPayload: payloadComparison.authority?.installedPayload || null,
1140
+ currentPayload: summarizeSbom(payloadSbom),
1141
+ currentPayloadMatches: payloadComparison.currentPayloadMatches,
561
1142
  currentDocumentBytesAvailable: false,
562
1143
  currentDocumentRehashed: false,
563
1144
  registryOriginComparable,
@@ -651,6 +1232,57 @@ function compareRemoteSbom(expected, actual) {
651
1232
  };
652
1233
  }
653
1234
 
1235
+ function compareRemoteSbomPayload(expected, actual) {
1236
+ if (!expected.url) {
1237
+ return expected.format === PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA
1238
+ ? { status: "mismatch", comparable: true, expected, actual }
1239
+ : { status: "not-declared", comparable: false, expected, actual };
1240
+ }
1241
+ if (!isMarketplacePayloadSbomFormat(expected.format)) {
1242
+ return { status: "not-comparable", comparable: false, expected, actual };
1243
+ }
1244
+ if (actual.evidencePresent && !actual.evidenceValid) {
1245
+ return { status: "mismatch", comparable: true, expected, actual };
1246
+ }
1247
+ const comparisonRequired =
1248
+ expected.format === PLUGIN_MARKETPLACE_CANONICAL_PAYLOAD_SBOM_SCHEMA ||
1249
+ (expected.format === PLUGIN_MARKETPLACE_PAYLOAD_SBOM_SCHEMA &&
1250
+ Boolean(expected.url && expected.documentSha256));
1251
+ if (!actual.fetched) {
1252
+ return comparisonRequired
1253
+ ? { status: "mismatch", comparable: true, expected, actual }
1254
+ : { status: "not-observed", comparable: false, expected, actual };
1255
+ }
1256
+ if (!actual.payloadComparisonPresent) {
1257
+ return comparisonRequired
1258
+ ? { status: "mismatch", comparable: true, expected, actual }
1259
+ : { status: "not-observed", comparable: false, expected, actual };
1260
+ }
1261
+ if (!actual.registryOriginComparable) {
1262
+ return { status: "unbound", comparable: false, expected, actual };
1263
+ }
1264
+ const catalogPayloadMatches =
1265
+ !expected.payloadSha256 ||
1266
+ expected.payloadSha256 === actual.remotePayload?.digest;
1267
+ return {
1268
+ status:
1269
+ actual.payloadComparisonValid &&
1270
+ actual.currentPayloadMatches &&
1271
+ catalogPayloadMatches &&
1272
+ actual.registryOriginMatches
1273
+ ? "matched"
1274
+ : "mismatch",
1275
+ comparable: true,
1276
+ expected: {
1277
+ url: expected.url,
1278
+ format: expected.format,
1279
+ documentSha256: expected.documentSha256,
1280
+ payloadSha256: expected.payloadSha256,
1281
+ },
1282
+ actual,
1283
+ };
1284
+ }
1285
+
654
1286
  function normalizeCatalogAuthority(value) {
655
1287
  if (!value || typeof value !== "object") return null;
656
1288
  return {
@@ -659,6 +1291,7 @@ function normalizeCatalogAuthority(value) {
659
1291
  candidateDigest: cleanDigest(value.candidateDigest),
660
1292
  selectionDigest: cleanDigest(value.selectionDigest),
661
1293
  updateImpactDigest: cleanDigest(value.updateImpactDigest),
1294
+ publisherAuthority: normalizePublisherAuthority(value.publisherAuthority),
662
1295
  };
663
1296
  }
664
1297
 
@@ -888,6 +1521,13 @@ function sha256Canonical(value) {
888
1521
  return sha256(canonicalJson(value));
889
1522
  }
890
1523
 
1524
+ function exactObjectKeys(value, expectedKeys) {
1525
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1526
+ const keys = Object.keys(value).sort(stableCompare);
1527
+ const expected = [...expectedKeys].sort(stableCompare);
1528
+ return canonicalJson(keys) === canonicalJson(expected);
1529
+ }
1530
+
891
1531
  function canonicalJson(value) {
892
1532
  if (value == null || typeof value !== "object") return JSON.stringify(value);
893
1533
  if (Array.isArray(value)) {