@skillsmith/core 0.11.3 → 0.11.5

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 (304) hide show
  1. package/CHANGELOG.md +34 -9
  2. package/README.md +10 -7
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/src/api/client.d.ts.map +1 -1
  5. package/dist/src/api/client.js +15 -5
  6. package/dist/src/api/client.js.map +1 -1
  7. package/dist/src/api/client.private-registry.d.ts +80 -0
  8. package/dist/src/api/client.private-registry.d.ts.map +1 -0
  9. package/dist/src/api/client.private-registry.js +97 -0
  10. package/dist/src/api/client.private-registry.js.map +1 -0
  11. package/dist/src/api/client.private-registry.test.d.ts +9 -0
  12. package/dist/src/api/client.private-registry.test.d.ts.map +1 -0
  13. package/dist/src/api/client.private-registry.test.js +127 -0
  14. package/dist/src/api/client.private-registry.test.js.map +1 -0
  15. package/dist/src/api/client.toSkill.test.d.ts +25 -0
  16. package/dist/src/api/client.toSkill.test.d.ts.map +1 -0
  17. package/dist/src/api/client.toSkill.test.js +97 -0
  18. package/dist/src/api/client.toSkill.test.js.map +1 -0
  19. package/dist/src/api/client.token-refresh.d.ts +8 -0
  20. package/dist/src/api/client.token-refresh.d.ts.map +1 -1
  21. package/dist/src/api/client.token-refresh.js +26 -0
  22. package/dist/src/api/client.token-refresh.js.map +1 -1
  23. package/dist/src/api/client.token-refresh.test.js +82 -0
  24. package/dist/src/api/client.token-refresh.test.js.map +1 -1
  25. package/dist/src/api/index.d.ts +2 -0
  26. package/dist/src/api/index.d.ts.map +1 -1
  27. package/dist/src/api/index.js +9 -0
  28. package/dist/src/api/index.js.map +1 -1
  29. package/dist/src/api/security-summary.d.ts +73 -0
  30. package/dist/src/api/security-summary.d.ts.map +1 -0
  31. package/dist/src/api/security-summary.js +88 -0
  32. package/dist/src/api/security-summary.js.map +1 -0
  33. package/dist/src/api/security-summary.test.d.ts +9 -0
  34. package/dist/src/api/security-summary.test.d.ts.map +1 -0
  35. package/dist/src/api/security-summary.test.js +94 -0
  36. package/dist/src/api/security-summary.test.js.map +1 -0
  37. package/dist/src/config/config-atomic-write.d.ts +30 -10
  38. package/dist/src/config/config-atomic-write.d.ts.map +1 -1
  39. package/dist/src/config/config-atomic-write.js +33 -80
  40. package/dist/src/config/config-atomic-write.js.map +1 -1
  41. package/dist/src/config/config-atomic-write.test.d.ts +8 -0
  42. package/dist/src/config/config-atomic-write.test.d.ts.map +1 -1
  43. package/dist/src/config/config-atomic-write.test.js +64 -12
  44. package/dist/src/config/config-atomic-write.test.js.map +1 -1
  45. package/dist/src/config/owned-lock.acquire.d.ts +53 -0
  46. package/dist/src/config/owned-lock.acquire.d.ts.map +1 -0
  47. package/dist/src/config/owned-lock.acquire.js +140 -0
  48. package/dist/src/config/owned-lock.acquire.js.map +1 -0
  49. package/dist/src/config/owned-lock.claim.d.ts +84 -0
  50. package/dist/src/config/owned-lock.claim.d.ts.map +1 -0
  51. package/dist/src/config/owned-lock.claim.js +310 -0
  52. package/dist/src/config/owned-lock.claim.js.map +1 -0
  53. package/dist/src/config/owned-lock.d.ts +113 -0
  54. package/dist/src/config/owned-lock.d.ts.map +1 -0
  55. package/dist/src/config/owned-lock.js +121 -0
  56. package/dist/src/config/owned-lock.js.map +1 -0
  57. package/dist/src/config/owned-lock.test.d.ts +12 -0
  58. package/dist/src/config/owned-lock.test.d.ts.map +1 -0
  59. package/dist/src/config/owned-lock.test.js +412 -0
  60. package/dist/src/config/owned-lock.test.js.map +1 -0
  61. package/dist/src/config/owned-lock.types.d.ts +78 -0
  62. package/dist/src/config/owned-lock.types.d.ts.map +1 -0
  63. package/dist/src/config/owned-lock.types.js +30 -0
  64. package/dist/src/config/owned-lock.types.js.map +1 -0
  65. package/dist/src/db/createDatabase.d.ts.map +1 -1
  66. package/dist/src/db/createDatabase.js +19 -7
  67. package/dist/src/db/createDatabase.js.map +1 -1
  68. package/dist/src/embeddings/hnsw-store.exports.d.ts +11 -0
  69. package/dist/src/embeddings/hnsw-store.exports.d.ts.map +1 -0
  70. package/dist/src/embeddings/hnsw-store.exports.js +10 -0
  71. package/dist/src/embeddings/hnsw-store.exports.js.map +1 -0
  72. package/dist/src/embeddings/index.d.ts +1 -2
  73. package/dist/src/embeddings/index.d.ts.map +1 -1
  74. package/dist/src/embeddings/index.js +17 -3
  75. package/dist/src/embeddings/index.js.map +1 -1
  76. package/dist/src/embeddings/probe.d.ts.map +1 -1
  77. package/dist/src/embeddings/probe.js +4 -8
  78. package/dist/src/embeddings/probe.js.map +1 -1
  79. package/dist/src/exports/services.d.ts +4 -1
  80. package/dist/src/exports/services.d.ts.map +1 -1
  81. package/dist/src/exports/services.js +22 -0
  82. package/dist/src/exports/services.js.map +1 -1
  83. package/dist/src/index.d.ts +5 -3
  84. package/dist/src/index.d.ts.map +1 -1
  85. package/dist/src/index.js +17 -3
  86. package/dist/src/index.js.map +1 -1
  87. package/dist/src/install/index.d.ts +1 -1
  88. package/dist/src/install/index.d.ts.map +1 -1
  89. package/dist/src/install/index.js +1 -1
  90. package/dist/src/install/index.js.map +1 -1
  91. package/dist/src/install/paths.d.ts +10 -0
  92. package/dist/src/install/paths.d.ts.map +1 -1
  93. package/dist/src/install/paths.js +19 -0
  94. package/dist/src/install/paths.js.map +1 -1
  95. package/dist/src/logging/logger.test.js +57 -0
  96. package/dist/src/logging/logger.test.js.map +1 -1
  97. package/dist/src/logging/rotation.d.ts +27 -2
  98. package/dist/src/logging/rotation.d.ts.map +1 -1
  99. package/dist/src/logging/rotation.js +44 -3
  100. package/dist/src/logging/rotation.js.map +1 -1
  101. package/dist/src/logging/rotation.test.js +69 -0
  102. package/dist/src/logging/rotation.test.js.map +1 -1
  103. package/dist/src/logging/types.d.ts +8 -2
  104. package/dist/src/logging/types.d.ts.map +1 -1
  105. package/dist/src/security/index.d.ts +2 -1
  106. package/dist/src/security/index.d.ts.map +1 -1
  107. package/dist/src/security/index.js +3 -0
  108. package/dist/src/security/index.js.map +1 -1
  109. package/dist/src/security/scanner/SecurityScanner.d.ts +2 -2
  110. package/dist/src/security/scanner/SecurityScanner.d.ts.map +1 -1
  111. package/dist/src/security/scanner/SecurityScanner.evidence.d.ts +76 -0
  112. package/dist/src/security/scanner/SecurityScanner.evidence.d.ts.map +1 -0
  113. package/dist/src/security/scanner/SecurityScanner.evidence.js +176 -0
  114. package/dist/src/security/scanner/SecurityScanner.evidence.js.map +1 -0
  115. package/dist/src/security/scanner/SecurityScanner.exec.d.ts +8 -1
  116. package/dist/src/security/scanner/SecurityScanner.exec.d.ts.map +1 -1
  117. package/dist/src/security/scanner/SecurityScanner.exec.js +58 -2
  118. package/dist/src/security/scanner/SecurityScanner.exec.js.map +1 -1
  119. package/dist/src/security/scanner/SecurityScanner.helpers.d.ts +35 -10
  120. package/dist/src/security/scanner/SecurityScanner.helpers.d.ts.map +1 -1
  121. package/dist/src/security/scanner/SecurityScanner.helpers.js +106 -59
  122. package/dist/src/security/scanner/SecurityScanner.helpers.js.map +1 -1
  123. package/dist/src/security/scanner/SecurityScanner.js +41 -13
  124. package/dist/src/security/scanner/SecurityScanner.js.map +1 -1
  125. package/dist/src/security/scanner/SecurityScanner.pii.d.ts.map +1 -1
  126. package/dist/src/security/scanner/SecurityScanner.pii.js +23 -1
  127. package/dist/src/security/scanner/SecurityScanner.pii.js.map +1 -1
  128. package/dist/src/security/scanner/SecurityScanner.scanners.d.ts.map +1 -1
  129. package/dist/src/security/scanner/SecurityScanner.scanners.js +15 -3
  130. package/dist/src/security/scanner/SecurityScanner.scanners.js.map +1 -1
  131. package/dist/src/security/scanner/SecurityScanner.ssrf.d.ts +11 -1
  132. package/dist/src/security/scanner/SecurityScanner.ssrf.d.ts.map +1 -1
  133. package/dist/src/security/scanner/SecurityScanner.ssrf.js +18 -7
  134. package/dist/src/security/scanner/SecurityScanner.ssrf.js.map +1 -1
  135. package/dist/src/security/scanner/index.d.ts +3 -3
  136. package/dist/src/security/scanner/index.d.ts.map +1 -1
  137. package/dist/src/security/scanner/index.js +5 -2
  138. package/dist/src/security/scanner/index.js.map +1 -1
  139. package/dist/src/security/scanner/multiline-category-closure.test.d.ts +80 -0
  140. package/dist/src/security/scanner/multiline-category-closure.test.d.ts.map +1 -0
  141. package/dist/src/security/scanner/multiline-category-closure.test.js +287 -0
  142. package/dist/src/security/scanner/multiline-category-closure.test.js.map +1 -0
  143. package/dist/src/security/scanner/patterns.d.ts +41 -14
  144. package/dist/src/security/scanner/patterns.d.ts.map +1 -1
  145. package/dist/src/security/scanner/patterns.jailbreak.d.ts +41 -0
  146. package/dist/src/security/scanner/patterns.jailbreak.d.ts.map +1 -0
  147. package/dist/src/security/scanner/patterns.jailbreak.evidence.d.ts +35 -0
  148. package/dist/src/security/scanner/patterns.jailbreak.evidence.d.ts.map +1 -0
  149. package/dist/src/security/scanner/patterns.jailbreak.evidence.js +119 -0
  150. package/dist/src/security/scanner/patterns.jailbreak.evidence.js.map +1 -0
  151. package/dist/src/security/scanner/patterns.jailbreak.js +370 -0
  152. package/dist/src/security/scanner/patterns.jailbreak.js.map +1 -0
  153. package/dist/src/security/scanner/patterns.js +101 -82
  154. package/dist/src/security/scanner/patterns.js.map +1 -1
  155. package/dist/src/security/scanner/patterns.scope.d.ts +62 -0
  156. package/dist/src/security/scanner/patterns.scope.d.ts.map +1 -0
  157. package/dist/src/security/scanner/patterns.scope.js +207 -0
  158. package/dist/src/security/scanner/patterns.scope.js.map +1 -0
  159. package/dist/src/security/scanner/regex-utils.d.ts +20 -0
  160. package/dist/src/security/scanner/regex-utils.d.ts.map +1 -1
  161. package/dist/src/security/scanner/regex-utils.js +20 -0
  162. package/dist/src/security/scanner/regex-utils.js.map +1 -1
  163. package/dist/src/security/scanner/types.d.ts +35 -0
  164. package/dist/src/security/scanner/types.d.ts.map +1 -1
  165. package/dist/src/services/recommend-guard.d.ts +28 -0
  166. package/dist/src/services/recommend-guard.d.ts.map +1 -0
  167. package/dist/src/services/recommend-guard.js +34 -0
  168. package/dist/src/services/recommend-guard.js.map +1 -0
  169. package/dist/src/services/recommend-guard.test.d.ts +2 -0
  170. package/dist/src/services/recommend-guard.test.d.ts.map +1 -0
  171. package/dist/src/services/recommend-guard.test.js +28 -0
  172. package/dist/src/services/recommend-guard.test.js.map +1 -0
  173. package/dist/src/services/skill-installation.content.d.ts +88 -0
  174. package/dist/src/services/skill-installation.content.d.ts.map +1 -0
  175. package/dist/src/services/skill-installation.content.js +319 -0
  176. package/dist/src/services/skill-installation.content.js.map +1 -0
  177. package/dist/src/services/skill-installation.helpers.d.ts +32 -1
  178. package/dist/src/services/skill-installation.helpers.d.ts.map +1 -1
  179. package/dist/src/services/skill-installation.helpers.js +39 -6
  180. package/dist/src/services/skill-installation.helpers.js.map +1 -1
  181. package/dist/src/services/skill-installation.helpers.test.js +34 -2
  182. package/dist/src/services/skill-installation.helpers.test.js.map +1 -1
  183. package/dist/src/services/skill-installation.io.d.ts.map +1 -1
  184. package/dist/src/services/skill-installation.io.js +86 -2
  185. package/dist/src/services/skill-installation.io.js.map +1 -1
  186. package/dist/src/services/skill-installation.service.d.ts +20 -1
  187. package/dist/src/services/skill-installation.service.d.ts.map +1 -1
  188. package/dist/src/services/skill-installation.service.js +26 -4
  189. package/dist/src/services/skill-installation.service.js.map +1 -1
  190. package/dist/src/services/skill-installation.types.d.ts +42 -1
  191. package/dist/src/services/skill-installation.types.d.ts.map +1 -1
  192. package/dist/src/services/skill-installation.types.js.map +1 -1
  193. package/dist/src/services/skill-resolution.d.ts +67 -0
  194. package/dist/src/services/skill-resolution.d.ts.map +1 -0
  195. package/dist/src/services/skill-resolution.js +67 -0
  196. package/dist/src/services/skill-resolution.js.map +1 -0
  197. package/dist/src/services/skill-resolution.test.d.ts +2 -0
  198. package/dist/src/services/skill-resolution.test.d.ts.map +1 -0
  199. package/dist/src/services/skill-resolution.test.js +143 -0
  200. package/dist/src/services/skill-resolution.test.js.map +1 -0
  201. package/dist/src/sync/inventory-collector.d.ts.map +1 -1
  202. package/dist/src/sync/inventory-collector.js +2 -2
  203. package/dist/src/sync/inventory-collector.js.map +1 -1
  204. package/dist/src/utils/index.d.ts +1 -0
  205. package/dist/src/utils/index.d.ts.map +1 -1
  206. package/dist/src/utils/index.js +4 -0
  207. package/dist/src/utils/index.js.map +1 -1
  208. package/dist/src/utils/quiet-mode.d.ts +21 -0
  209. package/dist/src/utils/quiet-mode.d.ts.map +1 -0
  210. package/dist/src/utils/quiet-mode.js +26 -0
  211. package/dist/src/utils/quiet-mode.js.map +1 -0
  212. package/dist/tests/EmbeddingService.test.js +58 -1
  213. package/dist/tests/EmbeddingService.test.js.map +1 -1
  214. package/dist/tests/SecurityScanner.ai-defence.test.js +32 -7
  215. package/dist/tests/SecurityScanner.ai-defence.test.js.map +1 -1
  216. package/dist/tests/SecurityScanner.exec.test.js +79 -0
  217. package/dist/tests/SecurityScanner.exec.test.js.map +1 -1
  218. package/dist/tests/SecurityScanner.performance.test.js +5 -2
  219. package/dist/tests/SecurityScanner.performance.test.js.map +1 -1
  220. package/dist/tests/db/database-abstraction.test.js +45 -1
  221. package/dist/tests/db/database-abstraction.test.js.map +1 -1
  222. package/dist/tests/helpers/deterministic-dead-pid.d.ts +13 -0
  223. package/dist/tests/helpers/deterministic-dead-pid.d.ts.map +1 -0
  224. package/dist/tests/helpers/deterministic-dead-pid.js +31 -0
  225. package/dist/tests/helpers/deterministic-dead-pid.js.map +1 -0
  226. package/dist/tests/integration/owned-lock-lost-update.test.d.ts +12 -0
  227. package/dist/tests/integration/owned-lock-lost-update.test.d.ts.map +1 -0
  228. package/dist/tests/integration/owned-lock-lost-update.test.js +52 -0
  229. package/dist/tests/integration/owned-lock-lost-update.test.js.map +1 -0
  230. package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts +19 -0
  231. package/dist/tests/integration/owned-lock-reclaim-race.test.d.ts.map +1 -0
  232. package/dist/tests/integration/owned-lock-reclaim-race.test.js +177 -0
  233. package/dist/tests/integration/owned-lock-reclaim-race.test.js.map +1 -0
  234. package/dist/tests/security/ContinuousSecurity.performance.test.js +6 -0
  235. package/dist/tests/security/ContinuousSecurity.performance.test.js.map +1 -1
  236. package/dist/tests/security/ContinuousSecurity.test.js +15 -0
  237. package/dist/tests/security/ContinuousSecurity.test.js.map +1 -1
  238. package/dist/tests/security/ad-crlf-injection.test.d.ts +18 -0
  239. package/dist/tests/security/ad-crlf-injection.test.d.ts.map +1 -0
  240. package/dist/tests/security/ad-crlf-injection.test.js +150 -0
  241. package/dist/tests/security/ad-crlf-injection.test.js.map +1 -0
  242. package/dist/tests/security/ai-defence-multiline-promotion.test.d.ts +25 -0
  243. package/dist/tests/security/ai-defence-multiline-promotion.test.d.ts.map +1 -0
  244. package/dist/tests/security/ai-defence-multiline-promotion.test.js +138 -0
  245. package/dist/tests/security/ai-defence-multiline-promotion.test.js.map +1 -0
  246. package/dist/tests/security/nested-instruction-long-attribute.test.d.ts +15 -0
  247. package/dist/tests/security/nested-instruction-long-attribute.test.d.ts.map +1 -0
  248. package/dist/tests/security/nested-instruction-long-attribute.test.js +60 -0
  249. package/dist/tests/security/nested-instruction-long-attribute.test.js.map +1 -0
  250. package/dist/tests/security/pattern-scope.test.d.ts +17 -0
  251. package/dist/tests/security/pattern-scope.test.d.ts.map +1 -0
  252. package/dist/tests/security/pattern-scope.test.js +128 -0
  253. package/dist/tests/security/pattern-scope.test.js.map +1 -0
  254. package/dist/tests/security/pii-detection.test.js +39 -0
  255. package/dist/tests/security/pii-detection.test.js.map +1 -1
  256. package/dist/tests/security/scanner-evidence-tiers.test.d.ts +30 -0
  257. package/dist/tests/security/scanner-evidence-tiers.test.d.ts.map +1 -0
  258. package/dist/tests/security/scanner-evidence-tiers.test.js +258 -0
  259. package/dist/tests/security/scanner-evidence-tiers.test.js.map +1 -0
  260. package/dist/tests/security/scanner-privesc-severity.test.d.ts +23 -0
  261. package/dist/tests/security/scanner-privesc-severity.test.d.ts.map +1 -0
  262. package/dist/tests/security/scanner-privesc-severity.test.js +43 -0
  263. package/dist/tests/security/scanner-privesc-severity.test.js.map +1 -0
  264. package/dist/tests/security/scanner-regression-guard.exec-locality.test.d.ts +20 -0
  265. package/dist/tests/security/scanner-regression-guard.exec-locality.test.d.ts.map +1 -0
  266. package/dist/tests/security/scanner-regression-guard.exec-locality.test.js +71 -0
  267. package/dist/tests/security/scanner-regression-guard.exec-locality.test.js.map +1 -0
  268. package/dist/tests/security/scanner-regression-guard.test.d.ts +24 -0
  269. package/dist/tests/security/scanner-regression-guard.test.d.ts.map +1 -1
  270. package/dist/tests/security/scanner-regression-guard.test.js +59 -3
  271. package/dist/tests/security/scanner-regression-guard.test.js.map +1 -1
  272. package/dist/tests/security/scanner-ssrf-word-boundary.test.d.ts +19 -0
  273. package/dist/tests/security/scanner-ssrf-word-boundary.test.d.ts.map +1 -0
  274. package/dist/tests/security/scanner-ssrf-word-boundary.test.js +65 -0
  275. package/dist/tests/security/scanner-ssrf-word-boundary.test.js.map +1 -0
  276. package/dist/tests/security.test.js +11 -2
  277. package/dist/tests/security.test.js.map +1 -1
  278. package/dist/tests/skill-scanner/allowlist.test.js +15 -7
  279. package/dist/tests/skill-scanner/allowlist.test.js.map +1 -1
  280. package/dist/tests/unit/services/skill-installation.content.test.d.ts +10 -0
  281. package/dist/tests/unit/services/skill-installation.content.test.d.ts.map +1 -0
  282. package/dist/tests/unit/services/skill-installation.content.test.js +290 -0
  283. package/dist/tests/unit/services/skill-installation.content.test.js.map +1 -0
  284. package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts +8 -0
  285. package/dist/tests/unit/services/skill-installation.io.symlink.test.d.ts.map +1 -0
  286. package/dist/tests/unit/services/skill-installation.io.symlink.test.js +72 -0
  287. package/dist/tests/unit/services/skill-installation.io.symlink.test.js.map +1 -0
  288. package/dist/tests/unit/services/skill-installation.io.test.js +54 -1
  289. package/dist/tests/unit/services/skill-installation.io.test.js.map +1 -1
  290. package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts +15 -0
  291. package/dist/tests/unit/services/skill-installation.service.error-codes.test.d.ts.map +1 -0
  292. package/dist/tests/unit/services/skill-installation.service.error-codes.test.js +190 -0
  293. package/dist/tests/unit/services/skill-installation.service.error-codes.test.js.map +1 -0
  294. package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts +17 -0
  295. package/dist/tests/unit/services/skill-installation.service.multi-client.test.d.ts.map +1 -0
  296. package/dist/tests/unit/services/skill-installation.service.multi-client.test.js +126 -0
  297. package/dist/tests/unit/services/skill-installation.service.multi-client.test.js.map +1 -0
  298. package/dist/tests/unit/services/skill-installation.service.test.js +7 -112
  299. package/dist/tests/unit/services/skill-installation.service.test.js.map +1 -1
  300. package/dist/tests/utils/quiet-mode.test.d.ts +12 -0
  301. package/dist/tests/utils/quiet-mode.test.d.ts.map +1 -0
  302. package/dist/tests/utils/quiet-mode.test.js +42 -0
  303. package/dist/tests/utils/quiet-mode.test.js.map +1 -0
  304. package/package.json +17 -5
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Claim parsing and lock-file mechanics (exclusive creation, reclaim,
3
+ * release) for the owned-lock primitive.
4
+ * @module @skillsmith/core/config/owned-lock.claim
5
+ * @see owned-lock.ts for the full soundness argument and the PUBLIC API.
6
+ * @see owned-lock.acquire.ts for the core acquire loop + `StuckLockError`,
7
+ * split into a sibling file purely to keep both under the repo's
8
+ * 500-line-per-file gate.
9
+ *
10
+ * INTERNAL module -- not part of the public surface (no `package.json`
11
+ * subpath export). Co-located tests and the cross-process race-test child
12
+ * harness import directly from here, by relative path, specifically to
13
+ * reach `createLockExclusive`'s `linkSyncOverride` test seam -- see
14
+ * `owned-lock.acquire.ts` for the analogous rationale on the acquire loop's
15
+ * own destructive test-only options.
16
+ */
17
+ import { randomBytes } from 'node:crypto';
18
+ import { chmodSync, closeSync, fstatSync, linkSync, openSync, readSync, unlinkSync, writeFileSync, } from 'node:fs';
19
+ import { hostname } from 'node:os';
20
+ import { basename, dirname, join } from 'node:path';
21
+ import { HARDLINK_UNAVAILABLE_CODES, LOCK_RETRY_DELAY_MS, MAX_LOCK_BYTES, RECLAIM_LOCK_TIMEOUT_MS, } from './owned-lock.types.js';
22
+ export function randomHex(bytes) {
23
+ return randomBytes(bytes).toString('hex');
24
+ }
25
+ /**
26
+ * Synchronously block the calling thread for `ms` milliseconds without
27
+ * spinning the CPU. `Atomics.wait` on a throwaway `SharedArrayBuffer` is the
28
+ * standard synchronous-sleep primitive in Node (unlike browsers, Node does
29
+ * not forbid calling it on the main thread).
30
+ */
31
+ export function sleepSync(ms) {
32
+ const view = new Int32Array(new SharedArrayBuffer(4));
33
+ Atomics.wait(view, 0, 0, ms);
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // Claim parsing + bounded, TOCTOU-free read
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * Parse raw lock-file text into a {@link Claim}. A v1 claim is one line of
40
+ * canonical JSON; a legacy claim is a bare decimal integer (today's
41
+ * `acquireConfigLock` format, `String(process.pid)`); anything else --
42
+ * including valid JSON at a DIFFERENT `v` -- is `unparseable` and therefore
43
+ * NEVER auto-reclaimed.
44
+ */
45
+ export function parseClaim(text) {
46
+ const trimmed = text.trim();
47
+ if (trimmed.length === 0)
48
+ return { kind: 'unparseable' };
49
+ if (/^[0-9]+$/.test(trimmed)) {
50
+ const pid = Number(trimmed);
51
+ if (Number.isSafeInteger(pid))
52
+ return { kind: 'legacy', pid };
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(text);
57
+ }
58
+ catch {
59
+ return { kind: 'unparseable' };
60
+ }
61
+ if (typeof parsed !== 'object' || parsed === null)
62
+ return { kind: 'unparseable' };
63
+ const p = parsed;
64
+ if (p['v'] === 1 &&
65
+ typeof p['pid'] === 'number' &&
66
+ typeof p['token'] === 'string' &&
67
+ typeof p['host'] === 'string' &&
68
+ typeof p['acquiredAt'] === 'number') {
69
+ return {
70
+ kind: 'v1',
71
+ pid: p['pid'],
72
+ token: p['token'],
73
+ host: p['host'],
74
+ acquiredAt: p['acquiredAt'],
75
+ };
76
+ }
77
+ return { kind: 'unparseable' };
78
+ }
79
+ /**
80
+ * Bounded, TOCTOU-free claim read: open once, `fstat` THAT fd (not the
81
+ * path), refuse above {@link MAX_LOCK_BYTES}, read from the same fd. Returns
82
+ * `{ kind: 'absent' }` on `ENOENT` (or any other open failure).
83
+ */
84
+ export function readClaim(path) {
85
+ let fd;
86
+ try {
87
+ fd = openSync(path, 'r');
88
+ }
89
+ catch {
90
+ return { kind: 'absent' };
91
+ }
92
+ try {
93
+ const st = fstatSync(fd);
94
+ if (st.size > MAX_LOCK_BYTES)
95
+ return { kind: 'unparseable' };
96
+ const buf = Buffer.allocUnsafe(MAX_LOCK_BYTES);
97
+ const n = readSync(fd, buf, 0, MAX_LOCK_BYTES, 0);
98
+ return parseClaim(buf.subarray(0, n).toString('utf8'));
99
+ }
100
+ catch {
101
+ return { kind: 'unparseable' };
102
+ }
103
+ finally {
104
+ closeSync(fd);
105
+ }
106
+ }
107
+ export function isAutoReclaimDisabled() {
108
+ return process.env['SKILLSMITH_LOCK_NO_AUTO_RECLAIM'] === '1';
109
+ }
110
+ /**
111
+ * Conservative in every ambiguous direction: PID reuse and a not-our-signal
112
+ * `EPERM` both read as ALIVE (declining to reclaim costs a timeout; an
113
+ * incorrect reclaim costs a lost caller). `pid <= 0` is rejected WITHOUT
114
+ * probing -- `kill(0, 0)` signals the process GROUP, which would make the
115
+ * liveness probe meaningless.
116
+ */
117
+ export function isOwnerDefinitelyDead(claim, killProbe = process.kill) {
118
+ if (isAutoReclaimDisabled())
119
+ return false;
120
+ if (claim.kind !== 'v1')
121
+ return false; // legacy | unparseable | absent -- never (D-5/D-6)
122
+ if (claim.host !== hostname())
123
+ return false;
124
+ if (!Number.isInteger(claim.pid) || claim.pid <= 0)
125
+ return false;
126
+ try {
127
+ killProbe(claim.pid, 0);
128
+ return false; // signalable -> alive
129
+ }
130
+ catch (err) {
131
+ if (err.code === 'ESRCH')
132
+ return true; // no such process -> dead
133
+ return false; // EPERM (exists, not ours) or any other errno -> assume alive
134
+ }
135
+ }
136
+ export function classifyRefusal(claim) {
137
+ if (isAutoReclaimDisabled())
138
+ return 'reclaim-disabled';
139
+ if (claim.kind === 'legacy')
140
+ return 'legacy';
141
+ if (claim.kind === 'unparseable' || claim.kind === 'absent')
142
+ return 'unparseable';
143
+ return 'held';
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // Exclusive creation -- atomic AND content-complete (D-6)
147
+ // ---------------------------------------------------------------------------
148
+ /**
149
+ * Create the temp file that {@link createLockExclusive} will `linkSync` into
150
+ * place, EXCLUSIVELY (`wx`) rather than with the default truncating `w`.
151
+ * `randomHex(8)` collisions are astronomically unlikely, but a `w`-flag
152
+ * write is a silent last-writer-wins on collision -- including clobbering
153
+ * another caller's temp file AFTER it has already been `linkSync`'d into a
154
+ * live lock (the temp file's own content no longer matters to that lock at
155
+ * that point, but a `w`-flag collision earlier in the window, before the
156
+ * link, could still corrupt an in-flight sibling attempt). `wx` turns any
157
+ * collision into a loud `EEXIST` retry with a fresh random suffix instead of
158
+ * a silent overwrite.
159
+ */
160
+ function writeTempClaimExclusive(dir, base, recordJson) {
161
+ for (let attempt = 0; attempt < 5; attempt++) {
162
+ const tmp = join(dir, `.${base}.${randomHex(8)}.tmp`);
163
+ let fd;
164
+ try {
165
+ fd = openSync(tmp, 'wx', 0o600);
166
+ }
167
+ catch (err) {
168
+ if (err.code === 'EEXIST')
169
+ continue; // collision -- retry with a fresh suffix
170
+ throw err;
171
+ }
172
+ try {
173
+ writeFileSync(fd, recordJson);
174
+ }
175
+ finally {
176
+ closeSync(fd);
177
+ }
178
+ chmodSync(tmp, 0o600); // defeat a permissive umask BEFORE the file is reachable as a lock
179
+ return tmp;
180
+ }
181
+ throw new Error(`[skillsmith] could not create a unique temp claim file under ${dir} after 5 attempts`);
182
+ }
183
+ /**
184
+ * Create `path` exclusively via a temp file + `linkSync` (atomic; `EEXIST`
185
+ * if `path` exists) so a lock file is NEVER observable without a complete
186
+ * claim -- closing R2 (a writer could otherwise crash between create and
187
+ * write, leaving a permanently unreclaimable main lock). On filesystems
188
+ * without hardlink support this throws rather than falling back to a
189
+ * non-atomic `openSync('wx')` + separate write -- that two-step sequence
190
+ * would itself reopen R2 (an observer between the two steps, or a crash in
191
+ * between, sees an empty/truncated lock that is then PERMANENTLY
192
+ * unreclaimable, since an `unparseable` claim is never auto-reclaimed by
193
+ * design). Failing closed on an unsupported filesystem is the sound
194
+ * resolution the reviewer required; hardlink support is effectively
195
+ * universal on the filesystems Node actually runs on.
196
+ *
197
+ * @param linkSyncOverride - @internal test seam (owned-lock.test.ts item 14)
198
+ * to exercise the hardlink-unavailable fail-closed path deterministically.
199
+ * Never set outside that test.
200
+ */
201
+ export function createLockExclusive(path, recordJson, linkSyncOverride) {
202
+ const dir = dirname(path);
203
+ const tmp = writeTempClaimExclusive(dir, basename(path), recordJson);
204
+ const doLink = linkSyncOverride ?? linkSync;
205
+ try {
206
+ doLink(tmp, path);
207
+ return true;
208
+ }
209
+ catch (err) {
210
+ const code = err.code;
211
+ if (code === 'EEXIST')
212
+ return false;
213
+ if (code && HARDLINK_UNAVAILABLE_CODES.has(code)) {
214
+ throw new Error(`[skillsmith] cannot create an owned lock at ${path}: this filesystem does not ` +
215
+ `support hardlinks (${code}), which the lock's atomic-and-content-complete creation ` +
216
+ `guarantee depends on. Failing closed rather than falling back to a non-atomic write ` +
217
+ `(which could leave a permanently unreclaimable lock on a crash mid-write).`);
218
+ }
219
+ throw err;
220
+ }
221
+ finally {
222
+ try {
223
+ unlinkSync(tmp);
224
+ }
225
+ catch {
226
+ // Already gone (nlink 2 -> 1 on a successful link), or never created.
227
+ }
228
+ }
229
+ }
230
+ /**
231
+ * Validate and destroy a stale main lock inside a region from which every
232
+ * other reclaimer is excluded (the reclaim lock). This is the ONLY code path
233
+ * that may ever unlink `<target>.lock` on the strength of a liveness
234
+ * inference: a caller's own pre-filter is advisory and discarded; only the
235
+ * re-read performed HERE, under the reclaim lock, authorizes the unlink.
236
+ */
237
+ export function tryReclaimUnderLock(lockPath, reclaimPath, opts) {
238
+ const rToken = randomHex(8);
239
+ const rRecord = JSON.stringify({
240
+ v: 1,
241
+ pid: process.pid,
242
+ token: rToken,
243
+ host: hostname(),
244
+ acquiredAt: Date.now(),
245
+ }) + '\n';
246
+ const rDeadline = Date.now() + RECLAIM_LOCK_TIMEOUT_MS;
247
+ for (;;) {
248
+ if (createLockExclusive(reclaimPath, rRecord, opts.linkSyncOverride))
249
+ break;
250
+ if (Date.now() >= rDeadline)
251
+ return 'unavailable'; // concurrent reclaimer, or an R1 orphan
252
+ sleepSync(LOCK_RETRY_DELAY_MS);
253
+ }
254
+ try {
255
+ // ===== RECLAIM CRITICAL SECTION -- mutually exclusive against every other reclaimer =====
256
+ const claim = readClaim(lockPath); // AUTHORITATIVE re-read
257
+ if (!opts.unsafeSkipRevalidation) {
258
+ if (claim.kind === 'absent')
259
+ return 'gone';
260
+ if (!isOwnerDefinitelyDead(claim))
261
+ return 'not-stale';
262
+ }
263
+ else if (claim.kind === 'absent') {
264
+ return 'gone'; // even the negative control cannot unlink something not there
265
+ }
266
+ try {
267
+ unlinkSync(lockPath);
268
+ }
269
+ catch (err) {
270
+ if (err.code === 'ENOENT')
271
+ return 'gone';
272
+ throw err;
273
+ }
274
+ return 'reclaimed';
275
+ }
276
+ finally {
277
+ releaseOwned(reclaimPath, rToken);
278
+ }
279
+ }
280
+ // ---------------------------------------------------------------------------
281
+ // Release -- ownership-verified, idempotent
282
+ // ---------------------------------------------------------------------------
283
+ export function releaseOwned(path, token) {
284
+ const claim = readClaim(path);
285
+ if (claim.kind === 'v1' && claim.token === token) {
286
+ try {
287
+ unlinkSync(path);
288
+ }
289
+ catch (err) {
290
+ if (err.code !== 'ENOENT') {
291
+ console.warn(`[skillsmith] lock_release_unlink_failed: could not remove ${path}: ${err.message}`);
292
+ }
293
+ }
294
+ }
295
+ else {
296
+ // We no longer own it -- unlinking would destroy a live holder's lock
297
+ // (it was reclaimed from us after we appeared dead, or hand-edited).
298
+ console.warn(`[skillsmith] lock_release_not_owner: ${path} is no longer owned by this process -- not removing it.`);
299
+ }
300
+ }
301
+ export function makeRelease(lockPath, token) {
302
+ let released = false;
303
+ return () => {
304
+ if (released)
305
+ return;
306
+ released = true;
307
+ releaseOwned(lockPath, token);
308
+ };
309
+ }
310
+ //# sourceMappingURL=owned-lock.claim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-lock.claim.js","sourceRoot":"","sources":["../../../src/config/owned-lock.claim.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Two-level owned-lock primitive (SMI-5883 Wave 2, design revision 4).
3
+ * @module @skillsmith/core/config/owned-lock
4
+ *
5
+ * Replaces the single-level, age-based `acquireConfigLock` mechanism (which
6
+ * `config-atomic-write.ts` now implements as a thin wrapper over
7
+ * {@link acquireOwnedLock}). The single-level design was found unsound in
8
+ * round-3 review: it turned "I inspected this path and concluded the holder
9
+ * is dead" (computed in one region) into an unconditional
10
+ * `unlinkSync(lockPath)` (performed in another), with no mutual exclusion
11
+ * between the two. Any process that recreated the path in between had its
12
+ * LIVE lock destroyed by a contender acting on a stale decision.
13
+ *
14
+ * The general defect, stated so the fix can be checked against it: it is
15
+ * never sound to turn "I inspected this path and concluded X" into an
16
+ * unconditional destructive operation on that path, unless the interval
17
+ * between inspection and destruction excludes every actor that could change
18
+ * what the path holds. This module does not make the destruction
19
+ * conditional (POSIX offers no delete-if-inode primitive) -- it makes the
20
+ * interval exclusive, via a SECOND lock file (`<target>.lock.reclaim`,
21
+ * `owned-lock.claim.ts`'s `tryReclaimUnderLock`) that must be held before
22
+ * any reclaimer may re-read and then unlink `<target>.lock`. Every
23
+ * reclaimer therefore validates and destroys inside a region from which
24
+ * every other reclaimer -- and, by construction, every other mutator of
25
+ * that path -- is excluded. The authorization to reclaim can no longer go
26
+ * stale between being computed and being consumed, because nothing that
27
+ * could invalidate it is permitted to run in between.
28
+ *
29
+ * Two files, asymmetric policies:
30
+ * - `<target>.lock` -- the caller's lock. Auto-reclaimed, but ONLY
31
+ * from inside the reclaim critical section.
32
+ * - `<target>.lock.reclaim` -- serializes reclaim DECISIONS about the lock
33
+ * above. Strict, NEVER auto-reclaimed (a staleness heuristic here would
34
+ * reintroduce the identical duplicate-delete race one level up).
35
+ *
36
+ * This module is a thin wrapper over `owned-lock.acquire.ts`'s
37
+ * {@link acquireOwnedLockCore} -- it exists specifically so that the two
38
+ * destructive test-only seams on the core loop's options
39
+ * (`unsafeSkipReclaimRevalidation`, `linkSyncOverride`) are structurally
40
+ * unreachable from the public `@skillsmith/core/config/owned-lock` subpath
41
+ * export: this file's exported {@link AcquireOwnedLockOptions} simply does
42
+ * not have those fields, and this file never re-exports the internal
43
+ * `AcquireOwnedLockCoreOptions` type or `acquireOwnedLockCore` function
44
+ * that do. See owned-lock.acquire.ts's module comment for the full rationale.
45
+ *
46
+ * New-race audit (N1-N9) and residual risks (R1-R5) are documented in the
47
+ * SMI-5883 design doc; the short version: R1 (`SIGKILL` inside the
48
+ * ~3-syscall reclaim critical section orphans the reclaim lock) is accepted
49
+ * and loudly diagnosable ({@link StuckLockError} names both files + the
50
+ * manual unstick procedure below); legacy (pre-v1, bare-decimal-PID) claims
51
+ * and unparseable claims are NEVER auto-reclaimed (D-5/D-6) because they
52
+ * carry no `host` field, so "dead on this host" cannot establish "dead" for
53
+ * a claim that might belong to a process on a different host sharing a
54
+ * networked `$HOME`.
55
+ *
56
+ * Manual unstick (required whenever a `StuckLockError` fires with a reason
57
+ * other than `held`):
58
+ * 1. Confirm no skillsmith process is running:
59
+ * ps -ax | grep -E '[s]killsmith|[s]klx'
60
+ * 2. Inspect the claim (read-only, safe):
61
+ * cat <lockPath>
62
+ * cat <reclaimPath> # only if the error named it
63
+ * 3. Remove ONLY the file(s) the error named:
64
+ * rm <lockPath>
65
+ * rm <reclaimPath>
66
+ *
67
+ * Opt-out: `SKILLSMITH_LOCK_NO_AUTO_RECLAIM=1` disables auto-reclaim
68
+ * entirely (degrades to the strict, no-reclaim design) -- a sound escape
69
+ * hatch for R5 (an `os.hostname()` collision across two containers with
70
+ * distinct PID namespaces bind-mounting the same `$HOME`). Registered in
71
+ * `docs/internal/process/guards-and-opt-outs.md`.
72
+ */
73
+ import { StuckLockError } from './owned-lock.acquire.js';
74
+ import type { AcquireOwnedLockOptions } from './owned-lock.types.js';
75
+ export type { AcquireOwnedLockOptions, StuckLockReason } from './owned-lock.types.js';
76
+ export { LOCK_ACQUIRE_TIMEOUT_MS, LOCK_RETRY_DELAY_MS, MAX_LOCK_BYTES, RECLAIM_LOCK_TIMEOUT_MS, RECLAIM_PROBE_AFTER_MS, RECLAIM_PROBE_INTERVAL_MS, } from './owned-lock.types.js';
77
+ export { StuckLockError };
78
+ /**
79
+ * Acquire an exclusive, cross-process, OWNED lock guarding `target`. Unlike
80
+ * a plain create-exclusive lock, ownership is verified on release (a token,
81
+ * not just presence) and staleness is verified by PROCESS LIVENESS, not file
82
+ * age -- and reclaimed only from inside a second, strict-no-auto-reclaim
83
+ * lock (`<target>.lock.reclaim`) that serializes reclaim decisions. See the
84
+ * module docstring for the full soundness argument.
85
+ *
86
+ * @param target - Path being guarded (NOT the lock file itself -- the lock
87
+ * file is `${target}.lock`, the reclaim lock `${target}.lock.reclaim`).
88
+ * @returns A release function. Callers MUST call it exactly once, in a
89
+ * `finally` block. Idempotent -- a second call is a no-op.
90
+ *
91
+ * SMI-5883 code-review round 2: `opts` is destructured field-by-field into a
92
+ * FRESH object rather than forwarded as-is. TypeScript's excess-property
93
+ * check only fires on an object LITERAL passed directly at the call site --
94
+ * a caller passing a variable, a cast, or plain JavaScript (no type checking
95
+ * at all) is not constrained by `AcquireOwnedLockOptions` omitting the two
96
+ * unsafe fields, and `acquireOwnedLockCore` reads them by property name at
97
+ * runtime. Rebuilding the object here means an injected
98
+ * `unsafeSkipReclaimRevalidation`/`linkSyncOverride` on the caller's `opts`
99
+ * is structurally dropped before it ever reaches the core loop, regardless
100
+ * of what extra properties `opts` carries.
101
+ *
102
+ * SMI-5883 code-review round 3: the two unsafe fields are ALSO set to
103
+ * `undefined` explicitly, as OWN properties of the fresh object, rather than
104
+ * simply omitted. A plain `{ ...safeFields }` object still inherits from
105
+ * `Object.prototype` -- if that prototype were ever globally polluted (a
106
+ * distinct, severe vulnerability class in its own right, reachable only by
107
+ * an attacker who already has arbitrary code execution in this process),
108
+ * ordinary property access on an object with no OWN `unsafeSkipReclaimRevalidation`/
109
+ * `linkSyncOverride` would still resolve them via the prototype chain. An
110
+ * explicit own-property `undefined` shadows any such inherited value.
111
+ */
112
+ export declare function acquireOwnedLock(target: string, opts?: AcquireOwnedLockOptions): () => void;
113
+ //# sourceMappingURL=owned-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-lock.d.ts","sourceRoot":"","sources":["../../../src/config/owned-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH,OAAO,EAAwB,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAC9E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AAEpE,YAAY,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACrF,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,cAAc,EAAE,CAAA;AAEzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,uBAA4B,GAAG,MAAM,IAAI,CAU/F"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Two-level owned-lock primitive (SMI-5883 Wave 2, design revision 4).
3
+ * @module @skillsmith/core/config/owned-lock
4
+ *
5
+ * Replaces the single-level, age-based `acquireConfigLock` mechanism (which
6
+ * `config-atomic-write.ts` now implements as a thin wrapper over
7
+ * {@link acquireOwnedLock}). The single-level design was found unsound in
8
+ * round-3 review: it turned "I inspected this path and concluded the holder
9
+ * is dead" (computed in one region) into an unconditional
10
+ * `unlinkSync(lockPath)` (performed in another), with no mutual exclusion
11
+ * between the two. Any process that recreated the path in between had its
12
+ * LIVE lock destroyed by a contender acting on a stale decision.
13
+ *
14
+ * The general defect, stated so the fix can be checked against it: it is
15
+ * never sound to turn "I inspected this path and concluded X" into an
16
+ * unconditional destructive operation on that path, unless the interval
17
+ * between inspection and destruction excludes every actor that could change
18
+ * what the path holds. This module does not make the destruction
19
+ * conditional (POSIX offers no delete-if-inode primitive) -- it makes the
20
+ * interval exclusive, via a SECOND lock file (`<target>.lock.reclaim`,
21
+ * `owned-lock.claim.ts`'s `tryReclaimUnderLock`) that must be held before
22
+ * any reclaimer may re-read and then unlink `<target>.lock`. Every
23
+ * reclaimer therefore validates and destroys inside a region from which
24
+ * every other reclaimer -- and, by construction, every other mutator of
25
+ * that path -- is excluded. The authorization to reclaim can no longer go
26
+ * stale between being computed and being consumed, because nothing that
27
+ * could invalidate it is permitted to run in between.
28
+ *
29
+ * Two files, asymmetric policies:
30
+ * - `<target>.lock` -- the caller's lock. Auto-reclaimed, but ONLY
31
+ * from inside the reclaim critical section.
32
+ * - `<target>.lock.reclaim` -- serializes reclaim DECISIONS about the lock
33
+ * above. Strict, NEVER auto-reclaimed (a staleness heuristic here would
34
+ * reintroduce the identical duplicate-delete race one level up).
35
+ *
36
+ * This module is a thin wrapper over `owned-lock.acquire.ts`'s
37
+ * {@link acquireOwnedLockCore} -- it exists specifically so that the two
38
+ * destructive test-only seams on the core loop's options
39
+ * (`unsafeSkipReclaimRevalidation`, `linkSyncOverride`) are structurally
40
+ * unreachable from the public `@skillsmith/core/config/owned-lock` subpath
41
+ * export: this file's exported {@link AcquireOwnedLockOptions} simply does
42
+ * not have those fields, and this file never re-exports the internal
43
+ * `AcquireOwnedLockCoreOptions` type or `acquireOwnedLockCore` function
44
+ * that do. See owned-lock.acquire.ts's module comment for the full rationale.
45
+ *
46
+ * New-race audit (N1-N9) and residual risks (R1-R5) are documented in the
47
+ * SMI-5883 design doc; the short version: R1 (`SIGKILL` inside the
48
+ * ~3-syscall reclaim critical section orphans the reclaim lock) is accepted
49
+ * and loudly diagnosable ({@link StuckLockError} names both files + the
50
+ * manual unstick procedure below); legacy (pre-v1, bare-decimal-PID) claims
51
+ * and unparseable claims are NEVER auto-reclaimed (D-5/D-6) because they
52
+ * carry no `host` field, so "dead on this host" cannot establish "dead" for
53
+ * a claim that might belong to a process on a different host sharing a
54
+ * networked `$HOME`.
55
+ *
56
+ * Manual unstick (required whenever a `StuckLockError` fires with a reason
57
+ * other than `held`):
58
+ * 1. Confirm no skillsmith process is running:
59
+ * ps -ax | grep -E '[s]killsmith|[s]klx'
60
+ * 2. Inspect the claim (read-only, safe):
61
+ * cat <lockPath>
62
+ * cat <reclaimPath> # only if the error named it
63
+ * 3. Remove ONLY the file(s) the error named:
64
+ * rm <lockPath>
65
+ * rm <reclaimPath>
66
+ *
67
+ * Opt-out: `SKILLSMITH_LOCK_NO_AUTO_RECLAIM=1` disables auto-reclaim
68
+ * entirely (degrades to the strict, no-reclaim design) -- a sound escape
69
+ * hatch for R5 (an `os.hostname()` collision across two containers with
70
+ * distinct PID namespaces bind-mounting the same `$HOME`). Registered in
71
+ * `docs/internal/process/guards-and-opt-outs.md`.
72
+ */
73
+ import { acquireOwnedLockCore, StuckLockError } from './owned-lock.acquire.js';
74
+ export { LOCK_ACQUIRE_TIMEOUT_MS, LOCK_RETRY_DELAY_MS, MAX_LOCK_BYTES, RECLAIM_LOCK_TIMEOUT_MS, RECLAIM_PROBE_AFTER_MS, RECLAIM_PROBE_INTERVAL_MS, } from './owned-lock.types.js';
75
+ export { StuckLockError };
76
+ /**
77
+ * Acquire an exclusive, cross-process, OWNED lock guarding `target`. Unlike
78
+ * a plain create-exclusive lock, ownership is verified on release (a token,
79
+ * not just presence) and staleness is verified by PROCESS LIVENESS, not file
80
+ * age -- and reclaimed only from inside a second, strict-no-auto-reclaim
81
+ * lock (`<target>.lock.reclaim`) that serializes reclaim decisions. See the
82
+ * module docstring for the full soundness argument.
83
+ *
84
+ * @param target - Path being guarded (NOT the lock file itself -- the lock
85
+ * file is `${target}.lock`, the reclaim lock `${target}.lock.reclaim`).
86
+ * @returns A release function. Callers MUST call it exactly once, in a
87
+ * `finally` block. Idempotent -- a second call is a no-op.
88
+ *
89
+ * SMI-5883 code-review round 2: `opts` is destructured field-by-field into a
90
+ * FRESH object rather than forwarded as-is. TypeScript's excess-property
91
+ * check only fires on an object LITERAL passed directly at the call site --
92
+ * a caller passing a variable, a cast, or plain JavaScript (no type checking
93
+ * at all) is not constrained by `AcquireOwnedLockOptions` omitting the two
94
+ * unsafe fields, and `acquireOwnedLockCore` reads them by property name at
95
+ * runtime. Rebuilding the object here means an injected
96
+ * `unsafeSkipReclaimRevalidation`/`linkSyncOverride` on the caller's `opts`
97
+ * is structurally dropped before it ever reaches the core loop, regardless
98
+ * of what extra properties `opts` carries.
99
+ *
100
+ * SMI-5883 code-review round 3: the two unsafe fields are ALSO set to
101
+ * `undefined` explicitly, as OWN properties of the fresh object, rather than
102
+ * simply omitted. A plain `{ ...safeFields }` object still inherits from
103
+ * `Object.prototype` -- if that prototype were ever globally polluted (a
104
+ * distinct, severe vulnerability class in its own right, reachable only by
105
+ * an attacker who already has arbitrary code execution in this process),
106
+ * ordinary property access on an object with no OWN `unsafeSkipReclaimRevalidation`/
107
+ * `linkSyncOverride` would still resolve them via the prototype chain. An
108
+ * explicit own-property `undefined` shadows any such inherited value.
109
+ */
110
+ export function acquireOwnedLock(target, opts = {}) {
111
+ return acquireOwnedLockCore(target, {
112
+ timeoutMs: opts.timeoutMs,
113
+ label: opts.label,
114
+ reclaimProbeAfterMs: opts.reclaimProbeAfterMs,
115
+ onReclaimBoundary: opts.onReclaimBoundary,
116
+ onReclaimOutcome: opts.onReclaimOutcome,
117
+ unsafeSkipReclaimRevalidation: undefined,
118
+ linkSyncOverride: undefined,
119
+ });
120
+ }
121
+ //# sourceMappingURL=owned-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-lock.js","sourceRoot":"","sources":["../../../src/config/owned-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAI9E,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,cAAc,EAAE,CAAA;AAEzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,OAAgC,EAAE;IACjF,OAAO,oBAAoB,CAAC,MAAM,EAAE;QAClC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,mBAAmB,EAAE,IAAI,CAAC,mBAAmB;QAC7C,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;QACzC,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;QACvC,6BAA6B,EAAE,SAAS;QACxC,gBAAgB,EAAE,SAAS;KAC5B,CAAC,CAAA;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Unit tests for the two-level owned-lock primitive (SMI-5883 §8a).
3
+ * @module @skillsmith/core/config/owned-lock.test
4
+ *
5
+ * Single-process tests of `acquireOwnedLock` itself. Cross-process tests
6
+ * (the reclaim-race proof, the orphaned-reclaim-lock residual, and the
7
+ * end-to-end lost-update stress test) live in
8
+ * `packages/core/tests/integration/owned-lock-reclaim-race.test.ts` (§8b/8c)
9
+ * and `packages/core/tests/integration/owned-lock-lost-update.test.ts` (§8f).
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=owned-lock.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-lock.test.d.ts","sourceRoot":"","sources":["../../../src/config/owned-lock.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}