chainlesschain 0.166.15 → 0.166.17

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 +3 -3
  2. package/completions/_cc +1 -1
  3. package/completions/cc.bash +1 -1
  4. package/completions/cc.fish +1 -1
  5. package/completions/cc.ps1 +1 -1
  6. package/package.json +2 -2
  7. package/src/assets/web-panel/assets/{AIOps-To-6rimH.js → AIOps-CVW2n2rh.js} +1 -1
  8. package/src/assets/web-panel/assets/{ActionButton-CpdCnTv3.js → ActionButton-DtuM163O.js} +1 -1
  9. package/src/assets/web-panel/assets/{Analytics-DK7VNES_.js → Analytics-BXvl_qHu.js} +3 -3
  10. package/src/assets/web-panel/assets/{AppLayout-NZ5FvhCx.js → AppLayout-VDBTFKOp.js} +5 -5
  11. package/src/assets/web-panel/assets/{Artifacts-NXceQPmN.js → Artifacts-BNz2xCiG.js} +1 -1
  12. package/src/assets/web-panel/assets/{Audit-B2d64DH2.js → Audit-DTu9Hn9P.js} +1 -1
  13. package/src/assets/web-panel/assets/{BackgroundAgents-ByYgvEVY.js → BackgroundAgents-D2OCPzLe.js} +1 -1
  14. package/src/assets/web-panel/assets/{Backup-BXvQLDvN.js → Backup-C9M-vG7G.js} +1 -1
  15. package/src/assets/web-panel/assets/{BaseInput-C4GZk-As.js → BaseInput-BUkCNiUQ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Chat-CvPuohL8.js → Chat-OJ2ajF7l.js} +5 -5
  17. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DTftcclX.js → ChatBubbleRenderer-BdZt76Ab.js} +1 -1
  18. package/src/assets/web-panel/assets/{Checkbox-C8dQGIqD.js → Checkbox-C1gXKDsc.js} +1 -1
  19. package/src/assets/web-panel/assets/{Codegen-DRM6S1W4.js → Codegen-BXbznLg7.js} +1 -1
  20. package/src/assets/web-panel/assets/{Col-BDM8l-r5.js → Col-CbCq5N0i.js} +1 -1
  21. package/src/assets/web-panel/assets/{Community-Bvmfi1fX.js → Community-V8Ks6Yun.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compact-DGQTCima.js → Compact-DkeHQTpQ.js} +1 -1
  23. package/src/assets/web-panel/assets/{Compliance-D_0C-2P6.js → Compliance-JeLKwn4Q.js} +1 -1
  24. package/src/assets/web-panel/assets/{Cowork-3C_q99ok.js → Cowork-azfetcBz.js} +3 -3
  25. package/src/assets/web-panel/assets/{Cron-CIiMQ1Hl.js → Cron-Q691i_Gp.js} +2 -2
  26. package/src/assets/web-panel/assets/{Crosschain-peebGnES.js → Crosschain-BLjDRbr_.js} +1 -1
  27. package/src/assets/web-panel/assets/{DID-OkX4KB9T.js → DID-D8ptXplK.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dashboard-D9lm0Wxa.js → Dashboard-Ctj2VVZL.js} +2 -2
  29. package/src/assets/web-panel/assets/{Dropdown-CitActXx.js → Dropdown-D8VgkMJ8.js} +1 -1
  30. package/src/assets/web-panel/assets/EmailListRenderer-CQPe5S6q.js +1 -0
  31. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CBWAi8XL.js → FamilyGuardDashboard-ySA-7SiN.js} +1 -1
  32. package/src/assets/web-panel/assets/{Federation-afj8jDcp.js → Federation-1PMD-fBf.js} +1 -1
  33. package/src/assets/web-panel/assets/{FormItemContext-Dabt59KY.js → FormItemContext-C_jr6U4Q.js} +1 -1
  34. package/src/assets/web-panel/assets/{GenericCardRenderer-BQpXA9kj.js → GenericCardRenderer-Bu3bKf2B.js} +1 -1
  35. package/src/assets/web-panel/assets/{Git-D2p2Moo1.js → Git-Dv4Fa_T5.js} +2 -2
  36. package/src/assets/web-panel/assets/{Governance-DkFfPykv.js → Governance-DOEy1n1k.js} +1 -1
  37. package/src/assets/web-panel/assets/{Inference-C1DKpeN2.js → Inference-DkvqzWcJ.js} +1 -1
  38. package/src/assets/web-panel/assets/{KnowledgeGraph-CjfVg4oQ.js → KnowledgeGraph-DdUJK949.js} +1 -1
  39. package/src/assets/web-panel/assets/{Logs-D-zMvdSf.js → Logs-CRelcdUZ.js} +2 -2
  40. package/src/assets/web-panel/assets/{MarkdownRenderer-jzEpGkbc.js → MarkdownRenderer-0AHbbTA4.js} +1 -1
  41. package/src/assets/web-panel/assets/{Marketplace-DfAjJLyS.js → Marketplace-BuuBKsev.js} +1 -1
  42. package/src/assets/web-panel/assets/{McpTools-BGDHQES1.js → McpTools-DlgpEmA1.js} +3 -3
  43. package/src/assets/web-panel/assets/{Memory-BYqrGAPS.js → Memory-DTfdqUGy.js} +2 -2
  44. package/src/assets/web-panel/assets/{MobileBridge-CQ6FEtty.js → MobileBridge-CXLE5ZFO.js} +1 -1
  45. package/src/assets/web-panel/assets/{MobileProjects-BpMvh5JW.js → MobileProjects-Dz25OB0n.js} +1 -1
  46. package/src/assets/web-panel/assets/{Mtc-CWH25SrU.js → Mtc-kEL0fhgD.js} +2 -2
  47. package/src/assets/web-panel/assets/{MtcAudit-D6DiLmei.js → MtcAudit-BE4UvFJU.js} +2 -2
  48. package/src/assets/web-panel/assets/{Multisig-BSxRN4jD.js → Multisig-CSApSn8c.js} +3 -3
  49. package/src/assets/web-panel/assets/{NLProgramming-C0FDPKT4.js → NLProgramming-D11FpKWx.js} +1 -1
  50. package/src/assets/web-panel/assets/{Notes-VKR78bH-.js → Notes-CDL-D2CS.js} +3 -3
  51. package/src/assets/web-panel/assets/{NotificationSettings-DLSklSZB.js → NotificationSettings-C0Q6lO-c.js} +1 -1
  52. package/src/assets/web-panel/assets/OrderTableRenderer-ZRE0OKbj.js +1 -0
  53. package/src/assets/web-panel/assets/{Organization-BB3pB27e.js → Organization-Dc-maVAq.js} +4 -4
  54. package/src/assets/web-panel/assets/{Overflow-Cr7bzwcs.js → Overflow-B5mUpSvi.js} +1 -1
  55. package/src/assets/web-panel/assets/{P2P-D1fkMMzI.js → P2P-TjXV53wq.js} +2 -2
  56. package/src/assets/web-panel/assets/PdhVaultBrowser-DvV2btIr.js +7 -0
  57. package/src/assets/web-panel/assets/{Permissions-c3Ijv0v4.js → Permissions-DKRzQ2O6.js} +4 -4
  58. package/src/assets/web-panel/assets/{PersonalDataHub-BJwbh3P2.js → PersonalDataHub-Dx2eqft8.js} +4 -4
  59. package/src/assets/web-panel/assets/{Pipeline-C4fhXafB.js → Pipeline-CnYZMB5p.js} +1 -1
  60. package/src/assets/web-panel/assets/{Privacy-DzSx3CD-.js → Privacy-BonKpPnp.js} +1 -1
  61. package/src/assets/web-panel/assets/{ProjectInit-D9fL5fED.js → ProjectInit-DWO0Du3J.js} +2 -2
  62. package/src/assets/web-panel/assets/{ProjectSettings-CB5FVrOR.js → ProjectSettings-C9jqyYRv.js} +2 -2
  63. package/src/assets/web-panel/assets/{Projects-DWhwdgfY.js → Projects-CYpKwXbz.js} +1 -1
  64. package/src/assets/web-panel/assets/{Providers-B9ZH4faS.js → Providers-CW-fkwJq.js} +1 -1
  65. package/src/assets/web-panel/assets/{QrScannerModal-BLKDHT8_.js → QrScannerModal-Cw0J3Zud.js} +1 -1
  66. package/src/assets/web-panel/assets/{QuickAsk-D1oYUcSY.js → QuickAsk-BH4A4IpR.js} +1 -1
  67. package/src/assets/web-panel/assets/{Recommend-CC2nmrOD.js → Recommend-CDwMwJus.js} +1 -1
  68. package/src/assets/web-panel/assets/{RemoteSession-qW9QF4-r.js → RemoteSession-DKIPwzTf.js} +2 -2
  69. package/src/assets/web-panel/assets/{Reputation-BDWqfzcr.js → Reputation-CklDj7qS.js} +1 -1
  70. package/src/assets/web-panel/assets/{Row-BBV2iCjT.js → Row-DJWZKsH0.js} +1 -1
  71. package/src/assets/web-panel/assets/{RssFeed-CcwrNwZB.js → RssFeed-BfSLCewo.js} +3 -3
  72. package/src/assets/web-panel/assets/{Search-Dr6fZ0D_.js → Search-B0IST--x.js} +1 -1
  73. package/src/assets/web-panel/assets/{Security-BPzaoCU6.js → Security-h1sYNWtt.js} +4 -4
  74. package/src/assets/web-panel/assets/{Services-BoKUU1I_.js → Services-Gh0-oxI_.js} +2 -2
  75. package/src/assets/web-panel/assets/{Skeleton-C8lT8GUg.js → Skeleton-CNDV_jOs.js} +1 -1
  76. package/src/assets/web-panel/assets/{Skills-DPpWeicz.js → Skills-ByCLTh1n.js} +1 -1
  77. package/src/assets/web-panel/assets/{Sla-UiijzpMU.js → Sla-BfyrJTJz.js} +1 -1
  78. package/src/assets/web-panel/assets/{SpeechSettings-DoofrLwS.js → SpeechSettings-BQzo9kNX.js} +1 -1
  79. package/src/assets/web-panel/assets/{SyncSettings-DXnyfTDw.js → SyncSettings-B6fS6z_1.js} +2 -2
  80. package/src/assets/web-panel/assets/{Tasks-g_WHjKeg.js → Tasks-C-uKCP-l.js} +1 -1
  81. package/src/assets/web-panel/assets/{Templates-DRuCh9g2.js → Templates-D6oDy7wL.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tenant-QHIq1pH1.js → Tenant-D3LT2wNQ.js} +1 -1
  83. package/src/assets/web-panel/assets/{Terminal-G0fuI216.js → Terminal-BteLcv_O.js} +2 -2
  84. package/src/assets/web-panel/assets/{TimelineRenderer-4MT7SpGk.js → TimelineRenderer-DHHuxfqH.js} +1 -1
  85. package/src/assets/web-panel/assets/{Tokens-B5xT5umA.js → Tokens-BRQeykXw.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trigger-D7HPbNgc.js → Trigger-p2XaFWEu.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trust-CxH69dU5.js → Trust-BxadBMft.js} +1 -1
  88. package/src/assets/web-panel/assets/{UkeySign-CPPosveH.js → UkeySign-DRBP7LDk.js} +1 -1
  89. package/src/assets/web-panel/assets/{VideoEditing-Du5lGwAt.js → VideoEditing-CZ3NZkh7.js} +1 -1
  90. package/src/assets/web-panel/assets/{Wallet-BBIGNESK.js → Wallet-C3NTBN7G.js} +4 -4
  91. package/src/assets/web-panel/assets/{WebAuthn-Cc3HSGmv.js → WebAuthn-C7LVgMlb.js} +5 -5
  92. package/src/assets/web-panel/assets/{WorkflowEditor-DBoVPNCh.js → WorkflowEditor-0_Iveog7.js} +1 -1
  93. package/src/assets/web-panel/assets/{chat-DHcjgiem.js → chat-DzzluYo3.js} +1 -1
  94. package/src/assets/web-panel/assets/{colors-DrRrvJIV.js → colors-DSMYXpaj.js} +1 -1
  95. package/src/assets/web-panel/assets/{compact-item-BuY_sAoN.js → compact-item-W76u1VSV.js} +1 -1
  96. package/src/assets/web-panel/assets/{createContext-CepH1Fha.js → createContext-otsESjb5.js} +1 -1
  97. package/src/assets/web-panel/assets/devWarning-BztaXF84.js +1 -0
  98. package/src/assets/web-panel/assets/{hasIn-Dh80Ot6o.js → hasIn-3sNLrvug.js} +1 -1
  99. package/src/assets/web-panel/assets/index-4jP_hG_4.js +1 -0
  100. package/src/assets/web-panel/assets/{index-LBkSOwky.js → index-B1I1AYJs.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-B0rvJjId.js → index-B5LOYdYm.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-D6tOVlq-.js → index-B9X_jqXe.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D7lstbTh.js → index-BA8mZxd-.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-BPDEOOlA.js → index-BDlDO2px.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DuSUCH7h.js → index-BJsQio5O.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CEfVkKIY.js → index-BRsD1PT2.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CYydOga6.js → index-BXLx9HfA.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-DkZP_QaB.js → index-BcGJtmHM.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BnVIvc15.js → index-BdtmEKat.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-DuRYBWrj.js → index-BpcQYzP7.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-By0qqC3S.js → index-C2aht9-M.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C10033eO.js → index-CE7tSow-.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-9crgJ3Id.js → index-CENWwjef.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CNQK9DIr.js → index-CKoFyHYQ.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-DUNyTmkS.js → index-Cg86EzNp.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-CnRK8_Dd.js → index-ClOFeZ-t.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BW9Pycxh.js → index-CtFOt5x1.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-D5_K3GKu.js → index-D7A5U2IH.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DRy3_beb.js → index-DH4cl52p.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DWANFMlq.js → index-DLDh1OW3.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-C5vhNRqS.js → index-DMmdIpLA.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Ca5d64IH.js → index-DS9WYDMf.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-zGH9zH3w.js → index-DTA8Iavv.js} +3 -3
  124. package/src/assets/web-panel/assets/{index-BoRy2yup.js → index-DZKAzEge.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-Cx21_p3m.js → index-DdU7IkKX.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-B9Gh-SOn.js → index-Ddd4mird.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CiUx-tkV.js → index-DmpaC2yk.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DYN49m7k.js → index-GlIOE4ja.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-CUnd5hyp.js → index-MhQQ3h8q.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CW_23-gU.js → index-NaO9AE-F.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-0TYLnd11.js → index-UUHCwfNq.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-NS9dW1F1.js → index-pVm_iUre.js} +1 -1
  133. package/src/assets/web-panel/assets/{index-hYaigdGm.js → index-q7ftLqNt.js} +1 -1
  134. package/src/assets/web-panel/assets/{index-A5lVwYVq.js → index-uSCkbpZH.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-BtqigRzf.js → index-vBKPcmAP.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-BiZQooWK.js → index-vUmYkqmu.js} +1 -1
  137. package/src/assets/web-panel/assets/index-zxwuAXL5.js +1 -0
  138. package/src/assets/web-panel/assets/{initDefaultProps-nI_X3wKl.js → initDefaultProps-DF6llUXV.js} +1 -1
  139. package/src/assets/web-panel/assets/{motion-LUHbQu_R.js → motion-vtDV8_5K.js} +1 -1
  140. package/src/assets/web-panel/assets/{move-DHI_KvJt.js → move-B6c0bdy2.js} +1 -1
  141. package/src/assets/web-panel/assets/{mtc-parser-AYdL_7Mo.js → mtc-parser-DlLYWuBK.js} +1 -1
  142. package/src/assets/web-panel/assets/{omit-CtV3HRKa.js → omit-BH0zOxLN.js} +1 -1
  143. package/src/assets/web-panel/assets/{pickAttrs-CcJLTOu4.js → pickAttrs-D9di8_In.js} +1 -1
  144. package/src/assets/web-panel/assets/{placementArrow-DHbiLUps.js → placementArrow-BDSngufM.js} +1 -1
  145. package/src/assets/web-panel/assets/{responsiveObserve-BC2DGkWP.js → responsiveObserve-C_EawMvP.js} +1 -1
  146. package/src/assets/web-panel/assets/{slide-B4ga4pPY.js → slide-B_4wShxA.js} +1 -1
  147. package/src/assets/web-panel/assets/{statusUtils-hzphgKfH.js → statusUtils-CbQu6qJ-.js} +1 -1
  148. package/src/assets/web-panel/assets/{styleChecker-IfN9Bxoi.js → styleChecker-CvXJWYXb.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFlexGapSupport-pcGKJ4pT.js → useFlexGapSupport-jjY1XknO.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFs-COXhQpTG.js → useFs-CMXBdsK_.js} +1 -1
  151. package/src/assets/web-panel/assets/{usePersonalDataHub-xp9F9szv.js → usePersonalDataHub-YbAhhPXx.js} +1 -1
  152. package/src/assets/web-panel/assets/{vnode-BdSQy1Ve.js → vnode-BFCTKc0H.js} +1 -1
  153. package/src/assets/web-panel/assets/{zoom-CHCt5P55.js → zoom-CY9aBl5O.js} +1 -1
  154. package/src/assets/web-panel/index.html +1 -1
  155. package/src/command-help-index.json +2 -2
  156. package/src/command-manifest.json +2 -2
  157. package/src/commands/evolution.js +30 -17
  158. package/src/commands/learning.js +95 -9
  159. package/src/commands/team.js +5 -0
  160. package/src/data/changelog.json +28 -2
  161. package/src/lib/agent-team/task-lease.js +12 -1
  162. package/src/lib/capability-manifest.js +212 -1
  163. package/src/lib/evolution/bounded-skill-improvement-pilot.js +498 -0
  164. package/src/lib/evolution/evolution-artifact-ports.js +3277 -0
  165. package/src/lib/evolution/evolution-eval-gate.js +6442 -0
  166. package/src/lib/evolution/evolution-evidence-projector.js +3786 -0
  167. package/src/lib/evolution/evolution-ledger-ports.js +2689 -0
  168. package/src/lib/evolution/evolution-ledger.js +4280 -0
  169. package/src/lib/evolution/evolution-run-runtime-adapter.js +5 -0
  170. package/src/lib/evolution/skill-candidate-registry.js +2258 -0
  171. package/src/lib/evolution/skill-evaluated-promotion-durability.js +899 -0
  172. package/src/lib/evolution/skill-evaluated-promotion.js +481 -0
  173. package/src/lib/evolution/skill-execution-manifest.js +942 -0
  174. package/src/lib/evolution/skill-mutation-authority.js +1677 -0
  175. package/src/lib/evolution/skill-promotion-controller.js +618 -0
  176. package/src/lib/evolution/skill-release-registry.js +3277 -0
  177. package/src/lib/evolution/skill-target-matrix-eval.js +3233 -0
  178. package/src/lib/evolution/skill-writer-inventory-manifest.js +998 -0
  179. package/src/lib/evolution/skill-writer-inventory-validator.js +816 -0
  180. package/src/lib/evolution/wiki-informed-skill-proposer.js +343 -0
  181. package/src/lib/formal-quality-eval-runtime.js +100 -0
  182. package/src/lib/graph-kernel/cutover-contract.js +7 -0
  183. package/src/lib/graph-kernel/cutover-ledger.js +8 -7
  184. package/src/lib/graph-kernel/evidence-digest.js +47 -0
  185. package/src/lib/graph-kernel/evolution-run-runtime-adapter.js +5 -0
  186. package/src/lib/graph-kernel/graph-runtime-surfaces.json +8 -1
  187. package/src/lib/graph-kernel/production-cutover-evidence.js +342 -15
  188. package/src/lib/graph-kernel/production-source-evidence.js +1599 -0
  189. package/src/lib/graph-kernel/retirement-evidence.js +15 -5
  190. package/src/lib/graph-kernel/runtime-surface-manifest.js +67 -2
  191. package/src/lib/headless-manifest.js +3 -1
  192. package/src/lib/hooks-v2-runtime.js +20 -0
  193. package/src/lib/learning/skill-improver.js +311 -43
  194. package/src/lib/learning/skill-synthesizer.js +236 -16
  195. package/src/lib/secure-fs.js +20 -0
  196. package/src/runtime/agent-core.js +68 -0
  197. package/src/assets/web-panel/assets/EmailListRenderer-DSmFkNKV.js +0 -1
  198. package/src/assets/web-panel/assets/OrderTableRenderer-Dpj7D6tC.js +0 -1
  199. package/src/assets/web-panel/assets/PdhVaultBrowser-BiDEbLNA.js +0 -7
  200. package/src/assets/web-panel/assets/devWarning-DIb8j04q.js +0 -1
  201. package/src/assets/web-panel/assets/index-BJc3t4PC.js +0 -1
  202. package/src/assets/web-panel/assets/index-CkksSUnI.js +0 -1
