@stigmer/server 3.22.1 → 3.23.1-dev.20260923075205

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 (329) hide show
  1. package/dist/artifactstorage/artifact-storage.d.ts +29 -31
  2. package/dist/artifactstorage/artifact-storage.d.ts.map +1 -1
  3. package/dist/artifactstorage/artifact-storage.js +5 -7
  4. package/dist/artifactstorage/artifact-storage.js.map +1 -1
  5. package/dist/artifactstorage/r2-storage.d.ts +12 -7
  6. package/dist/artifactstorage/r2-storage.d.ts.map +1 -1
  7. package/dist/artifactstorage/r2-storage.js +6 -14
  8. package/dist/artifactstorage/r2-storage.js.map +1 -1
  9. package/dist/authorization/authorizer.d.ts +4 -8
  10. package/dist/authorization/authorizer.d.ts.map +1 -1
  11. package/dist/authorization/authorizer.js +5 -9
  12. package/dist/authorization/authorizer.js.map +1 -1
  13. package/dist/authorization/derived-tuples.d.ts.map +1 -1
  14. package/dist/authorization/derived-tuples.js +10 -25
  15. package/dist/authorization/derived-tuples.js.map +1 -1
  16. package/dist/authorization/evaluator.d.ts +7 -8
  17. package/dist/authorization/evaluator.d.ts.map +1 -1
  18. package/dist/authorization/evaluator.js +4 -20
  19. package/dist/authorization/evaluator.js.map +1 -1
  20. package/dist/authorization/list-read-scope.d.ts.map +1 -1
  21. package/dist/authorization/list-read-scope.js +5 -9
  22. package/dist/authorization/list-read-scope.js.map +1 -1
  23. package/dist/authorization/model/agent.d.ts.map +1 -1
  24. package/dist/authorization/model/agent.js +4 -5
  25. package/dist/authorization/model/agent.js.map +1 -1
  26. package/dist/authorization/model/agent_instance.d.ts.map +1 -1
  27. package/dist/authorization/model/agent_instance.js +2 -2
  28. package/dist/authorization/model/agent_instance.js.map +1 -1
  29. package/dist/authorization/model/mcp_server.d.ts.map +1 -1
  30. package/dist/authorization/model/mcp_server.js +2 -2
  31. package/dist/authorization/model/mcp_server.js.map +1 -1
  32. package/dist/authorization/model/plugin.d.ts.map +1 -1
  33. package/dist/authorization/model/plugin.js +3 -4
  34. package/dist/authorization/model/plugin.js.map +1 -1
  35. package/dist/authorization/model/rewrite.d.ts +8 -10
  36. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  37. package/dist/authorization/model/rewrite.js +0 -3
  38. package/dist/authorization/model/rewrite.js.map +1 -1
  39. package/dist/authorization/model/skill.d.ts.map +1 -1
  40. package/dist/authorization/model/skill.js +2 -2
  41. package/dist/authorization/model/skill.js.map +1 -1
  42. package/dist/authorization/model/workflow.d.ts.map +1 -1
  43. package/dist/authorization/model/workflow.js +2 -2
  44. package/dist/authorization/model/workflow.js.map +1 -1
  45. package/dist/authorization/model/workflow_instance.d.ts.map +1 -1
  46. package/dist/authorization/model/workflow_instance.js +2 -2
  47. package/dist/authorization/model/workflow_instance.js.map +1 -1
  48. package/dist/authorization/organization-directory.d.ts.map +1 -1
  49. package/dist/authorization/organization-directory.js +2 -3
  50. package/dist/authorization/organization-directory.js.map +1 -1
  51. package/dist/authorization/store-test-kit.d.ts +0 -6
  52. package/dist/authorization/store-test-kit.d.ts.map +1 -1
  53. package/dist/authorization/store-test-kit.js +16 -62
  54. package/dist/authorization/store-test-kit.js.map +1 -1
  55. package/dist/authorization/tuples.d.ts +13 -32
  56. package/dist/authorization/tuples.d.ts.map +1 -1
  57. package/dist/authorization/tuples.js +3 -11
  58. package/dist/authorization/tuples.js.map +1 -1
  59. package/dist/boot/compose.d.ts.map +1 -1
  60. package/dist/boot/compose.js +49 -24
  61. package/dist/boot/compose.js.map +1 -1
  62. package/dist/boot/config.d.ts +4 -3
  63. package/dist/boot/config.d.ts.map +1 -1
  64. package/dist/boot/config.js.map +1 -1
  65. package/dist/boot/skill-transfer-origin.d.ts +52 -0
  66. package/dist/boot/skill-transfer-origin.d.ts.map +1 -0
  67. package/dist/boot/skill-transfer-origin.js +54 -0
  68. package/dist/boot/skill-transfer-origin.js.map +1 -0
  69. package/dist/domain/agent/controller.d.ts.map +1 -1
  70. package/dist/domain/agent/controller.js +4 -4
  71. package/dist/domain/agent/controller.js.map +1 -1
  72. package/dist/domain/agentchannel/controller.d.ts.map +1 -1
  73. package/dist/domain/agentchannel/controller.js +3 -1
  74. package/dist/domain/agentchannel/controller.js.map +1 -1
  75. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  76. package/dist/domain/agentexecution/controller.js +3 -1
  77. package/dist/domain/agentexecution/controller.js.map +1 -1
  78. package/dist/domain/agentinstance/controller.d.ts.map +1 -1
  79. package/dist/domain/agentinstance/controller.js +3 -4
  80. package/dist/domain/agentinstance/controller.js.map +1 -1
  81. package/dist/domain/agentshare/constants.d.ts +6 -11
  82. package/dist/domain/agentshare/constants.d.ts.map +1 -1
  83. package/dist/domain/agentshare/constants.js +8 -16
  84. package/dist/domain/agentshare/constants.js.map +1 -1
  85. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  86. package/dist/domain/agentshare/controller.js +3 -1
  87. package/dist/domain/agentshare/controller.js.map +1 -1
  88. package/dist/domain/agentshare/steps.d.ts +18 -25
  89. package/dist/domain/agentshare/steps.d.ts.map +1 -1
  90. package/dist/domain/agentshare/steps.js +41 -142
  91. package/dist/domain/agentshare/steps.js.map +1 -1
  92. package/dist/domain/environment/controller.d.ts.map +1 -1
  93. package/dist/domain/environment/controller.js +2 -3
  94. package/dist/domain/environment/controller.js.map +1 -1
  95. package/dist/domain/executioncontext/controller.d.ts.map +1 -1
  96. package/dist/domain/executioncontext/controller.js +2 -1
  97. package/dist/domain/executioncontext/controller.js.map +1 -1
  98. package/dist/domain/mcpserver/controller.d.ts.map +1 -1
  99. package/dist/domain/mcpserver/controller.js +3 -4
  100. package/dist/domain/mcpserver/controller.js.map +1 -1
  101. package/dist/domain/plugin/controller.d.ts +3 -7
  102. package/dist/domain/plugin/controller.d.ts.map +1 -1
  103. package/dist/domain/plugin/controller.js +9 -11
  104. package/dist/domain/plugin/controller.js.map +1 -1
  105. package/dist/domain/plugin/push.d.ts +7 -5
  106. package/dist/domain/plugin/push.d.ts.map +1 -1
  107. package/dist/domain/plugin/push.js +8 -14
  108. package/dist/domain/plugin/push.js.map +1 -1
  109. package/dist/domain/schedule/controller.d.ts.map +1 -1
  110. package/dist/domain/schedule/controller.js +3 -1
  111. package/dist/domain/schedule/controller.js.map +1 -1
  112. package/dist/domain/session/controller.d.ts.map +1 -1
  113. package/dist/domain/session/controller.js +3 -2
  114. package/dist/domain/session/controller.js.map +1 -1
  115. package/dist/domain/skill/constants.d.ts +26 -3
  116. package/dist/domain/skill/constants.d.ts.map +1 -1
  117. package/dist/domain/skill/constants.js +26 -3
  118. package/dist/domain/skill/constants.js.map +1 -1
  119. package/dist/domain/skill/controller.d.ts +6 -11
  120. package/dist/domain/skill/controller.d.ts.map +1 -1
  121. package/dist/domain/skill/controller.js +33 -24
  122. package/dist/domain/skill/controller.js.map +1 -1
  123. package/dist/domain/skill/push.d.ts +2 -2
  124. package/dist/domain/skill/push.d.ts.map +1 -1
  125. package/dist/domain/skill/push.js +2 -2
  126. package/dist/domain/skill/push.js.map +1 -1
  127. package/dist/domain/skill/transfer/handler.d.ts +17 -9
  128. package/dist/domain/skill/transfer/handler.d.ts.map +1 -1
  129. package/dist/domain/skill/transfer/handler.js +19 -8
  130. package/dist/domain/skill/transfer/handler.js.map +1 -1
  131. package/dist/domain/skill/transfer/slots.d.ts +26 -41
  132. package/dist/domain/skill/transfer/slots.d.ts.map +1 -1
  133. package/dist/domain/skill/transfer/slots.js +80 -87
  134. package/dist/domain/skill/transfer/slots.js.map +1 -1
  135. package/dist/domain/skill/transfer/staging.d.ts +48 -0
  136. package/dist/domain/skill/transfer/staging.d.ts.map +1 -0
  137. package/dist/domain/skill/transfer/staging.js +127 -0
  138. package/dist/domain/skill/transfer/staging.js.map +1 -0
  139. package/dist/domain/workflow/agent-call-references.d.ts +54 -0
  140. package/dist/domain/workflow/agent-call-references.d.ts.map +1 -0
  141. package/dist/domain/workflow/agent-call-references.js +97 -0
  142. package/dist/domain/workflow/agent-call-references.js.map +1 -0
  143. package/dist/domain/workflow/controller.d.ts.map +1 -1
  144. package/dist/domain/workflow/controller.js +14 -6
  145. package/dist/domain/workflow/controller.js.map +1 -1
  146. package/dist/domain/workflow/registry/data/task-kind-registry.json +1 -1
  147. package/dist/domain/workflow/validation/task-config-constraints.d.ts +10 -1
  148. package/dist/domain/workflow/validation/task-config-constraints.d.ts.map +1 -1
  149. package/dist/domain/workflow/validation/task-config-constraints.js +3 -2
  150. package/dist/domain/workflow/validation/task-config-constraints.js.map +1 -1
  151. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  152. package/dist/domain/workflowexecution/controller.js +4 -2
  153. package/dist/domain/workflowexecution/controller.js.map +1 -1
  154. package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
  155. package/dist/domain/workflowinstance/controller.js +3 -4
  156. package/dist/domain/workflowinstance/controller.js.map +1 -1
  157. package/dist/extensions/authorization-queries.d.ts +6 -5
  158. package/dist/extensions/authorization-queries.d.ts.map +1 -1
  159. package/dist/extensions/list-read-scope.d.ts +1 -1
  160. package/dist/extensions/resource-authorization.d.ts +3 -2
  161. package/dist/extensions/resource-authorization.d.ts.map +1 -1
  162. package/dist/index.d.ts +1 -1
  163. package/dist/index.d.ts.map +1 -1
  164. package/dist/index.js.map +1 -1
  165. package/dist/pipeline/apiresource-meta.d.ts +12 -3
  166. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  167. package/dist/pipeline/apiresource-meta.js +21 -10
  168. package/dist/pipeline/apiresource-meta.js.map +1 -1
  169. package/dist/pipeline/steps/authorization-tuples.d.ts +9 -3
  170. package/dist/pipeline/steps/authorization-tuples.d.ts.map +1 -1
  171. package/dist/pipeline/steps/authorization-tuples.js +9 -11
  172. package/dist/pipeline/steps/authorization-tuples.js.map +1 -1
  173. package/dist/pipeline/steps/references.d.ts +178 -20
  174. package/dist/pipeline/steps/references.d.ts.map +1 -1
  175. package/dist/pipeline/steps/references.js +344 -43
  176. package/dist/pipeline/steps/references.js.map +1 -1
  177. package/dist/pipeline/steps/resolve-artifact-source.d.ts +11 -8
  178. package/dist/pipeline/steps/resolve-artifact-source.d.ts.map +1 -1
  179. package/dist/pipeline/steps/resolve-artifact-source.js +8 -4
  180. package/dist/pipeline/steps/resolve-artifact-source.js.map +1 -1
  181. package/dist/query/search/controller.js +0 -1
  182. package/dist/query/search/controller.js.map +1 -1
  183. package/dist/query/search/criteria.d.ts +1 -5
  184. package/dist/query/search/criteria.d.ts.map +1 -1
  185. package/dist/query/search/criteria.js +3 -13
  186. package/dist/query/search/criteria.js.map +1 -1
  187. package/dist/query/search/handler.d.ts +5 -4
  188. package/dist/query/search/handler.d.ts.map +1 -1
  189. package/dist/query/search/handler.js +7 -6
  190. package/dist/query/search/handler.js.map +1 -1
  191. package/dist/query/search/query-store.d.ts.map +1 -1
  192. package/dist/query/search/query-store.js +0 -2
  193. package/dist/query/search/query-store.js.map +1 -1
  194. package/dist/store/interface.d.ts +3 -6
  195. package/dist/store/interface.d.ts.map +1 -1
  196. package/dist/store/interface.js.map +1 -1
  197. package/dist/store/postgres/migrations.d.ts +3 -1
  198. package/dist/store/postgres/migrations.d.ts.map +1 -1
  199. package/dist/store/postgres/migrations.js +35 -1
  200. package/dist/store/postgres/migrations.js.map +1 -1
  201. package/dist/store/postgres/store.d.ts.map +1 -1
  202. package/dist/store/postgres/store.js +4 -9
  203. package/dist/store/postgres/store.js.map +1 -1
  204. package/dist/store/public-visibility-retired.d.ts +67 -0
  205. package/dist/store/public-visibility-retired.d.ts.map +1 -0
  206. package/dist/store/public-visibility-retired.js +55 -0
  207. package/dist/store/public-visibility-retired.js.map +1 -0
  208. package/dist/store/sqlite/migrations.d.ts +3 -1
  209. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  210. package/dist/store/sqlite/migrations.js +36 -1
  211. package/dist/store/sqlite/migrations.js.map +1 -1
  212. package/dist/store/sqlite/store.d.ts.map +1 -1
  213. package/dist/store/sqlite/store.js +4 -8
  214. package/dist/store/sqlite/store.js.map +1 -1
  215. package/package.json +6 -6
  216. package/src/artifactstorage/__tests__/artifact-storage.test.ts +36 -33
  217. package/src/artifactstorage/__tests__/r2-storage.test.ts +11 -13
  218. package/src/artifactstorage/artifact-storage.ts +37 -33
  219. package/src/artifactstorage/r2-storage.ts +6 -15
  220. package/src/authorization/README.md +24 -26
  221. package/src/authorization/__tests__/authorizer.test.ts +10 -9
  222. package/src/authorization/__tests__/derived-tuples.test.ts +4 -11
  223. package/src/authorization/__tests__/evaluator.test.ts +14 -73
  224. package/src/authorization/__tests__/facts.test.ts +2 -2
  225. package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +19 -27
  226. package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +6 -98
  227. package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +4 -61
  228. package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +1 -15
  229. package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +1 -21
  230. package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +1 -25
  231. package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +1 -3
  232. package/src/authorization/__tests__/list-read-scope.test.ts +18 -17
  233. package/src/authorization/__tests__/store-tests.test.ts +2 -6
  234. package/src/authorization/__tests__/tuples.test.ts +12 -11
  235. package/src/authorization/authorizer.ts +4 -9
  236. package/src/authorization/derived-tuples.ts +14 -31
  237. package/src/authorization/evaluator.ts +8 -44
  238. package/src/authorization/list-read-scope.ts +5 -11
  239. package/src/authorization/model/agent.ts +2 -5
  240. package/src/authorization/model/agent_instance.ts +0 -2
  241. package/src/authorization/model/mcp_server.ts +0 -2
  242. package/src/authorization/model/plugin.ts +1 -4
  243. package/src/authorization/model/rewrite.ts +8 -13
  244. package/src/authorization/model/skill.ts +0 -2
  245. package/src/authorization/model/workflow.ts +0 -2
  246. package/src/authorization/model/workflow_instance.ts +0 -2
  247. package/src/authorization/organization-directory.ts +1 -3
  248. package/src/authorization/store-test-kit.ts +16 -75
  249. package/src/authorization/tuples.ts +16 -39
  250. package/src/boot/__tests__/skill-transfer-origin.test.ts +72 -0
  251. package/src/boot/compose.ts +62 -25
  252. package/src/boot/config.ts +4 -3
  253. package/src/boot/skill-transfer-origin.ts +76 -0
  254. package/src/domain/agent/__tests__/agent.test.ts +11 -4
  255. package/src/domain/agent/controller.ts +7 -7
  256. package/src/domain/agentchannel/__tests__/agentchannel.test.ts +223 -48
  257. package/src/domain/agentchannel/controller.ts +6 -1
  258. package/src/domain/agentexecution/controller.ts +6 -1
  259. package/src/domain/agentinstance/controller.ts +6 -12
  260. package/src/domain/agentshare/__tests__/agentshare.test.ts +196 -172
  261. package/src/domain/agentshare/__tests__/create-authorization.test.ts +16 -31
  262. package/src/domain/agentshare/constants.ts +14 -26
  263. package/src/domain/agentshare/controller.ts +6 -1
  264. package/src/domain/agentshare/steps.ts +41 -194
  265. package/src/domain/environment/controller.ts +5 -8
  266. package/src/domain/executioncontext/controller.ts +5 -1
  267. package/src/domain/mcpserver/__tests__/mcpserver.test.ts +115 -28
  268. package/src/domain/mcpserver/__tests__/oauth-handshake.test.ts +53 -18
  269. package/src/domain/mcpserver/controller.ts +6 -12
  270. package/src/domain/plugin/controller.ts +16 -25
  271. package/src/domain/plugin/push.ts +8 -16
  272. package/src/domain/schedule/controller.ts +6 -1
  273. package/src/domain/session/controller.ts +12 -4
  274. package/src/domain/skill/__tests__/skill.test.ts +5 -3
  275. package/src/domain/skill/__tests__/slots.test.ts +113 -60
  276. package/src/domain/skill/__tests__/staging.test.ts +304 -0
  277. package/src/domain/skill/constants.ts +28 -3
  278. package/src/domain/skill/controller.ts +44 -42
  279. package/src/domain/skill/push.ts +3 -3
  280. package/src/domain/skill/transfer/handler.ts +33 -15
  281. package/src/domain/skill/transfer/slots.ts +89 -100
  282. package/src/domain/skill/transfer/staging.ts +191 -0
  283. package/src/domain/workflow/__tests__/agent-call-references.test.ts +222 -0
  284. package/src/domain/workflow/agent-call-references.ts +157 -0
  285. package/src/domain/workflow/controller.ts +28 -10
  286. package/src/domain/workflow/registry/data/task-kind-registry.json +1 -1
  287. package/src/domain/workflow/validation/task-config-constraints.ts +7 -3
  288. package/src/domain/workflowexecution/controller.ts +7 -2
  289. package/src/domain/workflowinstance/controller.ts +6 -12
  290. package/src/extensions/__tests__/built-in-authorization-composed.test.ts +32 -19
  291. package/src/extensions/__tests__/extension-composition.test.ts +4 -4
  292. package/src/extensions/__tests__/list-read-scope-composed.test.ts +9 -6
  293. package/src/extensions/authorization-queries.ts +6 -5
  294. package/src/extensions/list-read-scope.ts +1 -1
  295. package/src/extensions/resource-authorization.ts +3 -5
  296. package/src/index.ts +0 -1
  297. package/src/pipeline/__tests__/supports-visibility.test.ts +112 -0
  298. package/src/pipeline/apiresource-meta.ts +21 -10
  299. package/src/pipeline/steps/__tests__/authorization-tuples.test.ts +18 -31
  300. package/src/pipeline/steps/__tests__/references.test.ts +703 -0
  301. package/src/pipeline/steps/authorization-tuples.ts +9 -11
  302. package/src/pipeline/steps/references.ts +578 -70
  303. package/src/pipeline/steps/resolve-artifact-source.ts +20 -13
  304. package/src/query/search/__tests__/criteria.test.ts +0 -4
  305. package/src/query/search/__tests__/query-store.test.ts +11 -40
  306. package/src/query/search/controller.ts +0 -1
  307. package/src/query/search/criteria.ts +0 -14
  308. package/src/query/search/handler.ts +6 -7
  309. package/src/query/search/query-store.ts +0 -2
  310. package/src/store/__tests__/public-visibility-retired.test.ts +127 -0
  311. package/src/store/__tests__/store-contract.ts +6 -51
  312. package/src/store/interface.ts +3 -6
  313. package/src/store/postgres/__tests__/migrations.test.ts +187 -6
  314. package/src/store/postgres/__tests__/store-contract.test.ts +0 -2
  315. package/src/store/postgres/migrations.ts +48 -1
  316. package/src/store/postgres/store.ts +4 -11
  317. package/src/store/public-visibility-retired.ts +119 -0
  318. package/src/store/sqlite/__tests__/migrations.test.ts +191 -5
  319. package/src/store/sqlite/migrations.ts +45 -1
  320. package/src/store/sqlite/store.ts +4 -10
  321. package/src/transport/registry/__tests__/lanes.test.ts +1 -1
  322. package/dist/pipeline/steps/visibility-gates.d.ts +0 -27
  323. package/dist/pipeline/steps/visibility-gates.d.ts.map +0 -1
  324. package/dist/pipeline/steps/visibility-gates.js +0 -139
  325. package/dist/pipeline/steps/visibility-gates.js.map +0 -1
  326. package/src/authorization/__tests__/fixtures/fga/cross-org-share-creation.fga.yaml +0 -98
  327. package/src/authorization/__tests__/fixtures/fga/public-visibility-setter.fga.yaml +0 -52
  328. package/src/pipeline/steps/__tests__/visibility-gates.test.ts +0 -187
  329. package/src/pipeline/steps/visibility-gates.ts +0 -174
