@synopackageland/cli 0.6.1 → 0.7.0

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 (578) hide show
  1. package/CHANGELOG.md +613 -0
  2. package/README.md +81 -89
  3. package/dist/add.js +39 -16
  4. package/dist/build-spk/delegate-scope.d.ts +6 -0
  5. package/dist/build-spk/delegate-scope.js +34 -0
  6. package/dist/build-spk/dsm-arch.d.ts +7 -1
  7. package/dist/build-spk/dsm-arch.js +24 -28
  8. package/dist/build-spk/dsm-platforms.json +39 -0
  9. package/dist/build-spk/dsm-version.d.ts +11 -0
  10. package/dist/build-spk/dsm-version.js +17 -0
  11. package/dist/build-spk/gateway.d.ts +1 -1
  12. package/dist/build-spk/gateway.js +2 -2
  13. package/dist/build-spk/generator.js +30 -5
  14. package/dist/build-spk/notices.js +4 -1
  15. package/dist/build-spk/resource-worker.js +3 -0
  16. package/dist/build-spk/wizard-messages.d.ts +66 -0
  17. package/dist/build-spk/wizard-messages.js +75 -9
  18. package/dist/cli.js +15 -7
  19. package/dist/contract/schema-validate.js +8 -0
  20. package/dist/contract/validate.js +30 -0
  21. package/dist/deploy/deploy.d.ts +1 -1
  22. package/dist/deploy/deploy.js +8 -0
  23. package/dist/deploy/plan.js +1 -1
  24. package/dist/deploy/spk-version.js +1 -1
  25. package/dist/error-registry.js +43 -15
  26. package/dist/host/dsm-client.d.ts +8 -4
  27. package/dist/host/dsm-client.js +10 -4
  28. package/dist/host/types.d.ts +2 -1
  29. package/dist/info.js +18 -2
  30. package/dist/init.d.ts +3 -0
  31. package/dist/init.js +49 -18
  32. package/dist/package-metadata.d.ts +21 -0
  33. package/dist/package-metadata.js +101 -0
  34. package/dist/portal/alias.d.ts +25 -0
  35. package/dist/portal/alias.js +48 -0
  36. package/dist/portal/package.d.ts +33 -0
  37. package/dist/portal/package.js +116 -0
  38. package/dist/runtime/delegate-contract.d.ts +79 -0
  39. package/dist/runtime/delegate-contract.js +27 -0
  40. package/dist/runtime/executor-build.d.ts +1 -1
  41. package/dist/runtime/executor-build.js +5 -4
  42. package/dist/runtime/executor-inspect.d.ts +3 -0
  43. package/dist/runtime/executor-inspect.js +11 -5
  44. package/dist/runtime/executor-policy.d.ts +12 -0
  45. package/dist/runtime/executor-policy.js +43 -3
  46. package/dist/runtime/executor-scope-task.d.ts +4 -0
  47. package/dist/runtime/executor-scope-task.js +21 -0
  48. package/dist/runtime/executor-tasks.d.ts +2 -1
  49. package/dist/runtime/executor-tasks.js +4 -4
  50. package/dist/skill.js +1 -0
  51. package/dist/standalone/api-permissions.d.ts +10 -0
  52. package/dist/standalone/api-permissions.js +19 -2
  53. package/dist/standalone/background.d.ts +16 -0
  54. package/dist/standalone/background.js +69 -2
  55. package/dist/standalone/binary.d.ts +1 -1
  56. package/dist/standalone/binary.js +22 -8
  57. package/dist/standalone/build.d.ts +1 -1
  58. package/dist/standalone/build.js +38 -8
  59. package/dist/standalone/daemon.d.ts +1 -1
  60. package/dist/standalone/daemon.js +2 -1
  61. package/dist/standalone/dsm-window.d.ts +3 -1
  62. package/dist/standalone/dsm-window.js +72 -15
  63. package/dist/standalone/inspect-spk.d.ts +8 -0
  64. package/dist/standalone/inspect-spk.js +105 -26
  65. package/dist/standalone/lifecycle.js +86 -8
  66. package/dist/standalone/manifest.d.ts +13 -0
  67. package/dist/standalone/manifest.js +57 -8
  68. package/dist/standalone/native-authorization-window.d.ts +1 -1
  69. package/dist/standalone/native-authorization-window.js +2 -0
  70. package/dist/standalone/native-layout.d.ts +3 -0
  71. package/dist/standalone/native-layout.js +37 -5
  72. package/dist/standalone/portal-window.d.ts +21 -0
  73. package/dist/standalone/portal-window.js +155 -0
  74. package/dist/standalone/portal.d.ts +4 -0
  75. package/dist/standalone/portal.js +4 -0
  76. package/dist/standalone/resource.d.ts +5 -3
  77. package/dist/standalone/resource.js +6 -3
  78. package/dist/standalone/root-window.js +62 -15
  79. package/dist/standalone/spk.d.ts +1 -1
  80. package/dist/standalone/spk.js +25 -7
  81. package/dist/templates/_daemon/main_test.go +61 -8
  82. package/dist/templates/_executor/README.md +127 -16
  83. package/dist/templates/_executor/anchor.go +146 -0
  84. package/dist/templates/_executor/anchor_test.go +126 -0
  85. package/dist/templates/_executor/apppriv_review_test.go +1 -1
  86. package/dist/templates/_executor/cancel_contract_test.go +236 -0
  87. package/dist/templates/_executor/copy.go +64 -0
  88. package/dist/templates/_executor/copy_test.go +88 -0
  89. package/dist/templates/_executor/dsm.go +23 -10
  90. package/dist/templates/_executor/dsm_test.go +95 -1
  91. package/dist/templates/_executor/executor.go +260 -49
  92. package/dist/templates/_executor/executor_test.go +36 -6
  93. package/dist/templates/_executor/identity.go +27 -1
  94. package/dist/templates/_executor/identity_kernel_linux_test.go +60 -0
  95. package/dist/templates/_executor/identity_linux.go +82 -0
  96. package/dist/templates/_executor/identity_security_test.go +43 -0
  97. package/dist/templates/_executor/internal/lifecycle/lifecycle.go +5 -0
  98. package/dist/templates/_executor/internal/lifecycle/lifecycle_test.go +61 -3
  99. package/dist/templates/_executor/internal/taskscript/boot.sh +9 -9
  100. package/dist/templates/_executor/internal/taskscript/cmd/main.go +2 -2
  101. package/dist/templates/_executor/internal/taskscript/control.sh +15 -10
  102. package/dist/templates/_executor/internal/taskscript/tasks.go +5 -3
  103. package/dist/templates/_executor/lifecycle_test.go +11 -0
  104. package/dist/templates/_executor/link_linux.go +28 -0
  105. package/dist/templates/_executor/long_running_test.go +530 -0
  106. package/dist/templates/_executor/main.go +72 -20
  107. package/dist/templates/_executor/move_linux.go +490 -0
  108. package/dist/templates/_executor/move_linux_test.go +460 -0
  109. package/dist/templates/_executor/move_validation.go +46 -0
  110. package/dist/templates/_executor/move_validation_test.go +187 -0
  111. package/dist/templates/_executor/operations.go +926 -0
  112. package/dist/templates/_executor/platform_linux.go +24 -173
  113. package/dist/templates/_executor/platform_linux_test.go +9 -65
  114. package/dist/templates/_executor/platform_other.go +30 -4
  115. package/dist/templates/_executor/process.go +3 -0
  116. package/dist/templates/_executor/records.go +242 -0
  117. package/dist/templates/_executor/scope_cancel_contract_test.go +91 -0
  118. package/dist/templates/_executor/scope_client.go +37 -0
  119. package/dist/templates/_executor/scope_contract.go +12 -0
  120. package/dist/templates/_executor/selected_scope.go +146 -0
  121. package/dist/templates/_executor/selected_scope_test.go +186 -0
  122. package/dist/templates/_executor/status_disabled_test.go +33 -0
  123. package/dist/templates/_executor/store.go +4 -3
  124. package/dist/templates/_executor/supervise.go +109 -0
  125. package/dist/templates/_executor/supervise_test.go +130 -0
  126. package/dist/templates/_executor/task_race_review_test.go +1 -1
  127. package/dist/templates/_executor/tasks.go +7 -8
  128. package/dist/templates/_executor/tasks_review_test.go +1 -1
  129. package/dist/templates/_executor/tasks_test.go +4 -4
  130. package/dist/templates/_executor/watch.go +8 -0
  131. package/dist/templates/_executor/watch_test.go +23 -2
  132. package/dist/templates/_executor/write_review_linux_test.go +4 -12
  133. package/dist/templates/_shared/AGENTS.md +129 -0
  134. package/dist/templates/binary/dsm-package.yaml +24 -0
  135. package/dist/templates/binary/i18n/messages/de.json +4 -0
  136. package/dist/templates/binary/i18n/messages/en.json +4 -0
  137. package/dist/templates/binary/i18n/messages/es.json +4 -0
  138. package/dist/templates/binary/i18n/messages/fr.json +4 -0
  139. package/dist/templates/binary/i18n/messages/it.json +4 -0
  140. package/dist/templates/binary/i18n/messages/ja.json +4 -0
  141. package/dist/templates/binary/i18n/messages/ko.json +4 -0
  142. package/dist/templates/binary/i18n/messages/zh-CN.json +4 -0
  143. package/dist/templates/binary/i18n/messages/zh-TW.json +4 -0
  144. package/dist/templates/binary/ui/app.js +19 -4
  145. package/dist/templates/native-authorization/AuthorizationGate.vue +5 -3
  146. package/dist/templates/native-authorization/i18n/de.json +1 -0
  147. package/dist/templates/native-authorization/i18n/en.json +1 -0
  148. package/dist/templates/native-authorization/i18n/es.json +2 -1
  149. package/dist/templates/native-authorization/i18n/fr.json +1 -0
  150. package/dist/templates/native-authorization/i18n/it.json +11 -10
  151. package/dist/templates/native-authorization/i18n/ja.json +1 -0
  152. package/dist/templates/native-authorization/i18n/ko.json +10 -9
  153. package/dist/templates/native-authorization/i18n/zh-CN.json +6 -5
  154. package/dist/templates/native-authorization/i18n/zh-TW.json +6 -5
  155. package/dist/templates/native-go/README.md +43 -10
  156. package/dist/templates/native-go/backend/cmd/app/main.go +27 -7
  157. package/dist/templates/native-go/backend/internal/api/entry_test.go +47 -0
  158. package/dist/templates/native-go/backend/internal/api/permissions_test.go +3 -3
  159. package/dist/templates/native-go/backend/internal/api/registry.go +11 -0
  160. package/dist/templates/native-go/backend/internal/api/sdk-permissions.json +14 -1
  161. package/dist/templates/native-go/backend/internal/api/server.go +18 -5
  162. package/dist/templates/native-go/backend/internal/app/delegate_config.go +57 -0
  163. package/dist/templates/native-go/backend/internal/app/delegate_config_test.go +28 -0
  164. package/dist/templates/native-go/backend/internal/app/portal.go +30 -0
  165. package/dist/templates/native-go/backend/internal/app/portal_test.go +55 -0
  166. package/dist/templates/native-go/backend/internal/app/routes.go +8 -2
  167. package/dist/templates/native-go/backend/internal/background/runner.go +1 -1
  168. package/dist/templates/native-go/backend/internal/background/store.go +16 -14
  169. package/dist/templates/native-go/backend/internal/dsm/admin.go +8 -2
  170. package/dist/templates/native-go/backend/internal/dsm/admin_linux.go +43 -13
  171. package/dist/templates/native-go/backend/internal/dsm/admin_linux_test.go +56 -0
  172. package/dist/templates/native-go/backend/internal/dsm/admin_other.go +4 -1
  173. package/dist/templates/native-go/backend/internal/dsm/apppriv.go +30 -0
  174. package/dist/templates/native-go/backend/internal/dsm/bridge_file.go +75 -0
  175. package/dist/templates/native-go/backend/internal/dsm/bridge_file_test.go +57 -0
  176. package/dist/templates/native-go/backend/internal/dsm/entry.go +177 -0
  177. package/dist/templates/native-go/backend/internal/dsm/entry_test.go +315 -0
  178. package/dist/templates/native-go/backend/internal/dsm/native/admin-armv7.elf +0 -0
  179. package/dist/templates/native-go/backend/internal/dsm/native/apppriv-aarch64.elf +0 -0
  180. package/dist/templates/native-go/backend/internal/dsm/native/apppriv-armv7.elf +0 -0
  181. package/dist/templates/native-go/backend/internal/dsm/native/apppriv-x86_64.elf +0 -0
  182. package/dist/templates/native-go/backend/internal/dsm/native/apppriv_check.c +33 -0
  183. package/dist/templates/native-go/backend/internal/dsm/native/build.sh +13 -4
  184. package/dist/templates/native-go/backend/internal/dsm/native/provenance.json +9 -3
  185. package/dist/templates/native-go/backend/internal/filestation/check_permission.go +71 -0
  186. package/dist/templates/native-go/backend/internal/filestation/check_permission_test.go +77 -0
  187. package/dist/templates/native-go/backend/internal/filestation/sdk_policy_test.go +39 -0
  188. package/dist/templates/native-go/backend/internal/filestation/service.go +1 -1
  189. package/dist/templates/native-go/backend/internal/portal/portal.go +138 -0
  190. package/dist/templates/native-go/backend/internal/portal/portal_test.go +89 -0
  191. package/dist/templates/native-go/dsm-package.yaml +20 -1
  192. package/dist/templates/native-go/i18n/glossary.json +2 -1
  193. package/dist/templates/native-go/i18n/messages/de.json +1 -0
  194. package/dist/templates/native-go/i18n/messages/en.json +1 -0
  195. package/dist/templates/native-go/i18n/messages/es.json +1 -0
  196. package/dist/templates/native-go/i18n/messages/fr.json +1 -0
  197. package/dist/templates/native-go/i18n/messages/it.json +1 -0
  198. package/dist/templates/native-go/i18n/messages/ja.json +1 -0
  199. package/dist/templates/native-go/i18n/messages/ko.json +1 -0
  200. package/dist/templates/native-go/i18n/messages/zh-CN.json +1 -0
  201. package/dist/templates/native-go/i18n/messages/zh-TW.json +1 -0
  202. package/dist/templates/native-go/ui/app.css +10 -0
  203. package/dist/templates/native-go/ui/app.js +11 -1
  204. package/dist/templates/native-go-delegate/DELEGATE.md +126 -5
  205. package/dist/templates/native-go-delegate/backend/cmd/background-worker/main.go +7 -0
  206. package/dist/templates/native-go-delegate/backend/internal/app/delegate.go +11 -1
  207. package/dist/templates/native-go-delegate/backend/internal/app/delegate_routes_test.go +14 -0
  208. package/dist/templates/native-go-delegate/backend/internal/background/delegated_moves.go +270 -0
  209. package/dist/templates/native-go-delegate/backend/internal/delegate/admission_recovery_test.go +410 -0
  210. package/dist/templates/native-go-delegate/backend/internal/delegate/cancel_contract_test.go +95 -0
  211. package/dist/templates/native-go-delegate/backend/internal/delegate/client.go +8 -0
  212. package/dist/templates/native-go-delegate/backend/internal/delegate/destination.go +110 -0
  213. package/dist/templates/native-go-delegate/backend/internal/delegate/destination_state.go +117 -0
  214. package/dist/templates/native-go-delegate/backend/internal/delegate/destination_state_test.go +60 -0
  215. package/dist/templates/native-go-delegate/backend/internal/delegate/destination_test.go +116 -0
  216. package/dist/templates/native-go-delegate/backend/internal/delegate/example.go +81 -0
  217. package/dist/templates/native-go-delegate/backend/internal/delegate/example_test.go +91 -0
  218. package/dist/templates/native-go-delegate/backend/internal/delegate/executor_contract_test.go +511 -0
  219. package/dist/templates/native-go-delegate/backend/internal/delegate/files.go +1 -1
  220. package/dist/templates/native-go-delegate/backend/internal/delegate/long_running_routes.go +92 -0
  221. package/dist/templates/native-go-delegate/backend/internal/delegate/long_running_routes_test.go +99 -0
  222. package/dist/templates/native-go-delegate/backend/internal/delegate/move.go +348 -0
  223. package/dist/templates/native-go-delegate/backend/internal/delegate/move_test.go +351 -0
  224. package/dist/templates/native-go-delegate/backend/internal/delegate/operations_list.go +65 -0
  225. package/dist/templates/native-go-delegate/backend/internal/delegate/operations_list_test.go +114 -0
  226. package/dist/templates/native-go-delegate/backend/internal/delegate/retry_test.go +1 -1
  227. package/dist/templates/native-go-delegate/backend/internal/delegate/service.go +58 -17
  228. package/dist/templates/native-go-delegate/backend/internal/delegate/staging.go +121 -0
  229. package/dist/templates/native-go-delegate/backend/internal/delegate/status_disabled_test.go +51 -0
  230. package/dist/templates/native-go-delegate/backend/internal/delegate/testdata/executor_contract.txt +112 -0
  231. package/dist/templates/native-go-delegate/backend/internal/delegate/transient_contract_test.go +137 -0
  232. package/dist/templates/native-go-delegate/backend/internal/filestation/files.go +1 -31
  233. package/dist/templates/native-go-delegate/frontend/src/CapabilityPanels.vue +30 -3
  234. package/dist/templates/native-go-delegate/frontend/src/delegate/BackgroundSavePanel.vue +113 -0
  235. package/dist/templates/native-go-delegate/frontend/src/delegate/copy.ts +5 -6
  236. package/dist/templates/native-go-delegate/frontend/test/background-save.test.ts +121 -0
  237. package/dist/templates/native-go-delegate/frontend/test/scope-settings.test.ts +49 -0
  238. package/dist/templates/native-go-delegate/test.sh +1 -0
  239. package/dist/templates/native-go-jobs/frontend/src/background/i18n/de.json +2 -2
  240. package/dist/templates/native-go-jobs/frontend/src/background/i18n/en.json +2 -2
  241. package/dist/templates/native-go-jobs/frontend/src/background/i18n/es.json +12 -12
  242. package/dist/templates/native-go-jobs/frontend/src/background/i18n/fr.json +4 -4
  243. package/dist/templates/native-go-jobs/frontend/src/background/i18n/it.json +17 -17
  244. package/dist/templates/native-go-jobs/frontend/src/background/i18n/ja.json +7 -7
  245. package/dist/templates/native-go-jobs/frontend/src/background/i18n/ko.json +15 -15
  246. package/dist/templates/native-go-jobs/frontend/src/background/i18n/zh-CN.json +3 -3
  247. package/dist/templates/native-go-jobs/frontend/src/background/i18n/zh-TW.json +2 -2
  248. package/dist/templates/native-go-jobs/frontend/test/background.test.ts +25 -28
  249. package/dist/templates/native-go-vue/README.md +4 -0
  250. package/dist/templates/native-go-vue/frontend/package-lock.json +4 -4
  251. package/dist/templates/native-go-vue/frontend/package.json +1 -1
  252. package/dist/templates/native-go-vue/frontend/src/App.vue +18 -13
  253. package/dist/templates/native-go-vue/frontend/src/context.ts +31 -0
  254. package/dist/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +52 -8
  255. package/dist/templates/native-go-vue/frontend/src/delegate/catalogs.ts +11 -0
  256. package/dist/templates/native-go-vue/frontend/src/delegate/copy.test.ts +12 -0
  257. package/dist/templates/native-go-vue/frontend/src/delegate/copy.ts +4 -4
  258. package/dist/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +4 -2
  259. package/dist/templates/native-go-vue/frontend/src/delegate/facade.test.ts +100 -0
  260. package/dist/templates/native-go-vue/frontend/src/delegate/facade.ts +85 -0
  261. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/de.json +46 -0
  262. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/en.json +12 -2
  263. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/es.json +46 -0
  264. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +46 -0
  265. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/it.json +46 -0
  266. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +46 -0
  267. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +46 -0
  268. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/save/de.json +42 -0
  269. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/save/en.json +42 -0
  270. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/save/es.json +42 -0
  271. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/save/fr.json +42 -0
  272. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/save/it.json +42 -0
  273. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/save/ja.json +42 -0
  274. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/save/ko.json +42 -0
  275. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/save/zh-CN.json +42 -0
  276. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/save/zh-TW.json +42 -0
  277. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +46 -0
  278. package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +15 -5
  279. package/dist/templates/native-go-vue/frontend/src/delegate/long-running.ts +57 -0
  280. package/dist/templates/native-go-vue/frontend/src/delegate/save-copy.test.ts +12 -0
  281. package/dist/templates/native-go-vue/frontend/src/delegate/save-copy.ts +24 -0
  282. package/dist/templates/native-go-vue/frontend/src/delegate/scoped-picker.test.ts +12 -0
  283. package/dist/templates/native-go-vue/frontend/src/delegate/scoped-picker.ts +21 -0
  284. package/dist/templates/native-go-vue/frontend/src/files/files.test.ts +7 -0
  285. package/dist/templates/native-go-vue/frontend/src/i18n/de.json +20 -0
  286. package/dist/templates/native-go-vue/frontend/src/i18n/en.json +20 -0
  287. package/dist/templates/native-go-vue/frontend/src/i18n/es.json +20 -0
  288. package/dist/templates/native-go-vue/frontend/src/i18n/fr.json +20 -0
  289. package/dist/templates/native-go-vue/frontend/src/i18n/it.json +20 -0
  290. package/dist/templates/native-go-vue/frontend/src/i18n/ja.json +20 -0
  291. package/dist/templates/native-go-vue/frontend/src/i18n/ko.json +20 -0
  292. package/dist/templates/native-go-vue/frontend/src/i18n/zh-CN.json +20 -0
  293. package/dist/templates/native-go-vue/frontend/src/i18n/zh-TW.json +20 -0
  294. package/dist/templates/native-go-vue/frontend/src/i18n.ts +13 -0
  295. package/dist/templates/native-go-vue/frontend/src/identity/LoginPage.vue +16 -14
  296. package/dist/templates/native-go-vue/frontend/src/identity/i18n/de.json +27 -0
  297. package/dist/templates/native-go-vue/frontend/src/identity/i18n/en.json +27 -0
  298. package/dist/templates/native-go-vue/frontend/src/identity/i18n/es.json +27 -0
  299. package/dist/templates/native-go-vue/frontend/src/identity/i18n/fr.json +27 -0
  300. package/dist/templates/native-go-vue/frontend/src/identity/i18n/it.json +27 -0
  301. package/dist/templates/native-go-vue/frontend/src/identity/i18n/ja.json +27 -0
  302. package/dist/templates/native-go-vue/frontend/src/identity/i18n/ko.json +27 -0
  303. package/dist/templates/native-go-vue/frontend/src/identity/i18n/zh-CN.json +27 -0
  304. package/dist/templates/native-go-vue/frontend/src/identity/i18n/zh-TW.json +27 -0
  305. package/dist/templates/native-go-vue/frontend/src/identity/i18n.ts +14 -0
  306. package/dist/templates/native-go-vue/frontend/src/identity/session.test.ts +4 -0
  307. package/dist/templates/native-go-vue/frontend/src/identity/session.ts +9 -8
  308. package/dist/templates/native-go-vue/frontend/src/nas/NasPicker.vue +31 -12
  309. package/dist/templates/native-go-vue/frontend/src/nas/destination.test.ts +48 -0
  310. package/dist/templates/native-go-vue/frontend/src/nas/i18n/files/de.json +1 -1
  311. package/dist/templates/native-go-vue/frontend/src/nas/i18n/files/en.json +1 -1
  312. package/dist/templates/native-go-vue/frontend/src/nas/i18n/files/es.json +10 -10
  313. package/dist/templates/native-go-vue/frontend/src/nas/i18n/files/fr.json +1 -1
  314. package/dist/templates/native-go-vue/frontend/src/nas/i18n/files/it.json +12 -12
  315. package/dist/templates/native-go-vue/frontend/src/nas/i18n/files/ja.json +15 -15
  316. package/dist/templates/native-go-vue/frontend/src/nas/i18n/files/ko.json +12 -12
  317. package/dist/templates/native-go-vue/frontend/src/nas/i18n/files/zh-CN.json +1 -1
  318. package/dist/templates/native-go-vue/frontend/src/nas/i18n/files/zh-TW.json +1 -1
  319. package/dist/templates/native-go-vue/frontend/src/portal/PortalSettings.vue +39 -0
  320. package/dist/templates/native-go-vue/frontend/src/shell/i18n/fr.json +1 -1
  321. package/dist/templates/native-go-vue/frontend/src/shell/i18n/ja.json +1 -1
  322. package/dist/templates/native-go-vue/frontend/test/apps.test.ts +14 -0
  323. package/dist/templates/native-go-vue/frontend/test/portal-settings.test.ts +71 -0
  324. package/dist/templates/native-go-vue/frontend/test/shell-i18n.test.ts +50 -0
  325. package/package.json +5 -1
  326. package/schema/package-metadata.schema.json +101 -0
  327. package/schema/synology-app.schema.json +112 -0
  328. package/skills/SKILL.md +106 -4
  329. package/skills/reference/create.md +101 -2
  330. package/skills/reference/deploy.md +2 -2
  331. package/skills/reference/files.md +113 -2
  332. package/skills/reference/identity.md +70 -1
  333. package/skills/reference/portal.md +17 -0
  334. package/skills/reference/ui-vue.md +12 -0
  335. package/templates/_daemon/main_test.go +61 -8
  336. package/templates/_executor/README.md +127 -16
  337. package/templates/_executor/anchor.go +146 -0
  338. package/templates/_executor/anchor_test.go +126 -0
  339. package/templates/_executor/apppriv_review_test.go +1 -1
  340. package/templates/_executor/cancel_contract_test.go +236 -0
  341. package/templates/_executor/copy.go +64 -0
  342. package/templates/_executor/copy_test.go +88 -0
  343. package/templates/_executor/dsm.go +23 -10
  344. package/templates/_executor/dsm_test.go +95 -1
  345. package/templates/_executor/executor.go +260 -49
  346. package/templates/_executor/executor_test.go +36 -6
  347. package/templates/_executor/identity.go +27 -1
  348. package/templates/_executor/identity_kernel_linux_test.go +60 -0
  349. package/templates/_executor/identity_linux.go +82 -0
  350. package/templates/_executor/identity_security_test.go +43 -0
  351. package/templates/_executor/internal/lifecycle/lifecycle.go +5 -0
  352. package/templates/_executor/internal/lifecycle/lifecycle_test.go +61 -3
  353. package/templates/_executor/internal/taskscript/boot.sh +9 -9
  354. package/templates/_executor/internal/taskscript/cmd/main.go +2 -2
  355. package/templates/_executor/internal/taskscript/control.sh +15 -10
  356. package/templates/_executor/internal/taskscript/tasks.go +5 -3
  357. package/templates/_executor/lifecycle_test.go +11 -0
  358. package/templates/_executor/link_linux.go +28 -0
  359. package/templates/_executor/long_running_test.go +530 -0
  360. package/templates/_executor/main.go +72 -20
  361. package/templates/_executor/move_linux.go +490 -0
  362. package/templates/_executor/move_linux_test.go +460 -0
  363. package/templates/_executor/move_validation.go +46 -0
  364. package/templates/_executor/move_validation_test.go +187 -0
  365. package/templates/_executor/operations.go +926 -0
  366. package/templates/_executor/platform_linux.go +24 -173
  367. package/templates/_executor/platform_linux_test.go +9 -65
  368. package/templates/_executor/platform_other.go +30 -4
  369. package/templates/_executor/process.go +3 -0
  370. package/templates/_executor/records.go +242 -0
  371. package/templates/_executor/scope_cancel_contract_test.go +91 -0
  372. package/templates/_executor/scope_client.go +37 -0
  373. package/templates/_executor/scope_contract.go +12 -0
  374. package/templates/_executor/selected_scope.go +146 -0
  375. package/templates/_executor/selected_scope_test.go +186 -0
  376. package/templates/_executor/status_disabled_test.go +33 -0
  377. package/templates/_executor/store.go +4 -3
  378. package/templates/_executor/supervise.go +109 -0
  379. package/templates/_executor/supervise_test.go +130 -0
  380. package/templates/_executor/task_race_review_test.go +1 -1
  381. package/templates/_executor/tasks.go +7 -8
  382. package/templates/_executor/tasks_review_test.go +1 -1
  383. package/templates/_executor/tasks_test.go +4 -4
  384. package/templates/_executor/watch.go +8 -0
  385. package/templates/_executor/watch_test.go +23 -2
  386. package/templates/_executor/write_review_linux_test.go +4 -12
  387. package/templates/_shared/AGENTS.md +129 -0
  388. package/templates/binary/dsm-package.yaml +24 -0
  389. package/templates/binary/i18n/messages/de.json +4 -0
  390. package/templates/binary/i18n/messages/en.json +4 -0
  391. package/templates/binary/i18n/messages/es.json +4 -0
  392. package/templates/binary/i18n/messages/fr.json +4 -0
  393. package/templates/binary/i18n/messages/it.json +4 -0
  394. package/templates/binary/i18n/messages/ja.json +4 -0
  395. package/templates/binary/i18n/messages/ko.json +4 -0
  396. package/templates/binary/i18n/messages/zh-CN.json +4 -0
  397. package/templates/binary/i18n/messages/zh-TW.json +4 -0
  398. package/templates/binary/ui/app.js +19 -4
  399. package/templates/native-authorization/AuthorizationGate.vue +5 -3
  400. package/templates/native-authorization/i18n/de.json +1 -0
  401. package/templates/native-authorization/i18n/en.json +1 -0
  402. package/templates/native-authorization/i18n/es.json +2 -1
  403. package/templates/native-authorization/i18n/fr.json +1 -0
  404. package/templates/native-authorization/i18n/it.json +11 -10
  405. package/templates/native-authorization/i18n/ja.json +1 -0
  406. package/templates/native-authorization/i18n/ko.json +10 -9
  407. package/templates/native-authorization/i18n/zh-CN.json +6 -5
  408. package/templates/native-authorization/i18n/zh-TW.json +6 -5
  409. package/templates/native-go/README.md +43 -10
  410. package/templates/native-go/backend/cmd/app/main.go +27 -7
  411. package/templates/native-go/backend/internal/api/entry_test.go +47 -0
  412. package/templates/native-go/backend/internal/api/permissions_test.go +3 -3
  413. package/templates/native-go/backend/internal/api/registry.go +11 -0
  414. package/templates/native-go/backend/internal/api/sdk-permissions.json +14 -1
  415. package/templates/native-go/backend/internal/api/server.go +18 -5
  416. package/templates/native-go/backend/internal/app/delegate_config.go +57 -0
  417. package/templates/native-go/backend/internal/app/delegate_config_test.go +28 -0
  418. package/templates/native-go/backend/internal/app/portal.go +30 -0
  419. package/templates/native-go/backend/internal/app/portal_test.go +55 -0
  420. package/templates/native-go/backend/internal/app/routes.go +8 -2
  421. package/templates/native-go/backend/internal/background/runner.go +1 -1
  422. package/templates/native-go/backend/internal/background/store.go +16 -14
  423. package/templates/native-go/backend/internal/dsm/admin.go +8 -2
  424. package/templates/native-go/backend/internal/dsm/admin_linux.go +43 -13
  425. package/templates/native-go/backend/internal/dsm/admin_linux_test.go +56 -0
  426. package/templates/native-go/backend/internal/dsm/admin_other.go +4 -1
  427. package/templates/native-go/backend/internal/dsm/apppriv.go +30 -0
  428. package/templates/native-go/backend/internal/dsm/bridge_file.go +75 -0
  429. package/templates/native-go/backend/internal/dsm/bridge_file_test.go +57 -0
  430. package/templates/native-go/backend/internal/dsm/entry.go +177 -0
  431. package/templates/native-go/backend/internal/dsm/entry_test.go +315 -0
  432. package/templates/native-go/backend/internal/dsm/native/admin-armv7.elf +0 -0
  433. package/templates/native-go/backend/internal/dsm/native/apppriv-aarch64.elf +0 -0
  434. package/templates/native-go/backend/internal/dsm/native/apppriv-armv7.elf +0 -0
  435. package/templates/native-go/backend/internal/dsm/native/apppriv-x86_64.elf +0 -0
  436. package/templates/native-go/backend/internal/dsm/native/apppriv_check.c +33 -0
  437. package/templates/native-go/backend/internal/dsm/native/build.sh +13 -4
  438. package/templates/native-go/backend/internal/dsm/native/provenance.json +9 -3
  439. package/templates/native-go/backend/internal/filestation/check_permission.go +71 -0
  440. package/templates/native-go/backend/internal/filestation/check_permission_test.go +77 -0
  441. package/templates/native-go/backend/internal/filestation/sdk_policy_test.go +39 -0
  442. package/templates/native-go/backend/internal/filestation/service.go +1 -1
  443. package/templates/native-go/backend/internal/portal/portal.go +138 -0
  444. package/templates/native-go/backend/internal/portal/portal_test.go +89 -0
  445. package/templates/native-go/dsm-package.yaml +20 -1
  446. package/templates/native-go/i18n/glossary.json +2 -1
  447. package/templates/native-go/i18n/messages/de.json +1 -0
  448. package/templates/native-go/i18n/messages/en.json +1 -0
  449. package/templates/native-go/i18n/messages/es.json +1 -0
  450. package/templates/native-go/i18n/messages/fr.json +1 -0
  451. package/templates/native-go/i18n/messages/it.json +1 -0
  452. package/templates/native-go/i18n/messages/ja.json +1 -0
  453. package/templates/native-go/i18n/messages/ko.json +1 -0
  454. package/templates/native-go/i18n/messages/zh-CN.json +1 -0
  455. package/templates/native-go/i18n/messages/zh-TW.json +1 -0
  456. package/templates/native-go/ui/app.css +10 -0
  457. package/templates/native-go/ui/app.js +11 -1
  458. package/templates/native-go-delegate/DELEGATE.md +126 -5
  459. package/templates/native-go-delegate/backend/cmd/background-worker/main.go +7 -0
  460. package/templates/native-go-delegate/backend/internal/app/delegate.go +11 -1
  461. package/templates/native-go-delegate/backend/internal/app/delegate_routes_test.go +14 -0
  462. package/templates/native-go-delegate/backend/internal/background/delegated_moves.go +270 -0
  463. package/templates/native-go-delegate/backend/internal/delegate/admission_recovery_test.go +410 -0
  464. package/templates/native-go-delegate/backend/internal/delegate/cancel_contract_test.go +95 -0
  465. package/templates/native-go-delegate/backend/internal/delegate/client.go +8 -0
  466. package/templates/native-go-delegate/backend/internal/delegate/destination.go +110 -0
  467. package/templates/native-go-delegate/backend/internal/delegate/destination_state.go +117 -0
  468. package/templates/native-go-delegate/backend/internal/delegate/destination_state_test.go +60 -0
  469. package/templates/native-go-delegate/backend/internal/delegate/destination_test.go +116 -0
  470. package/templates/native-go-delegate/backend/internal/delegate/example.go +81 -0
  471. package/templates/native-go-delegate/backend/internal/delegate/example_test.go +91 -0
  472. package/templates/native-go-delegate/backend/internal/delegate/executor_contract_test.go +511 -0
  473. package/templates/native-go-delegate/backend/internal/delegate/files.go +1 -1
  474. package/templates/native-go-delegate/backend/internal/delegate/long_running_routes.go +92 -0
  475. package/templates/native-go-delegate/backend/internal/delegate/long_running_routes_test.go +99 -0
  476. package/templates/native-go-delegate/backend/internal/delegate/move.go +348 -0
  477. package/templates/native-go-delegate/backend/internal/delegate/move_test.go +351 -0
  478. package/templates/native-go-delegate/backend/internal/delegate/operations_list.go +65 -0
  479. package/templates/native-go-delegate/backend/internal/delegate/operations_list_test.go +114 -0
  480. package/templates/native-go-delegate/backend/internal/delegate/retry_test.go +1 -1
  481. package/templates/native-go-delegate/backend/internal/delegate/service.go +58 -17
  482. package/templates/native-go-delegate/backend/internal/delegate/staging.go +121 -0
  483. package/templates/native-go-delegate/backend/internal/delegate/status_disabled_test.go +51 -0
  484. package/templates/native-go-delegate/backend/internal/delegate/testdata/executor_contract.txt +112 -0
  485. package/templates/native-go-delegate/backend/internal/delegate/transient_contract_test.go +137 -0
  486. package/templates/native-go-delegate/backend/internal/filestation/files.go +1 -31
  487. package/templates/native-go-delegate/frontend/src/CapabilityPanels.vue +30 -3
  488. package/templates/native-go-delegate/frontend/src/delegate/BackgroundSavePanel.vue +113 -0
  489. package/templates/native-go-delegate/frontend/src/delegate/copy.ts +5 -6
  490. package/templates/native-go-delegate/frontend/test/background-save.test.ts +121 -0
  491. package/templates/native-go-delegate/frontend/test/scope-settings.test.ts +49 -0
  492. package/templates/native-go-delegate/test.sh +1 -0
  493. package/templates/native-go-jobs/frontend/src/background/i18n/de.json +2 -2
  494. package/templates/native-go-jobs/frontend/src/background/i18n/en.json +2 -2
  495. package/templates/native-go-jobs/frontend/src/background/i18n/es.json +12 -12
  496. package/templates/native-go-jobs/frontend/src/background/i18n/fr.json +4 -4
  497. package/templates/native-go-jobs/frontend/src/background/i18n/it.json +17 -17
  498. package/templates/native-go-jobs/frontend/src/background/i18n/ja.json +7 -7
  499. package/templates/native-go-jobs/frontend/src/background/i18n/ko.json +15 -15
  500. package/templates/native-go-jobs/frontend/src/background/i18n/zh-CN.json +3 -3
  501. package/templates/native-go-jobs/frontend/src/background/i18n/zh-TW.json +2 -2
  502. package/templates/native-go-jobs/frontend/test/background.test.ts +25 -28
  503. package/templates/native-go-vue/README.md +4 -0
  504. package/templates/native-go-vue/frontend/package-lock.json +4 -4
  505. package/templates/native-go-vue/frontend/package.json +1 -1
  506. package/templates/native-go-vue/frontend/src/App.vue +18 -13
  507. package/templates/native-go-vue/frontend/src/context.ts +31 -0
  508. package/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +52 -8
  509. package/templates/native-go-vue/frontend/src/delegate/catalogs.ts +11 -0
  510. package/templates/native-go-vue/frontend/src/delegate/copy.test.ts +12 -0
  511. package/templates/native-go-vue/frontend/src/delegate/copy.ts +4 -4
  512. package/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +4 -2
  513. package/templates/native-go-vue/frontend/src/delegate/facade.test.ts +100 -0
  514. package/templates/native-go-vue/frontend/src/delegate/facade.ts +85 -0
  515. package/templates/native-go-vue/frontend/src/delegate/i18n/de.json +46 -0
  516. package/templates/native-go-vue/frontend/src/delegate/i18n/en.json +12 -2
  517. package/templates/native-go-vue/frontend/src/delegate/i18n/es.json +46 -0
  518. package/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +46 -0
  519. package/templates/native-go-vue/frontend/src/delegate/i18n/it.json +46 -0
  520. package/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +46 -0
  521. package/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +46 -0
  522. package/templates/native-go-vue/frontend/src/delegate/i18n/save/de.json +42 -0
  523. package/templates/native-go-vue/frontend/src/delegate/i18n/save/en.json +42 -0
  524. package/templates/native-go-vue/frontend/src/delegate/i18n/save/es.json +42 -0
  525. package/templates/native-go-vue/frontend/src/delegate/i18n/save/fr.json +42 -0
  526. package/templates/native-go-vue/frontend/src/delegate/i18n/save/it.json +42 -0
  527. package/templates/native-go-vue/frontend/src/delegate/i18n/save/ja.json +42 -0
  528. package/templates/native-go-vue/frontend/src/delegate/i18n/save/ko.json +42 -0
  529. package/templates/native-go-vue/frontend/src/delegate/i18n/save/zh-CN.json +42 -0
  530. package/templates/native-go-vue/frontend/src/delegate/i18n/save/zh-TW.json +42 -0
  531. package/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +46 -0
  532. package/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +15 -5
  533. package/templates/native-go-vue/frontend/src/delegate/long-running.ts +57 -0
  534. package/templates/native-go-vue/frontend/src/delegate/save-copy.test.ts +12 -0
  535. package/templates/native-go-vue/frontend/src/delegate/save-copy.ts +24 -0
  536. package/templates/native-go-vue/frontend/src/delegate/scoped-picker.test.ts +12 -0
  537. package/templates/native-go-vue/frontend/src/delegate/scoped-picker.ts +21 -0
  538. package/templates/native-go-vue/frontend/src/files/files.test.ts +7 -0
  539. package/templates/native-go-vue/frontend/src/i18n/de.json +20 -0
  540. package/templates/native-go-vue/frontend/src/i18n/en.json +20 -0
  541. package/templates/native-go-vue/frontend/src/i18n/es.json +20 -0
  542. package/templates/native-go-vue/frontend/src/i18n/fr.json +20 -0
  543. package/templates/native-go-vue/frontend/src/i18n/it.json +20 -0
  544. package/templates/native-go-vue/frontend/src/i18n/ja.json +20 -0
  545. package/templates/native-go-vue/frontend/src/i18n/ko.json +20 -0
  546. package/templates/native-go-vue/frontend/src/i18n/zh-CN.json +20 -0
  547. package/templates/native-go-vue/frontend/src/i18n/zh-TW.json +20 -0
  548. package/templates/native-go-vue/frontend/src/i18n.ts +13 -0
  549. package/templates/native-go-vue/frontend/src/identity/LoginPage.vue +16 -14
  550. package/templates/native-go-vue/frontend/src/identity/i18n/de.json +27 -0
  551. package/templates/native-go-vue/frontend/src/identity/i18n/en.json +27 -0
  552. package/templates/native-go-vue/frontend/src/identity/i18n/es.json +27 -0
  553. package/templates/native-go-vue/frontend/src/identity/i18n/fr.json +27 -0
  554. package/templates/native-go-vue/frontend/src/identity/i18n/it.json +27 -0
  555. package/templates/native-go-vue/frontend/src/identity/i18n/ja.json +27 -0
  556. package/templates/native-go-vue/frontend/src/identity/i18n/ko.json +27 -0
  557. package/templates/native-go-vue/frontend/src/identity/i18n/zh-CN.json +27 -0
  558. package/templates/native-go-vue/frontend/src/identity/i18n/zh-TW.json +27 -0
  559. package/templates/native-go-vue/frontend/src/identity/i18n.ts +14 -0
  560. package/templates/native-go-vue/frontend/src/identity/session.test.ts +4 -0
  561. package/templates/native-go-vue/frontend/src/identity/session.ts +9 -8
  562. package/templates/native-go-vue/frontend/src/nas/NasPicker.vue +31 -12
  563. package/templates/native-go-vue/frontend/src/nas/destination.test.ts +48 -0
  564. package/templates/native-go-vue/frontend/src/nas/i18n/files/de.json +1 -1
  565. package/templates/native-go-vue/frontend/src/nas/i18n/files/en.json +1 -1
  566. package/templates/native-go-vue/frontend/src/nas/i18n/files/es.json +10 -10
  567. package/templates/native-go-vue/frontend/src/nas/i18n/files/fr.json +1 -1
  568. package/templates/native-go-vue/frontend/src/nas/i18n/files/it.json +12 -12
  569. package/templates/native-go-vue/frontend/src/nas/i18n/files/ja.json +15 -15
  570. package/templates/native-go-vue/frontend/src/nas/i18n/files/ko.json +12 -12
  571. package/templates/native-go-vue/frontend/src/nas/i18n/files/zh-CN.json +1 -1
  572. package/templates/native-go-vue/frontend/src/nas/i18n/files/zh-TW.json +1 -1
  573. package/templates/native-go-vue/frontend/src/portal/PortalSettings.vue +39 -0
  574. package/templates/native-go-vue/frontend/src/shell/i18n/fr.json +1 -1
  575. package/templates/native-go-vue/frontend/src/shell/i18n/ja.json +1 -1
  576. package/templates/native-go-vue/frontend/test/apps.test.ts +14 -0
  577. package/templates/native-go-vue/frontend/test/portal-settings.test.ts +71 -0
  578. package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +50 -0