@@ -0,0 +1,2258 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { types as utilTypes } from "node:util";
5
+ import { getHomeDir } from "../paths.js";
6
+ import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
7
+ import {
8
+ verifySkillDependencyLock,
9
+ verifySkillRuntimeManifest,
10
+ verifySkillTargetMatrix,
11
+ } from "./skill-execution-manifest.js";
12
+
13
+ export const SKILL_CANDIDATE_SCHEMA = "chainlesschain.skill-candidate/v2";
14
+ export const SKILL_CANDIDATE_TENANT_MARKER_SCHEMA =
15
+ "chainlesschain.skill-candidate-tenant-marker/v1";
16
+ export const SKILL_CANDIDATE_MIGRATION_REQUIRED_CODE =
17
+ "SKILL_CANDIDATE_MIGRATION_REQUIRED";
18
+ export const SKILL_CANDIDATE_STORE_LIMIT_CODE = "SKILL_CANDIDATE_STORE_LIMIT";
19
+ export const SKILL_CANDIDATE_STATUS = "draft";
20
+ export const SKILL_CANDIDATE_CONTENT_TYPE =
21
+ "text/markdown; charset=utf-8; profile=skill";
22
+ export const SKILL_CANDIDATE_MAX_CONTENT_BYTES = 1024 * 1024;
23
+ export const SKILL_CANDIDATE_MAX_ARTIFACT_BYTES = 2 * 1024 * 1024;
24
+ export const SKILL_CANDIDATE_TARGET_MATRIX_ADMISSION_AUTHORITY_SCHEMA =
25
+ "chainlesschain.skill-candidate-target-matrix-admission-authority/v1";
26
+ export const SKILL_CANDIDATE_TARGET_MATRIX_ADMISSION_REQUEST_SCHEMA =
27
+ "chainlesschain.skill-candidate-target-matrix-admission-request/v1";
28
+ export const SKILL_CANDIDATE_TARGET_MATRIX_ADMISSION_RESOLUTION_SCHEMA =
29
+ "chainlesschain.skill-candidate-target-matrix-admission-resolution/v1";
30
+
31
+ const MAX_SOURCE_EVIDENCE_REFS = 256;
32
+ const MAX_REQUESTED_CAPABILITIES = 128;
33
+ const MAX_TARGET_RUNTIMES = 64;
34
+ export const SKILL_CANDIDATE_TENANT_SCAN_MAX_ENTRIES = 4_096;
35
+ export const SKILL_CANDIDATE_TENANT_SCAN_MAX_BYTES = 16 * 1024 * 1024;
36
+ export const SKILL_CANDIDATE_TENANT_SCAN_MAX_NODES = 100_000;
37
+ const MAX_TENANT_SCAN_DEPTH = 64;
38
+ const MAX_LIST_LIMIT = 10_000;
39
+ const DEFAULT_LIST_LIMIT = 1_000;
40
+ const MAX_TENANT_MARKER_BYTES = 4096;
41
+ const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
42
+ const TENANT_KEY_PATTERN = /^[a-f0-9]{64}$/u;
43
+ const CANDIDATE_FILE_PATTERN = /^([a-f0-9]{64})\.json$/u;
44
+ const SKILL_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
45
+ const NAMESPACED_ID_PATTERN = /^[a-z][a-z0-9]*(?:[._:-][a-z0-9]+)*$/u;
46
+ const TENANT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u;
47
+ const EVIDENCE_REF_PATTERN = /^[a-z][a-z0-9+.-]{1,31}:[^\s\\]+$/u;
48
+ const LEGACY_SKILL_CANDIDATE_SCHEMA = "chainlesschain.skill-candidate/v1";
49
+ const CANDIDATE_DIGEST_DOMAIN = "chainlesschain.skill-candidate/v2";
50
+ const TENANT_KEY_DOMAIN = "chainlesschain.skill-candidate-tenant-key/v1";
51
+ const TENANT_MARKER_DIGEST_DOMAIN =
52
+ "chainlesschain.skill-candidate-tenant-marker/v1";
53
+ const TENANT_MARKER_COMPONENT = "skill-candidate-registry";
54
+ const TENANT_MARKER_FILE = "_tenant.json";
55
+
56
+ const CANDIDATE_KEYS = new Set([
57
+ "candidateId",
58
+ "content",
59
+ "contentDigest",
60
+ "contentType",
61
+ "dependencyLock",
62
+ "dependencyLockDigest",
63
+ "derivationMode",
64
+ "evalRunId",
65
+ "parentDigest",
66
+ "proposerModel",
67
+ "requestedCapabilities",
68
+ "runtimeManifest",
69
+ "runtimeManifestDigest",
70
+ "schema",
71
+ "skillName",
72
+ "sourceEvidenceRefs",
73
+ "status",
74
+ "targetMatrix",
75
+ "targetMatrixRoot",
76
+ "targetRuntimes",
77
+ "tenantId",
78
+ "wikiRevision",
79
+ ]);
80
+
81
+ const CREATE_INPUT_KEYS = new Set([
82
+ "content",
83
+ "dependencyLock",
84
+ "derivationMode",
85
+ "evalRunId",
86
+ "parentDigest",
87
+ "proposerModel",
88
+ "requestedCapabilities",
89
+ "runtimeManifest",
90
+ "skillName",
91
+ "sourceEvidenceRefs",
92
+ "targetMatrix",
93
+ "tenantId",
94
+ "wikiRevision",
95
+ ]);
96
+
97
+ const CREATE_REQUIRED_KEYS = new Set([
98
+ "content",
99
+ "dependencyLock",
100
+ "derivationMode",
101
+ "runtimeManifest",
102
+ "skillName",
103
+ "sourceEvidenceRefs",
104
+ "targetMatrix",
105
+ "tenantId",
106
+ ]);
107
+ const TARGET_MATRIX_ADMISSION_KEYS = new Set([
108
+ "expectedEnvironmentBindings",
109
+ "expectedTargetMatrixRoot",
110
+ ]);
111
+ const TARGET_MATRIX_ADMISSION_AUTHORITY_KEYS = new Set([
112
+ "authorityId",
113
+ "handlerArtifactDigest",
114
+ "resolve",
115
+ "revision",
116
+ "schema",
117
+ "trust",
118
+ ]);
119
+ const TARGET_MATRIX_ADMISSION_RESOLUTION_KEYS = new Set([
120
+ "admitted",
121
+ "authorityId",
122
+ "dependencyLockDigest",
123
+ "expectedEnvironmentBindings",
124
+ "expectedTargetMatrixRoot",
125
+ "handlerArtifactDigest",
126
+ "revision",
127
+ "runtimeManifestDigest",
128
+ "schema",
129
+ "skillName",
130
+ "tenantId",
131
+ "trust",
132
+ ]);
133
+ const TENANT_MARKER_KEYS = new Set([
134
+ "component",
135
+ "markerDigest",
136
+ "schema",
137
+ "tenantId",
138
+ "tenantKey",
139
+ ]);
140
+ const REGISTRY_OPTION_KEYS = new Set([
141
+ "fsImpl",
142
+ "randomToken",
143
+ "rootDir",
144
+ "secure",
145
+ "tenantId",
146
+ "targetMatrixAdmissionAuthority",
147
+ ]);
148
+ const LIST_OPTION_KEYS = new Set(["limit"]);
149
+ const PROPOSER_MODEL_KEYS = new Set(["model", "provider", "version"]);
150
+ const SOURCE_EVIDENCE_KEYS = new Set(["digest", "ref"]);
151
+ const DERIVATION_MODES = new Set(["wiki", "record-replay", "manual-import"]);
152
+
153
+ export class SkillCandidateRegistryError extends Error {
154
+ constructor(code, message, details = {}) {
155
+ super(message, details.cause ? { cause: details.cause } : undefined);
156
+ this.name = "SkillCandidateRegistryError";
157
+ this.code = code;
158
+ for (const [key, value] of Object.entries(details)) {
159
+ if (key !== "cause") this[key] = value;
160
+ }
161
+ }
162
+ }
163
+
164
+ function registryError(code, message, details = {}) {
165
+ return new SkillCandidateRegistryError(code, message, details);
166
+ }
167
+
168
+ function migrationRequired(message, details = {}) {
169
+ return registryError(
170
+ SKILL_CANDIDATE_MIGRATION_REQUIRED_CODE,
171
+ message,
172
+ details,
173
+ );
174
+ }
175
+
176
+ function tenantScanLimit(message, details = {}) {
177
+ return registryError(SKILL_CANDIDATE_STORE_LIMIT_CODE, message, details);
178
+ }
179
+
180
+ function rejectProxy(value, label) {
181
+ if (
182
+ value !== null &&
183
+ (typeof value === "object" || typeof value === "function") &&
184
+ utilTypes.isProxy(value)
185
+ ) {
186
+ throw registryError(
187
+ "SKILL_CANDIDATE_INVALID",
188
+ `${label} must not be a Proxy`,
189
+ );
190
+ }
191
+ }
192
+
193
+ function isPlainObject(value) {
194
+ rejectProxy(value, "record");
195
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
196
+ const prototype = Object.getPrototypeOf(value);
197
+ return prototype === Object.prototype || prototype === null;
198
+ }
199
+
200
+ function assertPlainObject(value, label) {
201
+ if (!isPlainObject(value)) {
202
+ throw registryError(
203
+ "SKILL_CANDIDATE_INVALID",
204
+ `${label} must be a plain object`,
205
+ );
206
+ }
207
+ }
208
+
209
+ function assertDataRecord(
210
+ value,
211
+ allowed,
212
+ label,
213
+ { exact = false, required = new Set() } = {},
214
+ ) {
215
+ assertPlainObject(value, label);
216
+ const keys = Reflect.ownKeys(value);
217
+ if (
218
+ keys.some((key) => typeof key !== "string" || !allowed.has(key)) ||
219
+ keys.length > allowed.size ||
220
+ (exact && keys.length !== allowed.size) ||
221
+ [...required].some((key) => !keys.includes(key))
222
+ ) {
223
+ throw registryError(
224
+ "SKILL_CANDIDATE_INVALID",
225
+ exact || required.size > 0
226
+ ? `${label} must contain exactly the required supported fields`
227
+ : `${label} contains unsupported fields`,
228
+ );
229
+ }
230
+ for (const key of keys) {
231
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
232
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
233
+ throw registryError(
234
+ "SKILL_CANDIDATE_INVALID",
235
+ `${label}.${String(key)} must be an enumerable own data property`,
236
+ );
237
+ }
238
+ }
239
+ }
240
+
241
+ function ownData(value, key, label, fallback) {
242
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
243
+ if (!descriptor) return fallback;
244
+ if (!("value" in descriptor) || !descriptor.enumerable) {
245
+ throw registryError(
246
+ "SKILL_CANDIDATE_INVALID",
247
+ `${label}.${key} must be an enumerable own data property`,
248
+ );
249
+ }
250
+ return descriptor.value;
251
+ }
252
+
253
+ function canonicalJson(value) {
254
+ if (value === null || typeof value !== "object") {
255
+ return JSON.stringify(value);
256
+ }
257
+ if (Array.isArray(value)) {
258
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
259
+ }
260
+ return `{${Object.keys(value)
261
+ .sort()
262
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
263
+ .join(",")}}`;
264
+ }
265
+
266
+ function compareStrings(left, right) {
267
+ if (left < right) return -1;
268
+ if (left > right) return 1;
269
+ return 0;
270
+ }
271
+
272
+ function sha256(value) {
273
+ return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
274
+ }
275
+
276
+ function domainDigest(domain, value) {
277
+ const hash = crypto.createHash("sha256");
278
+ hash.update(domain, "utf8");
279
+ hash.update("\0", "utf8");
280
+ updateCanonicalHash(hash, value, { bytes: 0 });
281
+ return `sha256:${hash.digest("hex")}`;
282
+ }
283
+
284
+ function updateCanonicalHash(hash, value, state) {
285
+ const update = (fragment) => {
286
+ const bytes = Buffer.byteLength(fragment, "utf8");
287
+ if (state.bytes > SKILL_CANDIDATE_MAX_ARTIFACT_BYTES - bytes) {
288
+ throw registryError(
289
+ "SKILL_CANDIDATE_INVALID",
290
+ `candidate artifact exceeds ${SKILL_CANDIDATE_MAX_ARTIFACT_BYTES} bytes`,
291
+ );
292
+ }
293
+ state.bytes += bytes;
294
+ hash?.update(fragment, "utf8");
295
+ };
296
+ if (value === null || typeof value !== "object") {
297
+ const serialized = JSON.stringify(value);
298
+ if (typeof serialized !== "string") {
299
+ throw registryError(
300
+ "SKILL_CANDIDATE_INVALID",
301
+ "candidate artifact must be canonical JSON",
302
+ );
303
+ }
304
+ update(serialized);
305
+ return;
306
+ }
307
+ if (Array.isArray(value)) {
308
+ update("[");
309
+ for (let index = 0; index < value.length; index += 1) {
310
+ if (index > 0) update(",");
311
+ updateCanonicalHash(hash, value[index], state);
312
+ }
313
+ update("]");
314
+ return;
315
+ }
316
+ const keys = Object.keys(value).sort();
317
+ update("{");
318
+ for (let index = 0; index < keys.length; index += 1) {
319
+ if (index > 0) update(",");
320
+ const key = keys[index];
321
+ update(JSON.stringify(key));
322
+ update(":");
323
+ updateCanonicalHash(hash, value[key], state);
324
+ }
325
+ update("}");
326
+ }
327
+
328
+ function canonicalByteLength(value) {
329
+ const state = { bytes: 0 };
330
+ updateCanonicalHash(null, value, state);
331
+ return state.bytes;
332
+ }
333
+
334
+ function contentDigest(content) {
335
+ return sha256(Buffer.from(content, "utf8"));
336
+ }
337
+
338
+ function candidateDigest(core) {
339
+ return domainDigest(CANDIDATE_DIGEST_DOMAIN, core);
340
+ }
341
+
342
+ function normalizeDigest(value, label, { nullable = false } = {}) {
343
+ if (nullable && value == null) return null;
344
+ if (typeof value !== "string" || !DIGEST_PATTERN.test(value)) {
345
+ throw registryError(
346
+ "SKILL_CANDIDATE_INVALID",
347
+ `${label} must be a lowercase sha256 digest${nullable ? " or null" : ""}`,
348
+ );
349
+ }
350
+ return value;
351
+ }
352
+
353
+ function normalizeBoundedString(value, label, maxLength) {
354
+ if (
355
+ typeof value !== "string" ||
356
+ value.length < 1 ||
357
+ value.length > maxLength ||
358
+ value.trim() !== value ||
359
+ [...value].some((character) => {
360
+ const codePoint = character.codePointAt(0);
361
+ return codePoint < 0x20 || codePoint === 0x7f;
362
+ })
363
+ ) {
364
+ throw registryError(
365
+ "SKILL_CANDIDATE_INVALID",
366
+ `${label} must be a non-empty bounded string without control characters`,
367
+ );
368
+ }
369
+ return value;
370
+ }
371
+
372
+ function normalizeSkillName(value) {
373
+ const name = normalizeBoundedString(value, "skillName", 128);
374
+ if (!SKILL_NAME_PATTERN.test(name)) {
375
+ throw registryError(
376
+ "SKILL_CANDIDATE_INVALID",
377
+ "skillName must use kebab-case",
378
+ );
379
+ }
380
+ return name;
381
+ }
382
+
383
+ function normalizeTenantId(value, label = "tenantId") {
384
+ const tenantId = normalizeBoundedString(value, label, 256);
385
+ if (!TENANT_ID_PATTERN.test(tenantId)) {
386
+ throw registryError(
387
+ "SKILL_CANDIDATE_INVALID",
388
+ `${label} must be a canonical tenant identifier`,
389
+ );
390
+ }
391
+ return tenantId;
392
+ }
393
+
394
+ function normalizeContent(value) {
395
+ if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
396
+ throw registryError(
397
+ "SKILL_CANDIDATE_INVALID",
398
+ "content must be non-empty Skill Markdown without NUL bytes",
399
+ );
400
+ }
401
+ const size = Buffer.byteLength(value, "utf8");
402
+ if (size > SKILL_CANDIDATE_MAX_CONTENT_BYTES) {
403
+ throw registryError(
404
+ "SKILL_CANDIDATE_INVALID",
405
+ `content exceeds ${SKILL_CANDIDATE_MAX_CONTENT_BYTES} bytes`,
406
+ );
407
+ }
408
+ return value;
409
+ }
410
+
411
+ function normalizeNamespacedId(value, label) {
412
+ const normalized = normalizeBoundedString(value, label, 128);
413
+ if (!NAMESPACED_ID_PATTERN.test(normalized)) {
414
+ throw registryError(
415
+ "SKILL_CANDIDATE_INVALID",
416
+ `${label} must be a lowercase namespaced identifier`,
417
+ );
418
+ }
419
+ return normalized;
420
+ }
421
+
422
+ function normalizeStandardArray(value, label, maximum) {
423
+ rejectProxy(value, label);
424
+ if (
425
+ !Array.isArray(value) ||
426
+ Object.getPrototypeOf(value) !== Array.prototype ||
427
+ value.length > maximum
428
+ ) {
429
+ throw registryError(
430
+ "SKILL_CANDIDATE_INVALID",
431
+ `${label} must be a standard array with at most ${maximum} entries`,
432
+ );
433
+ }
434
+ const keys = Reflect.ownKeys(value);
435
+ if (
436
+ keys.length !== value.length + 1 ||
437
+ keys.some(
438
+ (key) =>
439
+ typeof key !== "string" ||
440
+ (key !== "length" && !/^(?:0|[1-9]\d*)$/u.test(key)),
441
+ )
442
+ ) {
443
+ throw registryError(
444
+ "SKILL_CANDIDATE_INVALID",
445
+ `${label} must be a dense bounded standard array`,
446
+ );
447
+ }
448
+ const entries = [];
449
+ for (let index = 0; index < value.length; index += 1) {
450
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
451
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
452
+ throw registryError(
453
+ "SKILL_CANDIDATE_INVALID",
454
+ `${label}[${index}] must be an enumerable own data property`,
455
+ );
456
+ }
457
+ entries.push(descriptor.value);
458
+ }
459
+ return entries;
460
+ }
461
+
462
+ function normalizeUniqueStringList(value, label, maximum) {
463
+ const normalized = normalizeStandardArray(value, label, maximum).map(
464
+ (entry) => normalizeNamespacedId(entry, label),
465
+ );
466
+ if (new Set(normalized).size !== normalized.length) {
467
+ throw registryError(
468
+ "SKILL_CANDIDATE_INVALID",
469
+ `${label} must not contain duplicates`,
470
+ );
471
+ }
472
+ return normalized.sort();
473
+ }
474
+
475
+ function normalizeSourceEvidenceRefs(value) {
476
+ const entries = normalizeStandardArray(
477
+ value,
478
+ "sourceEvidenceRefs",
479
+ MAX_SOURCE_EVIDENCE_REFS,
480
+ );
481
+ const normalized = entries.map((entry, index) => {
482
+ assertDataRecord(
483
+ entry,
484
+ SOURCE_EVIDENCE_KEYS,
485
+ `sourceEvidenceRefs[${index}]`,
486
+ { exact: true },
487
+ );
488
+ const ref = normalizeBoundedString(
489
+ ownData(entry, "ref", `sourceEvidenceRefs[${index}]`),
490
+ `sourceEvidenceRefs[${index}].ref`,
491
+ 2048,
492
+ );
493
+ if (!EVIDENCE_REF_PATTERN.test(ref)) {
494
+ throw registryError(
495
+ "SKILL_CANDIDATE_INVALID",
496
+ `sourceEvidenceRefs[${index}].ref must be an absolute opaque URI`,
497
+ );
498
+ }
499
+ return {
500
+ digest: normalizeDigest(
501
+ ownData(entry, "digest", `sourceEvidenceRefs[${index}]`),
502
+ `sourceEvidenceRefs[${index}].digest`,
503
+ ),
504
+ ref,
505
+ };
506
+ });
507
+ const keys = normalized.map((entry) => `${entry.ref}\0${entry.digest}`);
508
+ if (new Set(keys).size !== keys.length) {
509
+ throw registryError(
510
+ "SKILL_CANDIDATE_INVALID",
511
+ "sourceEvidenceRefs must not contain duplicates",
512
+ );
513
+ }
514
+ return normalized.sort(
515
+ (left, right) =>
516
+ compareStrings(left.ref, right.ref) ||
517
+ compareStrings(left.digest, right.digest),
518
+ );
519
+ }
520
+
521
+ function normalizeNullableReference(value, label) {
522
+ if (value == null) return null;
523
+ const normalized = normalizeBoundedString(value, label, 512);
524
+ if (/\s|\\/u.test(normalized)) {
525
+ throw registryError(
526
+ "SKILL_CANDIDATE_INVALID",
527
+ `${label} must be an opaque identifier, not a filesystem path`,
528
+ );
529
+ }
530
+ return normalized;
531
+ }
532
+
533
+ function normalizeProposerModel(value) {
534
+ if (value == null) return null;
535
+ assertDataRecord(value, PROPOSER_MODEL_KEYS, "proposerModel", {
536
+ exact: true,
537
+ });
538
+ return {
539
+ provider: normalizeBoundedString(
540
+ ownData(value, "provider", "proposerModel"),
541
+ "proposerModel.provider",
542
+ 128,
543
+ ),
544
+ model: normalizeBoundedString(
545
+ ownData(value, "model", "proposerModel"),
546
+ "proposerModel.model",
547
+ 256,
548
+ ),
549
+ version: normalizeBoundedString(
550
+ ownData(value, "version", "proposerModel"),
551
+ "proposerModel.version",
552
+ 128,
553
+ ),
554
+ };
555
+ }
556
+
557
+ function normalizeDerivationMode(value) {
558
+ if (!DERIVATION_MODES.has(value)) {
559
+ throw registryError(
560
+ "SKILL_CANDIDATE_INVALID",
561
+ "derivationMode must be wiki, record-replay, or manual-import",
562
+ );
563
+ }
564
+ return value;
565
+ }
566
+
567
+ function deepFreeze(value, seen = new WeakSet()) {
568
+ if (
569
+ !value ||
570
+ typeof value !== "object" ||
571
+ Object.isFrozen(value) ||
572
+ seen.has(value)
573
+ ) {
574
+ return value;
575
+ }
576
+ seen.add(value);
577
+ for (const key of Reflect.ownKeys(value)) {
578
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
579
+ if (descriptor && "value" in descriptor) deepFreeze(descriptor.value, seen);
580
+ }
581
+ return Object.freeze(value);
582
+ }
583
+
584
+ function chargeTenantScanNodes(root, budget, filePath) {
585
+ let current = root;
586
+ let depth = 0;
587
+ const frames = [];
588
+ while (true) {
589
+ budget.nodes += 1;
590
+ if (budget.nodes > SKILL_CANDIDATE_TENANT_SCAN_MAX_NODES) {
591
+ throw tenantScanLimit(
592
+ "candidate tenant scan exceeded its aggregate node budget",
593
+ { path: filePath },
594
+ );
595
+ }
596
+ if (depth > MAX_TENANT_SCAN_DEPTH) {
597
+ throw tenantScanLimit(
598
+ "candidate tenant scan exceeded its aggregate structure depth budget",
599
+ { path: filePath },
600
+ );
601
+ }
602
+
603
+ if (current !== null && typeof current === "object") {
604
+ const keys = Array.isArray(current) ? null : Object.keys(current);
605
+ const length = keys === null ? current.length : keys.length;
606
+ if (length > SKILL_CANDIDATE_TENANT_SCAN_MAX_NODES - budget.nodes) {
607
+ throw tenantScanLimit(
608
+ "candidate tenant scan exceeded its aggregate node budget",
609
+ { path: filePath },
610
+ );
611
+ }
612
+ if (length > 0) {
613
+ frames.push({ collection: current, depth, index: 1, keys });
614
+ current = keys === null ? current[0] : current[keys[0]];
615
+ depth += 1;
616
+ continue;
617
+ }
618
+ }
619
+
620
+ let advanced = false;
621
+ while (frames.length > 0) {
622
+ const frame = frames.at(-1);
623
+ const length =
624
+ frame.keys === null ? frame.collection.length : frame.keys.length;
625
+ if (frame.index < length) {
626
+ const key = frame.keys === null ? frame.index : frame.keys[frame.index];
627
+ frame.index += 1;
628
+ current = frame.collection[key];
629
+ depth = frame.depth + 1;
630
+ advanced = true;
631
+ break;
632
+ }
633
+ frames.pop();
634
+ }
635
+ if (!advanced) return;
636
+ }
637
+ }
638
+
639
+ function normalizeAdmissionContext(value) {
640
+ assertDataRecord(
641
+ value,
642
+ TARGET_MATRIX_ADMISSION_KEYS,
643
+ "target matrix admission context",
644
+ { exact: true },
645
+ );
646
+ return {
647
+ expectedEnvironmentBindings: ownData(
648
+ value,
649
+ "expectedEnvironmentBindings",
650
+ "target matrix admission context",
651
+ ),
652
+ expectedTargetMatrixRoot: ownData(
653
+ value,
654
+ "expectedTargetMatrixRoot",
655
+ "target matrix admission context",
656
+ ),
657
+ };
658
+ }
659
+
660
+ function normalizeAdmissionRevision(value, label) {
661
+ if (!Number.isSafeInteger(value) || Object.is(value, -0) || value < 1) {
662
+ throw registryError(
663
+ "SKILL_CANDIDATE_INVALID",
664
+ `${label} must be a positive safe integer`,
665
+ );
666
+ }
667
+ return value;
668
+ }
669
+
670
+ function normalizeTargetMatrixAdmissionAuthority(value) {
671
+ assertDataRecord(
672
+ value,
673
+ TARGET_MATRIX_ADMISSION_AUTHORITY_KEYS,
674
+ "target matrix admission authority",
675
+ { exact: true },
676
+ );
677
+ const schema = ownData(value, "schema", "target matrix admission authority");
678
+ const trust = ownData(value, "trust", "target matrix admission authority");
679
+ const resolver = ownData(
680
+ value,
681
+ "resolve",
682
+ "target matrix admission authority",
683
+ );
684
+ rejectProxy(resolver, "target matrix admission authority.resolve");
685
+ if (
686
+ schema !== SKILL_CANDIDATE_TARGET_MATRIX_ADMISSION_AUTHORITY_SCHEMA ||
687
+ trust !== "trusted" ||
688
+ typeof resolver !== "function"
689
+ ) {
690
+ throw registryError(
691
+ "SKILL_CANDIDATE_INVALID",
692
+ "target matrix admission authority descriptor is invalid",
693
+ );
694
+ }
695
+ const descriptor = deepFreeze({
696
+ schema,
697
+ authorityId: normalizeNamespacedId(
698
+ ownData(value, "authorityId", "target matrix admission authority"),
699
+ "target matrix admission authority.authorityId",
700
+ ),
701
+ trust,
702
+ revision: normalizeAdmissionRevision(
703
+ ownData(value, "revision", "target matrix admission authority"),
704
+ "target matrix admission authority.revision",
705
+ ),
706
+ handlerArtifactDigest: normalizeDigest(
707
+ ownData(
708
+ value,
709
+ "handlerArtifactDigest",
710
+ "target matrix admission authority",
711
+ ),
712
+ "target matrix admission authority.handlerArtifactDigest",
713
+ ),
714
+ });
715
+ const resolve = (request) => Reflect.apply(resolver, value, [request]);
716
+ Object.freeze(value);
717
+ return { descriptor, resolve };
718
+ }
719
+
720
+ function buildTargetMatrixAdmissionRequest(input) {
721
+ assertDataRecord(input, CREATE_INPUT_KEYS, "candidate input", {
722
+ required: CREATE_REQUIRED_KEYS,
723
+ });
724
+ const ownerTenantId = normalizeTenantId(
725
+ ownData(input, "tenantId", "candidate input"),
726
+ );
727
+ const name = normalizeSkillName(
728
+ ownData(input, "skillName", "candidate input"),
729
+ );
730
+ let dependencyLock;
731
+ let runtimeManifest;
732
+ try {
733
+ dependencyLock = verifySkillDependencyLock(
734
+ ownData(input, "dependencyLock", "candidate input"),
735
+ );
736
+ runtimeManifest = verifySkillRuntimeManifest(
737
+ ownData(input, "runtimeManifest", "candidate input"),
738
+ );
739
+ } catch (cause) {
740
+ throw registryError(
741
+ "SKILL_CANDIDATE_INVALID",
742
+ "candidate execution artifacts failed pre-admission verification",
743
+ { cause },
744
+ );
745
+ }
746
+ const targetMatrix = ownData(input, "targetMatrix", "candidate input");
747
+ assertPlainObject(targetMatrix, "candidate input.targetMatrix");
748
+ return {
749
+ request: deepFreeze({
750
+ schema: SKILL_CANDIDATE_TARGET_MATRIX_ADMISSION_REQUEST_SCHEMA,
751
+ tenantId: ownerTenantId,
752
+ skillName: name,
753
+ dependencyLockDigest: dependencyLock.dependencyLockDigest,
754
+ runtimeManifestDigest: runtimeManifest.runtimeManifestDigest,
755
+ proposedTargetMatrixRoot: normalizeDigest(
756
+ ownData(targetMatrix, "targetMatrixRoot", "candidate target matrix"),
757
+ "candidate targetMatrixRoot",
758
+ ),
759
+ }),
760
+ dependencyLock,
761
+ runtimeManifest,
762
+ targetMatrix,
763
+ };
764
+ }
765
+
766
+ function verifyTargetMatrixAdmissionResolution(
767
+ value,
768
+ authorityDescriptor,
769
+ request,
770
+ artifacts,
771
+ ) {
772
+ if (value === false || value === null || value === undefined) {
773
+ throw registryError(
774
+ "SKILL_CANDIDATE_ADMISSION_REJECTED",
775
+ "target matrix admission authority did not synchronously admit the candidate",
776
+ );
777
+ }
778
+ assertDataRecord(
779
+ value,
780
+ TARGET_MATRIX_ADMISSION_RESOLUTION_KEYS,
781
+ "target matrix admission resolution",
782
+ { exact: true },
783
+ );
784
+ const expectedEnvironmentBindings = ownData(
785
+ value,
786
+ "expectedEnvironmentBindings",
787
+ "target matrix admission resolution",
788
+ );
789
+ const proposedCells = ownData(
790
+ artifacts.targetMatrix,
791
+ "cells",
792
+ "candidate target matrix",
793
+ );
794
+ const resolvedCells = normalizeStandardArray(
795
+ expectedEnvironmentBindings,
796
+ "target matrix admission resolution.expectedEnvironmentBindings",
797
+ MAX_TARGET_RUNTIMES,
798
+ );
799
+ const candidateCells = normalizeStandardArray(
800
+ proposedCells,
801
+ "candidate target matrix.cells",
802
+ MAX_TARGET_RUNTIMES,
803
+ );
804
+ if (
805
+ value === artifacts.targetMatrix ||
806
+ expectedEnvironmentBindings === proposedCells ||
807
+ resolvedCells.some((cell) => candidateCells.includes(cell)) ||
808
+ ownData(value, "admitted", "target matrix admission resolution") !== true ||
809
+ ownData(value, "schema", "target matrix admission resolution") !==
810
+ SKILL_CANDIDATE_TARGET_MATRIX_ADMISSION_RESOLUTION_SCHEMA ||
811
+ ownData(value, "authorityId", "target matrix admission resolution") !==
812
+ authorityDescriptor.authorityId ||
813
+ ownData(value, "trust", "target matrix admission resolution") !==
814
+ authorityDescriptor.trust ||
815
+ ownData(value, "revision", "target matrix admission resolution") !==
816
+ authorityDescriptor.revision ||
817
+ ownData(
818
+ value,
819
+ "handlerArtifactDigest",
820
+ "target matrix admission resolution",
821
+ ) !== authorityDescriptor.handlerArtifactDigest ||
822
+ ownData(value, "tenantId", "target matrix admission resolution") !==
823
+ request.tenantId ||
824
+ ownData(value, "skillName", "target matrix admission resolution") !==
825
+ request.skillName ||
826
+ ownData(
827
+ value,
828
+ "dependencyLockDigest",
829
+ "target matrix admission resolution",
830
+ ) !== request.dependencyLockDigest ||
831
+ ownData(
832
+ value,
833
+ "runtimeManifestDigest",
834
+ "target matrix admission resolution",
835
+ ) !== request.runtimeManifestDigest ||
836
+ ownData(
837
+ value,
838
+ "expectedTargetMatrixRoot",
839
+ "target matrix admission resolution",
840
+ ) !== request.proposedTargetMatrixRoot
841
+ ) {
842
+ throw registryError(
843
+ "SKILL_CANDIDATE_ADMISSION_REJECTED",
844
+ "target matrix admission resolution is not exactly bound to its authority and request",
845
+ );
846
+ }
847
+ let verifiedTargetMatrix;
848
+ try {
849
+ verifiedTargetMatrix = verifySkillTargetMatrix(artifacts.targetMatrix, {
850
+ dependencyLock: artifacts.dependencyLock,
851
+ expectedEnvironmentBindings,
852
+ expectedTargetMatrixRoot: request.proposedTargetMatrixRoot,
853
+ runtimeManifest: artifacts.runtimeManifest,
854
+ });
855
+ } catch (cause) {
856
+ throw registryError(
857
+ "SKILL_CANDIDATE_ADMISSION_REJECTED",
858
+ "target matrix admission resolution failed exact matrix verification",
859
+ { cause },
860
+ );
861
+ }
862
+ return deepFreeze({
863
+ expectedEnvironmentBindings: verifiedTargetMatrix.cells.map((cell) => ({
864
+ ...cell,
865
+ })),
866
+ expectedTargetMatrixRoot: verifiedTargetMatrix.targetMatrixRoot,
867
+ });
868
+ }
869
+
870
+ function verifyExecutionArtifacts(input, tenantId, admissionContext) {
871
+ let dependencyLock;
872
+ let runtimeManifest;
873
+ let targetMatrix;
874
+ try {
875
+ dependencyLock = verifySkillDependencyLock(
876
+ ownData(input, "dependencyLock", "candidate input"),
877
+ );
878
+ runtimeManifest = verifySkillRuntimeManifest(
879
+ ownData(input, "runtimeManifest", "candidate input"),
880
+ );
881
+ targetMatrix = verifySkillTargetMatrix(
882
+ ownData(input, "targetMatrix", "candidate input"),
883
+ {
884
+ dependencyLock,
885
+ expectedEnvironmentBindings:
886
+ admissionContext.expectedEnvironmentBindings,
887
+ expectedTargetMatrixRoot: admissionContext.expectedTargetMatrixRoot,
888
+ runtimeManifest,
889
+ },
890
+ );
891
+ } catch (cause) {
892
+ throw registryError(
893
+ "SKILL_CANDIDATE_INVALID",
894
+ "candidate execution artifacts failed verification",
895
+ { cause },
896
+ );
897
+ }
898
+ if (
899
+ dependencyLock.tenantId !== tenantId ||
900
+ runtimeManifest.tenantId !== tenantId ||
901
+ targetMatrix.tenantId !== tenantId
902
+ ) {
903
+ throw registryError(
904
+ "SKILL_CANDIDATE_INVALID",
905
+ "candidate and execution artifacts must belong to the same tenant",
906
+ );
907
+ }
908
+ return { dependencyLock, runtimeManifest, targetMatrix };
909
+ }
910
+
911
+ function candidateCore(input, verificationContext) {
912
+ assertDataRecord(input, CREATE_INPUT_KEYS, "candidate input", {
913
+ required: CREATE_REQUIRED_KEYS,
914
+ });
915
+ const admissionContext = normalizeAdmissionContext(verificationContext);
916
+ const tenantId = normalizeTenantId(
917
+ ownData(input, "tenantId", "candidate input"),
918
+ );
919
+ const { dependencyLock, runtimeManifest, targetMatrix } =
920
+ verifyExecutionArtifacts(input, tenantId, admissionContext);
921
+ const evalRunId =
922
+ ownData(input, "evalRunId", "candidate input", null) ?? null;
923
+ if (evalRunId !== null) {
924
+ throw registryError(
925
+ "SKILL_CANDIDATE_INVALID",
926
+ "a draft candidate cannot carry an evalRunId",
927
+ );
928
+ }
929
+ const content = normalizeContent(
930
+ ownData(input, "content", "candidate input"),
931
+ );
932
+ const sourceEvidenceRefs = normalizeSourceEvidenceRefs(
933
+ ownData(input, "sourceEvidenceRefs", "candidate input", []),
934
+ );
935
+ const derivationMode = normalizeDerivationMode(
936
+ ownData(input, "derivationMode", "candidate input"),
937
+ );
938
+ const wikiRevision = normalizeNullableReference(
939
+ ownData(input, "wikiRevision", "candidate input", null) ?? null,
940
+ "wikiRevision",
941
+ );
942
+ const proposerModel = normalizeProposerModel(
943
+ ownData(input, "proposerModel", "candidate input", null) ?? null,
944
+ );
945
+ if (sourceEvidenceRefs.length === 0) {
946
+ throw registryError(
947
+ "SKILL_CANDIDATE_INVALID",
948
+ "a draft candidate must reference at least one digest-bound source evidence artifact",
949
+ );
950
+ }
951
+ if (
952
+ derivationMode === "wiki" &&
953
+ (wikiRevision === null || proposerModel === null)
954
+ ) {
955
+ throw registryError(
956
+ "SKILL_CANDIDATE_INVALID",
957
+ "a wiki-derived draft requires wikiRevision and proposerModel",
958
+ );
959
+ }
960
+ if (derivationMode !== "wiki" && wikiRevision !== null) {
961
+ throw registryError(
962
+ "SKILL_CANDIDATE_INVALID",
963
+ "record-replay and manual-import drafts must not claim a wikiRevision",
964
+ );
965
+ }
966
+ return {
967
+ schema: SKILL_CANDIDATE_SCHEMA,
968
+ status: SKILL_CANDIDATE_STATUS,
969
+ tenantId,
970
+ skillName: normalizeSkillName(
971
+ ownData(input, "skillName", "candidate input"),
972
+ ),
973
+ parentDigest: normalizeDigest(
974
+ ownData(input, "parentDigest", "candidate input", null) ?? null,
975
+ "parentDigest",
976
+ { nullable: true },
977
+ ),
978
+ contentDigest: contentDigest(content),
979
+ sourceEvidenceRefs,
980
+ derivationMode,
981
+ wikiRevision,
982
+ proposerModel,
983
+ dependencyLock,
984
+ dependencyLockDigest: dependencyLock.dependencyLockDigest,
985
+ runtimeManifest,
986
+ runtimeManifestDigest: runtimeManifest.runtimeManifestDigest,
987
+ targetMatrix,
988
+ targetMatrixRoot: targetMatrix.targetMatrixRoot,
989
+ targetRuntimes: normalizeUniqueStringList(
990
+ [...targetMatrix.targetRuntimes],
991
+ "targetRuntimes",
992
+ MAX_TARGET_RUNTIMES,
993
+ ),
994
+ requestedCapabilities: normalizeUniqueStringList(
995
+ ownData(input, "requestedCapabilities", "candidate input", []),
996
+ "requestedCapabilities",
997
+ MAX_REQUESTED_CAPABILITIES,
998
+ ),
999
+ evalRunId,
1000
+ contentType: SKILL_CANDIDATE_CONTENT_TYPE,
1001
+ content,
1002
+ };
1003
+ }
1004
+
1005
+ /** Build the only artifact shape accepted by the candidate registry. */
1006
+ export function buildSkillCandidateDraft(input, verificationContext) {
1007
+ const core = candidateCore(input, verificationContext);
1008
+ const candidate = deepFreeze({
1009
+ candidateId: candidateDigest(core),
1010
+ ...core,
1011
+ });
1012
+ serializeCandidate(candidate);
1013
+ return candidate;
1014
+ }
1015
+
1016
+ /** Recompute every derived field and reject non-canonical candidate objects. */
1017
+ export function verifySkillCandidateDraft(candidate, verificationContext) {
1018
+ assertPlainObject(candidate, "candidate artifact");
1019
+ const schema = ownData(candidate, "schema", "candidate artifact");
1020
+ if (schema === LEGACY_SKILL_CANDIDATE_SCHEMA) {
1021
+ throw migrationRequired(
1022
+ "legacy SkillCandidate v1 requires explicit tenant-scoped migration",
1023
+ );
1024
+ }
1025
+ assertDataRecord(candidate, CANDIDATE_KEYS, "candidate artifact", {
1026
+ exact: true,
1027
+ });
1028
+ if (
1029
+ schema !== SKILL_CANDIDATE_SCHEMA ||
1030
+ ownData(candidate, "status", "candidate artifact") !==
1031
+ SKILL_CANDIDATE_STATUS ||
1032
+ ownData(candidate, "contentType", "candidate artifact") !==
1033
+ SKILL_CANDIDATE_CONTENT_TYPE
1034
+ ) {
1035
+ throw registryError(
1036
+ "SKILL_CANDIDATE_INVALID",
1037
+ "candidate artifact schema, status, or content type is invalid",
1038
+ );
1039
+ }
1040
+ const targetMatrix = ownData(candidate, "targetMatrix", "candidate artifact");
1041
+ assertPlainObject(targetMatrix, "candidate artifact.targetMatrix");
1042
+ const admissionContext =
1043
+ verificationContext === undefined
1044
+ ? {
1045
+ expectedEnvironmentBindings: ownData(
1046
+ targetMatrix,
1047
+ "cells",
1048
+ "candidate artifact.targetMatrix",
1049
+ ),
1050
+ expectedTargetMatrixRoot: ownData(
1051
+ targetMatrix,
1052
+ "targetMatrixRoot",
1053
+ "candidate artifact.targetMatrix",
1054
+ ),
1055
+ }
1056
+ : verificationContext;
1057
+ const normalized = buildSkillCandidateDraft(
1058
+ {
1059
+ tenantId: ownData(candidate, "tenantId", "candidate artifact"),
1060
+ skillName: ownData(candidate, "skillName", "candidate artifact"),
1061
+ parentDigest: ownData(candidate, "parentDigest", "candidate artifact"),
1062
+ sourceEvidenceRefs: ownData(
1063
+ candidate,
1064
+ "sourceEvidenceRefs",
1065
+ "candidate artifact",
1066
+ ),
1067
+ derivationMode: ownData(
1068
+ candidate,
1069
+ "derivationMode",
1070
+ "candidate artifact",
1071
+ ),
1072
+ wikiRevision: ownData(candidate, "wikiRevision", "candidate artifact"),
1073
+ proposerModel: ownData(candidate, "proposerModel", "candidate artifact"),
1074
+ requestedCapabilities: ownData(
1075
+ candidate,
1076
+ "requestedCapabilities",
1077
+ "candidate artifact",
1078
+ ),
1079
+ evalRunId: ownData(candidate, "evalRunId", "candidate artifact"),
1080
+ content: ownData(candidate, "content", "candidate artifact"),
1081
+ dependencyLock: ownData(
1082
+ candidate,
1083
+ "dependencyLock",
1084
+ "candidate artifact",
1085
+ ),
1086
+ runtimeManifest: ownData(
1087
+ candidate,
1088
+ "runtimeManifest",
1089
+ "candidate artifact",
1090
+ ),
1091
+ targetMatrix,
1092
+ },
1093
+ admissionContext,
1094
+ );
1095
+ normalizeCandidateId(ownData(candidate, "candidateId", "candidate artifact"));
1096
+ normalizeDigest(
1097
+ ownData(candidate, "contentDigest", "candidate artifact"),
1098
+ "contentDigest",
1099
+ );
1100
+ normalizeDigest(
1101
+ ownData(candidate, "dependencyLockDigest", "candidate artifact"),
1102
+ "dependencyLockDigest",
1103
+ );
1104
+ normalizeDigest(
1105
+ ownData(candidate, "runtimeManifestDigest", "candidate artifact"),
1106
+ "runtimeManifestDigest",
1107
+ );
1108
+ normalizeDigest(
1109
+ ownData(candidate, "targetMatrixRoot", "candidate artifact"),
1110
+ "targetMatrixRoot",
1111
+ );
1112
+ normalizeUniqueStringList(
1113
+ ownData(candidate, "targetRuntimes", "candidate artifact"),
1114
+ "targetRuntimes",
1115
+ MAX_TARGET_RUNTIMES,
1116
+ );
1117
+ if (
1118
+ ownData(candidate, "candidateId", "candidate artifact") !==
1119
+ normalized.candidateId ||
1120
+ !serializeCandidate(candidate).equals(serializeCandidate(normalized))
1121
+ ) {
1122
+ throw registryError(
1123
+ "SKILL_CANDIDATE_INVALID",
1124
+ "candidate artifact digest verification failed",
1125
+ );
1126
+ }
1127
+ return normalized;
1128
+ }
1129
+
1130
+ function serializeCandidate(candidate) {
1131
+ const canonicalBytes = canonicalByteLength(candidate);
1132
+ if (canonicalBytes >= SKILL_CANDIDATE_MAX_ARTIFACT_BYTES) {
1133
+ throw registryError(
1134
+ "SKILL_CANDIDATE_INVALID",
1135
+ `candidate artifact exceeds ${SKILL_CANDIDATE_MAX_ARTIFACT_BYTES} bytes`,
1136
+ );
1137
+ }
1138
+ const bytes = Buffer.from(`${canonicalJson(candidate)}\n`, "utf8");
1139
+ if (bytes.length > SKILL_CANDIDATE_MAX_ARTIFACT_BYTES) {
1140
+ throw registryError(
1141
+ "SKILL_CANDIDATE_INVALID",
1142
+ `candidate artifact exceeds ${SKILL_CANDIDATE_MAX_ARTIFACT_BYTES} bytes`,
1143
+ );
1144
+ }
1145
+ return bytes;
1146
+ }
1147
+
1148
+ function normalizeCandidateId(value) {
1149
+ return normalizeDigest(value, "candidateId");
1150
+ }
1151
+
1152
+ function entryIdentity(stat) {
1153
+ return `${String(stat.dev)}:${String(stat.ino)}`;
1154
+ }
1155
+
1156
+ function realpath(fsImpl, value) {
1157
+ const implementation = fsImpl.realpathSync?.native || fsImpl.realpathSync;
1158
+ if (typeof implementation !== "function") {
1159
+ throw registryError(
1160
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1161
+ "filesystem realpath support is unavailable",
1162
+ );
1163
+ }
1164
+ return path.resolve(implementation(value));
1165
+ }
1166
+
1167
+ function samePath(left, right) {
1168
+ const resolvedLeft = path.resolve(left);
1169
+ const resolvedRight = path.resolve(right);
1170
+ return process.platform === "win32"
1171
+ ? resolvedLeft.toLowerCase() === resolvedRight.toLowerCase()
1172
+ : resolvedLeft === resolvedRight;
1173
+ }
1174
+
1175
+ function isContained(root, candidate) {
1176
+ const relation = path.relative(root, candidate);
1177
+ return (
1178
+ relation === "" ||
1179
+ (relation !== ".." &&
1180
+ !relation.startsWith(`..${path.sep}`) &&
1181
+ !path.isAbsolute(relation))
1182
+ );
1183
+ }
1184
+
1185
+ function fsyncDirectory(fsImpl, directory) {
1186
+ let descriptor = null;
1187
+ try {
1188
+ descriptor = fsImpl.openSync(directory, "r");
1189
+ fsImpl.fsyncSync(descriptor);
1190
+ return true;
1191
+ } catch (error) {
1192
+ if (
1193
+ process.platform === "win32" &&
1194
+ ["EACCES", "EINVAL", "EISDIR", "EPERM"].includes(error?.code)
1195
+ ) {
1196
+ return false;
1197
+ }
1198
+ throw error;
1199
+ } finally {
1200
+ if (descriptor !== null) fsImpl.closeSync(descriptor);
1201
+ }
1202
+ }
1203
+
1204
+ export function deriveSkillCandidateTenantKey(tenantId) {
1205
+ const normalized = normalizeTenantId(tenantId);
1206
+ return crypto
1207
+ .createHash("sha256")
1208
+ .update(TENANT_KEY_DOMAIN, "utf8")
1209
+ .update("\0", "utf8")
1210
+ .update(normalized, "utf8")
1211
+ .digest("hex");
1212
+ }
1213
+
1214
+ function buildTenantMarker(tenantId, tenantKey) {
1215
+ const core = {
1216
+ schema: SKILL_CANDIDATE_TENANT_MARKER_SCHEMA,
1217
+ component: TENANT_MARKER_COMPONENT,
1218
+ tenantId,
1219
+ tenantKey,
1220
+ };
1221
+ return deepFreeze({
1222
+ ...core,
1223
+ markerDigest: domainDigest(TENANT_MARKER_DIGEST_DOMAIN, core),
1224
+ });
1225
+ }
1226
+
1227
+ function serializeTenantMarker(marker) {
1228
+ const bytes = Buffer.from(`${canonicalJson(marker)}\n`, "utf8");
1229
+ if (bytes.length <= 1 || bytes.length > MAX_TENANT_MARKER_BYTES) {
1230
+ throw registryError(
1231
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1232
+ "candidate tenant marker exceeds its byte budget",
1233
+ );
1234
+ }
1235
+ return bytes;
1236
+ }
1237
+
1238
+ function verifyTenantMarker(value, expectedTenantId, expectedTenantKey) {
1239
+ try {
1240
+ assertDataRecord(value, TENANT_MARKER_KEYS, "candidate tenant marker", {
1241
+ exact: true,
1242
+ });
1243
+ const schema = ownData(value, "schema", "candidate tenant marker");
1244
+ const component = ownData(value, "component", "candidate tenant marker");
1245
+ const tenantId = normalizeTenantId(
1246
+ ownData(value, "tenantId", "candidate tenant marker"),
1247
+ "candidate tenant marker tenantId",
1248
+ );
1249
+ const tenantKey = ownData(value, "tenantKey", "candidate tenant marker");
1250
+ if (
1251
+ schema !== SKILL_CANDIDATE_TENANT_MARKER_SCHEMA ||
1252
+ component !== TENANT_MARKER_COMPONENT ||
1253
+ typeof tenantKey !== "string" ||
1254
+ !TENANT_KEY_PATTERN.test(tenantKey)
1255
+ ) {
1256
+ throw new Error("marker contract mismatch");
1257
+ }
1258
+ const normalized = buildTenantMarker(tenantId, tenantKey);
1259
+ if (
1260
+ ownData(value, "markerDigest", "candidate tenant marker") !==
1261
+ normalized.markerDigest ||
1262
+ tenantId !== expectedTenantId ||
1263
+ tenantKey !== expectedTenantKey ||
1264
+ tenantKey !== deriveSkillCandidateTenantKey(tenantId)
1265
+ ) {
1266
+ throw new Error("marker binding mismatch");
1267
+ }
1268
+ return normalized;
1269
+ } catch (cause) {
1270
+ if (
1271
+ cause instanceof SkillCandidateRegistryError &&
1272
+ cause.code === "SKILL_CANDIDATE_STORE_UNSAFE"
1273
+ ) {
1274
+ throw cause;
1275
+ }
1276
+ throw registryError(
1277
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1278
+ "candidate tenant marker is invalid or belongs to another tenant",
1279
+ { cause },
1280
+ );
1281
+ }
1282
+ }
1283
+
1284
+ function lstatOrNull(fsImpl, target) {
1285
+ try {
1286
+ return fsImpl.lstatSync(target);
1287
+ } catch (error) {
1288
+ if (error?.code === "ENOENT") return null;
1289
+ throw error;
1290
+ }
1291
+ }
1292
+
1293
+ function normalizeRegistryOptions(options) {
1294
+ assertDataRecord(
1295
+ options,
1296
+ REGISTRY_OPTION_KEYS,
1297
+ "candidate registry options",
1298
+ {
1299
+ required: new Set(["targetMatrixAdmissionAuthority", "tenantId"]),
1300
+ },
1301
+ );
1302
+ const tenantId = normalizeTenantId(
1303
+ ownData(options, "tenantId", "candidate registry options"),
1304
+ );
1305
+ const rootDir = ownData(
1306
+ options,
1307
+ "rootDir",
1308
+ "candidate registry options",
1309
+ path.join(getHomeDir(), "evolution", "registry", "candidates"),
1310
+ );
1311
+ if (typeof rootDir !== "string" || rootDir.length < 1) {
1312
+ throw registryError(
1313
+ "SKILL_CANDIDATE_INVALID",
1314
+ "candidate registry rootDir must be a non-empty path string",
1315
+ );
1316
+ }
1317
+ const secure = ownData(options, "secure", "candidate registry options", true);
1318
+ const fsImpl = ownData(options, "fsImpl", "candidate registry options", fs);
1319
+ const randomToken = ownData(
1320
+ options,
1321
+ "randomToken",
1322
+ "candidate registry options",
1323
+ () => crypto.randomBytes(16).toString("hex"),
1324
+ );
1325
+ const targetMatrixAdmissionAuthority =
1326
+ normalizeTargetMatrixAdmissionAuthority(
1327
+ ownData(
1328
+ options,
1329
+ "targetMatrixAdmissionAuthority",
1330
+ "candidate registry options",
1331
+ ),
1332
+ );
1333
+ rejectProxy(fsImpl, "candidate registry fsImpl");
1334
+ rejectProxy(randomToken, "candidate registry randomToken");
1335
+ if (
1336
+ (secure !== true && secure !== false) ||
1337
+ !fsImpl ||
1338
+ typeof fsImpl !== "object" ||
1339
+ typeof randomToken !== "function"
1340
+ ) {
1341
+ throw registryError(
1342
+ "SKILL_CANDIDATE_INVALID",
1343
+ "candidate registry options are invalid",
1344
+ );
1345
+ }
1346
+ return {
1347
+ fsImpl,
1348
+ randomToken,
1349
+ rootDir,
1350
+ secure,
1351
+ targetMatrixAdmissionAuthority,
1352
+ tenantId,
1353
+ };
1354
+ }
1355
+
1356
+ /**
1357
+ * Immutable, candidate-only Skill artifact storage.
1358
+ *
1359
+ * This class deliberately has no active pointer or promotion API. A later
1360
+ * promotion controller can consume verified drafts through read().
1361
+ */
1362
+ export class SkillCandidateRegistry {
1363
+ constructor(options) {
1364
+ const {
1365
+ fsImpl,
1366
+ randomToken,
1367
+ rootDir,
1368
+ secure,
1369
+ targetMatrixAdmissionAuthority,
1370
+ tenantId,
1371
+ } = normalizeRegistryOptions(options);
1372
+ this._fs = fsImpl;
1373
+ this._secure = secure;
1374
+ this._randomToken = randomToken;
1375
+ this._resolveTargetMatrixAdmission = targetMatrixAdmissionAuthority.resolve;
1376
+ this.targetMatrixAdmissionAuthority =
1377
+ targetMatrixAdmissionAuthority.descriptor;
1378
+ this.tenantId = tenantId;
1379
+ this.tenantKey = deriveSkillCandidateTenantKey(tenantId);
1380
+ const requestedBase = path.resolve(rootDir);
1381
+ try {
1382
+ const base = this._initializeDirectory(requestedBase, {
1383
+ recursive: true,
1384
+ });
1385
+ this.baseDir = base.path;
1386
+ this._assertNoLegacyBaseLayout();
1387
+ const tenants = this._initializeDirectory(
1388
+ path.join(this.baseDir, "tenants"),
1389
+ { parent: base },
1390
+ );
1391
+ const tenantRoot = this._initializeDirectory(
1392
+ path.join(tenants.path, this.tenantKey),
1393
+ { parent: tenants },
1394
+ );
1395
+ this.rootDir = tenantRoot.path;
1396
+ this._directories = deepFreeze({ base, tenantRoot, tenants });
1397
+ this._assertDirectories();
1398
+ this._markerPath = path.join(this.rootDir, TENANT_MARKER_FILE);
1399
+ this._initializeTenantMarker();
1400
+ this._assertBoundary();
1401
+ this._assertNoMixedTenantArtifacts();
1402
+ } catch (error) {
1403
+ if (error instanceof SkillCandidateRegistryError) throw error;
1404
+ throw registryError(
1405
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1406
+ "candidate registry root could not be initialized safely",
1407
+ { cause: error },
1408
+ );
1409
+ }
1410
+ Object.freeze(this);
1411
+ }
1412
+
1413
+ _initializeDirectory(
1414
+ requestedPath,
1415
+ { parent = null, recursive = false } = {},
1416
+ ) {
1417
+ const before = lstatOrNull(this._fs, requestedPath);
1418
+ if (before && (!before.isDirectory() || before.isSymbolicLink())) {
1419
+ throw registryError(
1420
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1421
+ "candidate registry directory path is unsafe",
1422
+ );
1423
+ }
1424
+ if (!before) {
1425
+ this._fs.mkdirSync(requestedPath, { recursive, mode: 0o700 });
1426
+ }
1427
+ let stat = this._fs.lstatSync(requestedPath);
1428
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
1429
+ throw registryError(
1430
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1431
+ "candidate registry directory must be non-symlink",
1432
+ );
1433
+ }
1434
+ const capturedIdentity = entryIdentity(stat);
1435
+ const canonical = realpath(this._fs, requestedPath);
1436
+ const canonicalStat = this._fs.lstatSync(canonical);
1437
+ const canonicalParent = realpath(this._fs, path.dirname(requestedPath));
1438
+ const expectedCanonical = path.join(
1439
+ canonicalParent,
1440
+ path.basename(requestedPath),
1441
+ );
1442
+ if (
1443
+ !canonicalStat.isDirectory() ||
1444
+ canonicalStat.isSymbolicLink() ||
1445
+ entryIdentity(canonicalStat) !== capturedIdentity ||
1446
+ !samePath(canonical, expectedCanonical) ||
1447
+ (parent &&
1448
+ (!isContained(parent.path, canonical) ||
1449
+ !samePath(path.dirname(canonical), parent.path)))
1450
+ ) {
1451
+ throw registryError(
1452
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1453
+ "candidate registry directory escaped its canonical parent",
1454
+ );
1455
+ }
1456
+ if (this._secure) {
1457
+ ensurePrivateDirectory(canonical, {
1458
+ applyWindowsAcl: true,
1459
+ failIfUnavailable: true,
1460
+ });
1461
+ stat = this._fs.lstatSync(canonical);
1462
+ if (
1463
+ !stat.isDirectory() ||
1464
+ stat.isSymbolicLink() ||
1465
+ entryIdentity(stat) !== capturedIdentity ||
1466
+ !samePath(realpath(this._fs, canonical), canonical)
1467
+ ) {
1468
+ throw registryError(
1469
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1470
+ "candidate registry directory changed during permission hardening",
1471
+ );
1472
+ }
1473
+ }
1474
+ return deepFreeze({ path: canonical, identity: capturedIdentity });
1475
+ }
1476
+
1477
+ _assertNoLegacyBaseLayout() {
1478
+ const legacyName = this._readDirectoryEntryNamesBounded(
1479
+ this.baseDir,
1480
+ "candidate registry base",
1481
+ ).find(
1482
+ (name) =>
1483
+ CANDIDATE_FILE_PATTERN.test(name) ||
1484
+ name === TENANT_MARKER_FILE ||
1485
+ name.startsWith(".candidate-"),
1486
+ );
1487
+ if (legacyName) {
1488
+ throw migrationRequired(
1489
+ "legacy unscoped SkillCandidate storage requires explicit migration",
1490
+ { path: path.join(this.baseDir, legacyName) },
1491
+ );
1492
+ }
1493
+ }
1494
+
1495
+ _readDirectoryEntryNamesBounded(directory, label) {
1496
+ if (typeof this._fs.opendirSync !== "function") {
1497
+ throw registryError(
1498
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1499
+ `${label} requires bounded synchronous directory enumeration`,
1500
+ );
1501
+ }
1502
+ let handle = null;
1503
+ let enumerationError = null;
1504
+ const names = [];
1505
+ try {
1506
+ handle = this._fs.opendirSync(directory);
1507
+ if (
1508
+ !handle ||
1509
+ typeof handle.readSync !== "function" ||
1510
+ typeof handle.closeSync !== "function"
1511
+ ) {
1512
+ throw registryError(
1513
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1514
+ `${label} returned an unsafe directory handle`,
1515
+ );
1516
+ }
1517
+ while (true) {
1518
+ const entry = handle.readSync();
1519
+ if (entry === null) break;
1520
+ if (!entry || typeof entry.name !== "string" || entry.name.length < 1) {
1521
+ throw registryError(
1522
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1523
+ `${label} returned an invalid directory entry`,
1524
+ );
1525
+ }
1526
+ if (names.length >= SKILL_CANDIDATE_TENANT_SCAN_MAX_ENTRIES) {
1527
+ throw tenantScanLimit(
1528
+ `${label} contains more than ${SKILL_CANDIDATE_TENANT_SCAN_MAX_ENTRIES} entries`,
1529
+ );
1530
+ }
1531
+ names.push(entry.name);
1532
+ }
1533
+ } catch (cause) {
1534
+ enumerationError =
1535
+ cause instanceof SkillCandidateRegistryError
1536
+ ? cause
1537
+ : registryError(
1538
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1539
+ `${label} could not be enumerated safely`,
1540
+ { cause },
1541
+ );
1542
+ }
1543
+ if (handle !== null && typeof handle.closeSync === "function") {
1544
+ try {
1545
+ handle.closeSync();
1546
+ } catch (cause) {
1547
+ enumerationError = registryError(
1548
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1549
+ `${label} directory handle could not be closed safely`,
1550
+ { cause, enumerationError },
1551
+ );
1552
+ }
1553
+ }
1554
+ if (enumerationError !== null) throw enumerationError;
1555
+ return names;
1556
+ }
1557
+
1558
+ _assertDirectories() {
1559
+ try {
1560
+ for (const entry of Object.values(this._directories)) {
1561
+ const stat = this._fs.lstatSync(entry.path);
1562
+ if (
1563
+ !stat.isDirectory() ||
1564
+ stat.isSymbolicLink() ||
1565
+ entryIdentity(stat) !== entry.identity ||
1566
+ !samePath(realpath(this._fs, entry.path), entry.path)
1567
+ ) {
1568
+ throw registryError(
1569
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1570
+ "candidate registry directory changed or became unsafe",
1571
+ );
1572
+ }
1573
+ }
1574
+ if (
1575
+ !samePath(path.dirname(this._directories.tenants.path), this.baseDir) ||
1576
+ !samePath(path.dirname(this.rootDir), this._directories.tenants.path) ||
1577
+ !isContained(this.baseDir, this.rootDir)
1578
+ ) {
1579
+ throw registryError(
1580
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1581
+ "candidate registry directory topology changed",
1582
+ );
1583
+ }
1584
+ } catch (error) {
1585
+ if (error instanceof SkillCandidateRegistryError) throw error;
1586
+ throw registryError(
1587
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1588
+ "candidate registry directory boundary is unavailable",
1589
+ { cause: error },
1590
+ );
1591
+ }
1592
+ }
1593
+
1594
+ _token(code) {
1595
+ const token = String(this._randomToken());
1596
+ if (!/^[a-f0-9]{32}$/u.test(token)) {
1597
+ throw registryError(code, "candidate registry random token is invalid");
1598
+ }
1599
+ return token;
1600
+ }
1601
+
1602
+ _initializeTenantMarker() {
1603
+ const existing = lstatOrNull(this._fs, this._markerPath);
1604
+ if (existing) {
1605
+ const verified = this._readAndVerifyTenantMarker();
1606
+ this._markerIdentity = verified.identity;
1607
+ return;
1608
+ }
1609
+ const entries = this._readDirectoryEntryNamesBounded(
1610
+ this.rootDir,
1611
+ "candidate tenant root",
1612
+ );
1613
+ if (entries.length !== 0) {
1614
+ throw migrationRequired(
1615
+ "unmarked or mixed-schema tenant candidate storage requires explicit migration",
1616
+ );
1617
+ }
1618
+ const marker = buildTenantMarker(this.tenantId, this.tenantKey);
1619
+ const bytes = serializeTenantMarker(marker);
1620
+ const temporaryPath = path.join(
1621
+ this.rootDir,
1622
+ `.tenant-${process.pid}-${this._token("SKILL_CANDIDATE_STORE_UNSAFE")}.tmp`,
1623
+ );
1624
+ let descriptor = null;
1625
+ let temporaryExists = false;
1626
+ try {
1627
+ descriptor = this._fs.openSync(temporaryPath, "wx", 0o600);
1628
+ temporaryExists = true;
1629
+ this._fs.writeFileSync(descriptor, bytes);
1630
+ this._fs.fsyncSync(descriptor);
1631
+ const written = this._fs.fstatSync(descriptor);
1632
+ if (
1633
+ !written.isFile() ||
1634
+ Number(written.nlink) !== 1 ||
1635
+ written.size !== bytes.length
1636
+ ) {
1637
+ throw registryError(
1638
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1639
+ "candidate tenant marker temporary file is unsafe",
1640
+ );
1641
+ }
1642
+ const writtenIdentity = entryIdentity(written);
1643
+ this._fs.closeSync(descriptor);
1644
+ descriptor = null;
1645
+ if (this._secure) {
1646
+ ensurePrivateFile(temporaryPath, {
1647
+ applyWindowsAcl: true,
1648
+ failIfUnavailable: true,
1649
+ });
1650
+ }
1651
+ const staged = this._fs.lstatSync(temporaryPath);
1652
+ if (
1653
+ !staged.isFile() ||
1654
+ staged.isSymbolicLink() ||
1655
+ Number(staged.nlink) !== 1 ||
1656
+ staged.size !== bytes.length ||
1657
+ entryIdentity(staged) !== writtenIdentity ||
1658
+ !samePath(realpath(this._fs, temporaryPath), temporaryPath)
1659
+ ) {
1660
+ throw registryError(
1661
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1662
+ "candidate tenant marker changed before publication",
1663
+ );
1664
+ }
1665
+ try {
1666
+ this._fs.linkSync(temporaryPath, this._markerPath);
1667
+ } catch (error) {
1668
+ if (error?.code !== "EEXIST") throw error;
1669
+ this._fs.unlinkSync(temporaryPath);
1670
+ temporaryExists = false;
1671
+ const verified = this._readAndVerifyTenantMarker();
1672
+ this._markerIdentity = verified.identity;
1673
+ return;
1674
+ }
1675
+ const linked = this._fs.lstatSync(this._markerPath);
1676
+ if (
1677
+ !linked.isFile() ||
1678
+ linked.isSymbolicLink() ||
1679
+ Number(linked.nlink) !== 2 ||
1680
+ entryIdentity(linked) !== writtenIdentity
1681
+ ) {
1682
+ throw registryError(
1683
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1684
+ "candidate tenant marker publication was unsafe",
1685
+ );
1686
+ }
1687
+ this._fs.unlinkSync(temporaryPath);
1688
+ temporaryExists = false;
1689
+ fsyncDirectory(this._fs, this.rootDir);
1690
+ const verified = this._readAndVerifyTenantMarker();
1691
+ this._markerIdentity = verified.identity;
1692
+ } finally {
1693
+ if (descriptor !== null) {
1694
+ try {
1695
+ this._fs.closeSync(descriptor);
1696
+ } catch {
1697
+ // The marker was not published through the authoritative path yet.
1698
+ }
1699
+ }
1700
+ if (temporaryExists) {
1701
+ try {
1702
+ this._fs.unlinkSync(temporaryPath);
1703
+ } catch {
1704
+ // Fail closed on the next open if a hard-linked marker remains.
1705
+ }
1706
+ }
1707
+ }
1708
+ }
1709
+
1710
+ _readBoundedRegularFile(filePath, maximum, code, label) {
1711
+ let descriptor = null;
1712
+ try {
1713
+ const before = this._fs.lstatSync(filePath);
1714
+ if (
1715
+ !before.isFile() ||
1716
+ before.isSymbolicLink() ||
1717
+ Number(before.nlink) !== 1 ||
1718
+ before.size <= 0 ||
1719
+ before.size > maximum ||
1720
+ !samePath(realpath(this._fs, filePath), filePath)
1721
+ ) {
1722
+ throw registryError(
1723
+ code,
1724
+ `${label} must be a bounded single-link regular file`,
1725
+ );
1726
+ }
1727
+ descriptor = this._fs.openSync(
1728
+ filePath,
1729
+ this._fs.constants.O_RDONLY | (this._fs.constants.O_NOFOLLOW || 0),
1730
+ );
1731
+ const opened = this._fs.fstatSync(descriptor);
1732
+ if (
1733
+ !opened.isFile() ||
1734
+ Number(opened.nlink) !== 1 ||
1735
+ entryIdentity(opened) !== entryIdentity(before) ||
1736
+ opened.size !== before.size
1737
+ ) {
1738
+ throw registryError(code, `${label} changed while it was opened`);
1739
+ }
1740
+ const bytes = this._fs.readFileSync(descriptor);
1741
+ const after = this._fs.fstatSync(descriptor);
1742
+ const afterPath = this._fs.lstatSync(filePath);
1743
+ if (
1744
+ Number(after.nlink) !== 1 ||
1745
+ entryIdentity(after) !== entryIdentity(opened) ||
1746
+ after.size !== opened.size ||
1747
+ bytes.length !== opened.size ||
1748
+ !afterPath.isFile() ||
1749
+ afterPath.isSymbolicLink() ||
1750
+ Number(afterPath.nlink) !== 1 ||
1751
+ entryIdentity(afterPath) !== entryIdentity(opened) ||
1752
+ !samePath(realpath(this._fs, filePath), filePath)
1753
+ ) {
1754
+ throw registryError(code, `${label} changed while it was read`);
1755
+ }
1756
+ return { bytes, identity: entryIdentity(opened) };
1757
+ } finally {
1758
+ if (descriptor !== null) this._fs.closeSync(descriptor);
1759
+ }
1760
+ }
1761
+
1762
+ _readAndVerifyTenantMarker() {
1763
+ let stored;
1764
+ try {
1765
+ stored = this._readBoundedRegularFile(
1766
+ this._markerPath,
1767
+ MAX_TENANT_MARKER_BYTES,
1768
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1769
+ "candidate tenant marker",
1770
+ );
1771
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(
1772
+ stored.bytes,
1773
+ );
1774
+ const marker = verifyTenantMarker(
1775
+ JSON.parse(text),
1776
+ this.tenantId,
1777
+ this.tenantKey,
1778
+ );
1779
+ if (!serializeTenantMarker(marker).equals(stored.bytes)) {
1780
+ throw registryError(
1781
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1782
+ "candidate tenant marker serialization is not canonical",
1783
+ );
1784
+ }
1785
+ return { identity: stored.identity, marker };
1786
+ } catch (error) {
1787
+ if (error instanceof SkillCandidateRegistryError) throw error;
1788
+ throw registryError(
1789
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1790
+ "candidate tenant marker could not be verified",
1791
+ { cause: error },
1792
+ );
1793
+ }
1794
+ }
1795
+
1796
+ _assertBoundary() {
1797
+ this._assertDirectories();
1798
+ const verified = this._readAndVerifyTenantMarker();
1799
+ if (
1800
+ this._markerIdentity !== undefined &&
1801
+ verified.identity !== this._markerIdentity
1802
+ ) {
1803
+ throw registryError(
1804
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1805
+ "candidate tenant marker identity changed",
1806
+ );
1807
+ }
1808
+ this._assertDirectories();
1809
+ }
1810
+
1811
+ _assertNoMixedTenantArtifacts() {
1812
+ this._assertBoundary();
1813
+ const names = this._readDirectoryEntryNamesBounded(
1814
+ this.rootDir,
1815
+ "candidate tenant scan",
1816
+ );
1817
+ const budget = { bytes: 0, nodes: 0 };
1818
+ for (const name of names) {
1819
+ if (name === TENANT_MARKER_FILE) continue;
1820
+ if (name.startsWith(".candidate-")) {
1821
+ throw migrationRequired(
1822
+ "legacy or interrupted candidate temporary storage requires explicit migration",
1823
+ { path: path.join(this.rootDir, name) },
1824
+ );
1825
+ }
1826
+ const match = CANDIDATE_FILE_PATTERN.exec(name);
1827
+ if (!match) {
1828
+ throw registryError(
1829
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1830
+ "candidate tenant root contains an unexpected entry",
1831
+ { path: path.join(this.rootDir, name) },
1832
+ );
1833
+ }
1834
+ const filePath = path.join(this.rootDir, name);
1835
+ let bytes;
1836
+ let parsed;
1837
+ try {
1838
+ bytes = this._readBoundedRegularFile(
1839
+ filePath,
1840
+ SKILL_CANDIDATE_MAX_ARTIFACT_BYTES,
1841
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1842
+ "stored candidate artifact",
1843
+ ).bytes;
1844
+ budget.bytes += bytes.length;
1845
+ if (budget.bytes > SKILL_CANDIDATE_TENANT_SCAN_MAX_BYTES) {
1846
+ throw tenantScanLimit(
1847
+ "candidate tenant scan exceeded its aggregate byte budget",
1848
+ { path: filePath },
1849
+ );
1850
+ }
1851
+ parsed = JSON.parse(
1852
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes),
1853
+ );
1854
+ } catch (cause) {
1855
+ if (cause instanceof SkillCandidateRegistryError) throw cause;
1856
+ throw registryError(
1857
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1858
+ "stored candidate artifact is not bounded UTF-8 JSON",
1859
+ { path: filePath, cause },
1860
+ );
1861
+ }
1862
+ chargeTenantScanNodes(parsed, budget, filePath);
1863
+ const schema =
1864
+ parsed && typeof parsed === "object"
1865
+ ? Object.getOwnPropertyDescriptor(parsed, "schema")?.value
1866
+ : undefined;
1867
+ if (schema === LEGACY_SKILL_CANDIDATE_SCHEMA) {
1868
+ throw migrationRequired(
1869
+ "legacy Candidate v1 requires explicit tenant-scoped migration",
1870
+ { path: filePath, schema },
1871
+ );
1872
+ }
1873
+ if (schema !== SKILL_CANDIDATE_SCHEMA) {
1874
+ throw registryError(
1875
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1876
+ "stored candidate artifact has a missing or unknown schema",
1877
+ { path: filePath, schema },
1878
+ );
1879
+ }
1880
+ let candidate;
1881
+ try {
1882
+ candidate = verifySkillCandidateDraft(parsed);
1883
+ } catch (cause) {
1884
+ throw registryError(
1885
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1886
+ "stored Candidate v2 artifact is corrupt",
1887
+ { path: filePath, cause },
1888
+ );
1889
+ }
1890
+ if (
1891
+ candidate.tenantId !== this.tenantId ||
1892
+ candidate.candidateId.slice("sha256:".length) !== match[1] ||
1893
+ !serializeCandidate(candidate).equals(bytes)
1894
+ ) {
1895
+ throw registryError(
1896
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1897
+ "stored Candidate v2 artifact does not match its tenant, filename, or bytes",
1898
+ { path: filePath },
1899
+ );
1900
+ }
1901
+ }
1902
+ this._assertBoundary();
1903
+ return names;
1904
+ }
1905
+
1906
+ _candidatePath(candidateId) {
1907
+ const normalizedId = normalizeCandidateId(candidateId);
1908
+ const filePath = path.resolve(
1909
+ this.rootDir,
1910
+ `${normalizedId.slice("sha256:".length)}.json`,
1911
+ );
1912
+ if (!isContained(this.rootDir, filePath) || filePath === this.rootDir) {
1913
+ throw registryError(
1914
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1915
+ "candidate path escaped the registry root",
1916
+ );
1917
+ }
1918
+ return filePath;
1919
+ }
1920
+
1921
+ _readBytes(filePath) {
1922
+ this._assertBoundary();
1923
+ try {
1924
+ return this._readBoundedRegularFile(
1925
+ filePath,
1926
+ SKILL_CANDIDATE_MAX_ARTIFACT_BYTES,
1927
+ "SKILL_CANDIDATE_CORRUPT",
1928
+ "candidate artifact",
1929
+ ).bytes;
1930
+ } finally {
1931
+ this._assertBoundary();
1932
+ }
1933
+ }
1934
+
1935
+ _resolveAdmissionContext(input) {
1936
+ const artifacts = buildTargetMatrixAdmissionRequest(input);
1937
+ if (artifacts.request.tenantId !== this.tenantId) {
1938
+ throw registryError(
1939
+ "SKILL_CANDIDATE_TENANT_MISMATCH",
1940
+ "candidate admission request does not match the registry tenant",
1941
+ );
1942
+ }
1943
+ let resolution;
1944
+ try {
1945
+ resolution = this._resolveTargetMatrixAdmission(artifacts.request);
1946
+ } catch (cause) {
1947
+ throw registryError(
1948
+ "SKILL_CANDIDATE_ADMISSION_REJECTED",
1949
+ "target matrix admission authority rejected the candidate",
1950
+ { cause },
1951
+ );
1952
+ }
1953
+ return verifyTargetMatrixAdmissionResolution(
1954
+ resolution,
1955
+ this.targetMatrixAdmissionAuthority,
1956
+ artifacts.request,
1957
+ artifacts,
1958
+ );
1959
+ }
1960
+
1961
+ create(input) {
1962
+ if (arguments.length !== 1) {
1963
+ throw registryError(
1964
+ "SKILL_CANDIDATE_INVALID",
1965
+ "candidate create accepts only input; admission context is registry-owned",
1966
+ );
1967
+ }
1968
+ this._assertNoMixedTenantArtifacts();
1969
+ const verificationContext = this._resolveAdmissionContext(input);
1970
+ const candidate = buildSkillCandidateDraft(input, verificationContext);
1971
+ if (candidate.tenantId !== this.tenantId) {
1972
+ throw registryError(
1973
+ "SKILL_CANDIDATE_TENANT_MISMATCH",
1974
+ "candidate tenant does not match the registry tenant",
1975
+ { candidateTenantId: candidate.tenantId, tenantId: this.tenantId },
1976
+ );
1977
+ }
1978
+ const bytes = serializeCandidate(candidate);
1979
+ const filePath = this._candidatePath(candidate.candidateId);
1980
+ const token = this._token("SKILL_CANDIDATE_WRITE_FAILED");
1981
+ this._assertNoMixedTenantArtifacts();
1982
+ const temporaryPath = path.resolve(
1983
+ this.rootDir,
1984
+ `.candidate-${process.pid}-${token}.tmp`,
1985
+ );
1986
+ if (!isContained(this.rootDir, temporaryPath)) {
1987
+ throw registryError(
1988
+ "SKILL_CANDIDATE_STORE_UNSAFE",
1989
+ "candidate temporary path escaped the registry root",
1990
+ );
1991
+ }
1992
+
1993
+ let descriptor = null;
1994
+ let published = false;
1995
+ let observedExisting = false;
1996
+ let temporaryExists = false;
1997
+ try {
1998
+ this._assertBoundary();
1999
+ descriptor = this._fs.openSync(temporaryPath, "wx", 0o600);
2000
+ temporaryExists = true;
2001
+ this._fs.writeFileSync(descriptor, bytes);
2002
+ this._fs.fsyncSync(descriptor);
2003
+ const written = this._fs.fstatSync(descriptor);
2004
+ if (
2005
+ !written.isFile() ||
2006
+ Number(written.nlink) !== 1 ||
2007
+ written.size !== bytes.length
2008
+ ) {
2009
+ throw registryError(
2010
+ "SKILL_CANDIDATE_WRITE_FAILED",
2011
+ "candidate temporary artifact was not written completely",
2012
+ { candidateId: candidate.candidateId, commitState: "not-committed" },
2013
+ );
2014
+ }
2015
+ const writtenIdentity = entryIdentity(written);
2016
+ this._fs.closeSync(descriptor);
2017
+ descriptor = null;
2018
+ if (this._secure) {
2019
+ ensurePrivateFile(temporaryPath, {
2020
+ applyWindowsAcl: true,
2021
+ failIfUnavailable: true,
2022
+ });
2023
+ }
2024
+ const staged = this._fs.lstatSync(temporaryPath);
2025
+ if (
2026
+ !staged.isFile() ||
2027
+ staged.isSymbolicLink() ||
2028
+ Number(staged.nlink) !== 1 ||
2029
+ staged.size !== bytes.length ||
2030
+ entryIdentity(staged) !== writtenIdentity ||
2031
+ !samePath(realpath(this._fs, temporaryPath), temporaryPath)
2032
+ ) {
2033
+ throw registryError(
2034
+ "SKILL_CANDIDATE_WRITE_FAILED",
2035
+ "candidate temporary artifact changed before publication",
2036
+ { candidateId: candidate.candidateId, commitState: "not-committed" },
2037
+ );
2038
+ }
2039
+ this._assertBoundary();
2040
+ try {
2041
+ this._fs.linkSync(temporaryPath, filePath);
2042
+ published = true;
2043
+ } catch (error) {
2044
+ if (error?.code !== "EEXIST") throw error;
2045
+ let existing;
2046
+ try {
2047
+ existing = this.read(candidate.candidateId);
2048
+ } catch (readError) {
2049
+ throw registryError(
2050
+ "SKILL_CANDIDATE_CONFLICT",
2051
+ "candidate digest path already exists but is not the same verified artifact",
2052
+ { candidateId: candidate.candidateId, cause: readError },
2053
+ );
2054
+ }
2055
+ if (!serializeCandidate(existing).equals(bytes)) {
2056
+ throw registryError(
2057
+ "SKILL_CANDIDATE_CONFLICT",
2058
+ "candidate digest collision or immutable artifact conflict",
2059
+ { candidateId: candidate.candidateId },
2060
+ );
2061
+ }
2062
+ observedExisting = true;
2063
+ fsyncDirectory(this._fs, this.rootDir);
2064
+ this._assertBoundary();
2065
+ return Object.freeze({ candidate: existing, created: false });
2066
+ }
2067
+ const linked = this._fs.lstatSync(filePath);
2068
+ if (
2069
+ !linked.isFile() ||
2070
+ linked.isSymbolicLink() ||
2071
+ Number(linked.nlink) !== 2 ||
2072
+ linked.size !== bytes.length ||
2073
+ entryIdentity(linked) !== writtenIdentity ||
2074
+ !samePath(realpath(this._fs, filePath), filePath)
2075
+ ) {
2076
+ throw registryError(
2077
+ "SKILL_CANDIDATE_COMMIT_UNKNOWN",
2078
+ "published candidate artifact has an unsafe identity",
2079
+ { candidateId: candidate.candidateId, commitState: "unknown" },
2080
+ );
2081
+ }
2082
+ this._fs.unlinkSync(temporaryPath);
2083
+ temporaryExists = false;
2084
+ const finalized = this._fs.lstatSync(filePath);
2085
+ if (
2086
+ !finalized.isFile() ||
2087
+ finalized.isSymbolicLink() ||
2088
+ Number(finalized.nlink) !== 1 ||
2089
+ finalized.size !== bytes.length ||
2090
+ entryIdentity(finalized) !== writtenIdentity
2091
+ ) {
2092
+ throw registryError(
2093
+ "SKILL_CANDIDATE_COMMIT_UNKNOWN",
2094
+ "candidate artifact did not finalize as a single-link file",
2095
+ { candidateId: candidate.candidateId, commitState: "unknown" },
2096
+ );
2097
+ }
2098
+ fsyncDirectory(this._fs, this.rootDir);
2099
+ let stored;
2100
+ let verified;
2101
+ try {
2102
+ stored = this._readBoundedRegularFile(
2103
+ filePath,
2104
+ SKILL_CANDIDATE_MAX_ARTIFACT_BYTES,
2105
+ "SKILL_CANDIDATE_COMMIT_UNKNOWN",
2106
+ "published candidate artifact",
2107
+ );
2108
+ verified = verifySkillCandidateDraft(
2109
+ JSON.parse(
2110
+ new TextDecoder("utf-8", { fatal: true }).decode(stored.bytes),
2111
+ ),
2112
+ );
2113
+ } catch (cause) {
2114
+ throw registryError(
2115
+ "SKILL_CANDIDATE_COMMIT_UNKNOWN",
2116
+ "published candidate artifact failed descriptor-safe verification",
2117
+ {
2118
+ candidateId: candidate.candidateId,
2119
+ commitState: "unknown",
2120
+ cause,
2121
+ },
2122
+ );
2123
+ }
2124
+ if (
2125
+ stored.identity !== writtenIdentity ||
2126
+ verified.tenantId !== this.tenantId ||
2127
+ verified.candidateId !== candidate.candidateId ||
2128
+ !stored.bytes.equals(bytes) ||
2129
+ !serializeCandidate(verified).equals(bytes)
2130
+ ) {
2131
+ throw registryError(
2132
+ "SKILL_CANDIDATE_COMMIT_UNKNOWN",
2133
+ "published candidate artifact does not exactly match the admitted candidate",
2134
+ { candidateId: candidate.candidateId, commitState: "unknown" },
2135
+ );
2136
+ }
2137
+ this._assertBoundary();
2138
+ return Object.freeze({ candidate, created: true });
2139
+ } catch (error) {
2140
+ const mayBeCommitted = published || observedExisting;
2141
+ if (error instanceof SkillCandidateRegistryError && !mayBeCommitted) {
2142
+ throw error;
2143
+ }
2144
+ throw registryError(
2145
+ mayBeCommitted
2146
+ ? "SKILL_CANDIDATE_COMMIT_UNKNOWN"
2147
+ : "SKILL_CANDIDATE_WRITE_FAILED",
2148
+ mayBeCommitted
2149
+ ? "candidate may have been published, but durability could not be confirmed"
2150
+ : "candidate artifact could not be published",
2151
+ {
2152
+ candidateId: candidate.candidateId,
2153
+ commitState: mayBeCommitted ? "unknown" : "not-committed",
2154
+ cause: error,
2155
+ },
2156
+ );
2157
+ } finally {
2158
+ if (descriptor !== null) {
2159
+ try {
2160
+ this._fs.closeSync(descriptor);
2161
+ } catch {
2162
+ // The authoritative path is still absent until the hard-link CAS.
2163
+ }
2164
+ }
2165
+ if (temporaryExists) {
2166
+ try {
2167
+ this._fs.unlinkSync(temporaryPath);
2168
+ } catch {
2169
+ // Hidden temporary files are never candidate artifacts or list entries.
2170
+ }
2171
+ }
2172
+ }
2173
+ }
2174
+
2175
+ read(candidateId) {
2176
+ const normalizedId = normalizeCandidateId(candidateId);
2177
+ const filePath = this._candidatePath(normalizedId);
2178
+ let bytes;
2179
+ try {
2180
+ bytes = this._readBytes(filePath);
2181
+ } catch (error) {
2182
+ if (error instanceof SkillCandidateRegistryError) throw error;
2183
+ if (error?.code === "ENOENT") {
2184
+ throw registryError(
2185
+ "SKILL_CANDIDATE_NOT_FOUND",
2186
+ "candidate artifact was not found",
2187
+ { candidateId: normalizedId },
2188
+ );
2189
+ }
2190
+ throw registryError(
2191
+ "SKILL_CANDIDATE_CORRUPT",
2192
+ "candidate artifact could not be read safely",
2193
+ { candidateId: normalizedId, cause: error },
2194
+ );
2195
+ }
2196
+
2197
+ let parsed;
2198
+ try {
2199
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
2200
+ parsed = JSON.parse(text);
2201
+ } catch (error) {
2202
+ throw registryError(
2203
+ "SKILL_CANDIDATE_CORRUPT",
2204
+ "candidate artifact is not canonical UTF-8 JSON",
2205
+ { candidateId: normalizedId, cause: error },
2206
+ );
2207
+ }
2208
+
2209
+ let candidate;
2210
+ try {
2211
+ candidate = verifySkillCandidateDraft(parsed);
2212
+ } catch (error) {
2213
+ if (error?.code === SKILL_CANDIDATE_MIGRATION_REQUIRED_CODE) throw error;
2214
+ throw registryError(
2215
+ "SKILL_CANDIDATE_CORRUPT",
2216
+ "candidate artifact failed schema or digest verification",
2217
+ { candidateId: normalizedId, cause: error },
2218
+ );
2219
+ }
2220
+ if (
2221
+ candidate.tenantId !== this.tenantId ||
2222
+ candidate.candidateId !== normalizedId ||
2223
+ !serializeCandidate(candidate).equals(bytes)
2224
+ ) {
2225
+ throw registryError(
2226
+ "SKILL_CANDIDATE_CORRUPT",
2227
+ "candidate filename or serialization does not match its digest",
2228
+ { candidateId: normalizedId },
2229
+ );
2230
+ }
2231
+ return candidate;
2232
+ }
2233
+
2234
+ list(options = {}) {
2235
+ assertDataRecord(options, LIST_OPTION_KEYS, "candidate list options");
2236
+ const limit = ownData(
2237
+ options,
2238
+ "limit",
2239
+ "candidate list options",
2240
+ DEFAULT_LIST_LIMIT,
2241
+ );
2242
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
2243
+ throw registryError(
2244
+ "SKILL_CANDIDATE_INVALID",
2245
+ `limit must be an integer from 1 to ${MAX_LIST_LIMIT}`,
2246
+ );
2247
+ }
2248
+ const names = this._assertNoMixedTenantArtifacts()
2249
+ .filter((name) => CANDIDATE_FILE_PATTERN.test(name))
2250
+ .sort();
2251
+ const candidates = names.slice(0, limit).map((name) => {
2252
+ const match = CANDIDATE_FILE_PATTERN.exec(name);
2253
+ return this.read(`sha256:${match[1]}`);
2254
+ });
2255
+ this._assertBoundary();
2256
+ return Object.freeze(candidates);
2257
+ }
2258
+ }