@@ -85,7 +85,10 @@ import {
85
85
  TARGET_RESOURCE_KEY,
86
86
  newLoadTargetStep,
87
87
  } from "../../pipeline/steps/load-target.js";
88
- import { newNormalizeReferencesStep } from "../../pipeline/steps/references.js";
88
+ import {
89
+ newNormalizeReferencesStep,
90
+ newValidateReferencesStep,
91
+ } from "../../pipeline/steps/references.js";
89
92
  import {
90
93
  newCleanupIamPoliciesStep,
91
94
  newCreateAuthorizationTuplesStep,
@@ -208,7 +211,8 @@ async function createSession(
208
211
  .addStep(newCheckDuplicateStep(deps.store))
209
212
  .addStep(newBuildNewStateStep())
210
213
  .addStep(newGuardReservedLabelsStep(deps.authorizer))
211
- .addStep(newNormalizeReferencesStep());
214
+ .addStep(newNormalizeReferencesStep())
215
+ .addStep(newValidateReferencesStep(deps.store));
212
216
  // The ratified pre-side-effect gate slot (blueprint 03 §3a; O4; Q2
213
217
  // ruling — see the create doc comment). Empty in OSS.
214
218
  for (const step of stepsForSlot<typeof SessionSchema>(
@@ -499,7 +503,9 @@ async function list(
499
503
  newAuthorizeStep(SessionQueryController.method.list, deps.authorizer),
500
504
  )
501
505
  .addStep(newValidateProtoStep())
502
- .addStep(newListAllSessionsStep(deps.store, deps.logger, deps.listReadScope))
506
+ .addStep(
507
+ newListAllSessionsStep(deps.store, deps.logger, deps.listReadScope),
508
+ )
503
509
  .build()
504
510
  .execute(reqCtx);
505
511
  return requireListResult(reqCtx.get(LIST_RESULT_KEY));
@@ -559,7 +565,9 @@ async function listByChannel(
559
565
  )
560
566
  .addStep(newValidateProtoStep())
561
567
  .addStep(newAuthorizeChannelAccessStep(deps.authorizer))
562
- .addStep(newFilterByChannelStep(deps.store, deps.logger, deps.listReadScope))
568
+ .addStep(
569
+ newFilterByChannelStep(deps.store, deps.logger, deps.listReadScope),
570
+ )
563
571
  .build()
564
572
  .execute(reqCtx);
565
573
  return requireListResult(reqCtx.get(LIST_RESULT_KEY));
@@ -434,7 +434,7 @@ describe("transfer lane (#675) — mint → PUT → push-by-ref → download", (
434
434
  );
435
435
  });
436
436
 
437
- it("a ref whose slot was minted but never uploaded is rejected at push", async () => {
437
+ it("a ref whose slot was minted but never uploaded is rejected at push — the one condition every driver can answer", async () => {
438
438
  const minted = await command.createArtifactUploadUrl({ org: ORG, sizeBytes: 1024n });
439
439
  const err = await expectCode(
440
440
  command.push({ org: ORG, artifactUploadRef: minted.artifactUploadRef }),
@@ -442,7 +442,7 @@ describe("transfer lane (#675) — mint → PUT → push-by-ref → download", (
442
442
  "empty slot",
443
443
  );
444
444
  expect(err.rawMessage).toBe(
445
- "artifact_upload_ref not usable: upload reference has no uploaded bytes — request a new upload URL via createArtifactUploadUrl",
445
+ "artifact_upload_ref not usable: upload reference unknown or expired — request a new upload URL via createArtifactUploadUrl",
446
446
  );
447
447
  });
448
448
 
@@ -499,7 +499,9 @@ describe("transfer lane (#675) — mint → PUT → push-by-ref → download", (
499
499
  const minted = await query.getArtifactDownloadUrl({
500
500
  artifactStorageKey: pushed.status!.artifactStorageKey,
501
501
  });
502
- expect(minted.ttlSeconds).toBe(0);
502
+ // The floor of the URL's validity, the same on every driver; the local
503
+ // lane's content-hash URL simply outlives it.
504
+ expect(minted.ttlSeconds).toBe(3600);
503
505
  expect(minted.sizeBytes).toBe(BigInt(artifact.length));
504
506
 
505
507
  const resp = await fetch(minted.url);
@@ -1,28 +1,39 @@
1
1
  /**
2
- * Pins the upload-slot registry against Go's slots_test.go: mint bounds,
3
- * ref shape, TTL expiry (injected clock), exact-declared-size enforcement
4
- * (short AND long bodies), strict single-use consume, staged-file cleanup,
5
- * expiry sweep on mint, and the boot-time staging wipe.
2
+ * Pins the upload-slot registry against Go's slots_test.go, keyed by the
3
+ * domain's staging key: reservation bounds and key shape, TTL expiry
4
+ * (injected clock), exact-declared-size enforcement (short AND long
5
+ * bodies), single upload per key, staged-file placement under the driver
6
+ * root, expiry sweep on reservation, the boot-time staging wipe, and the
7
+ * staging directory's recreation after the driver's delete pruned it.
6
8
  */
7
- import { existsSync, mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
9
+ import {
10
+ existsSync,
11
+ mkdtempSync,
12
+ mkdirSync,
13
+ readdirSync,
14
+ readFileSync,
15
+ rmSync,
16
+ writeFileSync,
17
+ } from "node:fs";
8
18
  import { tmpdir } from "node:os";
9
19
  import path from "node:path";
10
20
  import { Readable } from "node:stream";
11
21
 
12
22
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
13
23
 
24
+ import { STAGING_KEY_PREFIX } from "../constants.js";
14
25
  import {
15
26
  SizeMismatchError,
16
27
  SlotConsumedError,
17
- SlotEmptyError,
18
28
  SlotUnknownError,
19
29
  UploadSlots,
20
30
  } from "../transfer/slots.js";
31
+ import { newUploadRef, stagingKeyOf } from "../transfer/staging.js";
21
32
 
22
33
  const TTL_MS = 15 * 60 * 1000;
23
34
  const MAX_SIZE = 100 * 1024 * 1024;
24
35
 
25
- let dir: string;
36
+ let root: string;
26
37
  let stagingDir: string;
27
38
  let nowMs: number;
28
39
  let slots: UploadSlots;
@@ -31,101 +42,143 @@ function body(text: string): Readable {
31
42
  return Readable.from([Buffer.from(text)]);
32
43
  }
33
44
 
45
+ /** A fresh, well-formed staging key — what the domain's mint hands the registry. */
46
+ function freshKey(): string {
47
+ const key = stagingKeyOf(newUploadRef());
48
+ if (key === undefined) throw new Error("a minted ref always has a key");
49
+ return key;
50
+ }
51
+
34
52
  beforeEach(() => {
35
- dir = mkdtempSync(path.join(tmpdir(), "skill-slots-test-"));
36
- stagingDir = path.join(dir, "skills-staging");
53
+ root = mkdtempSync(path.join(tmpdir(), "skill-slots-test-"));
54
+ stagingDir = path.join(root, STAGING_KEY_PREFIX);
37
55
  nowMs = 1_000_000;
38
- slots = new UploadSlots(stagingDir, TTL_MS, MAX_SIZE, () => nowMs);
56
+ slots = new UploadSlots(
57
+ root,
58
+ STAGING_KEY_PREFIX,
59
+ TTL_MS,
60
+ MAX_SIZE,
61
+ () => nowMs,
62
+ );
39
63
  });
40
64
 
41
65
  afterEach(() => {
42
- rmSync(dir, { recursive: true, force: true });
66
+ rmSync(root, { recursive: true, force: true });
43
67
  });
44
68
 
45
69
  describe("UploadSlots", () => {
46
- it("mints sau_-prefixed 128-bit hex refs with the registry TTL", () => {
47
- const { ref, ttlMs } = slots.mint(1024);
48
- expect(ref).toMatch(/^sau_[0-9a-f]{32}$/);
49
- expect(ttlMs).toBe(TTL_MS);
70
+ it("reserves a key with the registry TTL", () => {
71
+ expect(slots.reserve(freshKey(), 1024)).toEqual({ ttlMs: TTL_MS });
50
72
  });
51
73
 
52
- it("rejects out-of-bounds declarations at mint", () => {
53
- expect(() => slots.mint(0)).toThrow("declared size 0 outside (0, 104857600]");
54
- expect(() => slots.mint(-1)).toThrow(`declared size -1 outside (0, ${MAX_SIZE}]`);
55
- expect(() => slots.mint(MAX_SIZE + 1)).toThrow(
74
+ it("rejects out-of-bounds declarations at reservation", () => {
75
+ expect(() => slots.reserve(freshKey(), 0)).toThrow(
76
+ "declared size 0 outside (0, 104857600]",
77
+ );
78
+ expect(() => slots.reserve(freshKey(), -1)).toThrow(
79
+ `declared size -1 outside (0, ${MAX_SIZE}]`,
80
+ );
81
+ expect(() => slots.reserve(freshKey(), MAX_SIZE + 1)).toThrow(
56
82
  `declared size ${MAX_SIZE + 1} outside (0, ${MAX_SIZE}]`,
57
83
  );
58
84
  });
59
85
 
60
- it("round-trips: mint → receive → consume returns the exact bytes and retires the slot", async () => {
61
- const { ref } = slots.mint(5);
62
- await slots.receive(ref, body("hello"));
63
- const data = await slots.consume(ref);
64
- expect(Buffer.from(data).toString()).toBe("hello");
65
- // Strictly single-use: the slot AND its staged file are gone.
66
- await expect(slots.consume(ref)).rejects.toThrow(SlotUnknownError);
67
- expect(readdirSync(stagingDir)).toEqual([]);
86
+ it("refuses a key outside the staging prefix or not shaped like a staged archive", () => {
87
+ expect(() => slots.reserve("skills/deadbeef.zip", 1)).toThrow(
88
+ `staging key skills/deadbeef.zip is outside ${STAGING_KEY_PREFIX}`,
89
+ );
90
+ expect(() =>
91
+ slots.reserve(`${STAGING_KEY_PREFIX}../escape.zip`, 1),
92
+ ).toThrow("does not name a staged archive");
93
+ expect(() => slots.reserve(`${STAGING_KEY_PREFIX}notes.txt`, 1)).toThrow(
94
+ "does not name a staged archive",
95
+ );
96
+ });
97
+
98
+ it("refuses to reserve the same key twice", () => {
99
+ const key = freshKey();
100
+ slots.reserve(key, 1);
101
+ expect(() => slots.reserve(key, 1)).toThrow(
102
+ `staging key ${key} is already reserved`,
103
+ );
104
+ });
105
+
106
+ it("stages a received body at the key under the root — where the driver reads it", async () => {
107
+ const key = freshKey();
108
+ slots.reserve(key, 5);
109
+ await slots.receive(key, body("hello"));
110
+ expect(readFileSync(path.join(root, key), "utf8")).toBe("hello");
68
111
  });
69
112
 
70
- it("rejects receive on an unknown ref", async () => {
71
- await expect(slots.receive("sau_nope", body("x"))).rejects.toThrow(SlotUnknownError);
113
+ it("rejects receive on an unreserved key", async () => {
114
+ await expect(slots.receive(freshKey(), body("x"))).rejects.toThrow(
115
+ SlotUnknownError,
116
+ );
72
117
  });
73
118
 
74
- it("rejects receive on an expired ref", async () => {
75
- const { ref } = slots.mint(1);
119
+ it("rejects receive on an expired key", async () => {
120
+ const key = freshKey();
121
+ slots.reserve(key, 1);
76
122
  nowMs += TTL_MS + 1;
77
- await expect(slots.receive(ref, body("x"))).rejects.toThrow(SlotUnknownError);
123
+ await expect(slots.receive(key, body("x"))).rejects.toThrow(
124
+ SlotUnknownError,
125
+ );
78
126
  });
79
127
 
80
- it("rejects a second upload to the same ref", async () => {
81
- const { ref } = slots.mint(1);
82
- await slots.receive(ref, body("x"));
83
- await expect(slots.receive(ref, body("x"))).rejects.toThrow(SlotConsumedError);
128
+ it("rejects a second upload to the same key", async () => {
129
+ const key = freshKey();
130
+ slots.reserve(key, 1);
131
+ await slots.receive(key, body("x"));
132
+ await expect(slots.receive(key, body("x"))).rejects.toThrow(
133
+ SlotConsumedError,
134
+ );
84
135
  });
85
136
 
86
137
  it("rejects a short body, naming both sizes, and stages nothing", async () => {
87
- const { ref } = slots.mint(10);
88
- await expect(slots.receive(ref, body("short"))).rejects.toThrow(
138
+ const key = freshKey();
139
+ slots.reserve(key, 10);
140
+ await expect(slots.receive(key, body("short"))).rejects.toThrow(
89
141
  "upload size mismatch: received 5 bytes, declared 10",
90
142
  );
91
- await expect(slots.receive(ref, body("short"))).rejects.toBeInstanceOf(SizeMismatchError);
143
+ await expect(slots.receive(key, body("short"))).rejects.toBeInstanceOf(
144
+ SizeMismatchError,
145
+ );
92
146
  expect(readdirSync(stagingDir)).toEqual([]);
93
147
  });
94
148
 
95
149
  it("rejects an over-declaration body without buffering it whole (declared+1 proof)", async () => {
96
- const { ref } = slots.mint(3);
97
- await expect(slots.receive(ref, body("longer than three"))).rejects.toThrow(
150
+ const key = freshKey();
151
+ slots.reserve(key, 3);
152
+ await expect(slots.receive(key, body("longer than three"))).rejects.toThrow(
98
153
  "upload size mismatch: received 4 bytes, declared 3",
99
154
  );
100
155
  });
101
156
 
102
- it("rejects consume on a minted-but-never-uploaded ref", async () => {
103
- const { ref } = slots.mint(1);
104
- await expect(slots.consume(ref)).rejects.toThrow(SlotEmptyError);
105
- });
106
-
107
- it("rejects consume on an expired ref", async () => {
108
- const { ref } = slots.mint(1);
109
- await slots.receive(ref, body("x"));
110
- nowMs += TTL_MS + 1;
111
- await expect(slots.consume(ref)).rejects.toThrow(SlotUnknownError);
112
- });
113
-
114
- it("sweeps expired slots and their staged files on mint", async () => {
115
- const { ref } = slots.mint(1);
116
- await slots.receive(ref, body("x"));
157
+ it("sweeps expired slots and their staged files on reservation", async () => {
158
+ const key = freshKey();
159
+ slots.reserve(key, 1);
160
+ await slots.receive(key, body("x"));
117
161
  expect(readdirSync(stagingDir)).toHaveLength(1);
118
162
  nowMs += TTL_MS + 1;
119
- slots.mint(1);
163
+ slots.reserve(freshKey(), 1);
120
164
  expect(readdirSync(stagingDir)).toEqual([]);
121
165
  });
122
166
 
167
+ it("recreates the staging directory when the driver's delete pruned it between uploads", async () => {
168
+ const key = freshKey();
169
+ slots.reserve(key, 1);
170
+ rmSync(stagingDir, { recursive: true, force: true });
171
+ await slots.receive(key, body("x"));
172
+ expect(readFileSync(path.join(root, key), "utf8")).toBe("x");
173
+ });
174
+
123
175
  it("wipes the staging directory at construction (orphans are unreachable)", () => {
124
- const orphanDir = path.join(dir, "staging-with-orphans");
176
+ const orphanRoot = path.join(root, "orphan-root");
177
+ const orphanDir = path.join(orphanRoot, STAGING_KEY_PREFIX);
125
178
  mkdirSync(orphanDir, { recursive: true });
126
- const orphan = path.join(orphanDir, "sau_orphan.zip");
179
+ const orphan = path.join(orphanDir, "deadbeef.zip");
127
180
  writeFileSync(orphan, "stale bytes");
128
- new UploadSlots(orphanDir, TTL_MS, MAX_SIZE);
181
+ new UploadSlots(orphanRoot, STAGING_KEY_PREFIX, TTL_MS, MAX_SIZE);
129
182
  expect(existsSync(orphan)).toBe(false);
130
183
  expect(readdirSync(orphanDir)).toEqual([]);
131
184
  });
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Pins the archive staging port (transfer/staging.ts): the reference ⇄
3
+ * staging-key mapping and its refusals; mint handing the driver the
4
+ * domain's key and the slot TTL; consume reading then deleting, with the
5
+ * delete best-effort and loud; the one "unknown or expired" condition for
6
+ * a malformed, missing or already-consumed reference; a driver fault
7
+ * propagating untouched; the download capability's floor TTL. First over
8
+ * a recording fake driver (what any bucket sees), then over the real local
9
+ * driver with its slot registry, wired exactly as boot/compose.ts wires it
10
+ * — the two drivers must be indistinguishable above this module.
11
+ */
12
+ import { mkdtempSync, rmSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import path from "node:path";
15
+ import { Readable } from "node:stream";
16
+
17
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
18
+
19
+ import {
20
+ ArtifactStorageNotFoundError,
21
+ LocalArtifactStorage,
22
+ } from "../../../artifactstorage/artifact-storage.js";
23
+ import type { ArtifactStorage } from "../../../artifactstorage/artifact-storage.js";
24
+ import { createLogger } from "../../../boot/logger.js";
25
+ import { SKILL_ARTIFACTS_PATH_PREFIX } from "../../../transport/constants.js";
26
+ import {
27
+ DEFAULT_SLOT_TTL_MS,
28
+ DOWNLOAD_URL_TTL_MS,
29
+ MAX_ZIP_SIZE,
30
+ STAGING_KEY_PREFIX,
31
+ UPLOADS_SEGMENT,
32
+ } from "../constants.js";
33
+ import { transferServeUrl, uploadUrl } from "../transfer/handler.js";
34
+ import { SlotUnknownError, UploadSlots } from "../transfer/slots.js";
35
+ import {
36
+ newArchiveStaging,
37
+ newUploadRef,
38
+ stagingKeyOf,
39
+ uploadRefOf,
40
+ } from "../transfer/staging.js";
41
+ import type { ArchiveStaging } from "../transfer/staging.js";
42
+
43
+ const logLines: string[] = [];
44
+ const logger = createLogger({
45
+ level: "error",
46
+ pretty: false,
47
+ write: (line) => {
48
+ logLines.push(line);
49
+ },
50
+ });
51
+
52
+ describe("the reference ⇄ staging key mapping", () => {
53
+ it("mints sau_-prefixed 128-bit hex references whose key sits under the staging prefix", () => {
54
+ const ref = newUploadRef();
55
+ expect(ref).toMatch(/^sau_[0-9a-f]{32}$/);
56
+ const key = stagingKeyOf(ref);
57
+ expect(key).toBe(`${STAGING_KEY_PREFIX}${ref.slice(4)}.zip`);
58
+ expect(uploadRefOf(key!)).toBe(ref);
59
+ });
60
+
61
+ it("answers no key for a reference this server could not have minted", () => {
62
+ for (const bad of [
63
+ "",
64
+ "sau_",
65
+ "sau_nope",
66
+ "sau_" + "g".repeat(32),
67
+ "sau_" + "0".repeat(31),
68
+ "skl_" + "0".repeat(32),
69
+ "sau_" + "A".repeat(32),
70
+ ]) {
71
+ expect(stagingKeyOf(bad), bad).toBeUndefined();
72
+ }
73
+ });
74
+
75
+ it("refuses to derive a reference from a foreign key", () => {
76
+ expect(() => uploadRefOf("skills/deadbeef.zip")).toThrow(
77
+ "is not a staging key this domain minted",
78
+ );
79
+ expect(() => uploadRefOf(`${STAGING_KEY_PREFIX}notes.txt`)).toThrow(
80
+ "is not a staging key this domain minted",
81
+ );
82
+ });
83
+ });
84
+
85
+ // ─── Over a recording fake: what a bucket driver is asked to do ──────────
86
+
87
+ interface Recorded {
88
+ presignPut: Array<{ key: string; declaredSizeBytes: number; ttlMs: number }>;
89
+ downloads: string[];
90
+ deletes: string[];
91
+ signed: Array<{ key: string; expiresInMs: number; downloadFilename: string }>;
92
+ }
93
+
94
+ function fakeDriver(
95
+ options: { failDelete?: boolean; failDownload?: Error } = {},
96
+ ): {
97
+ driver: ArtifactStorage;
98
+ blobs: Map<string, Uint8Array>;
99
+ recorded: Recorded;
100
+ } {
101
+ const blobs = new Map<string, Uint8Array>();
102
+ const recorded: Recorded = {
103
+ presignPut: [],
104
+ downloads: [],
105
+ deletes: [],
106
+ signed: [],
107
+ };
108
+ const driver: ArtifactStorage = {
109
+ upload: (key, data) => {
110
+ blobs.set(key, data);
111
+ return Promise.resolve();
112
+ },
113
+ download: (key) => {
114
+ recorded.downloads.push(key);
115
+ if (options.failDownload) return Promise.reject(options.failDownload);
116
+ const data = blobs.get(key);
117
+ return data === undefined
118
+ ? Promise.reject(new ArtifactStorageNotFoundError(key))
119
+ : Promise.resolve(data);
120
+ },
121
+ size: (key) => Promise.resolve(blobs.get(key)?.length ?? 0),
122
+ presignPut: (key, declaredSizeBytes, ttlMs) => {
123
+ recorded.presignPut.push({ key, declaredSizeBytes, ttlMs });
124
+ return Promise.resolve({
125
+ url: `https://bucket.test/${key}?sig=put`,
126
+ ttlMs: ttlMs / 2,
127
+ });
128
+ },
129
+ getSignedUrl: (key, expiresInMs, downloadFilename) => {
130
+ recorded.signed.push({ key, expiresInMs, downloadFilename });
131
+ return Promise.resolve(`https://bucket.test/${key}?sig=get`);
132
+ },
133
+ delete: (key) => {
134
+ recorded.deletes.push(key);
135
+ if (options.failDelete)
136
+ return Promise.reject(new Error("bucket refused the delete"));
137
+ blobs.delete(key);
138
+ return Promise.resolve();
139
+ },
140
+ exists: (key) => Promise.resolve(blobs.has(key)),
141
+ health: () => Promise.resolve(),
142
+ };
143
+ return { driver, blobs, recorded };
144
+ }
145
+
146
+ describe("newArchiveStaging over a bucket-shaped driver", () => {
147
+ beforeEach(() => {
148
+ logLines.length = 0;
149
+ });
150
+
151
+ it("mint hands the driver the domain's key and the slot TTL, and returns the driver's URL and granted TTL", async () => {
152
+ const { driver, recorded } = fakeDriver();
153
+ const staging = newArchiveStaging(driver, logger);
154
+ const minted = await staging.mint(1234);
155
+
156
+ expect(recorded.presignPut).toEqual([
157
+ {
158
+ key: stagingKeyOf(minted.ref),
159
+ declaredSizeBytes: 1234,
160
+ ttlMs: DEFAULT_SLOT_TTL_MS,
161
+ },
162
+ ]);
163
+ expect(minted.url).toBe(
164
+ `https://bucket.test/${stagingKeyOf(minted.ref)}?sig=put`,
165
+ );
166
+ expect(minted.ttlMs).toBe(DEFAULT_SLOT_TTL_MS / 2);
167
+ });
168
+
169
+ it("mint refuses a declaration outside the archive ceiling before any driver call", async () => {
170
+ const { driver, recorded } = fakeDriver();
171
+ const staging = newArchiveStaging(driver, logger);
172
+ await expect(staging.mint(0)).rejects.toThrow(
173
+ `declared size 0 outside (0, ${MAX_ZIP_SIZE}]`,
174
+ );
175
+ await expect(staging.mint(MAX_ZIP_SIZE + 1)).rejects.toThrow("outside");
176
+ expect(recorded.presignPut).toEqual([]);
177
+ });
178
+
179
+ it("consume reads the staged key, deletes it, and a replay is unknown", async () => {
180
+ const { driver, blobs, recorded } = fakeDriver();
181
+ const staging = newArchiveStaging(driver, logger);
182
+ const minted = await staging.mint(5);
183
+ const key = stagingKeyOf(minted.ref)!;
184
+ blobs.set(key, Buffer.from("hello"));
185
+
186
+ expect(Buffer.from(await staging.consume(minted.ref)).toString()).toBe(
187
+ "hello",
188
+ );
189
+ expect(recorded.downloads).toEqual([key]);
190
+ expect(recorded.deletes).toEqual([key]);
191
+ await expect(staging.consume(minted.ref)).rejects.toThrow(SlotUnknownError);
192
+ });
193
+
194
+ it("a malformed or never-uploaded reference is one condition: unknown or expired", async () => {
195
+ const { driver, recorded } = fakeDriver();
196
+ const staging = newArchiveStaging(driver, logger);
197
+ await expect(staging.consume("not-a-ref")).rejects.toThrow(
198
+ "upload reference unknown or expired",
199
+ );
200
+ // Nothing shaped like a key reaches the driver for a malformed ref.
201
+ expect(recorded.downloads).toEqual([]);
202
+
203
+ const minted = await staging.mint(5);
204
+ await expect(staging.consume(minted.ref)).rejects.toThrow(SlotUnknownError);
205
+ });
206
+
207
+ it("a failed delete is loud and does not fail the consume", async () => {
208
+ const { driver, blobs } = fakeDriver({ failDelete: true });
209
+ const staging = newArchiveStaging(driver, logger);
210
+ const minted = await staging.mint(5);
211
+ blobs.set(stagingKeyOf(minted.ref)!, Buffer.from("hello"));
212
+
213
+ expect(Buffer.from(await staging.consume(minted.ref)).toString()).toBe(
214
+ "hello",
215
+ );
216
+ expect(
217
+ logLines.some((line) =>
218
+ line.includes("failed to delete consumed staged artifact"),
219
+ ),
220
+ ).toBe(true);
221
+ });
222
+
223
+ it("a driver fault on download propagates as itself, never as an unusable reference", async () => {
224
+ const fault = new Error("bucket unreachable");
225
+ const { driver } = fakeDriver({ failDownload: fault });
226
+ const staging = newArchiveStaging(driver, logger);
227
+ const minted = await staging.mint(5);
228
+ await expect(staging.consume(minted.ref)).rejects.toBe(fault);
229
+ });
230
+
231
+ it("downloadUrl signs the stored key for the floor TTL and reports it", async () => {
232
+ const { driver, recorded } = fakeDriver();
233
+ const staging = newArchiveStaging(driver, logger);
234
+ const capability = await staging.downloadUrl("skills/abc.zip");
235
+ expect(recorded.signed).toEqual([
236
+ {
237
+ key: "skills/abc.zip",
238
+ expiresInMs: DOWNLOAD_URL_TTL_MS,
239
+ downloadFilename: "",
240
+ },
241
+ ]);
242
+ expect(capability).toEqual({
243
+ url: "https://bucket.test/skills/abc.zip?sig=get",
244
+ ttlMs: DOWNLOAD_URL_TTL_MS,
245
+ });
246
+ });
247
+ });
248
+
249
+ // ─── Over the real local driver: the slot registry behind the same port ──
250
+
251
+ describe("newArchiveStaging over LocalArtifactStorage and its slot registry", () => {
252
+ const BASE_URL = "http://localhost:8080";
253
+ let root: string;
254
+ let slots: UploadSlots;
255
+ let staging: ArchiveStaging;
256
+
257
+ beforeEach(() => {
258
+ root = mkdtempSync(path.join(tmpdir(), "archive-staging-local-"));
259
+ slots = new UploadSlots(root, STAGING_KEY_PREFIX, 60_000, 1024 * 1024);
260
+ const driver = new LocalArtifactStorage(root, transferServeUrl(BASE_URL), {
261
+ reserve: (key, declaredSizeBytes) => {
262
+ const { ttlMs } = slots.reserve(key, declaredSizeBytes);
263
+ return { url: uploadUrl(BASE_URL, uploadRefOf(key)), ttlMs };
264
+ },
265
+ });
266
+ staging = newArchiveStaging(driver, logger);
267
+ });
268
+
269
+ afterEach(() => {
270
+ rmSync(root, { recursive: true, force: true });
271
+ });
272
+
273
+ it("mint → lane receive → consume round-trips the bytes and retires the reference", async () => {
274
+ const bytes = Buffer.from("staged artifact bytes");
275
+ const minted = await staging.mint(bytes.length);
276
+
277
+ // The URL is the lane's wire shape and the ref is its last segment —
278
+ // exactly how the lane handler dispatches a PUT.
279
+ expect(minted.url).toBe(
280
+ `${BASE_URL}${SKILL_ARTIFACTS_PATH_PREFIX}${UPLOADS_SEGMENT}${minted.ref}`,
281
+ );
282
+ // The registry's slot TTL governs, not the domain's ask.
283
+ expect(minted.ttlMs).toBe(60_000);
284
+
285
+ await slots.receive(stagingKeyOf(minted.ref)!, Readable.from(bytes));
286
+ expect(Buffer.from(await staging.consume(minted.ref))).toEqual(bytes);
287
+ await expect(staging.consume(minted.ref)).rejects.toThrow(SlotUnknownError);
288
+ });
289
+
290
+ it("a reserved-but-never-uploaded reference is unknown, exactly as on a bucket", async () => {
291
+ const minted = await staging.mint(4);
292
+ await expect(staging.consume(minted.ref)).rejects.toThrow(
293
+ "upload reference unknown or expired",
294
+ );
295
+ });
296
+
297
+ it("downloadUrl renders the lane's own GET route with the floor TTL", async () => {
298
+ const capability = await staging.downloadUrl("skills/abc.zip");
299
+ expect(capability).toEqual({
300
+ url: `${BASE_URL}${SKILL_ARTIFACTS_PATH_PREFIX}/skills/abc.zip`,
301
+ ttlMs: DOWNLOAD_URL_TTL_MS,
302
+ });
303
+ });
304
+ });
@@ -27,10 +27,34 @@ export const MAX_SKILL_MD_SIZE = 1 * 1024 * 1024;
27
27
  /**
28
28
  * Upload-slot lifetime — generous enough for a 100MB upload on a slow
29
29
  * link, short enough that abandoned slots don't accumulate. Go:
30
- * transfer.DefaultSlotTTL (15 minutes).
30
+ * transfer.DefaultSlotTTL (15 minutes). The same lifetime is asked of a
31
+ * bucket driver's presigned PUT, so the reference and the URL it names
32
+ * expire together whichever driver serves them.
31
33
  */
32
34
  export const DEFAULT_SLOT_TTL_MS = 15 * 60 * 1000;
33
35
 
36
+ /**
37
+ * Download capability lifetime — the floor of a minted download URL's
38
+ * validity, reported as ttl_seconds. A bucket driver signs its GET for
39
+ * exactly this long, the lifetime the cloud edition already grants
40
+ * execution-artifact downloads; the local lane's URL is a content-hash
41
+ * capability that never expires, so the floor holds there trivially. An
42
+ * hour outlives any mount the runner performs while staying short enough
43
+ * that a leaked URL is not a standing grant.
44
+ */
45
+ export const DOWNLOAD_URL_TTL_MS = 60 * 60 * 1000;
46
+
47
+ /**
48
+ * Where staged uploads live in the blob store, under the skill store's own
49
+ * prefix: `skills/staging/<hex>.zip`. The retired cloud service staged
50
+ * under this exact prefix, so the bucket lifecycle rule that sweeps
51
+ * abandoned uploads is written for it, and on the local driver the boot
52
+ * wipe targets the same directory. Every staged object is a random
53
+ * 128-bit key, so its exposure through the download lane is the same
54
+ * capability the upload reference already grants.
55
+ */
56
+ export const STAGING_KEY_PREFIX = "skills/staging/";
57
+
34
58
  /**
35
59
  * Random capability-token size: 16 bytes = 128 bits of entropy, matching
36
60
  * the unguessability of the content-hash download keys. Go: refByteLen.
@@ -53,8 +77,9 @@ export const UPLOADS_SEGMENT = "/uploads/";
53
77
 
54
78
  /**
55
79
  * Downloads are restricted to the skill artifact store's own keys
56
- * ("skills/<hash>.zip"); everything else under the storage root — should
57
- * the two ever share one — stays unreachable from this lane.
80
+ * ("skills/<hash>.zip", and the staged uploads under STAGING_KEY_PREFIX);
81
+ * everything else under the storage root — should the two ever share one
82
+ * — stays unreachable from this lane.
58
83
  */
59
84
  export const DOWNLOAD_KEY_PREFIX = "skills/";
60
85