@hraness/slopcamera 3.2.4

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 (441) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +60 -0
  3. package/PRIVACY.md +46 -0
  4. package/README.md +311 -0
  5. package/SECURITY.md +21 -0
  6. package/apps/desktop/README.md +966 -0
  7. package/apps/desktop/analysis/protocol.ts +223 -0
  8. package/apps/desktop/application/capability-binding.ts +594 -0
  9. package/apps/desktop/application/context.ts +97 -0
  10. package/apps/desktop/application/creative-iteration.ts +1385 -0
  11. package/apps/desktop/application/default-registry.ts +146 -0
  12. package/apps/desktop/application/directing-contract.ts +218 -0
  13. package/apps/desktop/application/directing-plan.ts +178 -0
  14. package/apps/desktop/application/errors.ts +18 -0
  15. package/apps/desktop/application/gateway-port.ts +867 -0
  16. package/apps/desktop/application/html-overlay-browser-runtime.ts +687 -0
  17. package/apps/desktop/application/html-overlay-integrity.ts +337 -0
  18. package/apps/desktop/application/html-overlay-renderer.ts +45 -0
  19. package/apps/desktop/application/index.ts +13 -0
  20. package/apps/desktop/application/operation-completion-checkpoint.ts +333 -0
  21. package/apps/desktop/application/operation-effects.ts +94 -0
  22. package/apps/desktop/application/operation.ts +224 -0
  23. package/apps/desktop/application/operations/analysis/faces.ts +244 -0
  24. package/apps/desktop/application/operations/analysis/music.ts +248 -0
  25. package/apps/desktop/application/operations/analysis/project-auto-zooms.ts +302 -0
  26. package/apps/desktop/application/operations/analysis/project-inactivity.ts +381 -0
  27. package/apps/desktop/application/operations/analysis/scenes.ts +272 -0
  28. package/apps/desktop/application/operations/analysis/shared.ts +202 -0
  29. package/apps/desktop/application/operations/derive/edit-batch.ts +967 -0
  30. package/apps/desktop/application/operations/derive/follow-faces.ts +454 -0
  31. package/apps/desktop/application/operations/edit/bind-candidate-revision.ts +91 -0
  32. package/apps/desktop/application/operations/edit/create-candidate-revision.ts +377 -0
  33. package/apps/desktop/application/operations/edit/create-revision.ts +255 -0
  34. package/apps/desktop/application/operations/edit/freeze-revision.ts +182 -0
  35. package/apps/desktop/application/operations/gateway/image.ts +41 -0
  36. package/apps/desktop/application/operations/gateway/index.ts +17 -0
  37. package/apps/desktop/application/operations/gateway/shared.ts +122 -0
  38. package/apps/desktop/application/operations/gateway/speech.ts +41 -0
  39. package/apps/desktop/application/operations/gateway/transcription.ts +42 -0
  40. package/apps/desktop/application/operations/gateway/video.ts +41 -0
  41. package/apps/desktop/application/operations/index.ts +31 -0
  42. package/apps/desktop/application/operations/iteration/create-candidate.ts +240 -0
  43. package/apps/desktop/application/operations/iteration/create-matrix.ts +117 -0
  44. package/apps/desktop/application/operations/iteration/select.ts +155 -0
  45. package/apps/desktop/application/operations/iteration/shared.ts +250 -0
  46. package/apps/desktop/application/operations/media/audio-effects.ts +376 -0
  47. package/apps/desktop/application/operations/media/binding.ts +79 -0
  48. package/apps/desktop/application/operations/media/capabilities.ts +112 -0
  49. package/apps/desktop/application/operations/media/color-grade.ts +343 -0
  50. package/apps/desktop/application/operations/media/html-overlay.ts +802 -0
  51. package/apps/desktop/application/operations/media/index.ts +8 -0
  52. package/apps/desktop/application/operations/media/ingest.ts +303 -0
  53. package/apps/desktop/application/operations/media/overlay.ts +729 -0
  54. package/apps/desktop/application/operations/media/shared.ts +845 -0
  55. package/apps/desktop/application/operations/media/transform-platform.ts +120 -0
  56. package/apps/desktop/application/operations/project/commit-edits.ts +2054 -0
  57. package/apps/desktop/application/operations/project/promote-selection.ts +495 -0
  58. package/apps/desktop/application/operations/project/snapshot.ts +95 -0
  59. package/apps/desktop/application/operations/recording/index.ts +5 -0
  60. package/apps/desktop/application/operations/recording/pause.ts +34 -0
  61. package/apps/desktop/application/operations/recording/resume.ts +34 -0
  62. package/apps/desktop/application/operations/recording/shared.ts +199 -0
  63. package/apps/desktop/application/operations/recording/start.ts +169 -0
  64. package/apps/desktop/application/operations/recording/stop.ts +34 -0
  65. package/apps/desktop/application/operations/render/bind-candidate-output.ts +324 -0
  66. package/apps/desktop/application/operations/render/materialize-selection.ts +322 -0
  67. package/apps/desktop/application/operations/render/project-caption-plan.ts +287 -0
  68. package/apps/desktop/application/operations/render/project-plan.ts +939 -0
  69. package/apps/desktop/application/operations/render/project-spatial-receipt.ts +70 -0
  70. package/apps/desktop/application/operations/render/project.ts +1922 -0
  71. package/apps/desktop/application/operations/shared.ts +67 -0
  72. package/apps/desktop/application/operations/slopcamera-portable.ts +1188 -0
  73. package/apps/desktop/application/operations/slopcamera-visuals.ts +784 -0
  74. package/apps/desktop/application/operations/spatial-project.ts +209 -0
  75. package/apps/desktop/application/operations/spatial-render.ts +415 -0
  76. package/apps/desktop/application/operations/spatial-scene.ts +97 -0
  77. package/apps/desktop/application/operations/studio.ts +143 -0
  78. package/apps/desktop/application/output-publication-lease.ts +163 -0
  79. package/apps/desktop/application/project-publication-lease.ts +233 -0
  80. package/apps/desktop/application/project-store.ts +196 -0
  81. package/apps/desktop/application/receipts.ts +533 -0
  82. package/apps/desktop/application/registry.ts +182 -0
  83. package/apps/desktop/application/spatial-assets.ts +675 -0
  84. package/apps/desktop/application/spatial-compositor-cadence.ts +23 -0
  85. package/apps/desktop/application/spatial-project-authority.ts +35 -0
  86. package/apps/desktop/application/spatial-project-lease.ts +126 -0
  87. package/apps/desktop/application/spatial-project-render-preparation.ts +225 -0
  88. package/apps/desktop/application/spatial-project-store.ts +286 -0
  89. package/apps/desktop/application/spatial-render-projection.ts +201 -0
  90. package/apps/desktop/application/spatial-render.ts +677 -0
  91. package/apps/desktop/application/spatial-spz.ts +44 -0
  92. package/apps/desktop/application/spatial-world-import.ts +135 -0
  93. package/apps/desktop/application/spatial-world-provenance.ts +46 -0
  94. package/apps/desktop/application/studio-port.ts +58 -0
  95. package/apps/desktop/application/verified-receipt-reconciliation.ts +1574 -0
  96. package/apps/desktop/capture/protocol.ts +689 -0
  97. package/apps/desktop/cli/analyzer.ts +406 -0
  98. package/apps/desktop/cli/args.ts +3181 -0
  99. package/apps/desktop/cli/asset-ingest.ts +747 -0
  100. package/apps/desktop/cli/atomic-render-effects.ts +210 -0
  101. package/apps/desktop/cli/atomic-render-platform.ts +205 -0
  102. package/apps/desktop/cli/atomic-render.ts +50 -0
  103. package/apps/desktop/cli/audio-analysis.ts +252 -0
  104. package/apps/desktop/cli/bounded-map.ts +39 -0
  105. package/apps/desktop/cli/bundle-service.ts +256 -0
  106. package/apps/desktop/cli/capabilities.ts +212 -0
  107. package/apps/desktop/cli/capture-bundle.ts +1117 -0
  108. package/apps/desktop/cli/command-host-resources.ts +251 -0
  109. package/apps/desktop/cli/commands.ts +6836 -0
  110. package/apps/desktop/cli/directing-blob.ts +291 -0
  111. package/apps/desktop/cli/directing-media.ts +517 -0
  112. package/apps/desktop/cli/directing-quote.ts +89 -0
  113. package/apps/desktop/cli/directing-service.ts +288 -0
  114. package/apps/desktop/cli/emoji-assets.ts +538 -0
  115. package/apps/desktop/cli/errors.ts +29 -0
  116. package/apps/desktop/cli/face-analysis-service.ts +940 -0
  117. package/apps/desktop/cli/filter-script.ts +76 -0
  118. package/apps/desktop/cli/gateway-application-port.ts +1142 -0
  119. package/apps/desktop/cli/gateway-credential.ts +81 -0
  120. package/apps/desktop/cli/gateway-media-artifacts.ts +723 -0
  121. package/apps/desktop/cli/gateway-media-catalog.ts +922 -0
  122. package/apps/desktop/cli/gateway-media-service.ts +3106 -0
  123. package/apps/desktop/cli/gateway-media-signature.ts +122 -0
  124. package/apps/desktop/cli/gateway-provider-options.ts +562 -0
  125. package/apps/desktop/cli/gateway-scene-provider.ts +391 -0
  126. package/apps/desktop/cli/help.ts +501 -0
  127. package/apps/desktop/cli/html-overlay-gpu.ts +97 -0
  128. package/apps/desktop/cli/html-overlay-renderer.ts +2478 -0
  129. package/apps/desktop/cli/io.ts +290 -0
  130. package/apps/desktop/cli/main.ts +123 -0
  131. package/apps/desktop/cli/media-effects-service.ts +885 -0
  132. package/apps/desktop/cli/media-ingest-model.ts +364 -0
  133. package/apps/desktop/cli/media-ingest-platform.ts +227 -0
  134. package/apps/desktop/cli/media-ingest-program.ts +200 -0
  135. package/apps/desktop/cli/media-ingest.ts +30 -0
  136. package/apps/desktop/cli/music-analysis-service.ts +434 -0
  137. package/apps/desktop/cli/mutation-lock.ts +530 -0
  138. package/apps/desktop/cli/native-assets.d.ts +12 -0
  139. package/apps/desktop/cli/native-media-runtime.macos.ts +7 -0
  140. package/apps/desktop/cli/overlay-playback.ts +177 -0
  141. package/apps/desktop/cli/paths.ts +265 -0
  142. package/apps/desktop/cli/portable-surface.ts +242 -0
  143. package/apps/desktop/cli/project-camera-receipt.ts +112 -0
  144. package/apps/desktop/cli/project-face-camera.ts +394 -0
  145. package/apps/desktop/cli/project-inactivity-service.ts +552 -0
  146. package/apps/desktop/cli/project-media-integrity.ts +121 -0
  147. package/apps/desktop/cli/project-renderer.ts +1526 -0
  148. package/apps/desktop/cli/project-service.ts +519 -0
  149. package/apps/desktop/cli/project-state-transaction.ts +520 -0
  150. package/apps/desktop/cli/query-limits.ts +1 -0
  151. package/apps/desktop/cli/recording-controller.ts +1567 -0
  152. package/apps/desktop/cli/recording-daemon.ts +931 -0
  153. package/apps/desktop/cli/recording-ref.ts +56 -0
  154. package/apps/desktop/cli/renamed-environment.ts +6 -0
  155. package/apps/desktop/cli/renderer.ts +1439 -0
  156. package/apps/desktop/cli/run-cli-test-helper.ts +60 -0
  157. package/apps/desktop/cli/scene-analysis-service.ts +879 -0
  158. package/apps/desktop/cli/spatial-compositor-verifier.ts +213 -0
  159. package/apps/desktop/cli/spatial-project-service.ts +90 -0
  160. package/apps/desktop/cli/spatial-scene-service.ts +105 -0
  161. package/apps/desktop/cli/spatial-world-service.ts +39 -0
  162. package/apps/desktop/cli/speech-analysis-service.ts +682 -0
  163. package/apps/desktop/cli/studio-assemble.ts +58 -0
  164. package/apps/desktop/cli/studio-command.ts +60 -0
  165. package/apps/desktop/cli/studio-custody.ts +60 -0
  166. package/apps/desktop/cli/studio-encode.ts +216 -0
  167. package/apps/desktop/cli/studio-exr.ts +52 -0
  168. package/apps/desktop/cli/studio-files.ts +179 -0
  169. package/apps/desktop/cli/studio-output-validation.ts +162 -0
  170. package/apps/desktop/cli/studio-process.ts +145 -0
  171. package/apps/desktop/cli/studio-runtime.ts +77 -0
  172. package/apps/desktop/cli/studio-scaffold.ts +70 -0
  173. package/apps/desktop/cli/studio-service.ts +301 -0
  174. package/apps/desktop/cli/studio-spatial-asset.ts +248 -0
  175. package/apps/desktop/cli/studio-template-names.ts +3 -0
  176. package/apps/desktop/cli/studio-workflow.ts +19 -0
  177. package/apps/desktop/cli/time.ts +25 -0
  178. package/apps/desktop/cli/workflow-code.ts +584 -0
  179. package/apps/desktop/cli/workflow-runs.ts +576 -0
  180. package/apps/desktop/code/advanced.ts +139 -0
  181. package/apps/desktop/code/application-node-planner.ts +1288 -0
  182. package/apps/desktop/code/compiler.ts +329 -0
  183. package/apps/desktop/code/contracts.ts +13 -0
  184. package/apps/desktop/code/creative-recipes.ts +94 -0
  185. package/apps/desktop/code/define-workflow.ts +115 -0
  186. package/apps/desktop/code/editing.ts +284 -0
  187. package/apps/desktop/code/file-candidate-provenance.ts +420 -0
  188. package/apps/desktop/code/file-candidate.ts +69 -0
  189. package/apps/desktop/code/graph-builder.ts +10 -0
  190. package/apps/desktop/code/host-resource-policy.ts +55 -0
  191. package/apps/desktop/code/plan-contracts.ts +159 -0
  192. package/apps/desktop/code/planning.ts +361 -0
  193. package/apps/desktop/code/public.ts +134 -0
  194. package/apps/desktop/code/reference-projection.ts +62 -0
  195. package/apps/desktop/code/run-contracts.ts +466 -0
  196. package/apps/desktop/code/run-store.ts +2478 -0
  197. package/apps/desktop/code/runtime-identity.ts +328 -0
  198. package/apps/desktop/code/scheduler.ts +3067 -0
  199. package/apps/desktop/code/semantic-builder.ts +1436 -0
  200. package/apps/desktop/code/source-bundle.ts +1641 -0
  201. package/apps/desktop/code/source-typecheck.ts +211 -0
  202. package/apps/desktop/code/testing.ts +79 -0
  203. package/apps/desktop/code/worker-client.ts +1675 -0
  204. package/apps/desktop/code/worker-effects.ts +141 -0
  205. package/apps/desktop/code/worker-entry.ts +435 -0
  206. package/apps/desktop/code/worker-lease-guardian.ts +179 -0
  207. package/apps/desktop/code/worker-process-identity.ts +484 -0
  208. package/apps/desktop/code/worker-protocol.ts +211 -0
  209. package/apps/desktop/code/workflow-effects.ts +132 -0
  210. package/apps/desktop/contracts/analysis.ts +949 -0
  211. package/apps/desktop/contracts/edit.ts +576 -0
  212. package/apps/desktop/contracts/index.ts +9 -0
  213. package/apps/desktop/contracts/media-effects.ts +139 -0
  214. package/apps/desktop/contracts/project-render.ts +644 -0
  215. package/apps/desktop/contracts/project.ts +740 -0
  216. package/apps/desktop/contracts/recording.ts +1434 -0
  217. package/apps/desktop/contracts/render.ts +334 -0
  218. package/apps/desktop/contracts/runtime.ts +155 -0
  219. package/apps/desktop/contracts/spatial-compositor.ts +58 -0
  220. package/apps/desktop/contracts/spatial-project.ts +159 -0
  221. package/apps/desktop/contracts/spatial-world.ts +48 -0
  222. package/apps/desktop/contracts/video-effects.ts +120 -0
  223. package/apps/desktop/core/alignment-apply.ts +400 -0
  224. package/apps/desktop/core/audio-alignment.ts +466 -0
  225. package/apps/desktop/core/auto-zoom.ts +209 -0
  226. package/apps/desktop/core/canonical-json.ts +14 -0
  227. package/apps/desktop/core/emoji.ts +210 -0
  228. package/apps/desktop/core/events.ts +409 -0
  229. package/apps/desktop/core/face-framing.ts +533 -0
  230. package/apps/desktop/core/face-tracks.ts +326 -0
  231. package/apps/desktop/core/inactivity.ts +137 -0
  232. package/apps/desktop/core/index.ts +30 -0
  233. package/apps/desktop/core/intervals.ts +123 -0
  234. package/apps/desktop/core/montage.ts +236 -0
  235. package/apps/desktop/core/music-analysis.ts +518 -0
  236. package/apps/desktop/core/plan.ts +244 -0
  237. package/apps/desktop/core/project-auto-zoom.ts +75 -0
  238. package/apps/desktop/core/project-camera-spatial.ts +676 -0
  239. package/apps/desktop/core/project-camera.ts +403 -0
  240. package/apps/desktop/core/project-captions.ts +888 -0
  241. package/apps/desktop/core/project-inactivity.ts +125 -0
  242. package/apps/desktop/core/project-metadata.ts +927 -0
  243. package/apps/desktop/core/project-plan.ts +375 -0
  244. package/apps/desktop/core/project-render-plan.ts +303 -0
  245. package/apps/desktop/core/project-time.ts +469 -0
  246. package/apps/desktop/core/render-plan.ts +442 -0
  247. package/apps/desktop/core/scene-cache.ts +174 -0
  248. package/apps/desktop/core/scene-sampling.ts +448 -0
  249. package/apps/desktop/core/spatial-compositor.ts +22 -0
  250. package/apps/desktop/core/spatial-durability.ts +99 -0
  251. package/apps/desktop/core/spatial-project.ts +169 -0
  252. package/apps/desktop/core/spatial-shot-clock.ts +32 -0
  253. package/apps/desktop/core/speech-analysis.ts +333 -0
  254. package/apps/desktop/core/storage.ts +970 -0
  255. package/apps/desktop/core/time-map.ts +91 -0
  256. package/apps/desktop/core/time.ts +61 -0
  257. package/apps/desktop/core/video-effects.ts +660 -0
  258. package/apps/desktop/dist/cli/NebulaSans-Bold-26se8aek.otf +0 -0
  259. package/apps/desktop/dist/cli/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
  260. package/apps/desktop/dist/cli/NebulaSans-Book-5ax05zvn.woff2 +0 -0
  261. package/apps/desktop/dist/cli/NebulaSans-Book-8cenzchw.otf +0 -0
  262. package/apps/desktop/dist/cli/main.js +5086 -0
  263. package/apps/desktop/html-overlay/catalog.ts +171 -0
  264. package/apps/desktop/html-overlay/contracts.ts +372 -0
  265. package/apps/desktop/html-overlay/execution-profile.ts +77 -0
  266. package/apps/desktop/html-overlay/index.ts +98 -0
  267. package/apps/desktop/html-overlay/libraries.ts +263 -0
  268. package/apps/desktop/html-overlay/random.ts +49 -0
  269. package/apps/desktop/html-overlay/runtime.ts +813 -0
  270. package/apps/desktop/html-overlay/scaffolds.ts +792 -0
  271. package/apps/desktop/html-overlay/spark-runtime.ts +88 -0
  272. package/apps/desktop/html-overlay/spatial-splat-runtime.ts +60 -0
  273. package/apps/desktop/html-overlay/spatial.ts +720 -0
  274. package/apps/desktop/studio/assets/README.md +39 -0
  275. package/apps/desktop/studio/assets/contracts.ts +129 -0
  276. package/apps/desktop/studio/assets/network.ts +64 -0
  277. package/apps/desktop/studio/assets/poly-haven.ts +148 -0
  278. package/apps/desktop/studio/drivers/blender_driver.py +466 -0
  279. package/apps/desktop/studio/drivers/cadquery_driver.py +176 -0
  280. package/apps/desktop/studio/education/driver.py +308 -0
  281. package/apps/desktop/studio/education/lesson.py +180 -0
  282. package/apps/desktop/studio/education/toolkit.py +249 -0
  283. package/apps/desktop/workflows/chaptered-demo.ts +48 -0
  284. package/apps/desktop/workflows/creative-iteration.ts +107 -0
  285. package/apps/desktop/workflows/creative-selection.ts +92 -0
  286. package/apps/desktop/workflows/fragments.ts +486 -0
  287. package/apps/desktop/workflows/index.ts +93 -0
  288. package/apps/desktop/workflows/polished-screen-demo.ts +128 -0
  289. package/apps/desktop/workflows/scene-direction.ts +12 -0
  290. package/apps/desktop/workflows/social-variants.ts +75 -0
  291. package/apps/desktop/workflows/talking-head-cleanup.ts +53 -0
  292. package/dist/NebulaSans-Bold-26se8aek.otf +0 -0
  293. package/dist/NebulaSans-Bold-bcz7y08t.woff2 +0 -0
  294. package/dist/NebulaSans-Book-5ax05zvn.woff2 +0 -0
  295. package/dist/NebulaSans-Book-8cenzchw.otf +0 -0
  296. package/dist/cli.js +479 -0
  297. package/dist/code/advanced.js +270 -0
  298. package/dist/code/index.js +2240 -0
  299. package/dist/generate.js +24 -0
  300. package/dist/host-resources.js +26 -0
  301. package/dist/index-42zsesc1.js +2257 -0
  302. package/dist/index-7308egqr.js +48 -0
  303. package/dist/index-8txs6fkn.js +712 -0
  304. package/dist/index-mcy8z0br.js +192 -0
  305. package/dist/index-p63wavx0.js +2029 -0
  306. package/dist/index-r7gdhmsp.js +424 -0
  307. package/dist/index-sh6xbav6.js +1219 -0
  308. package/dist/index-z1w83f81.js +4 -0
  309. package/dist/index-z7239b4h.js +2363 -0
  310. package/dist/index-zfeg9hmw.js +2013 -0
  311. package/dist/index.js +241 -0
  312. package/dist/operations.js +29 -0
  313. package/dist/skill-install-dh0ntqba.js +10 -0
  314. package/dist/vectorize/worker.js +142 -0
  315. package/dist/workflow.js +16 -0
  316. package/docs/README.md +38 -0
  317. package/docs/studio.md +215 -0
  318. package/examples/studio/blender/README.md +26 -0
  319. package/examples/studio/blender/character.py +152 -0
  320. package/examples/studio/blender/cloth.py +101 -0
  321. package/examples/studio/blender/color_chart.py +29 -0
  322. package/examples/studio/blender/fluid.py +78 -0
  323. package/examples/studio/blender/import_model.py +74 -0
  324. package/examples/studio/blender/product.py +53 -0
  325. package/examples/studio/blender/shaded_street.py +481 -0
  326. package/examples/studio/blender/studio_scene.py +147 -0
  327. package/examples/studio/cadquery/README.md +13 -0
  328. package/examples/studio/cadquery/bracket.py +36 -0
  329. package/examples/studio/cadquery/import_step.py +12 -0
  330. package/examples/studio/education/README.md +46 -0
  331. package/examples/studio/education/lesson.json +99 -0
  332. package/examples/studio/education/scene.py +7 -0
  333. package/examples/studio/hybrid-scene.ts +96 -0
  334. package/examples/studio/native-workflow.ts +14 -0
  335. package/examples/studio/vgpu/README.md +64 -0
  336. package/examples/studio/vgpu/heat-field.mjs +290 -0
  337. package/package.json +304 -0
  338. package/packages/scene/src/index.ts +1 -0
  339. package/packages/scene/src/scene.ts +215 -0
  340. package/schema/diagram.schema.json +334 -0
  341. package/skills/slopcamera/SKILL.md +45 -0
  342. package/skills/slopcamera/agents/openai.yaml +4 -0
  343. package/skills/slopcamera/references/customization.md +77 -0
  344. package/skills/slopcamera/references/diagrams.md +153 -0
  345. package/skills/slopcamera/references/directed-scenes.md +42 -0
  346. package/skills/slopcamera/references/directing-video.md +18 -0
  347. package/skills/slopcamera/references/educational-video.md +26 -0
  348. package/skills/slopcamera/references/gateway-media.md +134 -0
  349. package/skills/slopcamera/references/install.md +62 -0
  350. package/skills/slopcamera/references/native-studio.md +31 -0
  351. package/skills/slopcamera/references/reference-led-3d.md +140 -0
  352. package/skills/slopcamera/references/rubber-stamp-examples/poster-example-1.jpg +0 -0
  353. package/skills/slopcamera/references/rubber-stamp-examples/poster-example-2.jpg +0 -0
  354. package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-1.png +0 -0
  355. package/skills/slopcamera/references/rubber-stamp-examples/stamp-style-2.png +0 -0
  356. package/skills/slopcamera/references/rubber-stamp-field-notes.md +144 -0
  357. package/skills/slopcamera/references/vectorization.md +29 -0
  358. package/skills/slopcamera/references/video-projects.md +187 -0
  359. package/skills/slopcamera/references/visual-communication.md +120 -0
  360. package/skills/slopcamera/references/workflows-sdk.md +45 -0
  361. package/skills/slopcamera/scripts/compose-rubber-stamp-field-note.ts +229 -0
  362. package/src/artifacts.ts +106 -0
  363. package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
  364. package/src/assets/fonts/nebula-sans/NebulaSans-Bold.otf +0 -0
  365. package/src/assets/fonts/nebula-sans/NebulaSans-Bold.woff2 +0 -0
  366. package/src/assets/fonts/nebula-sans/NebulaSans-Book.otf +0 -0
  367. package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  368. package/src/assets/fonts/nebula-sans/PROVENANCE.md +21 -0
  369. package/src/assets.d.ts +9 -0
  370. package/src/cli.ts +601 -0
  371. package/src/cloud-errors.ts +26 -0
  372. package/src/code/advanced.ts +10 -0
  373. package/src/code/boundary.ts +21 -0
  374. package/src/code/canonical-json.ts +129 -0
  375. package/src/code/compiler.ts +953 -0
  376. package/src/code/contracts.ts +576 -0
  377. package/src/code/define-workflow.ts +334 -0
  378. package/src/code/errors.ts +41 -0
  379. package/src/code/graph-builder.ts +945 -0
  380. package/src/code/index.ts +121 -0
  381. package/src/code/json-snapshot.ts +899 -0
  382. package/src/code/json-utf8.ts +88 -0
  383. package/src/code/portable-builder.ts +136 -0
  384. package/src/code/projection.ts +378 -0
  385. package/src/code/public-operations.ts +450 -0
  386. package/src/code/runtime.ts +580 -0
  387. package/src/code/sha256.ts +36 -0
  388. package/src/config.ts +180 -0
  389. package/src/desktop.ts +253 -0
  390. package/src/fs.ts +10 -0
  391. package/src/generate.ts +704 -0
  392. package/src/host-resource-posix.ts +161 -0
  393. package/src/host-resources.ts +1749 -0
  394. package/src/icons.ts +58 -0
  395. package/src/index.ts +200 -0
  396. package/src/label-layout.ts +130 -0
  397. package/src/layout.ts +413 -0
  398. package/src/lint.ts +127 -0
  399. package/src/mcp/boundary.ts +354 -0
  400. package/src/mcp/index.ts +33 -0
  401. package/src/mcp/server.ts +272 -0
  402. package/src/mcp/tools.ts +963 -0
  403. package/src/mcp/types.ts +71 -0
  404. package/src/operations.ts +775 -0
  405. package/src/parse.ts +861 -0
  406. package/src/process-environment.ts +17 -0
  407. package/src/render.ts +437 -0
  408. package/src/skill-install.ts +49 -0
  409. package/src/spatial-scene/authoring.ts +27 -0
  410. package/src/spatial-scene/camera-track.ts +83 -0
  411. package/src/spatial-scene/contracts.ts +277 -0
  412. package/src/spatial-scene/evaluate.ts +145 -0
  413. package/src/spatial-scene/gltf.ts +558 -0
  414. package/src/spatial-scene/identity.ts +260 -0
  415. package/src/spatial-scene/index.ts +10 -0
  416. package/src/spatial-scene/inspect.ts +68 -0
  417. package/src/spatial-scene/math.ts +280 -0
  418. package/src/spatial-scene/patch.ts +131 -0
  419. package/src/spatial-scene/time.ts +53 -0
  420. package/src/studio/contracts.ts +129 -0
  421. package/src/studio/index.ts +2 -0
  422. package/src/studio/plan.ts +156 -0
  423. package/src/studio/shared.ts +41 -0
  424. package/src/theme.ts +68 -0
  425. package/src/tldr.ts +474 -0
  426. package/src/types.ts +212 -0
  427. package/src/vectorize/archive.ts +101 -0
  428. package/src/vectorize/command.ts +797 -0
  429. package/src/vectorize/index.ts +27 -0
  430. package/src/vectorize/limits.ts +61 -0
  431. package/src/vectorize/metrics.ts +351 -0
  432. package/src/vectorize/pixels.ts +323 -0
  433. package/src/vectorize/supervisor.ts +339 -0
  434. package/src/vectorize/svg.ts +188 -0
  435. package/src/vectorize/tool.ts +542 -0
  436. package/src/vectorize/types.ts +113 -0
  437. package/src/vectorize/vectorize.ts +634 -0
  438. package/src/vectorize/worker-protocol.ts +40 -0
  439. package/src/vectorize/worker.ts +209 -0
  440. package/src/version.ts +1 -0
  441. package/src/workflow.ts +409 -0