@@ -0,0 +1,926 @@
1
+ package main
2
+
3
+ import (
4
+ "bufio"
5
+ "context"
6
+ "encoding/json"
7
+ "errors"
8
+ "os"
9
+ "path/filepath"
10
+ "strings"
11
+ "sync"
12
+ "time"
13
+ )
14
+
15
+ // Copy performs bulk work outside the commit lock. The callback is the sole
16
+ // authority to publish, after a durable intent and one final fence check.
17
+ type MoveWriter interface {
18
+ Move(context.Context, Subject, Operation, Delegation, func(func() (MoveReceipt, error)) (MoveReceipt, error)) (MoveReceipt, error)
19
+ }
20
+ type operationRuntime struct {
21
+ ctx context.Context
22
+ cancel context.CancelFunc
23
+ wake chan struct{}
24
+ done chan struct{}
25
+ cancelJob context.CancelFunc
26
+ job string
27
+ failed bool
28
+ cancelReason string // commitMu protects the first acknowledged cancellation cause.
29
+ }
30
+
31
+ const moveValidationTimeout = 30 * time.Second
32
+
33
+ const userCancelReason = "operation-canceled-by-user"
34
+
35
+ func (r *operationRuntime) cancelJobLocked(reason string) {
36
+ if r.cancelJob != nil {
37
+ if r.cancelReason == "" {
38
+ r.cancelReason = reason
39
+ }
40
+ r.cancelJob()
41
+ }
42
+ }
43
+
44
+ func isExtension(op string) bool {
45
+ return contains([]string{"files.moveFromStaging", "files.operationStatus", "files.cancelOperation", "delegate.disable", "delegate.enable", "delegate.audit"}, op)
46
+ }
47
+ func (e *Executor) startOperations(parent context.Context) error {
48
+ e.commitMu.Lock()
49
+ defer e.commitMu.Unlock()
50
+ if e.async != nil {
51
+ return errors.New("operations already started")
52
+ }
53
+ ops, err := e.Store.Operations()
54
+ if err != nil {
55
+ return err
56
+ }
57
+ for _, op := range ops {
58
+ if op.Package != e.Policy.Package {
59
+ return errors.New("operation package")
60
+ }
61
+ if op.State == "copying" {
62
+ op.State, op.Code = "interrupted", "interrupted"
63
+ if op.PublishIntent {
64
+ op.State, op.Code = "uncertain", "uncertain"
65
+ } else if op.CancelRequested != "" {
66
+ op.State, op.Code, op.Reason = "canceled", "OPERATION_CANCELED", op.CancelRequested
67
+ }
68
+ op.Updated = e.Now().UTC()
69
+ if err = e.Store.PutOperation(op); err != nil {
70
+ return err
71
+ }
72
+ }
73
+ }
74
+ ctx, cancel := context.WithCancel(parent)
75
+ r := &operationRuntime{ctx: ctx, cancel: cancel, wake: make(chan struct{}, 1), done: make(chan struct{})}
76
+ e.async = r
77
+ go e.operationLoop(r)
78
+ e.wakeLocked()
79
+ return nil
80
+ }
81
+ func (e *Executor) stopOperations() {
82
+ e.commitMu.Lock()
83
+ r := e.async
84
+ if r != nil {
85
+ r.cancel()
86
+ }
87
+ e.commitMu.Unlock()
88
+ if r != nil {
89
+ <-r.done
90
+ e.commitMu.Lock()
91
+ e.async = nil
92
+ e.commitMu.Unlock()
93
+ }
94
+ }
95
+ func (e *Executor) wakeLocked() {
96
+ if e.async != nil {
97
+ select {
98
+ case e.async.wake <- struct{}{}:
99
+ default:
100
+ }
101
+ }
102
+ }
103
+ func (e *Executor) cancelGrantLocked(id, reason string) {
104
+ if e.async == nil {
105
+ return
106
+ }
107
+ ops, err := e.Store.Operations()
108
+ if err != nil {
109
+ e.async.failed = true
110
+ e.async.cancelJobLocked("storage-error")
111
+ return
112
+ }
113
+ for _, op := range ops {
114
+ if op.ID == id && op.State == "queued" {
115
+ op.State = "canceled"
116
+ op.Code = "OPERATION_CANCELED"
117
+ op.Reason = reason
118
+ op.Updated = e.Now().UTC()
119
+ if e.Store.PutOperation(op) != nil {
120
+ e.async.failed = true
121
+ }
122
+ }
123
+ // A copy in flight records the revocation before its writer is signaled,
124
+ // so a stop or crash settles it canceled with this reason, not interrupted.
125
+ if op.ID == id && op.State == "copying" && op.CancelRequested == "" {
126
+ op.CancelRequested = reason
127
+ op.Updated = e.Now().UTC()
128
+ if e.Store.PutOperation(op) != nil {
129
+ e.async.failed = true
130
+ }
131
+ }
132
+ if op.ID == id && op.Job == e.async.job && e.async.cancelJob != nil {
133
+ e.async.cancelJobLocked(reason)
134
+ }
135
+ }
136
+ }
137
+ func (e *Executor) handleExtension(ctx context.Context, r Request) Response {
138
+ if ctx.Err() != nil {
139
+ return deny("EXECUTOR_PAUSED")
140
+ }
141
+ if r.Version != 1 {
142
+ return deny("INVALID_REQUEST")
143
+ }
144
+ if !contains(e.Policy.Operations, r.Op) {
145
+ return deny("POLICY_DENIED")
146
+ }
147
+ var p Params
148
+ if len(r.Params) == 0 {
149
+ r.Params = json.RawMessage(`{}`)
150
+ }
151
+ if strictJSON(r.Params, &p) != nil {
152
+ return deny("INVALID_REQUEST")
153
+ }
154
+ allowed := map[string]string{"files.moveFromStaging": "delegation_id job_id staging name size", "files.operationStatus": "delegation_id job_id", "files.cancelOperation": "delegation_id job_id", "delegate.disable": "", "delegate.enable": "", "delegate.audit": ""}
155
+ var keys map[string]json.RawMessage
156
+ if json.Unmarshal(r.Params, &keys) != nil {
157
+ return deny("INVALID_REQUEST")
158
+ }
159
+ for k := range keys {
160
+ if !contains(strings.Fields(allowed[r.Op]), k) {
161
+ return deny("INVALID_REQUEST")
162
+ }
163
+ }
164
+ if r.Op == "delegate.disable" || r.Op == "delegate.enable" || r.Op == "delegate.audit" {
165
+ return e.administration(ctx, r)
166
+ }
167
+ if !tokenRE.MatchString(p.Job) || !idRE.MatchString(p.ID) {
168
+ return deny("INVALID_REQUEST")
169
+ }
170
+ if r.Op != "files.moveFromStaging" {
171
+ // Authenticated management is owner-bound. Auth-less producer lookup/cancel
172
+ // requires BOTH protected grant reference and original job, never a new job.
173
+ var caller Subject
174
+ hasAuth := r.Auth.hasKeys || r.Auth.Cookie != "" || r.Auth.SynoToken != ""
175
+ if hasAuth {
176
+ var code string
177
+ caller, code = e.verified(ctx, r.Auth)
178
+ if code != "" {
179
+ return deny(code)
180
+ }
181
+ }
182
+ e.commitMu.Lock()
183
+ defer e.commitMu.Unlock()
184
+ op, err := e.Store.GetOperation(p.Job)
185
+ if os.IsNotExist(err) && r.Op == "files.cancelOperation" {
186
+ return e.cancelUnadmittedLocked(p, hasAuth, caller)
187
+ }
188
+ if err != nil || op.Package != e.Policy.Package || op.ID != p.ID {
189
+ return deny("operation-not-found")
190
+ }
191
+ if hasAuth && (caller.Username != op.Subject.Username || caller.UID != op.Subject.UID) {
192
+ return deny("operation-not-found")
193
+ }
194
+ if r.Op == "files.cancelOperation" {
195
+ if code := e.cancelOperationLocked(&op); code != "" {
196
+ return deny(code)
197
+ }
198
+ }
199
+ return success(publicOperation(op))
200
+ }
201
+ if len(keys) != 5 || !validRelative(p.Staging) || !validName(p.Name) {
202
+ return deny("INVALID_REQUEST")
203
+ }
204
+ if _, ok := keys["size"]; !ok || p.Size < 0 || p.Size > maxMoveBytes || string(keys["size"]) == "null" {
205
+ return deny("invalid-size")
206
+ }
207
+ if _, ok := e.Writer.(MoveWriter); !ok {
208
+ return deny("EXECUTOR_PAUSED")
209
+ }
210
+ // Never reinterpret provided credentials as offline authority.
211
+ if r.Auth.hasKeys || r.Auth.Cookie != "" || r.Auth.SynoToken != "" {
212
+ s, code := e.verified(ctx, r.Auth)
213
+ if code != "" {
214
+ return deny(code)
215
+ }
216
+ d, err := e.Store.Get(p.ID)
217
+ if err != nil || s.UID != d.Subject.UID || s.Username != d.Subject.Username {
218
+ return deny("DELEGATION_DENIED")
219
+ }
220
+ }
221
+ e.commitMu.Lock()
222
+ old, err := e.Store.GetOperation(p.Job)
223
+ e.commitMu.Unlock()
224
+ if err == nil {
225
+ if old.Package == e.Policy.Package && old.ID == p.ID && old.canceledBeforeAdmission() {
226
+ return success(publicOperation(old))
227
+ }
228
+ if old.Package != e.Policy.Package || old.ID != p.ID || old.Staging != p.Staging || old.Name != p.Name || old.Size != p.Size {
229
+ return deny("operation-conflict")
230
+ }
231
+ return success(publicOperation(old))
232
+ }
233
+ if !os.IsNotExist(err) {
234
+ return deny("STORAGE_ERROR")
235
+ }
236
+ e.commitMu.Lock()
237
+ disabled, disabledErr := e.Store.Disabled()
238
+ e.commitMu.Unlock()
239
+ if disabledErr != nil {
240
+ return deny("STORAGE_ERROR")
241
+ }
242
+ if disabled {
243
+ return deny("disabled")
244
+ }
245
+ d, err := e.Store.Get(p.ID)
246
+ if err != nil {
247
+ return deny("DELEGATION_DENIED")
248
+ }
249
+ s, code := e.validateMove(ctx, d)
250
+ if code != "" {
251
+ if code != "EXECUTOR_PAUSED" && e.invalidateMove(d) != nil {
252
+ return deny("STORAGE_ERROR")
253
+ }
254
+ return deny(code)
255
+ }
256
+ e.commitMu.Lock()
257
+ defer e.commitMu.Unlock()
258
+ if e.async == nil || e.async.ctx.Err() != nil || e.async.failed {
259
+ return deny("EXECUTOR_PAUSED")
260
+ }
261
+ if code = e.moveFenceLocked(d); code != "" {
262
+ return deny(code)
263
+ }
264
+ if old, err = e.Store.GetOperation(p.Job); err == nil {
265
+ if old.ID == p.ID && old.canceledBeforeAdmission() {
266
+ return success(publicOperation(old))
267
+ }
268
+ if old.ID != p.ID || old.Staging != p.Staging || old.Name != p.Name || old.Size != p.Size {
269
+ return deny("operation-conflict")
270
+ }
271
+ return success(publicOperation(old))
272
+ } else if !os.IsNotExist(err) {
273
+ return deny("STORAGE_ERROR")
274
+ }
275
+ ops, err := e.Store.Operations()
276
+ if err != nil {
277
+ return deny("STORAGE_ERROR")
278
+ }
279
+ queued := 0
280
+ for _, op := range ops {
281
+ if op.State == "queued" {
282
+ queued++
283
+ }
284
+ }
285
+ if queued >= maxQueued {
286
+ return deny("queue-full")
287
+ }
288
+ op := Operation{Version: 1, Package: e.Policy.Package, ID: d.ID, Job: p.Job, Subject: s, Staging: p.Staging, Name: p.Name, Size: p.Size, State: "queued", Updated: e.Now().UTC()}
289
+ if e.auditLocked(s, r.Op, op.Job, d.Path, "admitting") != nil || e.Store.PutOperation(op) != nil {
290
+ return deny("STORAGE_ERROR")
291
+ }
292
+ e.wakeLocked()
293
+ return success(publicOperation(op))
294
+ }
295
+ func publicOperation(op Operation) any {
296
+ out := map[string]any{"job_id": op.Job, "state": op.State}
297
+ if op.State == "committed" {
298
+ out["receipt"] = map[string]any{"path": op.Path, "size": op.Size}
299
+ }
300
+ if op.State == "copying" && op.CancelRequested != "" {
301
+ op.Code, op.Reason = "CANCEL_REQUESTED", op.CancelRequested
302
+ }
303
+ if op.Code != "" {
304
+ out["code"] = op.Code
305
+ }
306
+ if op.Reason != "" {
307
+ out["reason"] = op.Reason
308
+ }
309
+ return out
310
+ }
311
+
312
+ // cancelOperationLocked cancels a queued operation at once. A copy in flight
313
+ // keeps copying until its writer stops, so the request is made durable first:
314
+ // a restart settles it canceled instead of interrupted. Terminal operations and
315
+ // repeated requests are answered unchanged.
316
+ func (e *Executor) cancelOperationLocked(op *Operation) string {
317
+ switch {
318
+ case op.State == "queued":
319
+ op.State, op.Code, op.Reason = "canceled", "OPERATION_CANCELED", userCancelReason
320
+ case op.State == "copying" && op.CancelRequested == "":
321
+ op.CancelRequested = userCancelReason
322
+ default:
323
+ return ""
324
+ }
325
+ op.Updated = e.Now().UTC()
326
+ if e.Store.PutOperation(*op) != nil {
327
+ return "STORAGE_ERROR"
328
+ }
329
+ if op.State == "copying" && e.async != nil && e.async.job == op.Job {
330
+ e.async.cancelJobLocked(userCancelReason)
331
+ }
332
+ if e.auditLocked(op.Subject, "files.cancelOperation", op.Job, "", "canceled") != nil {
333
+ return "STORAGE_ERROR"
334
+ }
335
+ return ""
336
+ }
337
+
338
+ // cancelUnadmittedLocked answers a cancel that arrives before its admission,
339
+ // for example while a lost or slow admission is still being validated. A
340
+ // tombstone under the package grant makes that admission return canceled.
341
+ func (e *Executor) cancelUnadmittedLocked(p Params, hasAuth bool, caller Subject) Response {
342
+ d, err := e.Store.Get(p.ID)
343
+ if err != nil || d.Package != e.Policy.Package || (hasAuth && (caller.Username != d.Subject.Username || caller.UID != d.Subject.UID)) {
344
+ return deny("operation-not-found")
345
+ }
346
+ op := Operation{Version: 1, Package: e.Policy.Package, ID: d.ID, Job: p.Job, Subject: d.Subject, State: "canceled", Code: "OPERATION_CANCELED", Reason: userCancelReason, Updated: e.Now().UTC()}
347
+ if e.Store.PutOperation(op) != nil || e.auditLocked(op.Subject, "files.cancelOperation", op.Job, "", "canceled") != nil {
348
+ return deny("STORAGE_ERROR")
349
+ }
350
+ return success(publicOperation(op))
351
+ }
352
+
353
+ func (e *Executor) administration(ctx context.Context, r Request) Response {
354
+ s, code := e.verified(ctx, r.Auth)
355
+ if code != "" {
356
+ return deny(code)
357
+ }
358
+ if r.Op != "delegate.audit" && !s.Admin {
359
+ return deny("ADMIN_REQUIRED")
360
+ }
361
+ if e.DSM.Allow(ctx, s, e.Policy.App, e.Policy.Entry) != nil {
362
+ return deny("APP_DENIED")
363
+ }
364
+ e.commitMu.Lock()
365
+ locked := true
366
+ defer func() {
367
+ if locked {
368
+ e.commitMu.Unlock()
369
+ }
370
+ }()
371
+ if r.Op == "delegate.audit" {
372
+ out := []AuditEvent{}
373
+ total := 0
374
+ sizes := []int{}
375
+ for _, name := range []string{"audit.previous", "audit.jsonl"} {
376
+ f, err := os.Open(filepath.Join(e.Store.Dir, name))
377
+ if os.IsNotExist(err) {
378
+ continue
379
+ }
380
+ if err != nil {
381
+ return deny("STORAGE_ERROR")
382
+ }
383
+ scan := bufio.NewScanner(f)
384
+ scan.Buffer(make([]byte, 4096), maxRequest)
385
+ for scan.Scan() {
386
+ var event AuditEvent
387
+ if strictJSON(scan.Bytes(), &event) != nil {
388
+ f.Close()
389
+ return deny("STORAGE_ERROR")
390
+ }
391
+ if event.Package == e.Policy.Package && (s.Admin || (s.Username == event.Username && s.UID == event.UID)) {
392
+ out = append(out, event)
393
+ n := len(scan.Bytes()) + 1
394
+ total += n
395
+ sizes = append(sizes, n)
396
+ for len(out) > 256 || total > maxOutput-4096 {
397
+ total -= sizes[0]
398
+ sizes = sizes[1:]
399
+ out = out[1:]
400
+ }
401
+ }
402
+ }
403
+ err = scan.Err()
404
+ f.Close()
405
+ if err != nil {
406
+ return deny("STORAGE_ERROR")
407
+ }
408
+ }
409
+ return success(map[string]any{"events": out})
410
+ }
411
+ wasDisabled, disabledErr := e.Store.Disabled()
412
+ if disabledErr != nil {
413
+ return deny("STORAGE_ERROR")
414
+ }
415
+ if r.Op == "delegate.enable" && !wasDisabled {
416
+ return success(map[string]any{"disabled": false})
417
+ }
418
+ // Disable becomes durable FIRST, including when revocation subsequently fails.
419
+ if r.Op == "delegate.disable" {
420
+ if e.Store.SetDisabled(true) != nil {
421
+ return deny("STORAGE_ERROR")
422
+ }
423
+ }
424
+ ds, err := e.Store.List()
425
+ if err != nil {
426
+ return deny("STORAGE_ERROR")
427
+ }
428
+ for _, d := range ds {
429
+ if d.Package != e.Policy.Package {
430
+ continue
431
+ }
432
+ d.Revoked = true
433
+ if e.Store.Put(d) != nil {
434
+ return deny("STORAGE_ERROR")
435
+ }
436
+ e.cancelGrantLocked(d.ID, "integration-disabled")
437
+ }
438
+ if e.auditLocked(s, r.Op, "", "", r.Op) != nil {
439
+ return deny("STORAGE_ERROR")
440
+ }
441
+ if r.Op == "delegate.enable" {
442
+ if e.Store.SetDisabled(false) != nil {
443
+ return deny("STORAGE_ERROR")
444
+ }
445
+ }
446
+ pending := e.legacyPendingLocked("")
447
+ e.commitMu.Unlock()
448
+ locked = false
449
+ if waitLegacy(ctx, pending) != nil {
450
+ return deny("EXECUTOR_PAUSED")
451
+ }
452
+ return success(map[string]any{"disabled": r.Op == "delegate.disable"})
453
+ }
454
+ func (e *Executor) moveFenceLocked(d Delegation) string {
455
+ disabled, err := e.Store.Disabled()
456
+ if err != nil {
457
+ return "STORAGE_ERROR"
458
+ }
459
+ if disabled {
460
+ return "disabled"
461
+ }
462
+ fresh, err := e.Store.Get(d.ID)
463
+ if err != nil || fresh.Package != e.Policy.Package || fresh.Revoked || !e.scopeMatchesLocked(fresh) || fresh.Lifetime != "until-revoked" || fresh.Root != d.Root || fresh.Directory != d.Directory {
464
+ return "DELEGATION_DENIED"
465
+ }
466
+ return ""
467
+ }
468
+ func (e *Executor) validateMove(ctx context.Context, d Delegation) (Subject, string) {
469
+ if ctx.Err() != nil {
470
+ return Subject{}, "EXECUTOR_PAUSED"
471
+ }
472
+ if d.Package != e.Policy.Package || d.Revoked || d.Lifetime != "until-revoked" || e.Policy.Lifetime != d.Lifetime || !e.scopeMatches(d) || !contains(e.Policy.Destinations, d.DestinationKey) || !contains(d.Ops, "files.moveFromStaging") {
473
+ return Subject{}, "DELEGATION_DENIED"
474
+ }
475
+ s, code := e.eligible(ctx, d.Subject.Username, d.Subject.UID, true)
476
+ if ctx.Err() != nil {
477
+ return s, "EXECUTOR_PAUSED"
478
+ }
479
+ if code != "" {
480
+ return s, code
481
+ }
482
+ moveValidationStage(ctx, "scope-root")
483
+ root, err := e.DSM.ScopeRoot(ctx, s, d.Scope)
484
+ if moveValidationDeadline(ctx, err) {
485
+ return s, "EXECUTOR_PAUSED"
486
+ }
487
+ if ctx.Err() != nil {
488
+ return s, "EXECUTOR_PAUSED"
489
+ }
490
+ if err != nil {
491
+ return s, "SCOPE_DENIED"
492
+ }
493
+ moveValidationStage(ctx, "directory-identity")
494
+ check := d
495
+ check.Root = root
496
+ check.Path = scopeVirtual(d.Scope)
497
+ real, err := mappedDirectory(check, d.Path)
498
+ if err != nil || real != d.Root {
499
+ return s, "SCOPE_DENIED"
500
+ }
501
+ di, err := bindDirectory(real)
502
+ if err != nil || di != d.Directory {
503
+ return s, "SCOPE_DENIED"
504
+ }
505
+ moveValidationStage(ctx, "permission")
506
+ if _, err = e.DSM.API(ctx, s.Username, "files.checkPermission", map[string]any{"path": d.Path}); err != nil {
507
+ if ctx.Err() != nil || moveValidationDeadline(ctx, err) {
508
+ return s, "EXECUTOR_PAUSED"
509
+ }
510
+ return s, "PERMISSION_DENIED"
511
+ }
512
+ if ctx.Err() != nil {
513
+ return s, "EXECUTOR_PAUSED"
514
+ }
515
+ return s, ""
516
+ }
517
+ func (e *Executor) operationLoop(r *operationRuntime) {
518
+ defer close(r.done)
519
+ e.recoverParts(r)
520
+ for {
521
+ select {
522
+ case <-r.ctx.Done():
523
+ return
524
+ case <-r.wake:
525
+ }
526
+ for {
527
+ e.commitMu.Lock()
528
+ ops, err := e.Store.Operations()
529
+ if err != nil {
530
+ r.failed = true
531
+ e.commitMu.Unlock()
532
+ return
533
+ }
534
+ var op Operation
535
+ found := false
536
+ for _, item := range ops {
537
+ if item.State == "queued" {
538
+ op = item
539
+ found = true
540
+ break
541
+ }
542
+ }
543
+ if !found || r.ctx.Err() != nil || r.failed {
544
+ e.commitMu.Unlock()
545
+ break
546
+ }
547
+ ctx, cancel := context.WithTimeout(r.ctx, 24*time.Hour)
548
+ r.job = op.Job
549
+ r.cancelReason = ""
550
+ r.cancelJob = cancel
551
+ op.State = "copying"
552
+ op.Updated = e.Now().UTC()
553
+ if err = e.Store.PutOperation(op); err != nil {
554
+ r.failed = true
555
+ cancel()
556
+ e.commitMu.Unlock()
557
+ return
558
+ }
559
+ e.commitMu.Unlock()
560
+ e.performMove(ctx, r, op)
561
+ cancel()
562
+ e.commitMu.Lock()
563
+ r.job = ""
564
+ r.cancelJob = nil
565
+ e.commitMu.Unlock()
566
+ }
567
+ }
568
+ }
569
+ func (e *Executor) performMove(ctx context.Context, r *operationRuntime, op Operation) {
570
+ release, slotErr := e.acquireCopy(ctx)
571
+ if slotErr == nil {
572
+ defer release()
573
+ }
574
+ d, err := e.Store.Get(op.ID)
575
+ var s Subject
576
+ code := "DELEGATION_DENIED"
577
+ if err == nil {
578
+ s, code = e.validateMove(ctx, d)
579
+ }
580
+ if slotErr != nil {
581
+ code = "EXECUTOR_PAUSED"
582
+ }
583
+ var receipt MoveReceipt
584
+ if code == "" {
585
+ // A deadline is an unavailable observation, not evidence of revocation.
586
+ // Bulk bytes remain private; publication still requires fresh validation.
587
+ // Local revoke/scope/disable handlers cancel directly under commitMu, even
588
+ // while this bounded DSM check is blocked by load or swap.
589
+ pollCtx, stop := context.WithCancel(ctx)
590
+ var poll sync.WaitGroup
591
+ poll.Add(1)
592
+ go func() {
593
+ defer poll.Done()
594
+ diagnostics := 8
595
+ tick := time.NewTicker(5 * time.Second)
596
+ defer tick.Stop()
597
+ for {
598
+ select {
599
+ case <-pollCtx.Done():
600
+ return
601
+ case <-tick.C:
602
+ checkCtx, checkCancel := context.WithTimeout(pollCtx, moveValidationTimeout)
603
+ current, c, transient := e.observeMoveValidation(checkCtx, d, op.Job, &diagnostics)
604
+ transient = transient && pollCtx.Err() == nil
605
+ if c == "" && !sameIdentity(s, current) {
606
+ c = "IDENTITY_CHANGED"
607
+ }
608
+ checkCancel()
609
+ e.commitMu.Lock()
610
+ fence := e.moveFenceLocked(d)
611
+ if c != "" && c != "EXECUTOR_PAUSED" && pollCtx.Err() == nil {
612
+ fresh, x := e.Store.Get(d.ID)
613
+ if x == nil {
614
+ fresh.Revoked = true
615
+ if e.Store.Put(fresh) != nil {
616
+ r.failed = true
617
+ }
618
+ }
619
+ }
620
+ if fence != "" {
621
+ r.cancelJobLocked("grant-inactive")
622
+ } else if c != "" && !transient && pollCtx.Err() == nil {
623
+ r.cancelJobLocked("validation-" + strings.ToLower(strings.ReplaceAll(c, "_", "-")))
624
+ }
625
+ e.commitMu.Unlock()
626
+ }
627
+ }
628
+ }()
629
+ writerCtx := context.WithValue(ctx, partRecordKey{}, func(part partMetadata) error {
630
+ e.commitMu.Lock()
631
+ defer e.commitMu.Unlock()
632
+ if ctx.Err() != nil {
633
+ return ctx.Err()
634
+ }
635
+ if code := e.moveFenceLocked(d); code != "" {
636
+ return errors.New(code)
637
+ }
638
+ op.Part = &part
639
+ if err := e.Store.PutOperation(op); err != nil {
640
+ r.failed = true
641
+ return err
642
+ }
643
+ return nil
644
+ })
645
+ receipt, err = e.Writer.(MoveWriter).Move(writerCtx, s, op, d, func(publish func() (MoveReceipt, error)) (MoveReceipt, error) {
646
+ current, c := e.validateMove(ctx, d)
647
+ if c != "" {
648
+ code = c
649
+ if c != "EXECUTOR_PAUSED" && e.invalidateMove(d) != nil {
650
+ return MoveReceipt{}, errors.New("STORAGE_ERROR")
651
+ }
652
+ return MoveReceipt{}, errors.New(c)
653
+ }
654
+ // Group changes between opening the child and publishing require a new attempt.
655
+ if !sameIdentity(s, current) {
656
+ return MoveReceipt{}, errors.New("IDENTITY_CHANGED")
657
+ }
658
+ e.commitMu.Lock()
659
+ defer e.commitMu.Unlock()
660
+ if ctx.Err() != nil || r.ctx.Err() != nil {
661
+ return MoveReceipt{}, ctx.Err()
662
+ }
663
+ if c = e.moveFenceLocked(d); c != "" {
664
+ return MoveReceipt{}, errors.New(c)
665
+ }
666
+ op.PublishIntent = true
667
+ op.Updated = e.Now().UTC()
668
+ if e.Store.PutOperation(op) != nil {
669
+ r.failed = true
670
+ return MoveReceipt{}, errors.New("STORAGE_ERROR")
671
+ }
672
+ out, x := publish()
673
+ if x == nil {
674
+ op.State = "committed"
675
+ op.Path = strings.TrimSuffix(d.Path, "/") + "/" + op.Name
676
+ if out.CleanupPending {
677
+ op.Code = "CLEANUP_PENDING"
678
+ }
679
+ op.Updated = e.Now().UTC()
680
+ if e.Store.PutOperation(op) != nil {
681
+ r.failed = true
682
+ return out, errors.New("STORAGE_ERROR")
683
+ }
684
+ }
685
+ return out, x
686
+ })
687
+ stop()
688
+ poll.Wait()
689
+ }
690
+ e.commitMu.Lock()
691
+ defer e.commitMu.Unlock()
692
+ saved, x := e.Store.GetOperation(op.Job)
693
+ if x != nil {
694
+ r.failed = true
695
+ return
696
+ }
697
+ if saved.State == "committed" {
698
+ if receipt.CleanupPending {
699
+ saved.Code = "CLEANUP_PENDING"
700
+ }
701
+ if e.Store.PutOperation(saved) != nil {
702
+ r.failed = true
703
+ }
704
+ if e.auditLocked(op.Subject, "files.moveFromStaging", op.Job, saved.Path, "committed") != nil {
705
+ r.failed = true
706
+ }
707
+ return
708
+ }
709
+ saved.State = "interrupted"
710
+ saved.Code = code
711
+ var rejected publishRejectedError
712
+ if errors.As(err, &rejected) {
713
+ saved.State = "interrupted"
714
+ saved.Code = rejected.Code
715
+ } else if saved.PublishIntent {
716
+ saved.State = "uncertain"
717
+ saved.Code = "uncertain"
718
+ } else if saved.CancelRequested != "" {
719
+ saved.State = "canceled"
720
+ saved.Code = "OPERATION_CANCELED"
721
+ saved.Reason = saved.CancelRequested
722
+ } else if ctx.Err() != nil && r.ctx.Err() == nil {
723
+ saved.State = "canceled"
724
+ saved.Code = "OPERATION_CANCELED"
725
+ saved.Reason = r.cancelReason
726
+ if saved.Reason == "" {
727
+ saved.Reason = "operation-deadline-exceeded"
728
+ }
729
+ } else if r.ctx.Err() != nil && code == "" {
730
+ // The executor stopped the writer; nothing about the destination failed.
731
+ saved.Code = "EXECUTOR_PAUSED"
732
+ } else if err != nil {
733
+ if code == "" {
734
+ saved.Code = "WRITE_FAILED"
735
+ }
736
+ }
737
+ // Eligibility failure invalidates the grant; transient copy errors do not.
738
+ if code != "" && code != "EXECUTOR_PAUSED" {
739
+ fresh, x := e.Store.Get(op.ID)
740
+ if x == nil {
741
+ fresh.Revoked = true
742
+ if e.Store.Put(fresh) != nil {
743
+ r.failed = true
744
+ }
745
+ }
746
+ }
747
+ saved.Updated = e.Now().UTC()
748
+ if e.Store.PutOperation(saved) != nil || e.auditLocked(op.Subject, "files.moveFromStaging", op.Job, d.Path, saved.State) != nil {
749
+ r.failed = true
750
+ }
751
+ }
752
+ func sameIdentity(a, b Subject) bool {
753
+ return a.Username == b.Username && a.UID == b.UID && a.GID == b.GID && sameGroups(a.Groups, b.Groups)
754
+ }
755
+
756
+ // Legacy runner API calls use a short admission lease. Revoke/disable persist
757
+ // their fence, then drain these calls OUTSIDE the commit lock before replying.
758
+ func (e *Executor) beginLegacy(d Delegation) (func(), string) {
759
+ e.commitMu.Lock()
760
+ defer e.commitMu.Unlock()
761
+ disabled, err := e.Store.Disabled()
762
+ fresh, x := e.Store.Get(d.ID)
763
+ if err != nil || x != nil {
764
+ return nil, "STORAGE_ERROR"
765
+ }
766
+ if disabled {
767
+ return nil, "disabled"
768
+ }
769
+ if fresh.Revoked || !e.Now().Before(fresh.Expires) {
770
+ return nil, "DELEGATION_DENIED"
771
+ }
772
+ if e.legacyCalls == nil {
773
+ e.legacyCalls = map[string][]chan struct{}{}
774
+ }
775
+ done := make(chan struct{})
776
+ e.legacyCalls[d.ID] = append(e.legacyCalls[d.ID], done)
777
+ return func() {
778
+ e.commitMu.Lock()
779
+ defer e.commitMu.Unlock()
780
+ close(done)
781
+ calls := e.legacyCalls[d.ID]
782
+ for i, ch := range calls {
783
+ if ch == done {
784
+ e.legacyCalls[d.ID] = append(calls[:i], calls[i+1:]...)
785
+ break
786
+ }
787
+ }
788
+ if len(e.legacyCalls[d.ID]) == 0 {
789
+ delete(e.legacyCalls, d.ID)
790
+ }
791
+ }, ""
792
+ }
793
+ func (e *Executor) legacyPendingLocked(id string) []chan struct{} {
794
+ out := []chan struct{}{}
795
+ for key, calls := range e.legacyCalls {
796
+ if id == "" || key == id {
797
+ out = append(out, calls...)
798
+ }
799
+ }
800
+ return out
801
+ }
802
+ func waitLegacy(ctx context.Context, pending []chan struct{}) error {
803
+ for _, done := range pending {
804
+ select {
805
+ case <-ctx.Done():
806
+ return ctx.Err()
807
+ case <-done:
808
+ }
809
+ }
810
+ return nil
811
+ }
812
+
813
+ func (e *Executor) invalidateMove(d Delegation) error {
814
+ e.commitMu.Lock()
815
+ defer e.commitMu.Unlock()
816
+ fresh, err := e.Store.Get(d.ID)
817
+ if err != nil {
818
+ return err
819
+ }
820
+ fresh.Revoked = true
821
+ if err = e.Store.Put(fresh); err != nil {
822
+ return err
823
+ }
824
+ e.cancelGrantLocked(d.ID, "validation-denied")
825
+ return e.auditLocked(d.Subject, "delegate.invalidate", "", d.Path, "revoked")
826
+ }
827
+
828
+ type PartCleaner interface {
829
+ CleanupRecorded(context.Context, Subject, partMetadata) error
830
+ }
831
+
832
+ // PartPublisher reports whether an operation's recorded part inode is already
833
+ // the final name, so a crash after publish intent settles without a new copy.
834
+ type PartPublisher interface {
835
+ PublishedPart(context.Context, Operation) bool
836
+ }
837
+
838
+ func (e *Executor) settleUncertain(r *operationRuntime, op Operation) {
839
+ checker, ok := e.Writer.(PartPublisher)
840
+ if !ok || op.State != "uncertain" || op.Part == nil {
841
+ return
842
+ }
843
+ ctx, cancel := context.WithTimeout(r.ctx, 5*time.Second)
844
+ published := checker.PublishedPart(ctx, op)
845
+ cancel()
846
+ if !published {
847
+ return
848
+ }
849
+ e.commitMu.Lock()
850
+ defer e.commitMu.Unlock()
851
+ fresh, err := e.Store.GetOperation(op.Job)
852
+ d, x := e.Store.Get(op.ID)
853
+ if err != nil || x != nil || fresh.State != "uncertain" {
854
+ return
855
+ }
856
+ fresh.State, fresh.Code = "committed", ""
857
+ fresh.Path = strings.TrimSuffix(d.Path, "/") + "/" + fresh.Name
858
+ fresh.Updated = e.Now().UTC()
859
+ if e.Store.PutOperation(fresh) != nil || e.auditLocked(fresh.Subject, "files.moveFromStaging", fresh.Job, fresh.Path, "committed") != nil {
860
+ r.failed = true
861
+ }
862
+ }
863
+
864
+ func (e *Executor) recoverParts(r *operationRuntime) {
865
+ cleaner, ok := e.Writer.(PartCleaner)
866
+ if !ok {
867
+ return
868
+ }
869
+ e.commitMu.Lock()
870
+ ops, err := e.Store.Operations()
871
+ e.commitMu.Unlock()
872
+ if err != nil {
873
+ e.commitMu.Lock()
874
+ r.failed = true
875
+ e.commitMu.Unlock()
876
+ return
877
+ }
878
+ for _, op := range ops {
879
+ if op.Part == nil || op.State == "queued" || op.State == "copying" {
880
+ continue
881
+ }
882
+ e.settleUncertain(r, op)
883
+ s, code := e.eligible(r.ctx, op.Subject.Username, op.Subject.UID, true)
884
+ cleanupErr := errors.New("cleanup ineligible")
885
+ if code == "" {
886
+ ctx, cancel := context.WithTimeout(r.ctx, 5*time.Second)
887
+ cleanupErr = cleaner.CleanupRecorded(ctx, s, *op.Part)
888
+ cancel()
889
+ }
890
+ e.commitMu.Lock()
891
+ fresh, x := e.Store.GetOperation(op.Job)
892
+ if x == nil {
893
+ if cleanupErr == nil {
894
+ fresh.Part = nil
895
+ } else if fresh.State == "committed" {
896
+ fresh.Code = "CLEANUP_PENDING"
897
+ }
898
+ if e.Store.PutOperation(fresh) != nil {
899
+ r.failed = true
900
+ }
901
+ } else {
902
+ r.failed = true
903
+ }
904
+ e.commitMu.Unlock()
905
+ if r.ctx.Err() != nil {
906
+ return
907
+ }
908
+ }
909
+ }
910
+
911
+ // Legacy and permanent writers share one package-wide, cancelable bulk slot.
912
+ // Waiting for a large new copy must not hold a lock or outlive a legacy request.
913
+ func (e *Executor) acquireCopy(ctx context.Context) (func(), error) {
914
+ e.commitMu.Lock()
915
+ if e.copySlot == nil {
916
+ e.copySlot = make(chan struct{}, 1)
917
+ }
918
+ slot := e.copySlot
919
+ e.commitMu.Unlock()
920
+ select {
921
+ case <-ctx.Done():
922
+ return nil, ctx.Err()
923
+ case slot <- struct{}{}:
924
+ return func() { <-slot }, nil
925
+ }
926
+ }