@@ -0,0 +1,1749 @@
1
+ import { createHash, randomBytes } from "node:crypto"
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ constants,
6
+ fchmodSync,
7
+ fstatSync,
8
+ ftruncateSync,
9
+ fsyncSync,
10
+ lstatSync,
11
+ mkdirSync,
12
+ openSync,
13
+ readSync,
14
+ readdirSync,
15
+ realpathSync,
16
+ renameSync,
17
+ statSync,
18
+ unlinkSync,
19
+ writeSync,
20
+ type Stats,
21
+ } from "node:fs"
22
+ import { availableParallelism, homedir } from "node:os"
23
+ import { isAbsolute, join } from "node:path"
24
+ import { performance } from "node:perf_hooks"
25
+ import {
26
+ isHostResourcePlatformSupported,
27
+ tryLockHostResourceDescriptor,
28
+ unlockHostResourceDescriptor,
29
+ } from "./host-resource-posix.js"
30
+
31
+ const profileIdPattern = /^[a-z0-9][a-z0-9._/-]{0,119}$/u
32
+ const resourceNamePattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u
33
+ const ownerIdPattern = /^[0-9a-f]{32}$/u
34
+ const ticketPattern = /^[1-9][0-9]{0,19}$/u
35
+ const markerNamePattern = /^lease-([0-9]{20})-([0-9a-f]{32})\.lock$/u
36
+ const temporaryNamePattern = /^\.host-resources-[0-9a-f]{32}\.tmp$/u
37
+ const stateFileName = "state.json"
38
+ const controlFileName = "control.lock"
39
+ const initializedFileName = "initialized.lock"
40
+ const maximumResources = 64
41
+ const maximumProcessLocalProfiles = 64
42
+ const maximumDirectoryEntries = 1_024
43
+ const maximumFileBytes = 64 * 1_024
44
+ const maximumPathBytes = 4_096
45
+ const maximumResourceAmount = 1_000_000
46
+ const maximumTicket = 0xffff_ffff_ffff_ffffn
47
+ const defaultPollIntervalMilliseconds = 25
48
+ const defaultWaitTimeoutMilliseconds = 35_000
49
+ const maximumAdmissionBackoffMilliseconds = 250
50
+ const smallPollIntervalMilliseconds = 4
51
+ const maximumAdmissionBackoffMultiplier = 8
52
+
53
+ /** Largest supported explicit admission wait for any host-resource lease. */
54
+ export const HOST_RESOURCE_MAX_WAIT_MILLISECONDS = 24 * 60 * 60_000
55
+
56
+ export const slopcameraHostResourceNames = Object.freeze([
57
+ "cpu",
58
+ "local-io",
59
+ "ffmpeg",
60
+ "video-encode",
61
+ "vision",
62
+ "whisper",
63
+ "network",
64
+ "paid-call",
65
+ "browser",
66
+ "capture-device",
67
+ ] as const)
68
+
69
+ export type SlopcameraHostResourceName =
70
+ (typeof slopcameraHostResourceNames)[number]
71
+
72
+ export interface HostResourceCapacity {
73
+ readonly resource: string
74
+ readonly limit: number
75
+ }
76
+
77
+ export interface HostResourceProfile {
78
+ readonly id: string
79
+ readonly capacities: readonly HostResourceCapacity[]
80
+ }
81
+
82
+ export interface HostResourceClaim {
83
+ readonly resource: string
84
+ readonly amount: number
85
+ }
86
+
87
+ export interface HostResourceLease {
88
+ readonly claims: readonly HostResourceClaim[]
89
+ readonly inheritedFileDescriptor: number
90
+ readonly profile: HostResourceProfile
91
+ readonly ticket: string
92
+ assertOwned(): Promise<void>
93
+ }
94
+
95
+ export interface HostResourceLeaseOptions {
96
+ readonly signal?: AbortSignal
97
+ readonly waitTimeoutMilliseconds?: number
98
+ }
99
+
100
+ export interface HostResourceCoordinator {
101
+ readonly profile: HostResourceProfile
102
+ readonly scope: "machine" | "process"
103
+ withLease<T>(
104
+ claims: readonly HostResourceClaim[],
105
+ callback: (lease: HostResourceLease) => T | Promise<T>,
106
+ options?: HostResourceLeaseOptions,
107
+ ): Promise<T>
108
+ }
109
+
110
+ export interface HostResourceCoordinatorOptions {
111
+ readonly profile?: HostResourceProfile
112
+ readonly stateRoot?: string
113
+ readonly pollIntervalMilliseconds?: number
114
+ readonly waitTimeoutMilliseconds?: number
115
+ }
116
+
117
+ export type HostResourceErrorCode =
118
+ | "INVALID_PROFILE"
119
+ | "INVALID_CLAIMS"
120
+ | "UNSUPPORTED_PLATFORM"
121
+ | "UNSAFE_STATE"
122
+ | "PROFILE_MISMATCH"
123
+ | "WAIT_ABORTED"
124
+ | "WAIT_TIMEOUT"
125
+ | "OWNERSHIP_LOST"
126
+
127
+ export class HostResourceError extends Error {
128
+ readonly code: HostResourceErrorCode
129
+
130
+ constructor(code: HostResourceErrorCode, message: string, cause?: unknown) {
131
+ super(`[${code}] ${message}`, cause === undefined ? undefined : { cause })
132
+ this.name = "HostResourceError"
133
+ this.code = code
134
+ }
135
+ }
136
+
137
+ interface MarkerIdentity {
138
+ readonly device: number
139
+ readonly inode: number
140
+ }
141
+
142
+ interface MarkerDocument {
143
+ readonly version: 1
144
+ readonly owner: string
145
+ readonly profileSha256: string
146
+ readonly ticket: string
147
+ readonly phase: "A" | "W"
148
+ readonly claims: readonly HostResourceClaim[]
149
+ }
150
+
151
+ interface LiveMarker {
152
+ document: MarkerDocument
153
+ readonly identity: MarkerIdentity
154
+ readonly name: string
155
+ readonly path: string
156
+ }
157
+
158
+ interface OwnedMarker extends LiveMarker {
159
+ readonly descriptor: number
160
+ released: boolean
161
+ }
162
+
163
+ interface ResolvedStateRoot {
164
+ readonly path: string
165
+ }
166
+
167
+ interface ResolvedCoordinatorOptions {
168
+ readonly pollIntervalMilliseconds: number
169
+ readonly profile: HostResourceProfile
170
+ readonly profileSha256: string
171
+ readonly stateRoot: string
172
+ readonly waitTimeoutMilliseconds: number
173
+ }
174
+
175
+ function isRecord(value: unknown): value is Record<string, unknown> {
176
+ return typeof value === "object" && value !== null && !Array.isArray(value)
177
+ }
178
+
179
+ function hasExactKeys(
180
+ value: Readonly<Record<string, unknown>>,
181
+ expected: readonly string[],
182
+ ): boolean {
183
+ const actual = Object.keys(value).sort()
184
+ const normalizedExpected = [...expected].sort()
185
+ return actual.length === normalizedExpected.length
186
+ && actual.every((key, index) => key === normalizedExpected[index])
187
+ }
188
+
189
+ function boundedPositiveInteger(
190
+ value: unknown,
191
+ maximum: number,
192
+ ): value is number {
193
+ return Number.isSafeInteger(value) && (value as number) >= 1
194
+ && (value as number) <= maximum
195
+ }
196
+
197
+ function deepFreeze<T>(value: T): T {
198
+ if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
199
+ return value
200
+ }
201
+ for (const nested of Object.values(value)) deepFreeze(nested)
202
+ return Object.freeze(value)
203
+ }
204
+
205
+ function resourceEntries(
206
+ value: unknown,
207
+ amountKey: "amount" | "limit",
208
+ errorCode: "INVALID_CLAIMS" | "INVALID_PROFILE",
209
+ ): readonly Readonly<{ resource: string; value: number }>[] {
210
+ const minimumEntries = errorCode === "INVALID_CLAIMS" ? 0 : 1
211
+ if (
212
+ !Array.isArray(value)
213
+ || value.length < minimumEntries
214
+ || value.length > maximumResources
215
+ ) {
216
+ throw new HostResourceError(
217
+ errorCode,
218
+ `Host-resource ${amountKey === "limit" ? "capacities" : "claims"} must contain ${minimumEntries} through ${maximumResources} entries.`,
219
+ )
220
+ }
221
+ const seen = new Set<string>()
222
+ const result = value.map((candidate) => {
223
+ if (
224
+ !isRecord(candidate)
225
+ || !hasExactKeys(candidate, ["resource", amountKey])
226
+ || typeof candidate.resource !== "string"
227
+ || !resourceNamePattern.test(candidate.resource)
228
+ || !boundedPositiveInteger(candidate[amountKey], maximumResourceAmount)
229
+ || seen.has(candidate.resource)
230
+ ) {
231
+ throw new HostResourceError(
232
+ errorCode,
233
+ `Host-resource ${amountKey === "limit" ? "capacities" : "claims"} are malformed or duplicated.`,
234
+ )
235
+ }
236
+ seen.add(candidate.resource)
237
+ return { resource: candidate.resource, value: candidate[amountKey] as number }
238
+ })
239
+ return result.sort((left, right) => (
240
+ left.resource < right.resource ? -1 : left.resource > right.resource ? 1 : 0
241
+ ))
242
+ }
243
+
244
+ export function normalizeHostResourceProfile(value: unknown): HostResourceProfile {
245
+ if (
246
+ !isRecord(value)
247
+ || !hasExactKeys(value, ["id", "capacities"])
248
+ || typeof value.id !== "string"
249
+ || !profileIdPattern.test(value.id)
250
+ ) {
251
+ throw new HostResourceError(
252
+ "INVALID_PROFILE",
253
+ "Host-resource profile identity is invalid.",
254
+ )
255
+ }
256
+ const capacities = resourceEntries(
257
+ value.capacities,
258
+ "limit",
259
+ "INVALID_PROFILE",
260
+ ).map(({ resource, value: limit }) => ({ resource, limit }))
261
+ return deepFreeze({ id: value.id, capacities })
262
+ }
263
+
264
+ export function normalizeHostResourceClaims(
265
+ value: unknown,
266
+ profile: HostResourceProfile,
267
+ ): readonly HostResourceClaim[] {
268
+ const normalizedProfile = normalizeHostResourceProfile(profile)
269
+ const limits = new Map(
270
+ normalizedProfile.capacities.map(({ resource, limit }) => [resource, limit]),
271
+ )
272
+ const claims = resourceEntries(value, "amount", "INVALID_CLAIMS")
273
+ .map(({ resource, value: amount }) => {
274
+ const limit = limits.get(resource)
275
+ if (limit === undefined || amount > limit) {
276
+ throw new HostResourceError(
277
+ "INVALID_CLAIMS",
278
+ `Host-resource claim ${resource} is unavailable or exceeds its profile limit.`,
279
+ )
280
+ }
281
+ return { resource, amount }
282
+ })
283
+ return deepFreeze(claims)
284
+ }
285
+
286
+ export function defaultSlopcameraHostResourceProfile(
287
+ hostParallelism = availableParallelism(),
288
+ ): HostResourceProfile {
289
+ if (!boundedPositiveInteger(hostParallelism, maximumResourceAmount)) {
290
+ throw new HostResourceError(
291
+ "INVALID_PROFILE",
292
+ "Host parallelism must be a positive safe integer.",
293
+ )
294
+ }
295
+ const reserve = hostParallelism >= 6 ? 2 : hostParallelism >= 2 ? 1 : 0
296
+ return normalizeHostResourceProfile({
297
+ id: "slopcamera.host-resources/v1",
298
+ capacities: [
299
+ { resource: "cpu", limit: Math.max(1, hostParallelism - reserve) },
300
+ { resource: "local-io", limit: 2 },
301
+ { resource: "ffmpeg", limit: 2 },
302
+ { resource: "video-encode", limit: 1 },
303
+ { resource: "vision", limit: 1 },
304
+ { resource: "whisper", limit: 1 },
305
+ { resource: "network", limit: 4 },
306
+ { resource: "paid-call", limit: 1 },
307
+ { resource: "browser", limit: 1 },
308
+ { resource: "capture-device", limit: 1 },
309
+ ],
310
+ })
311
+ }
312
+
313
+ export function defaultSlopcameraHostResourceStateRoot(
314
+ platform: NodeJS.Platform = process.platform,
315
+ environment: Readonly<NodeJS.ProcessEnv> = process.env,
316
+ userHome = homedir(),
317
+ ): string {
318
+ if (!isHostResourcePlatformSupported(platform)) {
319
+ throw new HostResourceError(
320
+ "UNSUPPORTED_PLATFORM",
321
+ "Slopcamera host-resource coordination requires Darwin or Linux.",
322
+ )
323
+ }
324
+ if (platform === "darwin") {
325
+ return join(
326
+ userHome,
327
+ "Library",
328
+ "Application Support",
329
+ "Slopcamera",
330
+ "cli",
331
+ "host-resources-v1",
332
+ )
333
+ }
334
+ const configuredStateHome = environment.XDG_STATE_HOME
335
+ const stateHome = configuredStateHome !== undefined
336
+ && isAbsolute(configuredStateHome)
337
+ ? configuredStateHome
338
+ : join(userHome, ".local", "state")
339
+ return join(stateHome, "slopcamera", "host-resources-v1")
340
+ }
341
+
342
+
343
+ function canonicalProfile(profile: HostResourceProfile): string {
344
+ return JSON.stringify(profile)
345
+ }
346
+
347
+ function sha256(value: string): string {
348
+ return createHash("sha256").update(value).digest("hex")
349
+ }
350
+
351
+ function stateFailure(message: string, cause?: unknown): HostResourceError {
352
+ return new HostResourceError("UNSAFE_STATE", message, cause)
353
+ }
354
+
355
+ function isErrorCode(value: unknown, code: string): boolean {
356
+ return isRecord(value) && value.code === code
357
+ }
358
+
359
+ function ownerUid(): number {
360
+ const uid = process.geteuid?.() ?? process.getuid?.()
361
+ if (uid === undefined) throw stateFailure("Host-resource owner identity is unavailable.")
362
+ return uid
363
+ }
364
+
365
+ function identity(value: Pick<Stats, "dev" | "ino">): MarkerIdentity {
366
+ return { device: value.dev, inode: value.ino }
367
+ }
368
+
369
+ function sameIdentity(
370
+ left: MarkerIdentity,
371
+ right: MarkerIdentity | Pick<Stats, "dev" | "ino">,
372
+ ): boolean {
373
+ const device = "device" in right ? right.device : right.dev
374
+ const inode = "inode" in right ? right.inode : right.ino
375
+ return left.device === device && left.inode === inode
376
+ }
377
+
378
+ function privateDirectory(metadata: Stats): boolean {
379
+ return metadata.isDirectory()
380
+ && !metadata.isSymbolicLink()
381
+ && metadata.uid === ownerUid()
382
+ && (metadata.mode & 0o777) === 0o700
383
+ }
384
+
385
+ function privateRegularFile(metadata: Stats): boolean {
386
+ return metadata.isFile()
387
+ && !metadata.isSymbolicLink()
388
+ && metadata.nlink === 1
389
+ && metadata.uid === ownerUid()
390
+ && (metadata.mode & 0o777) === 0o600
391
+ }
392
+
393
+ function metadataOrNull(path: string): Stats | null {
394
+ try {
395
+ return lstatSync(path)
396
+ } catch (cause) {
397
+ if (isErrorCode(cause, "ENOENT")) return null
398
+ throw cause
399
+ }
400
+ }
401
+
402
+ function ensureStateRoot(path: string): ResolvedStateRoot {
403
+ if (
404
+ !isAbsolute(path)
405
+ || path.includes("\0")
406
+ || Buffer.byteLength(path, "utf8") > maximumPathBytes
407
+ ) {
408
+ throw stateFailure("Host-resource state root must be a bounded absolute path.")
409
+ }
410
+ let created = false
411
+ let metadata = metadataOrNull(path)
412
+ if (metadata === null) {
413
+ try {
414
+ mkdirSync(path, { recursive: true, mode: 0o700 })
415
+ created = true
416
+ } catch (cause) {
417
+ if (!isErrorCode(cause, "EEXIST")) {
418
+ throw stateFailure("Host-resource state root could not be created.", cause)
419
+ }
420
+ }
421
+ metadata = lstatSync(path)
422
+ if (created) chmodSync(path, 0o700)
423
+ }
424
+ if (!privateDirectory(metadata)) {
425
+ throw stateFailure("Host-resource state root is not one private owned directory.")
426
+ }
427
+ const canonical = realpathSync(path)
428
+ if (!privateDirectory(statSync(canonical))) {
429
+ throw stateFailure("Host-resource state root canonicalization is unsafe.")
430
+ }
431
+ return { path: canonical }
432
+ }
433
+
434
+ function openPrivateFile(path: string, exclusive = false): number {
435
+ const existing = metadataOrNull(path)
436
+ if (existing !== null && !privateRegularFile(existing)) {
437
+ throw stateFailure("Host-resource state contains an unsafe file.")
438
+ }
439
+ const flags = constants.O_RDWR
440
+ | constants.O_CREAT
441
+ | constants.O_NOFOLLOW
442
+ | (exclusive ? constants.O_EXCL : 0)
443
+ const descriptor = openSync(path, flags, 0o600)
444
+ try {
445
+ fchmodSync(descriptor, 0o600)
446
+ const opened = fstatSync(descriptor)
447
+ const published = lstatSync(path)
448
+ if (
449
+ !privateRegularFile(opened)
450
+ || !privateRegularFile(published)
451
+ || opened.dev !== published.dev
452
+ || opened.ino !== published.ino
453
+ ) {
454
+ throw stateFailure("Host-resource file identity changed while opening.")
455
+ }
456
+ return descriptor
457
+ } catch (cause) {
458
+ closeSync(descriptor)
459
+ throw cause
460
+ }
461
+ }
462
+
463
+ function readDescriptor(descriptor: number): string {
464
+ const metadata = fstatSync(descriptor)
465
+ if (!privateRegularFile(metadata) || metadata.size > maximumFileBytes) {
466
+ throw stateFailure("Host-resource state file is malformed.")
467
+ }
468
+ const bytes = Buffer.alloc(metadata.size)
469
+ let offset = 0
470
+ while (offset < bytes.length) {
471
+ const count = readSync(descriptor, bytes, offset, bytes.length - offset, offset)
472
+ if (count === 0) break
473
+ offset += count
474
+ }
475
+ if (offset !== bytes.length) throw stateFailure("Host-resource state read was incomplete.")
476
+ return bytes.toString("utf8")
477
+ }
478
+
479
+ function writeDescriptor(descriptor: number, value: string): void {
480
+ const bytes = Buffer.from(value, "utf8")
481
+ if (bytes.length > maximumFileBytes) {
482
+ throw stateFailure("Host-resource state write exceeds its bound.")
483
+ }
484
+ ftruncateSync(descriptor, 0)
485
+ let offset = 0
486
+ while (offset < bytes.length) {
487
+ const written = writeSync(
488
+ descriptor,
489
+ bytes,
490
+ offset,
491
+ bytes.length - offset,
492
+ offset,
493
+ )
494
+ if (written < 1) throw stateFailure("Host-resource state write was incomplete.")
495
+ offset += written
496
+ }
497
+ fsyncSync(descriptor)
498
+ }
499
+
500
+ interface StateDocument {
501
+ readonly version: 1
502
+ readonly profile: HostResourceProfile
503
+ readonly nextTicket: string
504
+ }
505
+
506
+ function stateJson(state: StateDocument): string {
507
+ return JSON.stringify({
508
+ version: state.version,
509
+ profile: state.profile,
510
+ nextTicket: state.nextTicket,
511
+ })
512
+ }
513
+
514
+ function markerJson(marker: MarkerDocument): string {
515
+ return JSON.stringify({
516
+ version: marker.version,
517
+ owner: marker.owner,
518
+ profileSha256: marker.profileSha256,
519
+ ticket: marker.ticket,
520
+ phase: marker.phase,
521
+ claims: marker.claims,
522
+ })
523
+ }
524
+
525
+ function syncDirectory(directory: string): void {
526
+ const descriptor = openSync(
527
+ directory,
528
+ constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
529
+ )
530
+ try {
531
+ if (!privateDirectory(fstatSync(descriptor))) {
532
+ throw stateFailure("Host-resource state root changed identity.")
533
+ }
534
+ fsyncSync(descriptor)
535
+ } finally {
536
+ closeSync(descriptor)
537
+ }
538
+ }
539
+
540
+ function atomicWriteState(directory: string, state: StateDocument): void {
541
+ const temporaryName = `.host-resources-${randomBytes(16).toString("hex")}.tmp`
542
+ const temporaryPath = join(directory, temporaryName)
543
+ const statePath = join(directory, stateFileName)
544
+ let descriptor: number | undefined
545
+ try {
546
+ descriptor = openPrivateFile(temporaryPath, true)
547
+ writeDescriptor(descriptor, stateJson(state))
548
+ closeSync(descriptor)
549
+ descriptor = undefined
550
+ const existing = metadataOrNull(statePath)
551
+ if (existing !== null && !privateRegularFile(existing)) {
552
+ throw stateFailure("Host-resource profile state is unsafe.")
553
+ }
554
+ renameSync(temporaryPath, statePath)
555
+ const published = lstatSync(statePath)
556
+ if (!privateRegularFile(published)) {
557
+ throw stateFailure("Host-resource profile state was not published safely.")
558
+ }
559
+ syncDirectory(directory)
560
+ } finally {
561
+ if (descriptor !== undefined) closeSync(descriptor)
562
+ try {
563
+ unlinkSync(temporaryPath)
564
+ } catch (cause) {
565
+ if (!isErrorCode(cause, "ENOENT")) {
566
+ // The unique private temporary file is inert. A later control owner
567
+ // removes it before admitting more work.
568
+ }
569
+ }
570
+ }
571
+ }
572
+
573
+ function parseState(value: string): StateDocument {
574
+ let parsed: unknown
575
+ try {
576
+ parsed = JSON.parse(value)
577
+ } catch (cause) {
578
+ throw stateFailure("Host-resource profile state is not valid JSON.", cause)
579
+ }
580
+ if (
581
+ !isRecord(parsed)
582
+ || !hasExactKeys(parsed, ["version", "profile", "nextTicket"])
583
+ || parsed.version !== 1
584
+ || typeof parsed.nextTicket !== "string"
585
+ || !ticketPattern.test(parsed.nextTicket)
586
+ ) {
587
+ throw stateFailure("Host-resource profile state is malformed.")
588
+ }
589
+ let profile: HostResourceProfile
590
+ try {
591
+ profile = normalizeHostResourceProfile(parsed.profile)
592
+ } catch (cause) {
593
+ throw stateFailure("Host-resource profile state is malformed.", cause)
594
+ }
595
+ const nextTicket = BigInt(parsed.nextTicket)
596
+ if (nextTicket > maximumTicket + 1n) {
597
+ throw stateFailure("Host-resource ticket state exceeds its bound.")
598
+ }
599
+ const state: StateDocument = {
600
+ version: 1,
601
+ profile,
602
+ nextTicket: parsed.nextTicket,
603
+ }
604
+ if (stateJson(state) !== value) {
605
+ throw stateFailure("Host-resource profile state is not normalized.")
606
+ }
607
+ return state
608
+ }
609
+
610
+ function readState(
611
+ directory: string,
612
+ profile: HostResourceProfile,
613
+ ): StateDocument {
614
+ const path = join(directory, stateFileName)
615
+ const initializedPath = join(directory, initializedFileName)
616
+ if (metadataOrNull(path) === null) {
617
+ if (
618
+ metadataOrNull(initializedPath) !== null
619
+ || readdirSync(directory).some((name) => markerNamePattern.test(name))
620
+ ) {
621
+ throw stateFailure("Host-resource profile state is missing.")
622
+ }
623
+ const initial: StateDocument = {
624
+ version: 1,
625
+ profile,
626
+ nextTicket: "1",
627
+ }
628
+ atomicWriteState(directory, initial)
629
+ const initialized = openPrivateFile(initializedPath, true)
630
+ closeSync(initialized)
631
+ syncDirectory(directory)
632
+ return initial
633
+ }
634
+ const descriptor = openPrivateFile(path)
635
+ try {
636
+ const state = parseState(readDescriptor(descriptor))
637
+ const initializedWasMissing = metadataOrNull(initializedPath) === null
638
+ const initialized = initializedWasMissing
639
+ ? openPrivateFile(initializedPath, true)
640
+ : openPrivateFile(initializedPath)
641
+ try {
642
+ if (fstatSync(initialized).size !== 0) {
643
+ throw stateFailure("Host-resource initialization state is malformed.")
644
+ }
645
+ } finally {
646
+ closeSync(initialized)
647
+ }
648
+ if (initializedWasMissing) syncDirectory(directory)
649
+ return state
650
+ } finally {
651
+ closeSync(descriptor)
652
+ }
653
+ }
654
+
655
+ function assertMatchingProfile(
656
+ state: StateDocument,
657
+ profile: HostResourceProfile,
658
+ ): void {
659
+ if (canonicalProfile(state.profile) === canonicalProfile(profile)) return
660
+ throw new HostResourceError(
661
+ "PROFILE_MISMATCH",
662
+ "The machine-global Slopcamera host-resource profile does not match this process.",
663
+ )
664
+ }
665
+
666
+ function reserveTicket(
667
+ directory: string,
668
+ state: StateDocument,
669
+ ): string {
670
+ const ticket = BigInt(state.nextTicket)
671
+ if (ticket > maximumTicket) {
672
+ throw stateFailure("Host-resource tickets are exhausted.")
673
+ }
674
+ atomicWriteState(directory, {
675
+ ...state,
676
+ nextTicket: String(ticket + 1n),
677
+ })
678
+ return String(ticket)
679
+ }
680
+
681
+ function duration(
682
+ value: number | undefined,
683
+ fallback: number,
684
+ maximum: number,
685
+ code: "INVALID_CLAIMS" | "INVALID_PROFILE",
686
+ label: string,
687
+ ): number {
688
+ const resolved = value ?? fallback
689
+ if (!boundedPositiveInteger(resolved, maximum)) {
690
+ throw new HostResourceError(code, `${label} is outside its supported bound.`)
691
+ }
692
+ return resolved
693
+ }
694
+
695
+ function throwIfWaitEnded(signal: AbortSignal | undefined, deadline: number): void {
696
+ if (signal?.aborted === true) {
697
+ throw new HostResourceError(
698
+ "WAIT_ABORTED",
699
+ "Host-resource admission was cancelled before execution.",
700
+ )
701
+ }
702
+ if (performance.now() >= deadline) {
703
+ throw new HostResourceError(
704
+ "WAIT_TIMEOUT",
705
+ "Host-resource admission exceeded its bounded wait.",
706
+ )
707
+ }
708
+ }
709
+
710
+ async function waitForRetry(
711
+ pollIntervalMilliseconds: number,
712
+ signal: AbortSignal | undefined,
713
+ deadline: number,
714
+ ): Promise<void> {
715
+ throwIfWaitEnded(signal, deadline)
716
+ const remaining = Math.max(1, Math.ceil(deadline - performance.now()))
717
+ await new Promise<void>((resolve, reject) => {
718
+ let settled = false
719
+ const finish = (): void => {
720
+ if (settled) return
721
+ settled = true
722
+ signal?.removeEventListener("abort", cancel)
723
+ resolve()
724
+ }
725
+ const cancel = (): void => {
726
+ if (settled) return
727
+ settled = true
728
+ clearTimeout(timer)
729
+ signal?.removeEventListener("abort", cancel)
730
+ reject(new HostResourceError(
731
+ "WAIT_ABORTED",
732
+ "Host-resource admission was cancelled before execution.",
733
+ ))
734
+ }
735
+ const timer = setTimeout(
736
+ finish,
737
+ Math.min(pollIntervalMilliseconds, remaining),
738
+ )
739
+ signal?.addEventListener("abort", cancel, { once: true })
740
+ if (signal?.aborted === true) cancel()
741
+ })
742
+ throwIfWaitEnded(signal, deadline)
743
+ }
744
+
745
+ function mixRetrySeed(ticket: string, retry: number): bigint {
746
+ const mask = 0xffff_ffff_ffff_ffffn
747
+ let value = (
748
+ BigInt(ticket)
749
+ + BigInt(retry + 1) * 0x9e37_79b9_7f4a_7c15n
750
+ ) & mask
751
+ value = ((value ^ (value >> 30n)) * 0xbf58_476d_1ce4_e5b9n) & mask
752
+ value = ((value ^ (value >> 27n)) * 0x94d0_49bb_1331_11ebn) & mask
753
+ return (value ^ (value >> 31n)) & mask
754
+ }
755
+
756
+ function admissionRetryDelayMilliseconds(
757
+ pollIntervalMilliseconds: number,
758
+ ticket: string,
759
+ unchangedRetryCount: number,
760
+ ): number {
761
+ // Explicit tiny intervals are commonly used by deterministic local callers.
762
+ // Keep them as the upper bound instead of silently stretching their loop.
763
+ const adaptiveLimit = pollIntervalMilliseconds <= smallPollIntervalMilliseconds
764
+ ? pollIntervalMilliseconds
765
+ : Math.max(
766
+ pollIntervalMilliseconds,
767
+ Math.min(
768
+ maximumAdmissionBackoffMilliseconds,
769
+ pollIntervalMilliseconds * maximumAdmissionBackoffMultiplier,
770
+ ),
771
+ )
772
+ const exponent = Math.min(
773
+ unchangedRetryCount,
774
+ Math.ceil(Math.log2(maximumAdmissionBackoffMultiplier)),
775
+ )
776
+ const ceiling = Math.min(
777
+ adaptiveLimit,
778
+ pollIntervalMilliseconds * (2 ** exponent),
779
+ )
780
+ const floor = Math.max(1, Math.ceil(ceiling / 2))
781
+ const width = ceiling - floor + 1
782
+ const offset = Number(
783
+ mixRetrySeed(ticket, unchangedRetryCount) % BigInt(width),
784
+ )
785
+ return floor + offset
786
+ }
787
+
788
+ async function withControlLock<T>(
789
+ directory: string,
790
+ pollIntervalMilliseconds: number,
791
+ signal: AbortSignal | undefined,
792
+ deadline: number,
793
+ callback: () => T,
794
+ ): Promise<T> {
795
+ const descriptor = openPrivateFile(join(directory, controlFileName))
796
+ let locked = false
797
+ try {
798
+ while (!(locked = tryLockHostResourceDescriptor(descriptor))) {
799
+ await waitForRetry(pollIntervalMilliseconds, signal, deadline)
800
+ }
801
+ throwIfWaitEnded(signal, deadline)
802
+ return callback()
803
+ } catch (cause) {
804
+ if (cause instanceof HostResourceError) throw cause
805
+ throw stateFailure("Host-resource control locking failed.", cause)
806
+ } finally {
807
+ if (locked) {
808
+ try {
809
+ unlockHostResourceDescriptor(descriptor)
810
+ } catch {
811
+ // Closing the descriptor still relinquishes the kernel lock.
812
+ }
813
+ }
814
+ closeSync(descriptor)
815
+ }
816
+ }
817
+
818
+ function normalizedTicketName(ticket: string): string {
819
+ return ticket.padStart(20, "0")
820
+ }
821
+
822
+ function parseMarker(
823
+ name: string,
824
+ source: string,
825
+ profile: HostResourceProfile,
826
+ profileSha256: string,
827
+ ): MarkerDocument {
828
+ const nameMatch = markerNamePattern.exec(name)
829
+ let parsed: unknown
830
+ try {
831
+ parsed = JSON.parse(source)
832
+ } catch (cause) {
833
+ throw stateFailure("A live host-resource marker is not valid JSON.", cause)
834
+ }
835
+ if (
836
+ nameMatch === null
837
+ || !isRecord(parsed)
838
+ || !hasExactKeys(parsed, [
839
+ "version",
840
+ "owner",
841
+ "profileSha256",
842
+ "ticket",
843
+ "phase",
844
+ "claims",
845
+ ])
846
+ || parsed.version !== 1
847
+ || typeof parsed.owner !== "string"
848
+ || !ownerIdPattern.test(parsed.owner)
849
+ || typeof parsed.profileSha256 !== "string"
850
+ || !/^[0-9a-f]{64}$/u.test(parsed.profileSha256)
851
+ || typeof parsed.ticket !== "string"
852
+ || !ticketPattern.test(parsed.ticket)
853
+ || (parsed.phase !== "A" && parsed.phase !== "W")
854
+ || normalizedTicketName(parsed.ticket) !== nameMatch[1]
855
+ || parsed.owner !== nameMatch[2]
856
+ ) {
857
+ throw stateFailure("A live host-resource marker is malformed.")
858
+ }
859
+ if (parsed.profileSha256 !== profileSha256) {
860
+ throw new HostResourceError(
861
+ "PROFILE_MISMATCH",
862
+ "A live lease belongs to a different host-resource profile.",
863
+ )
864
+ }
865
+ let claims: readonly HostResourceClaim[]
866
+ try {
867
+ claims = normalizeHostResourceClaims(parsed.claims, profile)
868
+ } catch (cause) {
869
+ throw stateFailure("A live host-resource marker has invalid claims.", cause)
870
+ }
871
+ const marker: MarkerDocument = {
872
+ version: 1,
873
+ owner: parsed.owner,
874
+ profileSha256: parsed.profileSha256,
875
+ ticket: parsed.ticket,
876
+ phase: parsed.phase,
877
+ claims,
878
+ }
879
+ if (markerJson(marker) !== source) {
880
+ throw stateFailure("A live host-resource marker is not normalized.")
881
+ }
882
+ return marker
883
+ }
884
+
885
+ function unlinkExactMarker(path: string, markerIdentity: MarkerIdentity): void {
886
+ const published = lstatSync(path)
887
+ if (!privateRegularFile(published) || !sameIdentity(markerIdentity, published)) {
888
+ throw new HostResourceError(
889
+ "OWNERSHIP_LOST",
890
+ "Host-resource marker identity changed before cleanup.",
891
+ )
892
+ }
893
+ unlinkSync(path)
894
+ }
895
+
896
+ function removeTemporaryFile(path: string): void {
897
+ const descriptor = openPrivateFile(path)
898
+ try {
899
+ const opened = fstatSync(descriptor)
900
+ const published = lstatSync(path)
901
+ if (opened.dev !== published.dev || opened.ino !== published.ino) {
902
+ throw stateFailure("Host-resource temporary state changed identity.")
903
+ }
904
+ unlinkSync(path)
905
+ } finally {
906
+ closeSync(descriptor)
907
+ }
908
+ }
909
+
910
+ function scanLiveMarkers(
911
+ directory: string,
912
+ profile: HostResourceProfile,
913
+ profileSha256: string,
914
+ ): readonly LiveMarker[] {
915
+ const entries = readdirSync(directory, { withFileTypes: true })
916
+ if (entries.length > maximumDirectoryEntries) {
917
+ throw stateFailure("Host-resource state contains too many entries.")
918
+ }
919
+ const live: LiveMarker[] = []
920
+ const tickets = new Set<string>()
921
+ const owners = new Set<string>()
922
+ for (const entry of entries) {
923
+ if (
924
+ entry.name === controlFileName
925
+ || entry.name === stateFileName
926
+ || entry.name === initializedFileName
927
+ ) continue
928
+ const path = join(directory, entry.name)
929
+ if (temporaryNamePattern.test(entry.name)) {
930
+ if (!entry.isFile() || entry.isSymbolicLink()) {
931
+ throw stateFailure("Host-resource temporary state is unsafe.")
932
+ }
933
+ removeTemporaryFile(path)
934
+ continue
935
+ }
936
+ if (!markerNamePattern.test(entry.name)) {
937
+ throw stateFailure("Host-resource state contains an unknown entry.")
938
+ }
939
+ const descriptor = openPrivateFile(path)
940
+ let locked = false
941
+ try {
942
+ const opened = fstatSync(descriptor)
943
+ const markerIdentity = identity(opened)
944
+ locked = tryLockHostResourceDescriptor(descriptor)
945
+ if (locked) {
946
+ // An unlocked marker has no live descriptor owner. The kernel lock is
947
+ // the sole liveness fact; PIDs and timestamps are deliberately absent.
948
+ unlinkExactMarker(path, markerIdentity)
949
+ continue
950
+ }
951
+ const document = parseMarker(
952
+ entry.name,
953
+ readDescriptor(descriptor),
954
+ profile,
955
+ profileSha256,
956
+ )
957
+ if (tickets.has(document.ticket) || owners.has(document.owner)) {
958
+ throw stateFailure("Live host-resource marker identity is duplicated.")
959
+ }
960
+ tickets.add(document.ticket)
961
+ owners.add(document.owner)
962
+ live.push({
963
+ document,
964
+ identity: markerIdentity,
965
+ name: entry.name,
966
+ path,
967
+ })
968
+ } finally {
969
+ if (locked) {
970
+ try {
971
+ unlockHostResourceDescriptor(descriptor)
972
+ } catch {
973
+ // Close remains the authoritative release.
974
+ }
975
+ }
976
+ closeSync(descriptor)
977
+ }
978
+ }
979
+ return live.sort((left, right) => {
980
+ const leftTicket = BigInt(left.document.ticket)
981
+ const rightTicket = BigInt(right.document.ticket)
982
+ return leftTicket < rightTicket ? -1 : leftTicket > rightTicket ? 1 : 0
983
+ })
984
+ }
985
+
986
+ function assertOwnedMarkerIdentity(marker: OwnedMarker): void {
987
+ if (marker.released) {
988
+ throw new HostResourceError(
989
+ "OWNERSHIP_LOST",
990
+ "Host-resource lease is no longer active.",
991
+ )
992
+ }
993
+ let held: Stats
994
+ let published: Stats
995
+ try {
996
+ held = fstatSync(marker.descriptor)
997
+ published = lstatSync(marker.path)
998
+ } catch (cause) {
999
+ throw new HostResourceError(
1000
+ "OWNERSHIP_LOST",
1001
+ "Host-resource marker is no longer reachable.",
1002
+ cause,
1003
+ )
1004
+ }
1005
+ if (
1006
+ !privateRegularFile(held)
1007
+ || !privateRegularFile(published)
1008
+ || !sameIdentity(marker.identity, held)
1009
+ || !sameIdentity(marker.identity, published)
1010
+ ) {
1011
+ throw new HostResourceError(
1012
+ "OWNERSHIP_LOST",
1013
+ "Host-resource descriptor no longer owns its published inode.",
1014
+ )
1015
+ }
1016
+ }
1017
+
1018
+ function publishWaitingMarker(
1019
+ directory: string,
1020
+ state: StateDocument,
1021
+ claims: readonly HostResourceClaim[],
1022
+ profileSha256: string,
1023
+ ): OwnedMarker {
1024
+ const ticket = reserveTicket(directory, state)
1025
+ const owner = randomBytes(16).toString("hex")
1026
+ const name = `lease-${normalizedTicketName(ticket)}-${owner}.lock`
1027
+ const path = join(directory, name)
1028
+ let descriptor: number | undefined
1029
+ try {
1030
+ descriptor = openPrivateFile(path, true)
1031
+ if (!tryLockHostResourceDescriptor(descriptor)) {
1032
+ throw stateFailure("A new host-resource marker could not be locked.")
1033
+ }
1034
+ const document: MarkerDocument = {
1035
+ version: 1,
1036
+ owner,
1037
+ profileSha256,
1038
+ ticket,
1039
+ phase: "W",
1040
+ claims,
1041
+ }
1042
+ writeDescriptor(descriptor, markerJson(document))
1043
+ return {
1044
+ descriptor,
1045
+ document,
1046
+ identity: identity(fstatSync(descriptor)),
1047
+ name,
1048
+ path,
1049
+ released: false,
1050
+ }
1051
+ } catch (cause) {
1052
+ if (descriptor !== undefined) {
1053
+ try {
1054
+ unlockHostResourceDescriptor(descriptor)
1055
+ } catch {
1056
+ // Closing remains the authoritative release.
1057
+ }
1058
+ closeSync(descriptor)
1059
+ }
1060
+ try {
1061
+ unlinkSync(path)
1062
+ } catch (cleanupCause) {
1063
+ if (!isErrorCode(cleanupCause, "ENOENT")) {
1064
+ // A later scanner removes an unlocked unique marker.
1065
+ }
1066
+ }
1067
+ throw cause
1068
+ }
1069
+ }
1070
+
1071
+ function sameMarker(left: LiveMarker, right: OwnedMarker): boolean {
1072
+ return left.name === right.name
1073
+ && left.document.owner === right.document.owner
1074
+ && left.document.ticket === right.document.ticket
1075
+ && sameIdentity(left.identity, right.identity)
1076
+ }
1077
+
1078
+ function claimsOverlap(
1079
+ left: readonly HostResourceClaim[],
1080
+ right: readonly HostResourceClaim[],
1081
+ ): boolean {
1082
+ const resources = new Set(left.map(({ resource }) => resource))
1083
+ return right.some(({ resource }) => resources.has(resource))
1084
+ }
1085
+
1086
+ function canFitClaims(
1087
+ profile: HostResourceProfile,
1088
+ active: readonly LiveMarker[],
1089
+ requested: readonly HostResourceClaim[],
1090
+ ): boolean {
1091
+ const used = new Map<string, number>()
1092
+ for (const marker of active) {
1093
+ for (const claim of marker.document.claims) {
1094
+ used.set(claim.resource, (used.get(claim.resource) ?? 0) + claim.amount)
1095
+ }
1096
+ }
1097
+ for (const { resource, limit } of profile.capacities) {
1098
+ const current = used.get(resource) ?? 0
1099
+ if (current > limit) {
1100
+ throw stateFailure("Live host-resource leases exceed the declared profile.")
1101
+ }
1102
+ }
1103
+ return requested.every(({ resource, amount }) => {
1104
+ const limit = profile.capacities.find(
1105
+ (capacity) => capacity.resource === resource,
1106
+ )?.limit
1107
+ return limit !== undefined && (used.get(resource) ?? 0) + amount <= limit
1108
+ })
1109
+ }
1110
+
1111
+ interface AdmissionAttempt {
1112
+ readonly admitted: boolean
1113
+ readonly queueState: string
1114
+ }
1115
+
1116
+ function admissionQueueState(
1117
+ marker: OwnedMarker,
1118
+ live: readonly LiveMarker[],
1119
+ ): string {
1120
+ const ticket = BigInt(marker.document.ticket)
1121
+ const blockers = live.filter((candidate) => {
1122
+ if (sameMarker(candidate, marker)) return false
1123
+ if (!claimsOverlap(candidate.document.claims, marker.document.claims)) {
1124
+ return false
1125
+ }
1126
+ return candidate.document.phase === "A"
1127
+ || BigInt(candidate.document.ticket) < ticket
1128
+ })
1129
+ return sha256(blockers.map((candidate) => (
1130
+ `${candidate.name}\0${markerJson(candidate.document)}`
1131
+ )).join("\n"))
1132
+ }
1133
+
1134
+ function attemptAdmission(
1135
+ marker: OwnedMarker,
1136
+ live: readonly LiveMarker[],
1137
+ profile: HostResourceProfile,
1138
+ ): AdmissionAttempt {
1139
+ assertOwnedMarkerIdentity(marker)
1140
+ const current = live.find((candidate) => sameMarker(candidate, marker))
1141
+ if (
1142
+ current === undefined
1143
+ || current.document.phase !== marker.document.phase
1144
+ || markerJson(current.document) !== markerJson(marker.document)
1145
+ ) {
1146
+ throw new HostResourceError(
1147
+ "OWNERSHIP_LOST",
1148
+ "Host-resource lease marker no longer matches its descriptor.",
1149
+ )
1150
+ }
1151
+ const ticket = BigInt(marker.document.ticket)
1152
+ const earlierOverlap = live.some((candidate) => (
1153
+ candidate.document.phase === "W"
1154
+ && BigInt(candidate.document.ticket) < ticket
1155
+ && claimsOverlap(candidate.document.claims, marker.document.claims)
1156
+ ))
1157
+ if (earlierOverlap) {
1158
+ return {
1159
+ admitted: false,
1160
+ queueState: admissionQueueState(marker, live),
1161
+ }
1162
+ }
1163
+ const active = live.filter((candidate) => candidate.document.phase === "A")
1164
+ if (!canFitClaims(profile, active, marker.document.claims)) {
1165
+ return {
1166
+ admitted: false,
1167
+ queueState: admissionQueueState(marker, live),
1168
+ }
1169
+ }
1170
+ const admitted: MarkerDocument = { ...marker.document, phase: "A" }
1171
+ writeDescriptor(marker.descriptor, markerJson(admitted))
1172
+ marker.document = admitted
1173
+ return { admitted: true, queueState: "" }
1174
+ }
1175
+
1176
+ function markerMatchesOwnedActive(
1177
+ candidate: LiveMarker,
1178
+ marker: OwnedMarker,
1179
+ ): boolean {
1180
+ return sameMarker(candidate, marker)
1181
+ && candidate.document.phase === "A"
1182
+ && marker.document.phase === "A"
1183
+ && markerJson(candidate.document) === markerJson(marker.document)
1184
+ }
1185
+
1186
+ async function admitMarker(
1187
+ marker: OwnedMarker,
1188
+ root: string,
1189
+ options: ResolvedCoordinatorOptions,
1190
+ signal: AbortSignal | undefined,
1191
+ deadline: number,
1192
+ state: () => StateDocument,
1193
+ ): Promise<void> {
1194
+ let previousQueueState: string | undefined
1195
+ let unchangedRetryCount = 0
1196
+ for (;;) {
1197
+ const attempt = await withControlLock(
1198
+ root,
1199
+ options.pollIntervalMilliseconds,
1200
+ signal,
1201
+ deadline,
1202
+ () => {
1203
+ const currentState = state()
1204
+ assertMatchingProfile(currentState, options.profile)
1205
+ const live = scanLiveMarkers(
1206
+ root,
1207
+ options.profile,
1208
+ options.profileSha256,
1209
+ )
1210
+ return attemptAdmission(marker, live, options.profile)
1211
+ },
1212
+ )
1213
+ if (attempt.admitted) return
1214
+ // The durable earlier-ticket marker remains the FIFO authority. Backoff
1215
+ // only changes when this candidate rechecks it, and relevant marker
1216
+ // progress restores the initial responsive interval without a watcher.
1217
+ if (attempt.queueState === previousQueueState) {
1218
+ unchangedRetryCount += 1
1219
+ } else {
1220
+ previousQueueState = attempt.queueState
1221
+ unchangedRetryCount = 0
1222
+ }
1223
+ await waitForRetry(
1224
+ admissionRetryDelayMilliseconds(
1225
+ options.pollIntervalMilliseconds,
1226
+ marker.document.ticket,
1227
+ unchangedRetryCount,
1228
+ ),
1229
+ signal,
1230
+ deadline,
1231
+ )
1232
+ }
1233
+ }
1234
+
1235
+ async function assertMarkerOwned(
1236
+ marker: OwnedMarker,
1237
+ root: string,
1238
+ options: ResolvedCoordinatorOptions,
1239
+ state: () => StateDocument,
1240
+ ): Promise<void> {
1241
+ assertOwnedMarkerIdentity(marker)
1242
+ const deadline = performance.now() + options.waitTimeoutMilliseconds
1243
+ await withControlLock(
1244
+ root,
1245
+ options.pollIntervalMilliseconds,
1246
+ undefined,
1247
+ deadline,
1248
+ () => {
1249
+ const currentState = state()
1250
+ assertMatchingProfile(currentState, options.profile)
1251
+ const live = scanLiveMarkers(
1252
+ root,
1253
+ options.profile,
1254
+ options.profileSha256,
1255
+ )
1256
+ if (!live.some((candidate) => markerMatchesOwnedActive(candidate, marker))) {
1257
+ throw new HostResourceError(
1258
+ "OWNERSHIP_LOST",
1259
+ "Host-resource lease is not backed by its exact active kernel lock.",
1260
+ )
1261
+ }
1262
+ },
1263
+ )
1264
+ }
1265
+
1266
+ async function releaseMarker(
1267
+ marker: OwnedMarker,
1268
+ root: string,
1269
+ options: ResolvedCoordinatorOptions,
1270
+ state: () => StateDocument,
1271
+ ): Promise<void> {
1272
+ if (marker.released) return
1273
+ marker.released = true
1274
+ try {
1275
+ closeSync(marker.descriptor)
1276
+ } catch (cause) {
1277
+ if (!isErrorCode(cause, "EBADF")) throw cause
1278
+ }
1279
+ const deadline = performance.now() + options.waitTimeoutMilliseconds
1280
+ await withControlLock(
1281
+ root,
1282
+ options.pollIntervalMilliseconds,
1283
+ undefined,
1284
+ deadline,
1285
+ () => {
1286
+ const currentState = state()
1287
+ assertMatchingProfile(currentState, options.profile)
1288
+ if (metadataOrNull(marker.path) === null) return
1289
+ const descriptor = openPrivateFile(marker.path)
1290
+ let locked = false
1291
+ try {
1292
+ const opened = fstatSync(descriptor)
1293
+ if (!sameIdentity(marker.identity, opened)) {
1294
+ throw new HostResourceError(
1295
+ "OWNERSHIP_LOST",
1296
+ "Host-resource marker inode changed before release.",
1297
+ )
1298
+ }
1299
+ locked = tryLockHostResourceDescriptor(descriptor)
1300
+ if (!locked) {
1301
+ // An explicitly inherited descriptor still owns the lease. Keep its
1302
+ // active marker visible until the descendant exits; a future scan
1303
+ // removes it using the released kernel lock alone.
1304
+ return
1305
+ }
1306
+ unlinkExactMarker(marker.path, marker.identity)
1307
+ } finally {
1308
+ if (locked) {
1309
+ try {
1310
+ unlockHostResourceDescriptor(descriptor)
1311
+ } catch {
1312
+ // Close remains the authoritative release.
1313
+ }
1314
+ }
1315
+ closeSync(descriptor)
1316
+ }
1317
+ },
1318
+ )
1319
+ }
1320
+
1321
+ function resolveCoordinatorOptions(
1322
+ options: HostResourceCoordinatorOptions,
1323
+ ): ResolvedCoordinatorOptions {
1324
+ if (!isHostResourcePlatformSupported(process.platform)) {
1325
+ throw new HostResourceError(
1326
+ "UNSUPPORTED_PLATFORM",
1327
+ "Slopcamera host-resource coordination requires Darwin or Linux.",
1328
+ )
1329
+ }
1330
+ const profile = normalizeHostResourceProfile(
1331
+ options.profile ?? defaultSlopcameraHostResourceProfile(),
1332
+ )
1333
+ const stateRoot = options.stateRoot
1334
+ ?? defaultSlopcameraHostResourceStateRoot()
1335
+ if (
1336
+ typeof stateRoot !== "string"
1337
+ || !isAbsolute(stateRoot)
1338
+ || stateRoot.includes("\0")
1339
+ || Buffer.byteLength(stateRoot, "utf8") > maximumPathBytes
1340
+ ) {
1341
+ throw stateFailure("Host-resource state root must be a bounded absolute path.")
1342
+ }
1343
+ return {
1344
+ pollIntervalMilliseconds: duration(
1345
+ options.pollIntervalMilliseconds,
1346
+ defaultPollIntervalMilliseconds,
1347
+ 1_000,
1348
+ "INVALID_PROFILE",
1349
+ "Host-resource poll interval",
1350
+ ),
1351
+ profile,
1352
+ profileSha256: sha256(canonicalProfile(profile)),
1353
+ stateRoot,
1354
+ waitTimeoutMilliseconds: duration(
1355
+ options.waitTimeoutMilliseconds,
1356
+ defaultWaitTimeoutMilliseconds,
1357
+ HOST_RESOURCE_MAX_WAIT_MILLISECONDS,
1358
+ "INVALID_PROFILE",
1359
+ "Host-resource wait timeout",
1360
+ ),
1361
+ }
1362
+ }
1363
+
1364
+ export function createHostResourceCoordinator(
1365
+ coordinatorOptions: HostResourceCoordinatorOptions = {},
1366
+ ): HostResourceCoordinator {
1367
+ const options = resolveCoordinatorOptions(coordinatorOptions)
1368
+ let resolvedRoot: ResolvedStateRoot | undefined
1369
+
1370
+ const root = (): string => {
1371
+ if (resolvedRoot === undefined) {
1372
+ resolvedRoot = ensureStateRoot(options.stateRoot)
1373
+ }
1374
+ return resolvedRoot.path
1375
+ }
1376
+
1377
+ const state = (): StateDocument => {
1378
+ return readState(root(), options.profile)
1379
+ }
1380
+
1381
+ const coordinator: HostResourceCoordinator = {
1382
+ profile: options.profile,
1383
+ scope: "machine",
1384
+ async withLease<T>(
1385
+ claimValue: readonly HostResourceClaim[],
1386
+ callback: (lease: HostResourceLease) => T | Promise<T>,
1387
+ leaseOptions: HostResourceLeaseOptions = {},
1388
+ ): Promise<T> {
1389
+ const claims = normalizeHostResourceClaims(claimValue, options.profile)
1390
+ if (typeof callback !== "function") {
1391
+ throw new HostResourceError(
1392
+ "INVALID_CLAIMS",
1393
+ "Host-resource lease callback must be a function.",
1394
+ )
1395
+ }
1396
+ const waitTimeoutMilliseconds = duration(
1397
+ leaseOptions.waitTimeoutMilliseconds,
1398
+ options.waitTimeoutMilliseconds,
1399
+ HOST_RESOURCE_MAX_WAIT_MILLISECONDS,
1400
+ "INVALID_CLAIMS",
1401
+ "Host-resource lease wait timeout",
1402
+ )
1403
+ const signal = leaseOptions.signal
1404
+ const deadline = performance.now() + waitTimeoutMilliseconds
1405
+ throwIfWaitEnded(signal, deadline)
1406
+ const directory = root()
1407
+ let marker: OwnedMarker | undefined
1408
+ let callbackSettledSuccessfully = false
1409
+ let callbackResult!: T
1410
+ let callbackFailure: unknown
1411
+ try {
1412
+ marker = await withControlLock(
1413
+ directory,
1414
+ options.pollIntervalMilliseconds,
1415
+ signal,
1416
+ deadline,
1417
+ () => {
1418
+ const currentState = state()
1419
+ assertMatchingProfile(currentState, options.profile)
1420
+ scanLiveMarkers(directory, options.profile, options.profileSha256)
1421
+ return publishWaitingMarker(
1422
+ directory,
1423
+ currentState,
1424
+ claims,
1425
+ options.profileSha256,
1426
+ )
1427
+ },
1428
+ )
1429
+ await admitMarker(
1430
+ marker,
1431
+ directory,
1432
+ options,
1433
+ signal,
1434
+ deadline,
1435
+ state,
1436
+ )
1437
+ const ownedMarker = marker
1438
+ const lease: HostResourceLease = Object.freeze({
1439
+ claims,
1440
+ inheritedFileDescriptor: ownedMarker.descriptor,
1441
+ profile: options.profile,
1442
+ ticket: ownedMarker.document.ticket,
1443
+ assertOwned: async (): Promise<void> => {
1444
+ await assertMarkerOwned(ownedMarker, directory, options, state)
1445
+ },
1446
+ })
1447
+ await lease.assertOwned()
1448
+ try {
1449
+ callbackResult = await callback(lease)
1450
+ callbackSettledSuccessfully = true
1451
+ // Detect callback-local descriptor/path misuse while the exact owner
1452
+ // is still available. External cancellation never releases capacity
1453
+ // until this callback has settled.
1454
+ await lease.assertOwned()
1455
+ } catch (cause) {
1456
+ callbackFailure = cause
1457
+ }
1458
+ } finally {
1459
+ if (marker !== undefined) {
1460
+ try {
1461
+ await releaseMarker(marker, directory, options, state)
1462
+ } catch (releaseFailure) {
1463
+ if (callbackFailure === undefined) callbackFailure = releaseFailure
1464
+ }
1465
+ }
1466
+ }
1467
+ if (!callbackSettledSuccessfully || callbackFailure !== undefined) {
1468
+ throw callbackFailure
1469
+ }
1470
+ return callbackResult
1471
+ },
1472
+ }
1473
+ return Object.freeze(coordinator)
1474
+ }
1475
+
1476
+ interface ProcessLocalRequest<T> {
1477
+ readonly callback: (lease: HostResourceLease) => T | Promise<T>
1478
+ readonly claims: readonly HostResourceClaim[]
1479
+ readonly reject: (cause: unknown) => void
1480
+ readonly resolve: (value: T) => void
1481
+ readonly signal: AbortSignal | undefined
1482
+ readonly ticket: string
1483
+ abortListener?: () => void
1484
+ active: boolean
1485
+ descriptor?: number
1486
+ settled: boolean
1487
+ timeout?: ReturnType<typeof setTimeout>
1488
+ }
1489
+
1490
+ interface ProcessLocalCoordinatorState {
1491
+ readonly active: Map<string, ProcessLocalRequest<unknown>>
1492
+ nextTicket: bigint
1493
+ readonly waiting: ProcessLocalRequest<unknown>[]
1494
+ }
1495
+
1496
+ const processLocalCoordinatorStates = new Map<
1497
+ string,
1498
+ ProcessLocalCoordinatorState
1499
+ >()
1500
+
1501
+ export function createProcessLocalHostResourceCoordinator(
1502
+ coordinatorOptions: Omit<HostResourceCoordinatorOptions, "stateRoot"> = {},
1503
+ ): HostResourceCoordinator {
1504
+ const profile = normalizeHostResourceProfile(
1505
+ coordinatorOptions.profile ?? defaultSlopcameraHostResourceProfile(),
1506
+ )
1507
+ const defaultWait = duration(
1508
+ coordinatorOptions.waitTimeoutMilliseconds,
1509
+ defaultWaitTimeoutMilliseconds,
1510
+ HOST_RESOURCE_MAX_WAIT_MILLISECONDS,
1511
+ "INVALID_PROFILE",
1512
+ "Host-resource wait timeout",
1513
+ )
1514
+ if (coordinatorOptions.pollIntervalMilliseconds !== undefined) {
1515
+ duration(
1516
+ coordinatorOptions.pollIntervalMilliseconds,
1517
+ defaultPollIntervalMilliseconds,
1518
+ 1_000,
1519
+ "INVALID_PROFILE",
1520
+ "Host-resource poll interval",
1521
+ )
1522
+ }
1523
+ const stateKey = canonicalProfile(profile)
1524
+ let localState = processLocalCoordinatorStates.get(stateKey)
1525
+ if (localState === undefined) {
1526
+ if (processLocalCoordinatorStates.size >= maximumProcessLocalProfiles) {
1527
+ throw new HostResourceError(
1528
+ "INVALID_PROFILE",
1529
+ "This process already coordinates the maximum number of host-resource profiles.",
1530
+ )
1531
+ }
1532
+ localState = {
1533
+ active: new Map<string, ProcessLocalRequest<unknown>>(),
1534
+ nextTicket: 1n,
1535
+ waiting: [],
1536
+ }
1537
+ processLocalCoordinatorStates.set(stateKey, localState)
1538
+ }
1539
+ const { active, waiting } = localState
1540
+
1541
+ const removeWaitHooks = (request: ProcessLocalRequest<unknown>): void => {
1542
+ if (request.timeout !== undefined) clearTimeout(request.timeout)
1543
+ if (request.abortListener !== undefined) {
1544
+ request.signal?.removeEventListener("abort", request.abortListener)
1545
+ }
1546
+ }
1547
+
1548
+ const activeMarkers = (): readonly LiveMarker[] => [...active.values()].map(
1549
+ (request) => ({
1550
+ document: {
1551
+ version: 1,
1552
+ owner: request.ticket.padStart(32, "0").slice(-32),
1553
+ profileSha256: sha256(canonicalProfile(profile)),
1554
+ ticket: request.ticket,
1555
+ phase: "A",
1556
+ claims: request.claims,
1557
+ },
1558
+ identity: { device: 0, inode: 0 },
1559
+ name: request.ticket,
1560
+ path: "",
1561
+ }),
1562
+ )
1563
+
1564
+ const pump = (): void => {
1565
+ for (let index = 0; index < waiting.length;) {
1566
+ const request = waiting[index]
1567
+ if (request === undefined) break
1568
+ const earlierOverlap = waiting.slice(0, index).some((earlier) =>
1569
+ claimsOverlap(earlier.claims, request.claims))
1570
+ if (
1571
+ earlierOverlap
1572
+ || !canFitClaims(profile, activeMarkers(), request.claims)
1573
+ ) {
1574
+ index += 1
1575
+ continue
1576
+ }
1577
+ waiting.splice(index, 1)
1578
+ removeWaitHooks(request)
1579
+ let descriptor: number
1580
+ try {
1581
+ descriptor = openSync(
1582
+ process.platform === "win32" ? "NUL" : "/dev/null",
1583
+ constants.O_RDONLY,
1584
+ )
1585
+ } catch (cause) {
1586
+ request.settled = true
1587
+ request.reject(new HostResourceError(
1588
+ "UNSAFE_STATE",
1589
+ "Process-local host-resource admission could not open its lease descriptor.",
1590
+ cause,
1591
+ ))
1592
+ continue
1593
+ }
1594
+ request.active = true
1595
+ active.set(request.ticket, request)
1596
+ request.descriptor = descriptor
1597
+ const assertOwned = async (): Promise<void> => {
1598
+ if (
1599
+ request.settled
1600
+ || !request.active
1601
+ || active.get(request.ticket) !== request
1602
+ || request.descriptor !== descriptor
1603
+ ) {
1604
+ throw new HostResourceError(
1605
+ "OWNERSHIP_LOST",
1606
+ "Process-local host-resource lease is no longer active.",
1607
+ )
1608
+ }
1609
+ try {
1610
+ fstatSync(descriptor)
1611
+ } catch (cause) {
1612
+ throw new HostResourceError(
1613
+ "OWNERSHIP_LOST",
1614
+ "Process-local host-resource descriptor is no longer open.",
1615
+ cause,
1616
+ )
1617
+ }
1618
+ }
1619
+ const lease: HostResourceLease = Object.freeze({
1620
+ assertOwned,
1621
+ claims: request.claims,
1622
+ inheritedFileDescriptor: descriptor,
1623
+ profile,
1624
+ ticket: request.ticket,
1625
+ })
1626
+ void (async () => {
1627
+ let succeeded = false
1628
+ let result: unknown
1629
+ let failure: unknown
1630
+ try {
1631
+ await lease.assertOwned()
1632
+ result = await request.callback(lease)
1633
+ await lease.assertOwned()
1634
+ succeeded = true
1635
+ } catch (cause) {
1636
+ failure = cause
1637
+ } finally {
1638
+ request.active = false
1639
+ request.settled = true
1640
+ active.delete(request.ticket)
1641
+ try {
1642
+ closeSync(descriptor)
1643
+ } catch (cause) {
1644
+ if (failure === undefined) failure = cause
1645
+ }
1646
+ pump()
1647
+ }
1648
+ if (succeeded && failure === undefined) request.resolve(result)
1649
+ else request.reject(failure)
1650
+ })()
1651
+ }
1652
+ }
1653
+
1654
+ const coordinator: HostResourceCoordinator = {
1655
+ profile,
1656
+ scope: "process",
1657
+ async withLease<T>(
1658
+ claimValue: readonly HostResourceClaim[],
1659
+ callback: (lease: HostResourceLease) => T | Promise<T>,
1660
+ leaseOptions: HostResourceLeaseOptions = {},
1661
+ ): Promise<T> {
1662
+ const claims = normalizeHostResourceClaims(claimValue, profile)
1663
+ if (typeof callback !== "function") {
1664
+ return Promise.reject(new HostResourceError(
1665
+ "INVALID_CLAIMS",
1666
+ "Host-resource lease callback must be a function.",
1667
+ ))
1668
+ }
1669
+ if (localState.nextTicket > maximumTicket) {
1670
+ return Promise.reject(stateFailure("Host-resource tickets are exhausted."))
1671
+ }
1672
+ const waitTimeoutMilliseconds = duration(
1673
+ leaseOptions.waitTimeoutMilliseconds,
1674
+ defaultWait,
1675
+ HOST_RESOURCE_MAX_WAIT_MILLISECONDS,
1676
+ "INVALID_CLAIMS",
1677
+ "Host-resource lease wait timeout",
1678
+ )
1679
+ const ticket = String(localState.nextTicket)
1680
+ localState.nextTicket += 1n
1681
+ return await new Promise<T>((resolve, reject) => {
1682
+ const request: ProcessLocalRequest<T> = {
1683
+ active: false,
1684
+ callback,
1685
+ claims,
1686
+ reject,
1687
+ resolve,
1688
+ settled: false,
1689
+ signal: leaseOptions.signal,
1690
+ ticket,
1691
+ }
1692
+ const removeWaiting = (cause: HostResourceError): void => {
1693
+ if (request.active || request.settled) return
1694
+ request.settled = true
1695
+ const index = waiting.indexOf(request as ProcessLocalRequest<unknown>)
1696
+ if (index >= 0) waiting.splice(index, 1)
1697
+ removeWaitHooks(request as ProcessLocalRequest<unknown>)
1698
+ reject(cause)
1699
+ pump()
1700
+ }
1701
+ request.abortListener = () => removeWaiting(new HostResourceError(
1702
+ "WAIT_ABORTED",
1703
+ "Host-resource admission was cancelled before execution.",
1704
+ ))
1705
+ request.timeout = setTimeout(
1706
+ () => removeWaiting(new HostResourceError(
1707
+ "WAIT_TIMEOUT",
1708
+ "Host-resource admission exceeded its bounded wait.",
1709
+ )),
1710
+ waitTimeoutMilliseconds,
1711
+ )
1712
+ request.timeout.unref?.()
1713
+ leaseOptions.signal?.addEventListener("abort", request.abortListener, {
1714
+ once: true,
1715
+ })
1716
+ if (leaseOptions.signal?.aborted === true) {
1717
+ request.abortListener()
1718
+ return
1719
+ }
1720
+ waiting.push(request as ProcessLocalRequest<unknown>)
1721
+ pump()
1722
+ })
1723
+ },
1724
+ }
1725
+ return Object.freeze(coordinator)
1726
+ }
1727
+
1728
+ export function createDefaultHostResourceCoordinator(
1729
+ options: HostResourceCoordinatorOptions = {},
1730
+ platform: NodeJS.Platform = process.platform,
1731
+ ): HostResourceCoordinator {
1732
+ if (isHostResourcePlatformSupported(platform)) {
1733
+ return createHostResourceCoordinator(options)
1734
+ }
1735
+ const {
1736
+ profile,
1737
+ pollIntervalMilliseconds,
1738
+ waitTimeoutMilliseconds,
1739
+ } = options
1740
+ return createProcessLocalHostResourceCoordinator({
1741
+ ...(profile === undefined ? {} : { profile }),
1742
+ ...(pollIntervalMilliseconds === undefined
1743
+ ? {}
1744
+ : { pollIntervalMilliseconds }),
1745
+ ...(waitTimeoutMilliseconds === undefined
1746
+ ? {}
1747
+ : { waitTimeoutMilliseconds }),
1748
+ })
1749
+ }