@maudecode/talaria-web 1.17.2 → 1.19.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 (623) hide show
  1. package/_release.json +8 -8
  2. package/dist/api/auth-raw.d.ts +1 -1
  3. package/dist/api/auth-raw.js +20 -20
  4. package/dist/api/auth-raw.js.map +1 -1
  5. package/dist/api/automation-raw.d.ts +7 -1
  6. package/dist/api/automation-raw.js +74 -27
  7. package/dist/api/automation-raw.js.map +1 -1
  8. package/dist/api/automation-router.d.ts +128 -23
  9. package/dist/api/automation-router.js +29 -13
  10. package/dist/api/automation-router.js.map +1 -1
  11. package/dist/api/chat-router.d.ts +1845 -40
  12. package/dist/api/chat-router.js +196 -114
  13. package/dist/api/chat-router.js.map +1 -1
  14. package/dist/api/git-router.d.ts +37 -3
  15. package/dist/api/git-router.js +47 -48
  16. package/dist/api/git-router.js.map +1 -1
  17. package/dist/api/raw-routes.d.ts +9 -1
  18. package/dist/api/raw-routes.js +286 -73
  19. package/dist/api/raw-routes.js.map +1 -1
  20. package/dist/api/router.d.ts +16 -6
  21. package/dist/api/router.js +28 -26
  22. package/dist/api/router.js.map +1 -1
  23. package/dist/api/session-visibility.d.ts +4 -4
  24. package/dist/api/session-visibility.js +26 -16
  25. package/dist/api/session-visibility.js.map +1 -1
  26. package/dist/api/sessions-router.d.ts +3312 -131
  27. package/dist/api/sessions-router.js +287 -157
  28. package/dist/api/sessions-router.js.map +1 -1
  29. package/dist/api/settings-router.d.ts +457 -8
  30. package/dist/api/settings-router.js +54 -33
  31. package/dist/api/settings-router.js.map +1 -1
  32. package/dist/api/sse-routes.d.ts +7 -2
  33. package/dist/api/sse-routes.js +109 -62
  34. package/dist/api/sse-routes.js.map +1 -1
  35. package/dist/api/tools-raw.d.ts +5 -0
  36. package/dist/api/tools-raw.js +63 -55
  37. package/dist/api/tools-raw.js.map +1 -1
  38. package/dist/api/tools-router.d.ts +998 -85
  39. package/dist/api/tools-router.js +447 -61
  40. package/dist/api/tools-router.js.map +1 -1
  41. package/dist/api/transcribe-stream.d.ts +16 -0
  42. package/dist/api/transcribe-stream.js +172 -0
  43. package/dist/api/transcribe-stream.js.map +1 -0
  44. package/dist/app.d.ts +4 -1
  45. package/dist/app.js +98 -59
  46. package/dist/app.js.map +1 -1
  47. package/dist/auth/gate.js +15 -11
  48. package/dist/auth/gate.js.map +1 -1
  49. package/dist/auth/oidc.d.ts +5 -2
  50. package/dist/auth/oidc.js +8 -7
  51. package/dist/auth/oidc.js.map +1 -1
  52. package/dist/auth/passkeys.d.ts +15 -14
  53. package/dist/auth/passkeys.js +89 -91
  54. package/dist/auth/passkeys.js.map +1 -1
  55. package/dist/auth/store.d.ts +17 -7
  56. package/dist/auth/store.js +93 -52
  57. package/dist/auth/store.js.map +1 -1
  58. package/dist/bin/talaria-web.js +12 -81
  59. package/dist/bin/talaria-web.js.map +1 -1
  60. package/dist/cli/ctl.js +2 -1
  61. package/dist/cli/ctl.js.map +1 -1
  62. package/dist/cli/dotenv.d.ts +5 -0
  63. package/dist/cli/dotenv.js +15 -2
  64. package/dist/cli/dotenv.js.map +1 -1
  65. package/dist/cli/launcher.js +5 -5
  66. package/dist/cli/launcher.js.map +1 -1
  67. package/dist/cli/main.d.ts +1 -0
  68. package/dist/cli/main.js +85 -0
  69. package/dist/cli/main.js.map +1 -0
  70. package/dist/cli/supervise.d.ts +2 -0
  71. package/dist/cli/supervise.js +67 -4
  72. package/dist/cli/supervise.js.map +1 -1
  73. package/dist/config/agent-config.d.ts +34 -11
  74. package/dist/config/agent-config.js +116 -60
  75. package/dist/config/agent-config.js.map +1 -1
  76. package/dist/config.d.ts +4 -0
  77. package/dist/config.js +19 -6
  78. package/dist/config.js.map +1 -1
  79. package/dist/fs/atomic.d.ts +1 -0
  80. package/dist/fs/atomic.js +4 -1
  81. package/dist/fs/atomic.js.map +1 -1
  82. package/dist/fs/stat.d.ts +1 -0
  83. package/dist/fs/stat.js +11 -0
  84. package/dist/fs/stat.js.map +1 -0
  85. package/dist/http/context.d.ts +31 -12
  86. package/dist/http/context.js +60 -24
  87. package/dist/http/context.js.map +1 -1
  88. package/dist/http/csp.d.ts +2 -1
  89. package/dist/http/csp.js +3 -2
  90. package/dist/http/csp.js.map +1 -1
  91. package/dist/onboarding.d.ts +2 -2
  92. package/dist/onboarding.js +10 -8
  93. package/dist/onboarding.js.map +1 -1
  94. package/dist/profiles/profiles.d.ts +20 -4
  95. package/dist/profiles/profiles.js +115 -30
  96. package/dist/profiles/profiles.js.map +1 -1
  97. package/dist/projects.d.ts +15 -6
  98. package/dist/projects.js +55 -47
  99. package/dist/projects.js.map +1 -1
  100. package/dist/providers/catalog.d.ts +84 -14
  101. package/dist/providers/catalog.js +474 -151
  102. package/dist/providers/catalog.js.map +1 -1
  103. package/dist/providers/env-file.d.ts +10 -3
  104. package/dist/providers/env-file.js +24 -75
  105. package/dist/providers/env-file.js.map +1 -1
  106. package/dist/providers/runtime-env.d.ts +4 -2
  107. package/dist/providers/runtime-env.js +9 -6
  108. package/dist/providers/runtime-env.js.map +1 -1
  109. package/dist/providers/tables.d.ts +3 -1
  110. package/dist/providers/tables.js +4 -2
  111. package/dist/providers/tables.js.map +1 -1
  112. package/dist/raster-image.d.ts +7 -0
  113. package/dist/raster-image.js +267 -0
  114. package/dist/raster-image.js.map +1 -0
  115. package/dist/redact.js +342 -46
  116. package/dist/redact.js.map +1 -1
  117. package/dist/release.d.ts +7 -0
  118. package/dist/release.js +14 -0
  119. package/dist/release.js.map +1 -1
  120. package/dist/runtime.d.ts +5 -1
  121. package/dist/runtime.js +305 -96
  122. package/dist/runtime.js.map +1 -1
  123. package/dist/server.js +15 -0
  124. package/dist/server.js.map +1 -1
  125. package/dist/sessions/anchor.d.ts +9 -3
  126. package/dist/sessions/anchor.js +28 -36
  127. package/dist/sessions/anchor.js.map +1 -1
  128. package/dist/sessions/background-events.d.ts +2 -0
  129. package/dist/sessions/background-events.js +10 -0
  130. package/dist/sessions/background-events.js.map +1 -0
  131. package/dist/sessions/background-tasks.d.ts +26 -27
  132. package/dist/sessions/background-tasks.js +89 -97
  133. package/dist/sessions/background-tasks.js.map +1 -1
  134. package/dist/sessions/background-updates.d.ts +15 -8
  135. package/dist/sessions/background-updates.js +101 -25
  136. package/dist/sessions/background-updates.js.map +1 -1
  137. package/dist/sessions/cli-sessions.d.ts +27 -6
  138. package/dist/sessions/cli-sessions.js +152 -42
  139. package/dist/sessions/cli-sessions.js.map +1 -1
  140. package/dist/sessions/completions.d.ts +77 -9
  141. package/dist/sessions/completions.js +482 -45
  142. package/dist/sessions/completions.js.map +1 -1
  143. package/dist/sessions/compress.d.ts +8 -0
  144. package/dist/sessions/compress.js +2 -2
  145. package/dist/sessions/compress.js.map +1 -1
  146. package/dist/sessions/drafts.d.ts +14 -10
  147. package/dist/sessions/drafts.js +38 -43
  148. package/dist/sessions/drafts.js.map +1 -1
  149. package/dist/sessions/export.d.ts +5 -0
  150. package/dist/sessions/export.js +30 -0
  151. package/dist/sessions/export.js.map +1 -1
  152. package/dist/sessions/gateway-sessions.d.ts +46 -0
  153. package/dist/sessions/gateway-sessions.js +378 -0
  154. package/dist/sessions/gateway-sessions.js.map +1 -0
  155. package/dist/sessions/gateway-watcher.d.ts +7 -3
  156. package/dist/sessions/gateway-watcher.js +31 -19
  157. package/dist/sessions/gateway-watcher.js.map +1 -1
  158. package/dist/sessions/goals.d.ts +31 -0
  159. package/dist/sessions/goals.js +75 -0
  160. package/dist/sessions/goals.js.map +1 -0
  161. package/dist/sessions/handoff.d.ts +32 -0
  162. package/dist/sessions/handoff.js +114 -0
  163. package/dist/sessions/handoff.js.map +1 -0
  164. package/dist/sessions/journal.d.ts +5 -79
  165. package/dist/sessions/journal.js +15 -606
  166. package/dist/sessions/journal.js.map +1 -1
  167. package/dist/sessions/list.d.ts +31 -16
  168. package/dist/sessions/list.js +139 -129
  169. package/dist/sessions/list.js.map +1 -1
  170. package/dist/sessions/merge.d.ts +69 -13
  171. package/dist/sessions/merge.js +265 -64
  172. package/dist/sessions/merge.js.map +1 -1
  173. package/dist/sessions/notices.d.ts +22 -0
  174. package/dist/sessions/notices.js +72 -0
  175. package/dist/sessions/notices.js.map +1 -0
  176. package/dist/sessions/pending.d.ts +10 -4
  177. package/dist/sessions/pending.js +83 -7
  178. package/dist/sessions/pending.js.map +1 -1
  179. package/dist/sessions/prefix-index.d.ts +82 -0
  180. package/dist/sessions/prefix-index.js +232 -0
  181. package/dist/sessions/prefix-index.js.map +1 -0
  182. package/dist/sessions/recovery.d.ts +19 -0
  183. package/dist/sessions/recovery.js +322 -0
  184. package/dist/sessions/recovery.js.map +1 -0
  185. package/dist/sessions/relay.d.ts +38 -19
  186. package/dist/sessions/relay.js +302 -79
  187. package/dist/sessions/relay.js.map +1 -1
  188. package/dist/sessions/service.d.ts +260 -55
  189. package/dist/sessions/service.js +1448 -462
  190. package/dist/sessions/service.js.map +1 -1
  191. package/dist/sessions/session.d.ts +24 -2
  192. package/dist/sessions/session.js +69 -7
  193. package/dist/sessions/session.js.map +1 -1
  194. package/dist/sessions/shares.d.ts +7 -6
  195. package/dist/sessions/shares.js +22 -45
  196. package/dist/sessions/shares.js.map +1 -1
  197. package/dist/sessions/source-kind.js +1 -0
  198. package/dist/sessions/source-kind.js.map +1 -1
  199. package/dist/sessions/state-db-identity.d.ts +18 -0
  200. package/dist/sessions/state-db-identity.js +67 -0
  201. package/dist/sessions/state-db-identity.js.map +1 -0
  202. package/dist/sessions/state-db-worker.d.ts +1 -0
  203. package/dist/sessions/state-db-worker.js +35 -0
  204. package/dist/sessions/state-db-worker.js.map +1 -0
  205. package/dist/sessions/state-db.d.ts +113 -7
  206. package/dist/sessions/state-db.js +861 -44
  207. package/dist/sessions/state-db.js.map +1 -1
  208. package/dist/sessions/storage/backend.d.ts +162 -0
  209. package/dist/sessions/storage/backend.js +19 -0
  210. package/dist/sessions/storage/backend.js.map +1 -0
  211. package/dist/sessions/storage/conformance.d.ts +20 -0
  212. package/dist/sessions/storage/conformance.js +237 -0
  213. package/dist/sessions/storage/conformance.js.map +1 -0
  214. package/dist/sessions/storage/document.d.ts +16 -0
  215. package/dist/sessions/storage/document.js +44 -0
  216. package/dist/sessions/storage/document.js.map +1 -0
  217. package/dist/sessions/storage/json-documents.d.ts +23 -0
  218. package/dist/sessions/storage/json-documents.js +233 -0
  219. package/dist/sessions/storage/json-documents.js.map +1 -0
  220. package/dist/sessions/storage/json-files.d.ts +12 -0
  221. package/dist/sessions/storage/json-files.js +124 -0
  222. package/dist/sessions/storage/json-files.js.map +1 -0
  223. package/dist/sessions/storage/json-installation.d.ts +3 -0
  224. package/dist/sessions/storage/json-installation.js +31 -0
  225. package/dist/sessions/storage/json-installation.js.map +1 -0
  226. package/dist/sessions/storage/json-journal.d.ts +114 -0
  227. package/dist/sessions/storage/json-journal.js +686 -0
  228. package/dist/sessions/storage/json-journal.js.map +1 -0
  229. package/dist/sessions/storage/json-recovery.d.ts +4 -0
  230. package/dist/sessions/storage/json-recovery.js +195 -0
  231. package/dist/sessions/storage/json-recovery.js.map +1 -0
  232. package/dist/sessions/storage/json.d.ts +68 -0
  233. package/dist/sessions/storage/json.js +258 -0
  234. package/dist/sessions/storage/json.js.map +1 -0
  235. package/dist/sessions/storage/row.d.ts +6 -0
  236. package/dist/sessions/storage/row.js +49 -0
  237. package/dist/sessions/storage/row.js.map +1 -0
  238. package/dist/sessions/storage/search-text.d.ts +4 -0
  239. package/dist/sessions/storage/search-text.js +16 -0
  240. package/dist/sessions/storage/search-text.js.map +1 -0
  241. package/dist/sessions/store.d.ts +65 -68
  242. package/dist/sessions/store.js +305 -602
  243. package/dist/sessions/store.js.map +1 -1
  244. package/dist/sessions/streams.d.ts +4 -0
  245. package/dist/sessions/streams.js +9 -1
  246. package/dist/sessions/streams.js.map +1 -1
  247. package/dist/sessions/titles.d.ts +2 -0
  248. package/dist/sessions/titles.js +5 -1
  249. package/dist/sessions/titles.js.map +1 -1
  250. package/dist/sessions/todo.d.ts +5 -0
  251. package/dist/sessions/todo.js +28 -21
  252. package/dist/sessions/todo.js.map +1 -1
  253. package/dist/sessions/tool-display.d.ts +5 -4
  254. package/dist/sessions/tool-display.js +30 -50
  255. package/dist/sessions/tool-display.js.map +1 -1
  256. package/dist/sessions/turn-context.d.ts +11 -2
  257. package/dist/sessions/turn-context.js +18 -11
  258. package/dist/sessions/turn-context.js.map +1 -1
  259. package/dist/sessions/turn.d.ts +122 -17
  260. package/dist/sessions/turn.js +939 -358
  261. package/dist/sessions/turn.js.map +1 -1
  262. package/dist/sessions/wakeup-pause.d.ts +54 -0
  263. package/dist/sessions/wakeup-pause.js +163 -0
  264. package/dist/sessions/wakeup-pause.js.map +1 -0
  265. package/dist/sessions/window.js +13 -12
  266. package/dist/sessions/window.js.map +1 -1
  267. package/dist/settings.d.ts +19 -7
  268. package/dist/settings.js +178 -184
  269. package/dist/settings.js.map +1 -1
  270. package/dist/sidecar/client.d.ts +17 -3
  271. package/dist/sidecar/client.js +78 -8
  272. package/dist/sidecar/client.js.map +1 -1
  273. package/dist/sidecar/discover.d.ts +2 -1
  274. package/dist/sidecar/discover.js +93 -10
  275. package/dist/sidecar/discover.js.map +1 -1
  276. package/dist/sidecar/env.d.ts +4 -0
  277. package/dist/sidecar/env.js +3 -0
  278. package/dist/sidecar/env.js.map +1 -0
  279. package/dist/sidecar/fake.d.ts +2 -2
  280. package/dist/sidecar/fake.js +19 -2
  281. package/dist/sidecar/fake.js.map +1 -1
  282. package/dist/startup.d.ts +13 -5
  283. package/dist/startup.js +48 -0
  284. package/dist/startup.js.map +1 -1
  285. package/dist/test/env-file.d.ts +5 -0
  286. package/dist/test/env-file.js +88 -0
  287. package/dist/test/env-file.js.map +1 -0
  288. package/dist/test/harness.d.ts +1 -0
  289. package/dist/test/harness.js +1 -1
  290. package/dist/test/harness.js.map +1 -1
  291. package/dist/test/passkeys.d.ts +22 -0
  292. package/dist/test/passkeys.js +62 -0
  293. package/dist/test/passkeys.js.map +1 -0
  294. package/dist/test/storage.d.ts +15 -0
  295. package/dist/test/storage.js +26 -0
  296. package/dist/test/storage.js.map +1 -0
  297. package/dist/test/ts-worker-hooks.d.ts +1 -0
  298. package/dist/test/ts-worker-hooks.js +24 -0
  299. package/dist/test/ts-worker-hooks.js.map +1 -0
  300. package/dist/text/diff.d.ts +9 -14
  301. package/dist/text/diff.js +74 -108
  302. package/dist/text/diff.js.map +1 -1
  303. package/dist/tools/commands.d.ts +6 -1
  304. package/dist/tools/commands.js +8 -2
  305. package/dist/tools/commands.js.map +1 -1
  306. package/dist/tools/connectors.d.ts +33 -0
  307. package/dist/tools/connectors.js +121 -0
  308. package/dist/tools/connectors.js.map +1 -0
  309. package/dist/tools/crons.d.ts +38 -1
  310. package/dist/tools/crons.js +122 -18
  311. package/dist/tools/crons.js.map +1 -1
  312. package/dist/tools/dashboard-plugins.d.ts +53 -0
  313. package/dist/tools/dashboard-plugins.js +188 -0
  314. package/dist/tools/dashboard-plugins.js.map +1 -0
  315. package/dist/tools/extensions.d.ts +18 -5
  316. package/dist/tools/extensions.js +344 -266
  317. package/dist/tools/extensions.js.map +1 -1
  318. package/dist/tools/gateway-restart.d.ts +62 -0
  319. package/dist/tools/gateway-restart.js +142 -0
  320. package/dist/tools/gateway-restart.js.map +1 -0
  321. package/dist/tools/health.d.ts +44 -0
  322. package/dist/tools/health.js +201 -46
  323. package/dist/tools/health.js.map +1 -1
  324. package/dist/tools/hygiene.d.ts +4 -3
  325. package/dist/tools/hygiene.js +23 -9
  326. package/dist/tools/hygiene.js.map +1 -1
  327. package/dist/tools/kanban.d.ts +4 -2
  328. package/dist/tools/kanban.js +28 -19
  329. package/dist/tools/kanban.js.map +1 -1
  330. package/dist/tools/mcp.d.ts +13 -0
  331. package/dist/tools/mcp.js +88 -0
  332. package/dist/tools/mcp.js.map +1 -1
  333. package/dist/tools/memory-masks.d.ts +5 -0
  334. package/dist/tools/memory-masks.js +256 -0
  335. package/dist/tools/memory-masks.js.map +1 -0
  336. package/dist/tools/memory.d.ts +29 -3
  337. package/dist/tools/memory.js +69 -27
  338. package/dist/tools/memory.js.map +1 -1
  339. package/dist/tools/plugin-settings.d.ts +15 -0
  340. package/dist/tools/plugin-settings.js +90 -0
  341. package/dist/tools/plugin-settings.js.map +1 -0
  342. package/dist/tools/project-os.d.ts +11 -0
  343. package/dist/tools/project-os.js +267 -0
  344. package/dist/tools/project-os.js.map +1 -0
  345. package/dist/tools/skills.d.ts +14 -5
  346. package/dist/tools/skills.js +49 -16
  347. package/dist/tools/skills.js.map +1 -1
  348. package/dist/tools/update-notifications.d.ts +34 -15
  349. package/dist/tools/update-notifications.js +141 -57
  350. package/dist/tools/update-notifications.js.map +1 -1
  351. package/dist/tools/update-rollback.d.ts +11 -0
  352. package/dist/tools/update-rollback.js +28 -0
  353. package/dist/tools/update-rollback.js.map +1 -0
  354. package/dist/tools/updates.d.ts +110 -32
  355. package/dist/tools/updates.js +1112 -294
  356. package/dist/tools/updates.js.map +1 -1
  357. package/dist/tools/wiki.d.ts +68 -0
  358. package/dist/tools/wiki.js +413 -0
  359. package/dist/tools/wiki.js.map +1 -0
  360. package/dist/util.d.ts +2 -0
  361. package/dist/util.js +5 -0
  362. package/dist/util.js.map +1 -1
  363. package/dist/workspace/escape.d.ts +61 -0
  364. package/dist/workspace/escape.js +149 -0
  365. package/dist/workspace/escape.js.map +1 -0
  366. package/dist/workspace/extract.d.ts +22 -0
  367. package/dist/workspace/extract.js +308 -0
  368. package/dist/workspace/extract.js.map +1 -0
  369. package/dist/workspace/fs.d.ts +12 -0
  370. package/dist/workspace/fs.js +92 -23
  371. package/dist/workspace/fs.js.map +1 -1
  372. package/dist/workspace/git.d.ts +34 -1
  373. package/dist/workspace/git.js +328 -35
  374. package/dist/workspace/git.js.map +1 -1
  375. package/dist/workspace/media.d.ts +16 -13
  376. package/dist/workspace/media.js +43 -61
  377. package/dist/workspace/media.js.map +1 -1
  378. package/dist/workspace/rollback.d.ts +36 -22
  379. package/dist/workspace/rollback.js +178 -272
  380. package/dist/workspace/rollback.js.map +1 -1
  381. package/dist/workspace/unzip.js +4 -1
  382. package/dist/workspace/unzip.js.map +1 -1
  383. package/dist/workspace/upload.d.ts +7 -4
  384. package/dist/workspace/upload.js +5 -2
  385. package/dist/workspace/upload.js.map +1 -1
  386. package/dist/workspace/workspaces.d.ts +15 -19
  387. package/dist/workspace/workspaces.js +55 -127
  388. package/dist/workspace/workspaces.js.map +1 -1
  389. package/dist/workspace/worktrees.d.ts +2 -1
  390. package/dist/workspace/worktrees.js +179 -42
  391. package/dist/workspace/worktrees.js.map +1 -1
  392. package/package.json +7 -3
  393. package/sidecar/agent_dependency.json +4 -4
  394. package/sidecar/agent_support.json +26 -0
  395. package/sidecar/scripts/record-fixtures.py +12 -3
  396. package/sidecar/scripts/replay_sidecar.py +32 -0
  397. package/sidecar/scripts/suite_runner.py +302 -0
  398. package/sidecar/scripts/test.sh +12 -4
  399. package/sidecar/talaria_sidecar/__init__.py +6 -1
  400. package/sidecar/talaria_sidecar/__main__.py +4 -2
  401. package/sidecar/talaria_sidecar/agent_mutation.py +975 -0
  402. package/sidecar/talaria_sidecar/agent_services.py +1053 -0
  403. package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
  404. package/sidecar/talaria_sidecar/home.py +56 -3
  405. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  406. package/sidecar/talaria_sidecar/methods/aux.py +166 -24
  407. package/sidecar/talaria_sidecar/methods/chat.py +852 -135
  408. package/sidecar/talaria_sidecar/methods/checkpoints.py +89 -0
  409. package/sidecar/talaria_sidecar/methods/commands.py +294 -4
  410. package/sidecar/talaria_sidecar/methods/config.py +123 -4
  411. package/sidecar/talaria_sidecar/methods/connectors.py +343 -0
  412. package/sidecar/talaria_sidecar/methods/cron.py +395 -36
  413. package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
  414. package/sidecar/talaria_sidecar/methods/gateway.py +212 -55
  415. package/sidecar/talaria_sidecar/methods/gateway_sessions.py +234 -0
  416. package/sidecar/talaria_sidecar/methods/goals.py +81 -41
  417. package/sidecar/talaria_sidecar/methods/kanban.py +108 -34
  418. package/sidecar/talaria_sidecar/methods/mcp.py +1 -1
  419. package/sidecar/talaria_sidecar/methods/memory.py +84 -0
  420. package/sidecar/talaria_sidecar/methods/oauth.py +5 -2
  421. package/sidecar/talaria_sidecar/methods/process.py +218 -15
  422. package/sidecar/talaria_sidecar/methods/profiles.py +121 -13
  423. package/sidecar/talaria_sidecar/methods/providers.py +217 -28
  424. package/sidecar/talaria_sidecar/methods/runtime.py +69 -1
  425. package/sidecar/talaria_sidecar/methods/skills.py +67 -4
  426. package/sidecar/talaria_sidecar/methods/state_db.py +316 -75
  427. package/sidecar/talaria_sidecar/methods/stt.py +159 -78
  428. package/sidecar/talaria_sidecar/methods/text.py +14 -1
  429. package/sidecar/talaria_sidecar/methods/toolsets.py +49 -0
  430. package/sidecar/talaria_sidecar/methods/usage.py +113 -14
  431. package/sidecar/talaria_sidecar/methods/worktree.py +26 -19
  432. package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
  433. package/sidecar/talaria_sidecar/rpc.py +45 -4
  434. package/sidecar/talaria_sidecar/runtime.py +98 -13
  435. package/sidecar/talaria_sidecar/speed.py +35 -0
  436. package/static/dist/FILES.txt +126 -126
  437. package/static/dist/assets/AppShell-cey33v9s.js +8 -0
  438. package/static/dist/assets/ChatPage-i8wnugr7.js +18 -0
  439. package/static/dist/assets/ConnectTargets-hg19553x.js +1 -0
  440. package/static/dist/assets/ExtensionRoute-cly4apm5.js +1 -0
  441. package/static/dist/assets/Field-j8uqoigz.js +9 -0
  442. package/static/dist/assets/HubRoute-e4bgz41j.js +2 -0
  443. package/static/dist/assets/Markdown-lahuichm.js +598 -0
  444. package/static/dist/assets/RightPanel-lbo4i0fn.js +1 -0
  445. package/static/dist/assets/Select-gqatm2e0.js +1 -0
  446. package/static/dist/assets/SessionListPanel-mfqayfjh.js +1 -0
  447. package/static/dist/assets/SettingsLayout-b8980u7k.js +1 -0
  448. package/static/dist/assets/States-e7dkgtxf.js +1 -0
  449. package/static/dist/assets/Toaster-bfqqy786.js +1 -0
  450. package/static/dist/assets/_app-n1n6pfx1.js +1 -0
  451. package/static/dist/assets/{_app.ext._extensionId-opyh9y0v.js → _app.ext._extensionId-buxjxav3.js} +1 -1
  452. package/static/dist/assets/_app.index-jt8zwiu2.js +1 -0
  453. package/static/dist/assets/_app.insights-cv76ml71.js +1 -0
  454. package/static/dist/assets/_app.kanban-h6ts53j7.js +1 -0
  455. package/static/dist/assets/_app.logs-dm873z90.js +1 -0
  456. package/static/dist/assets/_app.memory-glk5y6m8.js +1 -0
  457. package/static/dist/assets/_app.profiles-ka6zpstq.js +1 -0
  458. package/static/dist/assets/{_app.session._sessionId-bfd4h3id.js → _app.session._sessionId-g2vzghvc.js} +1 -1
  459. package/static/dist/assets/_app.settings-hgjdhzcd.js +1 -0
  460. package/static/dist/assets/_app.settings._section-jf2kmhae.js +3 -0
  461. package/static/dist/assets/_app.skills-cii852yq.js +1 -0
  462. package/static/dist/assets/_app.tasks-fp9q7atm.js +1 -0
  463. package/static/dist/assets/_app.todos-dw94qzue.js +1 -0
  464. package/static/dist/assets/_app.workspaces-inyygzze.js +1 -0
  465. package/static/dist/assets/{abnfDiagram-VCTEODGH-b2x1lqot.js → abnfDiagram-VCTEODGH-gjayq3ra.js} +1 -1
  466. package/static/dist/assets/architecture-7GRP2DOG-lp56972r.js +1 -0
  467. package/static/dist/assets/{architectureDiagram-5GKGNRK7-h38o5l7b.js → architectureDiagram-5GKGNRK7-nwb3xovn.js} +1 -1
  468. package/static/dist/assets/archive-b1j004vn.js +1 -0
  469. package/static/dist/assets/back-g74gkozu.js +1 -0
  470. package/static/dist/assets/{blockDiagram-I7D4REHJ-bqquxv2u.js → blockDiagram-I7D4REHJ-joedghi3.js} +1 -1
  471. package/static/dist/assets/{c4Diagram-7LVT6UL2-f3fb8w6z.js → c4Diagram-7LVT6UL2-hyrjrohe.js} +1 -1
  472. package/static/dist/assets/channel-es95o4y8.js +1 -0
  473. package/static/dist/assets/{chunk-4HAMMTFA-bzfyyzp3.js → chunk-4HAMMTFA-koouqbut.js} +1 -1
  474. package/static/dist/assets/{chunk-75Z2AOVW-ly99rfx5.js → chunk-75Z2AOVW-bl4bhdnt.js} +1 -1
  475. package/static/dist/assets/{chunk-DU6HZSFF-bg6ecfqf.js → chunk-DU6HZSFF-gpedttsp.js} +3 -3
  476. package/static/dist/assets/{chunk-F27PBJKO-e8k7mmqw.js → chunk-F27PBJKO-d8xxi0zi.js} +1 -1
  477. package/static/dist/assets/{chunk-GMAD6QVW-cmalnoaj.js → chunk-GMAD6QVW-jro0hl73.js} +1 -1
  478. package/static/dist/assets/{chunk-GVQU2GXP-gmif5o01.js → chunk-GVQU2GXP-djn6gbyu.js} +1 -1
  479. package/static/dist/assets/{chunk-IMKFNOWR-f1nwwh6u.js → chunk-IMKFNOWR-vw4p5p7o.js} +1 -1
  480. package/static/dist/assets/{chunk-L3NEJ4N5-oktfh5v7.js → chunk-L3NEJ4N5-n1hj77q8.js} +1 -1
  481. package/static/dist/assets/{chunk-OSK3NFVY-b1sdx96z.js → chunk-OSK3NFVY-oy0gwn2n.js} +1 -1
  482. package/static/dist/assets/{chunk-P2QGCYS3-fhu4nzxt.js → chunk-P2QGCYS3-onf1hz6c.js} +1 -1
  483. package/static/dist/assets/{chunk-POPQ4Y6H-em31rnef.js → chunk-POPQ4Y6H-m8sqn7cs.js} +1 -1
  484. package/static/dist/assets/{chunk-PWAF6VOD-bxhhyftw.js → chunk-PWAF6VOD-ek4smc8x.js} +1 -1
  485. package/static/dist/assets/{chunk-SHT3W25Y-fmrvwcav.js → chunk-SHT3W25Y-g9gfvefo.js} +3 -3
  486. package/static/dist/assets/{chunk-SVP7TREG-lymukl2j.js → chunk-SVP7TREG-f9nfv2cu.js} +1 -1
  487. package/static/dist/assets/{chunk-TICWLB2K-lmza6k23.js → chunk-TICWLB2K-bnk3m1mq.js} +1 -1
  488. package/static/dist/assets/classDiagram-ZZMXUADV-kmihoqxh.js +1 -0
  489. package/static/dist/assets/classDiagram-v2-VYDZK3BY-kmihoqxh.js +1 -0
  490. package/static/dist/assets/copied-bli9tbcx.js +1 -0
  491. package/static/dist/assets/{copy-fw5b1pc6.js → copy-jamtd1pf.js} +1 -1
  492. package/static/dist/assets/{cynefin-OW5HDTMX-czlhbyhs.js → cynefin-OW5HDTMX-f8xfdd3f.js} +1 -1
  493. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-gq7mvswb.js → cynefinDiagram-5FMLGOSQ-n9lb99h9.js} +1 -1
  494. package/static/dist/assets/{dagre-GXQ25YYZ-cqdbf7dr.js → dagre-GXQ25YYZ-dgailddi.js} +4 -4
  495. package/static/dist/assets/{diagram-S7CK7UJ4-o6arxa58.js → diagram-S7CK7UJ4-m6aeoe7c.js} +1 -1
  496. package/static/dist/assets/{diagram-UQ7AKVKN-nhnd68tz.js → diagram-UQ7AKVKN-jall3j0x.js} +1 -1
  497. package/static/dist/assets/{diagram-VSXAHHWV-dk2sclzn.js → diagram-VSXAHHWV-hbawi7hn.js} +1 -1
  498. package/static/dist/assets/{diagram-VX7I27RA-n3k5nyx3.js → diagram-VX7I27RA-hendl2tt.js} +1 -1
  499. package/static/dist/assets/{diagram-Z3DM3KII-m6q9cwdh.js → diagram-Z3DM3KII-igwc35p3.js} +1 -1
  500. package/static/dist/assets/{ebnfDiagram-PWID7BFC-lsa5641p.js → ebnfDiagram-PWID7BFC-c64knz08.js} +1 -1
  501. package/static/dist/assets/{erDiagram-RLTQ6QDP-fo2ckbe6.js → erDiagram-RLTQ6QDP-brdu9lcm.js} +1 -1
  502. package/static/dist/assets/eventmodeling-NTZA5JFV-njk1j8wl.js +1 -0
  503. package/static/dist/assets/flowDiagram-HODETNUW-nma6f0ga.js +1 -0
  504. package/static/dist/assets/folder-plus-lvt9kudb.js +1 -0
  505. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-nzqz6x9g.js → ganttDiagram-EL5Y4UJY-ma6jxj5i.js} +1 -1
  506. package/static/dist/assets/{gitGraph-4MIJSDKK-gukvcxe6.js → gitGraph-4MIJSDKK-naa9w6xa.js} +1 -1
  507. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-lqhdp8f0.js → gitGraphDiagram-WWUBYQGX-hzltvsod.js} +1 -1
  508. package/static/dist/assets/{highlighted-body-KPVGNVTW-igw6cp39.js → highlighted-body-KPVGNVTW-ghoavj8d.js} +1 -1
  509. package/static/dist/assets/host-hzm8n85n.js +1 -0
  510. package/static/dist/assets/index-drdcaqeq.css +2 -0
  511. package/static/dist/assets/index-jzm844he.js +11 -0
  512. package/static/dist/assets/{info-A6RAGUB7-gvu7a93l.js → info-A6RAGUB7-mn2ut5ko.js} +1 -1
  513. package/static/dist/assets/{infoDiagram-27XIBGKW-m8t8g6w1.js → infoDiagram-27XIBGKW-h6kuqvsb.js} +1 -1
  514. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-l07vkbgj.js → ishikawaDiagram-5VMMS53U-e7bnwq7h.js} +1 -1
  515. package/static/dist/assets/{journeyDiagram-3NMN7TZE-hybkgj8l.js → journeyDiagram-3NMN7TZE-etd1bi6p.js} +1 -1
  516. package/static/dist/assets/{kanban-definition-UXKFOSKX-by13uvtn.js → kanban-definition-UXKFOSKX-emb1g9dz.js} +4 -4
  517. package/static/dist/assets/lazyRouteComponent-o816ytzl.js +1 -0
  518. package/static/dist/assets/{line-osxq6fq7.js → line-h0ojzsvs.js} +1 -1
  519. package/static/dist/assets/link-k01ryaf9.js +1 -0
  520. package/static/dist/assets/login-bsae49ew.js +1 -0
  521. package/static/dist/assets/mermaid-HWGCJPDP-ihl34s74.js +1 -0
  522. package/static/dist/assets/{mermaid-parser.core-i91oxmou.js → mermaid-parser.core-bnre27u8.js} +3 -3
  523. package/static/dist/assets/{mindmap-definition-YA3MSWOX-m82mx8ww.js → mindmap-definition-YA3MSWOX-o1vathns.js} +1 -1
  524. package/static/dist/assets/onboarding-c65tysya.js +1 -0
  525. package/static/dist/assets/onboarding_model_label-gpn28uix.js +1 -0
  526. package/static/dist/assets/{packet-AYTQ26CC-nbtgge2o.js → packet-AYTQ26CC-hatq7m2e.js} +1 -1
  527. package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
  528. package/static/dist/assets/{pegDiagram-XKGWAZYB-f1zxzga1.js → pegDiagram-XKGWAZYB-gwisz8jv.js} +1 -1
  529. package/static/dist/assets/{pie-WAS4IAKB-of8vuhxs.js → pie-WAS4IAKB-fpftf0mk.js} +1 -1
  530. package/static/dist/assets/{pieDiagram-E7YTZNPT-o7y8ftio.js → pieDiagram-E7YTZNPT-khm1o9a6.js} +1 -1
  531. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-iqiwia1t.js → quadrantDiagram-AXDQQJYC-owqc4uqn.js} +1 -1
  532. package/static/dist/assets/{radar-RG4KPBEZ-delnsy2v.js → radar-RG4KPBEZ-cntfwjpq.js} +1 -1
  533. package/static/dist/assets/{railroad-74A4TZTK-nspnyq9u.js → railroad-74A4TZTK-a7t5eibp.js} +1 -1
  534. package/static/dist/assets/railroad-abnf-HS5TGJTU-ol8cdktf.js +1 -0
  535. package/static/dist/assets/railroad-ebnf-LZEXJU2U-i8vzd429.js +1 -0
  536. package/static/dist/assets/railroad-peg-WCYAUIDC-nw3ulewz.js +1 -0
  537. package/static/dist/assets/{railroadDiagram-O6MQD6OU-z41tqx5z.js → railroadDiagram-O6MQD6OU-etuh0qa0.js} +1 -1
  538. package/static/dist/assets/{refresh-cw-nt1dtsx4.js → refresh-cw-mqbkv2a3.js} +1 -1
  539. package/static/dist/assets/refresh-l29ljp76.js +1 -0
  540. package/static/dist/assets/{requirementDiagram-BXWQKSXE-bdvif194.js → requirementDiagram-BXWQKSXE-oq2ulk22.js} +1 -1
  541. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-h73e0rol.js → sankeyDiagram-P5KCCOFB-iqc02emv.js} +1 -1
  542. package/static/dist/assets/saved-ndd4t2xb.js +1 -0
  543. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-iv95mb1z.js → sequenceDiagram-WJ2MYXX4-i0v116q0.js} +2 -2
  544. package/static/dist/assets/settings_section_extensions_title-jhfo6o8z.js +1 -0
  545. package/static/dist/assets/share._token-c1ri0hy1.js +2 -0
  546. package/static/dist/assets/sliders-horizontal-j3jjt9tv.js +1 -0
  547. package/static/dist/assets/{stateDiagram-D77RDMKH-k417ixmf.js → stateDiagram-D77RDMKH-dnpt1ato.js} +1 -1
  548. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-bxjacpk6.js +1 -0
  549. package/static/dist/assets/{swimlanes-42K2YHIH-esola70w.js → swimlanes-42K2YHIH-loqbdazb.js} +1 -1
  550. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-id62p9hz.js +8 -0
  551. package/static/dist/assets/{timeline-definition-24CTP7MA-getrz2y8.js → timeline-definition-24CTP7MA-ol2hnvma.js} +1 -1
  552. package/static/dist/assets/toast-i3h34v6o.js +64 -0
  553. package/static/dist/assets/{treeView-Q6P3EWNA-hc7ap1q7.js → treeView-Q6P3EWNA-idrj6iox.js} +1 -1
  554. package/static/dist/assets/{treemap-WGGIJYW6-b7k1syy5.js → treemap-WGGIJYW6-lzr49vv5.js} +1 -1
  555. package/static/dist/assets/untitled-oaoya4qj.js +1 -0
  556. package/static/dist/assets/useForm-nfaxgmsk.js +1 -0
  557. package/static/dist/assets/{useParams-lhg566ew.js → useParams-gjc70qru.js} +1 -1
  558. package/static/dist/assets/{useSearch-lbrh6ejn.js → useSearch-e6ae539u.js} +1 -1
  559. package/static/dist/assets/{vennDiagram-4TSXK5OY-n6pcy5qb.js → vennDiagram-4TSXK5OY-bvwr9o2z.js} +1 -1
  560. package/static/dist/assets/{wardley-WFR3VGLG-lpa18vz1.js → wardley-WFR3VGLG-31ui8tma.js} +1 -1
  561. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-kpclwwg7.js → wardleyDiagram-VM6X3IG4-k5wqtx4b.js} +1 -1
  562. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-paa2n8y0.js → xychartDiagram-S5SC5T6Z-aqbgvujg.js} +1 -1
  563. package/static/dist/index.html +1 -1
  564. package/static/dist/sw.js +1 -1
  565. package/static/dist/assets/AppShell-fb2td9zi.js +0 -1
  566. package/static/dist/assets/ChatPage-cj96kiy9.js +0 -25
  567. package/static/dist/assets/Dialog-kxcg7ard.js +0 -1
  568. package/static/dist/assets/ExtensionRoute-jf7h0754.js +0 -1
  569. package/static/dist/assets/HubRoute-gmanpd5y.js +0 -2
  570. package/static/dist/assets/Markdown-mdwlvj3w.js +0 -598
  571. package/static/dist/assets/RightPanel-ftvrjlcp.js +0 -1
  572. package/static/dist/assets/Select-bilaypej.js +0 -1
  573. package/static/dist/assets/SessionListPanel-og8n36qs.js +0 -1
  574. package/static/dist/assets/SettingsLayout-jonjefmj.js +0 -1
  575. package/static/dist/assets/States-nmejrwrm.js +0 -1
  576. package/static/dist/assets/Toaster-nnl8zkt9.js +0 -1
  577. package/static/dist/assets/_app-gdidzd94.js +0 -1
  578. package/static/dist/assets/_app.index-hbarzbjx.js +0 -1
  579. package/static/dist/assets/_app.insights-luywlarz.js +0 -1
  580. package/static/dist/assets/_app.kanban-kincnbr8.js +0 -1
  581. package/static/dist/assets/_app.logs-oi2gr3gs.js +0 -1
  582. package/static/dist/assets/_app.memory-b6jfivt6.js +0 -1
  583. package/static/dist/assets/_app.profiles-lpvixmqp.js +0 -1
  584. package/static/dist/assets/_app.settings-vowx3iq0.js +0 -1
  585. package/static/dist/assets/_app.settings._section-cnntg1tu.js +0 -3
  586. package/static/dist/assets/_app.skills-fb57lpau.js +0 -1
  587. package/static/dist/assets/_app.tasks-eszpx1q7.js +0 -2
  588. package/static/dist/assets/_app.todos-nr05ac6w.js +0 -1
  589. package/static/dist/assets/_app.workspaces-d2luomu6.js +0 -1
  590. package/static/dist/assets/architecture-7GRP2DOG-hd0o7bde.js +0 -1
  591. package/static/dist/assets/back-bemaer09.js +0 -1
  592. package/static/dist/assets/cancel-drd0g6d9.js +0 -1
  593. package/static/dist/assets/channel-cpdgf867.js +0 -1
  594. package/static/dist/assets/classDiagram-ZZMXUADV-ko09sguk.js +0 -1
  595. package/static/dist/assets/classDiagram-v2-VYDZK3BY-ko09sguk.js +0 -1
  596. package/static/dist/assets/copied-ima3pysb.js +0 -1
  597. package/static/dist/assets/endpoints-d33siokx.js +0 -64
  598. package/static/dist/assets/eventmodeling-NTZA5JFV-emctlo64.js +0 -1
  599. package/static/dist/assets/flowDiagram-HODETNUW-bfi3moqa.js +0 -1
  600. package/static/dist/assets/index-cxq58xb1.js +0 -11
  601. package/static/dist/assets/index-hgtx08n5.css +0 -2
  602. package/static/dist/assets/lazyRouteComponent-af0lvgjx.js +0 -1
  603. package/static/dist/assets/link-jyxicdhg.js +0 -1
  604. package/static/dist/assets/login-ec21t39x.js +0 -1
  605. package/static/dist/assets/mermaid-HWGCJPDP-gfl8z3ep.js +0 -1
  606. package/static/dist/assets/onboarding-btnltvyz.js +0 -1
  607. package/static/dist/assets/onboarding_model_label-dp7xffoh.js +0 -1
  608. package/static/dist/assets/railroad-abnf-HS5TGJTU-nst1qxyn.js +0 -1
  609. package/static/dist/assets/railroad-ebnf-LZEXJU2U-ci37x8qy.js +0 -1
  610. package/static/dist/assets/railroad-peg-WCYAUIDC-aimkk07z.js +0 -1
  611. package/static/dist/assets/refresh-b4uams1i.js +0 -1
  612. package/static/dist/assets/saved-kg52csy7.js +0 -1
  613. package/static/dist/assets/settings_section_extensions_title-ej0iapxe.js +0 -1
  614. package/static/dist/assets/share._token-f97e94r0.js +0 -2
  615. package/static/dist/assets/sliders-horizontal-fj18it47.js +0 -1
  616. package/static/dist/assets/sse-fwpqtzbg.js +0 -1
  617. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hwn4apct.js +0 -1
  618. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d1h97oyg.js +0 -8
  619. package/static/dist/assets/todoStore-eginf9h4.js +0 -1
  620. package/static/dist/assets/tts-lxdxnmjd.js +0 -1
  621. package/static/dist/assets/untitled-ovpfthg4.js +0 -1
  622. package/static/dist/assets/useForm-bcblv1rx.js +0 -1
  623. package/static/dist/assets/useLocale-fi5jb8ma.js +0 -1
@@ -5,10 +5,12 @@ import { resolvePathLikePython } from '../workspace/paths.js';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { closeSync, existsSync, fstatSync, lstatSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
7
7
  import { dirname, join, relative, resolve, sep } from 'node:path';
8
- import { atomicWriteText } from '../fs/atomic.js';
8
+ import { InvalidDocument } from '../sessions/storage/backend.js';
9
+ import { readDocument } from '../sessions/storage/document.js';
9
10
  import { readZip } from '../workspace/unzip.js';
10
11
  import { isDict } from '../config/agent-config.js';
11
12
  import { str } from '../util.js';
13
+ import { SKIN_TOKEN_NAMES, SKIN_VALUE_RE } from '@maudecode/talaria-web-contracts';
12
14
  export class ExtensionError extends Error {
13
15
  status;
14
16
  constructor(message, status = 400) {
@@ -34,6 +36,9 @@ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
34
36
  const ALLOWED_ASSET_PREFIXES = ['/extensions/', '/static/'];
35
37
  const ALLOWED_DOTFILES = new Set(['.gitkeep', '.gitignore', '.gitattributes', '.env.example']);
36
38
  export const EXTENSION_MIME = { css: 'text/css', js: 'application/javascript', html: 'text/html', svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', ico: 'image/x-icon', gif: 'image/gif', webp: 'image/webp', woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf', otf: 'font/otf', wasm: 'application/wasm' };
39
+ const SKIN_TOKENS = new Set(SKIN_TOKEN_NAMES);
40
+ /** Skin keys and TTS engine ids share the frontend contract's slug. */
41
+ const DECLARATION_ID_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
37
42
  const TEXT_MIME = new Set(['text/css', 'application/javascript', 'text/html', 'image/svg+xml', 'text/plain']);
38
43
  export const validId = (v) => typeof v === 'string' && ID_RE.test(v.trim());
39
44
  const warn = (d, code, source) => { if (d && !d.warnings.some((w) => w.code === code && w.source === source))
@@ -83,6 +88,41 @@ function isSafeAssetUrl(value) {
83
88
  return isSafeRelativePath(decoded.slice(prefix.length));
84
89
  return false;
85
90
  }
91
+ /**
92
+ * Read `<root>/<rel>` for a static route: `rel` must be a clean relative path (no dot segments or dotfiles), and the
93
+ * read is an anchored, symlink-free open, so a link inside the root cannot expose its target. Python
94
+ * `(root / rel).resolve()`: a symlinked file is served when its target stays inside the root, never otherwise.
95
+ */
96
+ export function readContainedFile(root, rel) {
97
+ if (!isSafeRelativePath(rel))
98
+ return null;
99
+ const anchor = resolvePathLikePython(root);
100
+ let file = resolve(anchor, rel);
101
+ if (!file.startsWith(anchor + sep))
102
+ return null;
103
+ const real = resolvePathLikePython(file);
104
+ if (real !== file) {
105
+ if (!real.startsWith(anchor + sep))
106
+ return null;
107
+ file = real;
108
+ }
109
+ let fd;
110
+ try {
111
+ fd = openAnchoredFd(anchor, file, { wantDir: false });
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ try {
117
+ return fstatSync(fd).isFile() ? { body: readFileSync(fd), file } : null;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ finally {
123
+ closeSync(fd);
124
+ }
125
+ }
86
126
  /**
87
127
  * Python `_normalize_loopback_sidecar_origin` (`urlsplit`, not WHATWG): scheme + literal loopback host + optional
88
128
  * numeric port, nothing else — no path (not even `/`), no query, no fragment, no userinfo, no host aliases.
@@ -144,7 +184,7 @@ export function normalizeProxyPath(value) {
144
184
  const segments = decoded.split('/').slice(1);
145
185
  return segments.length && segments.every((s) => s && s !== '.' && s !== '..') ? candidate : null;
146
186
  }
147
- const text = (v, max = 160) => (typeof v === 'string' ? v.replaceAll(/[\x00-\x1f\x7f]/g, '').trim().slice(0, max) : '');
187
+ export const text = (v, max = 160) => (typeof v === 'string' ? v.replaceAll(/[\x00-\x1f\x7f]/g, '').trim().slice(0, max) : '');
148
188
  const entryText = (e, k) => (typeof e[k] === 'string' ? e[k].trim() : '');
149
189
  const storageOwned = (e) => isDict(e.permissions) && isDict(e.permissions.storage) && e.permissions.storage.owned === true;
150
190
  function settingsSchema(entry) {
@@ -268,6 +308,28 @@ function sidecarFromEntry(entry, d) {
268
308
  }
269
309
  return { id: entryText(entry, 'id'), name: entryText(entry, 'name'), type: 'loopback', origin, health_path: healthPath, health_url: `${origin}${healthPath}`, proxy_auth: proxyAuth };
270
310
  }
311
+ /**
312
+ * A manifest `theme` reduced to the frontend skin contract: allowlisted tokens whose values pass the shared value check,
313
+ * keyed `<extension-id>:<key>` so it never collides with a built-in skin or another extension's skin.
314
+ */
315
+ function themeFromEntry(raw, id) {
316
+ if (!isDict(raw) || typeof raw.key !== 'string' || !DECLARATION_ID_RE.test(raw.key))
317
+ return null;
318
+ const name = text(raw.name, 40);
319
+ const tokens = isDict(raw.tokens) ? Object.fromEntries(Object.entries(raw.tokens).filter(([k, v]) => SKIN_TOKENS.has(k) && typeof v === 'string' && SKIN_VALUE_RE.test(v))) : {};
320
+ if (!name || !Object.keys(tokens).length)
321
+ return null;
322
+ const colors = Array.isArray(raw.colors) ? raw.colors.filter((c) => typeof c === 'string' && SKIN_VALUE_RE.test(c)).slice(0, 3) : null;
323
+ return { key: `${id}:${raw.key}`, name, ...(raw.scheme === 'light' || raw.scheme === 'dark' ? { scheme: raw.scheme } : {}), ...(colors ? { colors } : {}), tokens };
324
+ }
325
+ function ttsFromEntry(raw) {
326
+ if (!isDict(raw) || typeof raw.id !== 'string' || !DECLARATION_ID_RE.test(raw.id))
327
+ return null;
328
+ const label = text(raw.label, 60);
329
+ return label ? { id: raw.id, label } : null;
330
+ }
331
+ /** Entries the gallery loader read from `<root>/<id>/`, keyed by object so a manifest can never claim gallery provenance. */
332
+ const galleryFolders = new WeakMap();
271
333
  export class ExtensionService {
272
334
  deps;
273
335
  registryCache = null;
@@ -278,8 +340,7 @@ export class ExtensionService {
278
340
  this.deps = deps;
279
341
  }
280
342
  get defaultRoot() { return join(this.deps.stateDir, 'extensions'); }
281
- get stateFile() { return join(this.deps.stateDir, 'extension-overrides.json'); }
282
- get installFile() { return join(this.deps.stateDir, 'extension-install-manifest.json'); }
343
+ revisions = new Map();
283
344
  root() {
284
345
  const raw = (this.deps.env.HERMES_WEBUI_EXTENSION_DIR ?? '').trim();
285
346
  if (raw) {
@@ -304,22 +365,25 @@ export class ExtensionService {
304
365
  return null;
305
366
  }
306
367
  }
307
- loadState(d) {
368
+ async loadState(d) {
308
369
  const empty = { version: 1, disabled_extensions: [], sidecar_proxy_consents: {} };
309
370
  const src = 'extension_state';
310
371
  let parsed;
311
372
  try {
312
- if (!existsSync(this.stateFile) || !statSync(this.stateFile).isFile())
373
+ const record = await readDocument(this.deps.documents, 'extensions', 'overrides', { replaceInvalid: true, maxBytes: MAX_STATE_BYTES });
374
+ this.revisions.set('overrides', record?.revision ?? null);
375
+ if (!record)
313
376
  return empty;
314
- const raw = readFileSync(this.stateFile);
315
- if (raw.length > MAX_STATE_BYTES) {
377
+ if (Buffer.byteLength(JSON.stringify(record.value)) > MAX_STATE_BYTES) {
316
378
  warn(d, 'extension_state_oversized', src);
317
379
  return empty;
318
380
  }
319
- parsed = JSON.parse(raw.toString('utf8'));
381
+ parsed = record.value;
320
382
  }
321
- catch {
322
- warn(d, 'extension_state_unreadable', src);
383
+ catch (error) {
384
+ if (error instanceof InvalidDocument && error.reason === 'too-large')
385
+ this.revisions.set('overrides', error.revision);
386
+ warn(d, error instanceof InvalidDocument && error.reason === 'too-large' ? 'extension_state_oversized' : 'extension_state_unreadable', src);
323
387
  return empty;
324
388
  }
325
389
  if (!isDict(parsed)) {
@@ -379,19 +443,17 @@ export class ExtensionService {
379
443
  warn(d, 'extension_state_invalid_entries', src);
380
444
  return { version: 1, disabled_extensions: disabled, sidecar_proxy_consents: consents };
381
445
  }
382
- writeState(state) {
383
- mkdirSync(this.deps.stateDir, { recursive: true });
384
- atomicWriteText(this.stateFile, JSON.stringify({ version: 1, disabled_extensions: [...new Set(state.disabled_extensions.filter(validId))].sort(), sidecar_proxy_consents: state.sidecar_proxy_consents }, null, 2));
446
+ async writeState(state) {
447
+ this.revisions.set('overrides', await this.deps.documents.put('extensions', 'overrides', { version: 1, disabled_extensions: [...new Set(state.disabled_extensions.filter(validId))].sort(), sidecar_proxy_consents: state.sidecar_proxy_consents }, this.revisions.get('overrides') ?? null));
385
448
  }
386
- loadInstallManifest() {
449
+ async loadInstallManifest() {
387
450
  const empty = { version: 1, installed: {} };
388
451
  try {
389
- if (!existsSync(this.installFile))
452
+ const record = await readDocument(this.deps.documents, 'extensions', 'install-manifest', { replaceInvalid: true, maxBytes: MAX_INSTALL_MANIFEST_BYTES });
453
+ this.revisions.set('install-manifest', record?.revision ?? null);
454
+ if (!record || Buffer.byteLength(JSON.stringify(record.value)) > MAX_INSTALL_MANIFEST_BYTES)
390
455
  return empty;
391
- const raw = readFileSync(this.installFile);
392
- if (raw.length > MAX_INSTALL_MANIFEST_BYTES)
393
- return empty;
394
- const parsed = JSON.parse(raw.toString('utf8'));
456
+ const parsed = record.value;
395
457
  if (!isDict(parsed) || !isDict(parsed.installed))
396
458
  return empty;
397
459
  const installed = {};
@@ -404,13 +466,14 @@ export class ExtensionService {
404
466
  }
405
467
  return { version: 1, installed };
406
468
  }
407
- catch {
469
+ catch (error) {
470
+ if (error instanceof InvalidDocument && error.reason === 'too-large')
471
+ this.revisions.set('install-manifest', error.revision);
408
472
  return empty;
409
473
  }
410
474
  }
411
- writeInstallManifest(manifest) {
412
- mkdirSync(this.deps.stateDir, { recursive: true });
413
- atomicWriteText(this.installFile, JSON.stringify(manifest, null, 2));
475
+ async writeInstallManifest(manifest) {
476
+ this.revisions.set('install-manifest', await this.deps.documents.put('extensions', 'install-manifest', manifest, this.revisions.get('install-manifest') ?? null));
414
477
  }
415
478
  manifestPath(root) {
416
479
  const raw = (this.deps.env.HERMES_WEBUI_EXTENSION_MANIFEST ?? '').trim();
@@ -431,16 +494,17 @@ export class ExtensionService {
431
494
  return [null, 'invalid_path'];
432
495
  return [manifest, 'configured'];
433
496
  }
434
- galleryManifest(root, d) {
435
- const installed = this.loadInstallManifest().installed;
497
+ async galleryManifest(root, d) {
498
+ const installed = (await this.loadInstallManifest()).installed;
436
499
  const entries = [];
437
500
  for (const id of Object.keys(installed).sort()) {
438
501
  if (!validId(id))
439
502
  continue;
440
- const file = join(root, id, 'manifest.json');
503
+ // `manifest.json` wins; a gallery zip may ship only `extension.json` (docs/EXTENSIONS.md).
441
504
  let manifest;
442
505
  try {
443
- if (!existsSync(file) || !statSync(file).isFile()) {
506
+ const file = ['manifest.json', 'extension.json'].map((name) => join(root, id, name)).find((p) => existsSync(p) && statSync(p).isFile());
507
+ if (!file) {
444
508
  warn(d, 'gallery_manifest_missing', 'gallery');
445
509
  continue;
446
510
  }
@@ -457,17 +521,26 @@ export class ExtensionService {
457
521
  }
458
522
  const withBase = (entry) => {
459
523
  const copy = { ...entry };
524
+ // Paths already rooted at the id (the `extensions.json` shape, as in the shipped example) are not prefixed twice.
525
+ const rebase = (v) => {
526
+ if (typeof v !== 'string' || !v.trim() || /^[a-z]+:|^\/\/|^\//i.test(v.trim()))
527
+ return v;
528
+ return v.trim().startsWith(`${id}/`) ? v.trim() : `${id}/${v.trim()}`;
529
+ };
460
530
  for (const key of ['scripts', 'stylesheets'])
461
531
  if (Array.isArray(copy[key]))
462
- copy[key] = copy[key].map((v) => (typeof v === 'string' && v.trim() && !/^[a-z]+:|^\/\/|^\//i.test(v.trim()) ? `${id}/${v.trim()}` : v));
532
+ copy[key] = copy[key].map(rebase);
533
+ if (typeof copy.panel === 'string')
534
+ copy.panel = rebase(copy.panel.trim().replace(/^(\.\/)+/, ''));
535
+ galleryFolders.set(copy, id);
463
536
  return copy;
464
537
  };
465
538
  if (isDict(manifest)) {
466
539
  const top = { id };
467
- for (const key of ['name', 'enabled', 'scripts', 'stylesheets', 'sidecar', 'permissions', 'settings_schema'])
540
+ for (const key of ['name', 'enabled', 'version', 'description', 'panel', 'nav', 'capabilities', 'scripts', 'stylesheets', 'sidecar', 'theme', 'tts', 'permissions', 'settings_schema'])
468
541
  if (key in manifest)
469
542
  top[key] = manifest[key];
470
- if (['scripts', 'stylesheets', 'sidecar', 'permissions', 'settings_schema'].some((k) => k in top))
543
+ if (['panel', 'scripts', 'stylesheets', 'sidecar', 'theme', 'tts', 'permissions', 'settings_schema'].some((k) => k in top))
471
544
  entries.push(withBase(top));
472
545
  }
473
546
  for (const entry of manifestEntries(manifest)) {
@@ -479,14 +552,14 @@ export class ExtensionService {
479
552
  }
480
553
  return entries.length ? { extensions: entries } : null;
481
554
  }
482
- loadManifest(root, d) {
555
+ async loadManifest(root, d) {
483
556
  const [file, pathStatus] = this.manifestPath(root);
484
557
  const status = { configured: pathStatus !== 'not_configured', loaded: false, status: pathStatus, _asset_base: '', entry_count: 0, script_count: 0, stylesheet_count: 0, sidecar_count: 0 };
485
558
  if (!file) {
486
559
  if (pathStatus === 'invalid_path')
487
560
  warn(d, 'manifest_invalid_path', 'manifest');
488
561
  else if (pathStatus === 'not_configured') {
489
- const gallery = this.galleryManifest(root, d);
562
+ const gallery = (await this.galleryManifest(root, d));
490
563
  if (gallery)
491
564
  return [gallery, { ...status, loaded: true, status: 'gallery_installed' }];
492
565
  }
@@ -650,9 +723,9 @@ export class ExtensionService {
650
723
  break;
651
724
  }
652
725
  }
653
- const { records } = await this.sidecarRecords(manifest, disabled, state, d);
726
+ const { records, byId } = await this.sidecarRecords(manifest, disabled, state, d);
654
727
  Object.assign(status, { loaded: true, status: str(status.status) || 'loaded', script_count: scripts.length, stylesheet_count: stylesheets.length, sidecar_count: records.length });
655
- return { scripts, stylesheets, sidecars: records };
728
+ return { scripts, stylesheets, sidecars: records, sidecarsById: byId };
656
729
  }
657
730
  /** Python `get_extension_status`. */
658
731
  /** Python `get_extension_config()["enabled"]` for the bootstrap feature flag: a valid root with a loadable manifest. */
@@ -661,33 +734,37 @@ export class ExtensionService {
661
734
  return this.root() !== null;
662
735
  }
663
736
  async status() {
737
+ return (await this.snapshot()).status;
738
+ }
739
+ /** The status payload plus the manifest and sidecar records it was built from, so `manifests()` reads the same load. */
740
+ async snapshot() {
664
741
  const d = { warnings: [] };
665
742
  const envDir = (this.deps.env.HERMES_WEBUI_EXTENSION_DIR ?? '').trim();
666
743
  const root = this.root();
667
- const state = this.loadState(d);
744
+ const state = (await this.loadState(d));
668
745
  const disabled = new Set(state.disabled_extensions);
669
746
  const manifestConfigured = Boolean((this.deps.env.HERMES_WEBUI_EXTENSION_MANIFEST ?? '').trim());
670
747
  if (envDir && !root)
671
748
  warn(d, 'extension_dir_unavailable', 'extension_dir');
672
749
  if (!root) {
673
- return { enabled: false, extension_dir_configured: true, extension_dir_valid: false, script_urls: [], stylesheet_urls: [], sidecars: [], counts: { script_urls: 0, stylesheet_urls: 0, sidecars: 0, manifest_extensions: 0, user_disabled: 0 }, manifest: { configured: manifestConfigured, loaded: false, status: manifestConfigured ? 'extension_disabled' : 'not_configured', entry_count: 0, script_count: 0, stylesheet_count: 0, sidecar_count: 0 }, extensions: [], warnings: d.warnings };
750
+ return { manifest: null, assetBase: '', sidecarsById: new Map(), status: { enabled: false, extension_dir_configured: true, extension_dir_valid: false, script_urls: [], stylesheet_urls: [], sidecars: [], counts: { script_urls: 0, stylesheet_urls: 0, sidecars: 0, manifest_extensions: 0, user_disabled: 0 }, manifest: { configured: manifestConfigured, loaded: false, status: manifestConfigured ? 'extension_disabled' : 'not_configured', entry_count: 0, script_count: 0, stylesheet_count: 0, sidecar_count: 0 }, extensions: [], warnings: d.warnings } };
674
751
  }
675
- const [manifest, status] = this.loadManifest(root, d);
752
+ const [manifest, status] = (await this.loadManifest(root, d));
676
753
  const consentIds = new Set(Object.keys(state.sidecar_proxy_consents));
677
754
  const ext = manifest !== null ? this.extensionState(manifest, disabled, d, consentIds) : { extensions: [], knownIds: new Set(), manifestDisabled: new Set() };
678
- const urls = manifest !== null ? await this.manifestUrls(manifest, status, disabled, state, d) : { scripts: [], stylesheets: [], sidecars: [] };
755
+ const urls = manifest !== null ? await this.manifestUrls(manifest, status, disabled, state, d) : { scripts: [], stylesheets: [], sidecars: [], sidecarsById: new Map() };
679
756
  const scriptUrls = this.envUrls('HERMES_WEBUI_EXTENSION_SCRIPT_URLS', urls.scripts.length ? urls.scripts : null, d);
680
757
  const stylesheetUrls = this.envUrls('HERMES_WEBUI_EXTENSION_STYLESHEET_URLS', urls.stylesheets.length ? urls.stylesheets : null, d);
681
758
  const publicStatus = Object.fromEntries(Object.entries(status).filter(([k]) => !k.startsWith('_')));
682
- return {
683
- enabled: true, extension_dir_configured: true, extension_dir_valid: true, script_urls: scriptUrls, stylesheet_urls: stylesheetUrls, sidecars: urls.sidecars,
684
- counts: { script_urls: scriptUrls.length, stylesheet_urls: stylesheetUrls.length, sidecars: urls.sidecars.length, manifest_extensions: ext.extensions.length, user_disabled: [...disabled].filter((id) => ext.knownIds.has(id)).length },
685
- manifest: publicStatus, extensions: ext.extensions, gallery_installed: this.loadInstallManifest().installed, warnings: d.warnings,
686
- };
759
+ return { manifest, assetBase: str(status._asset_base), sidecarsById: urls.sidecarsById, status: {
760
+ enabled: true, extension_dir_configured: true, extension_dir_valid: true, script_urls: scriptUrls, stylesheet_urls: stylesheetUrls, sidecars: urls.sidecars,
761
+ counts: { script_urls: scriptUrls.length, stylesheet_urls: stylesheetUrls.length, sidecars: urls.sidecars.length, manifest_extensions: ext.extensions.length, user_disabled: [...disabled].filter((id) => ext.knownIds.has(id)).length },
762
+ manifest: publicStatus, extensions: ext.extensions, gallery_installed: (await this.loadInstallManifest()).installed, warnings: d.warnings,
763
+ } };
687
764
  }
688
- loaded(root, d) {
689
- const state = this.loadState(d);
690
- const [manifest, status] = this.loadManifest(root, d);
765
+ async loaded(root, d) {
766
+ const state = (await this.loadState(d));
767
+ const [manifest, status] = (await this.loadManifest(root, d));
691
768
  if (manifest === null || status.loaded !== true)
692
769
  throw new ExtensionError('Extension manifest is not loaded', 409);
693
770
  return { manifest, state, disabled: new Set(state.disabled_extensions) };
@@ -702,14 +779,14 @@ export class ExtensionService {
702
779
  throw new ExtensionError('Invalid extension id', 400);
703
780
  if (typeof enabled !== 'boolean')
704
781
  throw new ExtensionError('enabled must be a boolean', 400);
705
- return this.mutateState(async () => this.setEnabledLocked(idRaw.trim(), enabled));
782
+ return this.mutateState(async () => (await this.setEnabledLocked(idRaw.trim(), enabled)));
706
783
  }
707
784
  async setEnabledLocked(id, enabled) {
708
785
  const root = this.root();
709
786
  if (!root)
710
787
  throw new ExtensionError('Extensions are not configured', 404);
711
788
  const d = { warnings: [] };
712
- const { manifest, state, disabled } = this.loaded(root, d);
789
+ const { manifest, state, disabled } = (await this.loaded(root, d));
713
790
  const ext = this.extensionState(manifest, disabled, d, new Set(Object.keys(state.sidecar_proxy_consents)));
714
791
  if (!ext.knownIds.has(id))
715
792
  throw new ExtensionError('Extension not found', 404);
@@ -719,7 +796,7 @@ export class ExtensionService {
719
796
  disabled.delete(id);
720
797
  else
721
798
  disabled.add(id);
722
- this.writeState({ disabled_extensions: [...disabled], sidecar_proxy_consents: Object.fromEntries(Object.entries(state.sidecar_proxy_consents).filter(([k]) => ext.knownIds.has(k))) });
799
+ await this.writeState({ disabled_extensions: [...disabled], sidecar_proxy_consents: Object.fromEntries(Object.entries(state.sidecar_proxy_consents).filter(([k]) => ext.knownIds.has(k))) });
723
800
  return this.status();
724
801
  }
725
802
  setConsent(idRaw, approved) {
@@ -727,14 +804,14 @@ export class ExtensionService {
727
804
  throw new ExtensionError('Invalid extension id', 400);
728
805
  if (typeof approved !== 'boolean')
729
806
  throw new ExtensionError('approved must be a boolean', 400);
730
- return this.mutateState(() => this.setConsentLocked(idRaw.trim(), approved));
807
+ return this.mutateState(async () => (await this.setConsentLocked(idRaw.trim(), approved)));
731
808
  }
732
809
  async setConsentLocked(id, approved) {
733
810
  const root = this.root();
734
811
  if (!root)
735
812
  throw new ExtensionError('Extensions are not configured', 404);
736
813
  const d = { warnings: [] };
737
- const { manifest, state, disabled } = this.loaded(root, d);
814
+ const { manifest, state, disabled } = (await this.loaded(root, d));
738
815
  const consents = { ...state.sidecar_proxy_consents };
739
816
  const ext = this.extensionState(manifest, disabled, d, new Set(Object.keys(consents)));
740
817
  if (!ext.knownIds.has(id))
@@ -752,7 +829,7 @@ export class ExtensionService {
752
829
  }
753
830
  else
754
831
  Reflect.deleteProperty(consents, id);
755
- this.writeState({ disabled_extensions: [...disabled], sidecar_proxy_consents: Object.fromEntries(Object.entries(consents).filter(([k]) => ext.knownIds.has(k))) });
832
+ await this.writeState({ disabled_extensions: [...disabled], sidecar_proxy_consents: Object.fromEntries(Object.entries(consents).filter(([k]) => ext.knownIds.has(k))) });
756
833
  return this.status();
757
834
  }
758
835
  /** Python `resolve_extension_sidecar_proxy_target` (legacy proxy auth only). */
@@ -767,7 +844,7 @@ export class ExtensionService {
767
844
  throw new ExtensionError('Extensions are not configured', 404);
768
845
  const id = idRaw.trim();
769
846
  const d = { warnings: [] };
770
- const { manifest, state, disabled } = this.loaded(root, d);
847
+ const { manifest, state, disabled } = (await this.loaded(root, d));
771
848
  const ext = this.extensionState(manifest, disabled, d, new Set(Object.keys(state.sidecar_proxy_consents)));
772
849
  if (!ext.knownIds.has(id))
773
850
  throw new ExtensionError('Extension not found', 404);
@@ -833,233 +910,219 @@ export class ExtensionService {
833
910
  }
834
911
  }
835
912
  async install(idRaw, downloadUrl, sha256) {
836
- if (!validId(idRaw))
837
- throw new ExtensionError('Invalid extension id');
838
- const id = idRaw.trim();
839
- if (typeof downloadUrl !== 'string' || !downloadUrl.startsWith('https://'))
840
- throw new ExtensionError('Invalid download URL');
841
- let host = '';
842
- try {
843
- host = new URL(downloadUrl).hostname;
844
- }
845
- catch {
846
- host = '';
847
- }
848
- if (!ALLOWED_DOWNLOAD_HOSTS.has(host))
849
- throw new ExtensionError('Invalid download URL');
850
- if (typeof sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(sha256))
851
- throw new ExtensionError('Invalid sha256');
852
- const root = this.writableRoot();
853
- if (!root)
854
- throw new ExtensionError('Extensions not configured', 404);
855
- let raw;
856
- try {
857
- const res = await this.galleryFetch(downloadUrl, 30_000);
858
- if (!res.ok)
859
- throw new Error(`download ${String(res.status)}`);
860
- const capped = await readCapped(res, MAX_ZIP_BYTES);
861
- if (!capped)
862
- throw new ExtensionError('Download too large');
863
- raw = capped;
864
- }
865
- catch (error) {
866
- throw new ExtensionError(error instanceof ExtensionError ? error.message : 'Download failed', error instanceof ExtensionError ? error.status : 502);
867
- }
868
- if (createHash('sha256').update(raw).digest('hex') !== sha256)
869
- throw new ExtensionError('SHA-256 mismatch');
870
- let entries;
871
- try {
872
- entries = readZip(raw);
873
- }
874
- catch {
875
- throw new ExtensionError('Invalid zip archive');
876
- }
877
- const files = entries.filter((e) => e.name && !e.isDir);
878
- if (files.reduce((acc, e) => acc + e.size, 0) > MAX_ZIP_BYTES * 10)
879
- throw new ExtensionError('Archive uncompressed size exceeds limit');
880
- if (files.length > 1024)
881
- throw new ExtensionError('Archive contains too many files');
882
- const prefix = files.every((e) => e.name.startsWith(`${id}/`)) ? `${id}/` : '';
883
- const stripped = (name) => (prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name);
884
- const anchor = resolvePathLikePython(root);
885
- const extDir = resolve(anchor, id);
886
- for (const e of files) {
887
- const decoded = fullyUnquote(stripped(e.name));
888
- if (!decoded || !isSafeArchiveMember(decoded))
889
- throw new ExtensionError('Unsafe archive member');
890
- const target = resolve(extDir, decoded);
891
- if (!target.startsWith(anchor + sep) || !target.startsWith(extDir + sep))
892
- throw new ExtensionError('Zip-slip detected');
893
- }
894
- let version = 'unknown';
895
- for (const vfile of ['extension.json', 'manifest.json']) {
896
- const entry = entries.find((e) => e.name === prefix + vfile);
897
- if (!entry)
898
- continue;
913
+ return await this.mutateState(async () => {
914
+ if (!validId(idRaw))
915
+ throw new ExtensionError('Invalid extension id');
916
+ const id = idRaw.trim();
917
+ if (typeof downloadUrl !== 'string' || !downloadUrl.startsWith('https://'))
918
+ throw new ExtensionError('Invalid download URL');
919
+ let host = '';
899
920
  try {
900
- const m = JSON.parse(entry.read().toString('utf8'));
901
- if (isDict(m) && typeof m.version === 'string') {
902
- version = m.version;
903
- break;
904
- }
921
+ host = new URL(downloadUrl).hostname;
905
922
  }
906
- catch { /* ignore */ }
907
- }
908
- try {
909
- if (lstatSync(extDir).isSymbolicLink())
910
- throw new ExtensionError('Extension directory is a symlink', 400);
911
- }
912
- catch (error) {
913
- if (error instanceof ExtensionError)
914
- throw error;
915
- }
916
- try {
917
- makeAnchoredDir(anchor, extDir);
918
- }
919
- catch {
920
- throw new ExtensionError('Extension directory is a symlink', 400);
921
- }
922
- const written = [];
923
- const rollback = () => {
924
- for (const p of written) {
925
- try {
926
- unlinkAnchored(anchor, p);
927
- }
928
- catch { /* gone */ }
923
+ catch {
924
+ host = '';
929
925
  }
926
+ if (!ALLOWED_DOWNLOAD_HOSTS.has(host))
927
+ throw new ExtensionError('Invalid download URL');
928
+ if (typeof sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(sha256))
929
+ throw new ExtensionError('Invalid sha256');
930
+ const root = this.writableRoot();
931
+ if (!root)
932
+ throw new ExtensionError('Extensions not configured', 404);
933
+ let raw;
930
934
  try {
931
- rmdirAnchored(anchor, extDir);
935
+ const res = await this.galleryFetch(downloadUrl, 30_000);
936
+ if (!res.ok)
937
+ throw new Error(`download ${String(res.status)}`);
938
+ const capped = await readCapped(res, MAX_ZIP_BYTES);
939
+ if (!capped)
940
+ throw new ExtensionError('Download too large');
941
+ raw = capped;
932
942
  }
933
- catch { /* not empty, or gone */ }
934
- };
935
- try {
936
- // Every destination is created or truncated through the anchored walk from the extension root, so a symlink
937
- // already inside the extension (or a parent swapped mid-extraction) can never redirect a write outside it.
943
+ catch (error) {
944
+ throw new ExtensionError(error instanceof ExtensionError ? error.message : 'Download failed', error instanceof ExtensionError ? error.status : 502);
945
+ }
946
+ if (createHash('sha256').update(raw).digest('hex') !== sha256)
947
+ throw new ExtensionError('SHA-256 mismatch');
948
+ let entries;
949
+ try {
950
+ entries = readZip(raw);
951
+ }
952
+ catch {
953
+ throw new ExtensionError('Invalid zip archive');
954
+ }
955
+ const files = entries.filter((e) => e.name && !e.isDir);
956
+ if (files.reduce((acc, e) => acc + e.size, 0) > MAX_ZIP_BYTES * 10)
957
+ throw new ExtensionError('Archive uncompressed size exceeds limit');
958
+ if (files.length > 1024)
959
+ throw new ExtensionError('Archive contains too many files');
960
+ const prefix = files.every((e) => e.name.startsWith(`${id}/`)) ? `${id}/` : '';
961
+ const stripped = (name) => (prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name);
962
+ const anchor = resolvePathLikePython(root);
963
+ const extDir = resolve(anchor, id);
938
964
  for (const e of files) {
939
- const dest = resolve(extDir, fullyUnquote(stripped(e.name)));
940
- let fd;
965
+ const decoded = fullyUnquote(stripped(e.name));
966
+ if (!decoded || !isSafeArchiveMember(decoded))
967
+ throw new ExtensionError('Unsafe archive member');
968
+ const target = resolve(extDir, decoded);
969
+ if (!target.startsWith(anchor + sep) || !target.startsWith(extDir + sep))
970
+ throw new ExtensionError('Zip-slip detected');
971
+ }
972
+ let version = 'unknown';
973
+ for (const vfile of ['extension.json', 'manifest.json']) {
974
+ const entry = entries.find((e) => e.name === prefix + vfile);
975
+ if (!entry)
976
+ continue;
941
977
  try {
942
- fd = openAnchoredCreateFd(anchor, dest);
978
+ const m = JSON.parse(entry.read().toString('utf8'));
979
+ if (isDict(m) && typeof m.version === 'string') {
980
+ version = m.version;
981
+ break;
982
+ }
943
983
  }
944
- catch (error) {
945
- if (!(error instanceof FileExistsError))
946
- throw error;
947
- fd = openAnchoredWriteFd(anchor, dest);
984
+ catch { /* ignore */ }
985
+ }
986
+ try {
987
+ if (lstatSync(extDir).isSymbolicLink())
988
+ throw new ExtensionError('Extension directory is a symlink', 400);
989
+ }
990
+ catch (error) {
991
+ if (error instanceof ExtensionError)
992
+ throw error;
993
+ }
994
+ try {
995
+ makeAnchoredDir(anchor, extDir);
996
+ }
997
+ catch {
998
+ throw new ExtensionError('Extension directory is a symlink', 400);
999
+ }
1000
+ const written = [];
1001
+ const rollback = () => {
1002
+ for (const p of written) {
1003
+ try {
1004
+ unlinkAnchored(anchor, p);
1005
+ }
1006
+ catch { /* gone */ }
948
1007
  }
949
1008
  try {
950
- writeFileSync(fd, e.read());
1009
+ rmdirAnchored(anchor, extDir);
951
1010
  }
952
- finally {
953
- closeSync(fd);
1011
+ catch { /* not empty, or gone */ }
1012
+ };
1013
+ try {
1014
+ // Every destination is created or truncated through the anchored walk from the extension root, so a symlink
1015
+ // already inside the extension (or a parent swapped mid-extraction) can never redirect a write outside it.
1016
+ for (const e of files) {
1017
+ const dest = resolve(extDir, fullyUnquote(stripped(e.name)));
1018
+ let fd;
1019
+ try {
1020
+ fd = openAnchoredCreateFd(anchor, dest);
1021
+ }
1022
+ catch (error) {
1023
+ if (!(error instanceof FileExistsError))
1024
+ throw error;
1025
+ fd = openAnchoredWriteFd(anchor, dest);
1026
+ }
1027
+ try {
1028
+ writeFileSync(fd, e.read());
1029
+ }
1030
+ finally {
1031
+ closeSync(fd);
1032
+ }
1033
+ written.push(dest);
954
1034
  }
955
- written.push(dest);
956
1035
  }
957
- }
958
- catch {
959
- rollback();
960
- throw new ExtensionError('Extraction failed', 500);
961
- }
962
- try {
963
- const manifest = this.loadInstallManifest();
964
- const record = { version, files: written.map((p) => relative(extDir, p).split(sep).join('/')), installed_at: new Date().toISOString() };
965
- const next = { version: 1, installed: { ...manifest.installed, [id]: record } };
966
- if (Buffer.byteLength(JSON.stringify(next, null, 2)) > MAX_INSTALL_MANIFEST_BYTES)
967
- throw new ExtensionError('Install manifest would exceed size limit');
968
- this.writeInstallManifest(next);
969
- }
970
- catch (error) {
971
- rollback();
972
- throw error instanceof ExtensionError ? error : new ExtensionError('Failed to record install', 500);
973
- }
974
- return { installed: true, id, version };
975
- }
976
- uninstall(idRaw) {
977
- if (!validId(idRaw))
978
- throw new ExtensionError('Invalid extension id');
979
- const id = idRaw.trim();
980
- const root = this.root();
981
- if (!root)
982
- throw new ExtensionError('Extensions not configured', 404);
983
- const manifest = this.loadInstallManifest();
984
- const entry = manifest.installed[id];
985
- if (!entry)
986
- throw new ExtensionError('Extension not installed', 404);
987
- const anchor = resolvePathLikePython(root);
988
- const extDir = resolve(anchor, id);
989
- for (const rel of entry.files) {
990
- if (!isSafeArchiveMember(rel))
991
- continue;
992
- const target = resolve(extDir, rel);
993
- if (!target.startsWith(extDir + sep))
994
- continue;
995
- try {
996
- unlinkAnchored(anchor, target);
1036
+ catch {
1037
+ rollback();
1038
+ throw new ExtensionError('Extraction failed', 500);
997
1039
  }
998
- catch { /* gone */ }
999
- }
1000
- // Prune now-empty directories, enumerating and removing each through the anchored walk so a directory replaced by
1001
- // a symlink is skipped rather than followed into its target.
1002
- const prune = (dir) => {
1003
- let subdirs;
1004
1040
  try {
1005
- subdirs = withAnchoredDir(anchor, dir, (child) => readdirSync(child('.'), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name));
1041
+ const manifest = (await this.loadInstallManifest());
1042
+ const record = { version, files: written.map((p) => relative(extDir, p).split(sep).join('/')), installed_at: new Date().toISOString() };
1043
+ const next = { version: 1, installed: { ...manifest.installed, [id]: record } };
1044
+ if (Buffer.byteLength(JSON.stringify(next, null, 2)) > MAX_INSTALL_MANIFEST_BYTES)
1045
+ throw new ExtensionError('Install manifest would exceed size limit');
1046
+ await this.writeInstallManifest(next);
1006
1047
  }
1007
- catch {
1008
- return;
1048
+ catch (error) {
1049
+ rollback();
1050
+ throw error instanceof ExtensionError ? error : new ExtensionError('Failed to record install', 500);
1009
1051
  }
1010
- for (const name of subdirs)
1011
- prune(join(dir, name));
1012
- try {
1013
- rmdirAnchored(anchor, dir);
1052
+ return { installed: true, id, version };
1053
+ });
1054
+ }
1055
+ async uninstall(idRaw) {
1056
+ return await this.mutateState(async () => {
1057
+ if (!validId(idRaw))
1058
+ throw new ExtensionError('Invalid extension id');
1059
+ const id = idRaw.trim();
1060
+ const root = this.root();
1061
+ if (!root)
1062
+ throw new ExtensionError('Extensions not configured', 404);
1063
+ const manifest = (await this.loadInstallManifest());
1064
+ const entry = manifest.installed[id];
1065
+ if (!entry)
1066
+ throw new ExtensionError('Extension not installed', 404);
1067
+ const anchor = resolvePathLikePython(root);
1068
+ const extDir = resolve(anchor, id);
1069
+ for (const rel of entry.files) {
1070
+ if (!isSafeArchiveMember(rel))
1071
+ continue;
1072
+ const target = resolve(extDir, rel);
1073
+ if (!target.startsWith(extDir + sep))
1074
+ continue;
1075
+ try {
1076
+ unlinkAnchored(anchor, target);
1077
+ }
1078
+ catch { /* gone */ }
1014
1079
  }
1015
- catch { /* not empty, or gone */ }
1016
- };
1017
- prune(extDir);
1018
- Reflect.deleteProperty(manifest.installed, id);
1019
- this.writeInstallManifest(manifest);
1020
- return { uninstalled: true, id };
1080
+ // Prune now-empty directories, enumerating and removing each through the anchored walk so a directory replaced by
1081
+ // a symlink is skipped rather than followed into its target.
1082
+ const prune = (dir) => {
1083
+ let subdirs;
1084
+ try {
1085
+ subdirs = withAnchoredDir(anchor, dir, (child) => readdirSync(child('.'), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name));
1086
+ }
1087
+ catch {
1088
+ return;
1089
+ }
1090
+ for (const name of subdirs)
1091
+ prune(join(dir, name));
1092
+ try {
1093
+ rmdirAnchored(anchor, dir);
1094
+ }
1095
+ catch { /* not empty, or gone */ }
1096
+ };
1097
+ prune(extDir);
1098
+ Reflect.deleteProperty(manifest.installed, id);
1099
+ await this.writeInstallManifest(manifest);
1100
+ return { uninstalled: true, id };
1101
+ });
1021
1102
  }
1022
1103
  /** Python `serve_extension_static`: bytes, MIME, sandbox CSP for HTML; null means 404. */
1023
1104
  staticFile(rel) {
1024
1105
  const root = this.root();
1025
- if (!root || !isSafeRelativePath(rel))
1106
+ const found = root ? readContainedFile(root, rel) : null;
1107
+ if (!found)
1026
1108
  return null;
1027
- // Python `serve_extension_static`: an anchored, symlink-free open so a link inside the extension dir cannot expose its target.
1028
- const anchor = resolvePathLikePython(root);
1029
- let file = resolve(anchor, rel);
1030
- if (!file.startsWith(anchor + sep))
1031
- return null;
1032
- // Python `(root / rel).resolve()`: a symlinked asset is served when its target stays inside the root, never otherwise.
1033
- const real = resolvePathLikePython(file);
1034
- if (real !== file) {
1035
- if (!real.startsWith(anchor + sep))
1036
- return null;
1037
- file = real;
1038
- }
1039
- let fd;
1040
- try {
1041
- fd = openAnchoredFd(anchor, file, { wantDir: false });
1042
- }
1043
- catch {
1044
- return null;
1045
- }
1046
- try {
1047
- if (!fstatSync(fd).isFile())
1048
- return null;
1049
- const ext = file.slice(file.lastIndexOf('.') + 1).toLowerCase();
1050
- const ct = EXTENSION_MIME[ext] ?? 'text/plain';
1051
- return { body: readFileSync(fd), contentType: TEXT_MIME.has(ct) ? `${ct}; charset=utf-8` : ct, html: ct === 'text/html' };
1052
- }
1053
- catch {
1054
- return null;
1055
- }
1056
- finally {
1057
- closeSync(fd);
1058
- }
1109
+ const ext = found.file.slice(found.file.lastIndexOf('.') + 1).toLowerCase();
1110
+ const ct = EXTENSION_MIME[ext] ?? 'text/plain';
1111
+ return { body: found.body, contentType: TEXT_MIME.has(ct) ? `${ct}; charset=utf-8` : ct, html: ct === 'text/html' };
1059
1112
  }
1060
- /** Python `api/extension_manifests.build_manifests` (extension entries only; dashboard plugins were dropped). */
1113
+ /** Python `api/extension_manifests.build_manifests` extension entries (the manifests route appends dashboard plugins). */
1061
1114
  async manifests() {
1062
- const status = await this.status();
1115
+ const { status, manifest, assetBase, sidecarsById } = await this.snapshot();
1116
+ // Status rows carry only enable state; panel, nav, capabilities, assets, and metadata come from the first manifest entry per id.
1117
+ const rawById = new Map();
1118
+ for (const e of manifestEntries(manifest)) {
1119
+ const id = entryText(e, 'id');
1120
+ if (!rawById.has(id))
1121
+ rawById.set(id, e);
1122
+ }
1123
+ // Gallery rows are the entries the gallery loader read from their own install folder; `uninstall` accepts exactly the
1124
+ // ids in the install manifest, so a configured entry that shares an installed id never offers Uninstall.
1125
+ const installed = isDict(status.gallery_installed) ? status.gallery_installed : {};
1063
1126
  const manifests = [];
1064
1127
  const seen = new Set();
1065
1128
  const ID = /^[a-z][a-z0-9_-]{0,63}$/;
@@ -1068,27 +1131,42 @@ export class ExtensionService {
1068
1131
  if (!ID.test(id) || seen.has(id))
1069
1132
  continue;
1070
1133
  seen.add(id);
1071
- const rawManifest = isDict(entry.manifest) ? entry.manifest : entry;
1134
+ const rawManifest = rawById.get(str(entry.id)) ?? {};
1072
1135
  const warnings = [];
1073
1136
  let panel = null;
1074
1137
  const rawPanel = rawManifest.panel;
1075
1138
  if (rawPanel !== null && rawPanel !== undefined) {
1076
- if (typeof rawPanel !== 'string' || !/^[A-Za-z0-9._/-]{1,300}$/.test(rawPanel) || rawPanel.split('/').includes('..') || rawPanel.startsWith('/'))
1139
+ // Like scripts, a panel path is relative to the manifest's folder; gallery entries were rebased onto `<id>/`.
1140
+ const url = typeof rawPanel === 'string' ? `extensions/${assetBase ? `${assetBase}/` : ''}${rawPanel.replace(/^(\.\/)+/, '')}` : '';
1141
+ if (typeof rawPanel !== 'string' || !/^[A-Za-z0-9._/-]{1,300}$/.test(rawPanel) || !isSafeRelativePath(url) || url.length > 400)
1077
1142
  warnings.push('panel_path_rejected');
1078
1143
  else if (!rawPanel.toLowerCase().endsWith('.html'))
1079
1144
  warnings.push('panel_not_html');
1080
1145
  else
1081
- panel = `extensions/${id}/${rawPanel.replace(/^[./]+/, '')}`;
1146
+ panel = url;
1082
1147
  }
1083
- const scripts = Array.isArray(entry.scripts) ? entry.scripts : Array.isArray(rawManifest.scripts) ? rawManifest.scripts : [];
1084
- const stylesheets = Array.isArray(entry.stylesheets) ? entry.stylesheets : Array.isArray(rawManifest.stylesheets) ? rawManifest.stylesheets : [];
1085
- const legacy = Boolean((scripts.length || stylesheets.length) && !panel);
1148
+ const scripts = Array.isArray(rawManifest.scripts) ? rawManifest.scripts : [];
1149
+ const stylesheets = Array.isArray(rawManifest.stylesheets) ? rawManifest.stylesheets : [];
1150
+ const theme = themeFromEntry(rawManifest.theme, id);
1151
+ if (!theme && rawManifest.theme != null)
1152
+ warnings.push('theme_rejected');
1153
+ // A skin is the one declaration that works without a panel, so it keeps an entry with leftover assets runnable.
1154
+ const legacy = Boolean((scripts.length || stylesheets.length) && !panel && !theme);
1086
1155
  const rawNav = isDict(rawManifest.nav) ? rawManifest.nav : {};
1087
- const nav = panel ? { label: text(rawNav.label, 40) || text(entry.name ?? rawManifest.name, 40) || id, ...(text(rawNav.icon, 40) ? { icon: text(rawNav.icon, 40) } : {}) } : null;
1088
- const permissions = isDict(entry.permissions) ? Object.fromEntries(Object.entries(entry.permissions).map(([k, v]) => [k, Boolean(v)])) : {};
1089
- const enabledFlag = entry.effective_enabled ?? entry.enabled ?? true;
1156
+ const nav = panel ? { label: text(rawNav.label, 40) || text(entry.name, 40) || id, ...(text(rawNav.icon, 40) ? { icon: text(rawNav.icon, 40) } : {}) } : null;
1157
+ const permissions = isDict(rawManifest.permissions) ? Object.fromEntries(Object.entries(rawManifest.permissions).filter(([k]) => k.length <= 64).map(([k, v]) => [k, Boolean(v)])) : {};
1158
+ const tts = ttsFromEntry(rawManifest.tts);
1159
+ if (!tts && rawManifest.tts != null)
1160
+ warnings.push('tts_rejected');
1161
+ // The sidecar the proxy would serve: same record, availability, and saved consent `proxyTarget` enforces
1162
+ // (which refuses token-v1 sidecars in this release).
1163
+ const record = sidecarsById.get(str(entry.id));
1164
+ const declared = isDict(record?.sidecar) && record.sidecar.proxy_auth !== 'token-v1' ? record.sidecar : null;
1165
+ const proxy = isDict(record?.proxy) ? record.proxy : null;
1166
+ const healthPath = declared ? str(declared.health_path) : '';
1167
+ const sidecar = declared && proxy?.available === true ? { origin: str(declared.origin), ...(healthPath.length <= 200 ? { health_path: healthPath } : {}), consented: proxy.consented === true } : null;
1090
1168
  const caps = (Array.isArray(rawManifest.capabilities) ? rawManifest.capabilities : []).filter((c) => typeof c === 'string' && ['settings', 'storage', 'sidecar', 'lifecycle', 'theme', 'tts', 'navigate', 'toast', 'session'].includes(c));
1091
- manifests.push({ id, name: text(entry.name ?? rawManifest.name, 80) || id, version: text(entry.version ?? rawManifest.version, 40), description: text(entry.description ?? rawManifest.description, 300), source: entry.gallery_installed || entry.source === 'gallery' ? 'gallery' : 'manifest', enabled: Boolean(enabledFlag) && !legacy, panel, nav, capabilities: [...new Set(caps)], permissions, settings_schema: Array.isArray(entry.settings_schema) ? entry.settings_schema : [], theme: null, tts: null, sidecar: null, legacy_injection: legacy, warnings });
1169
+ manifests.push({ id, name: text(entry.name, 80) || id, version: text(rawManifest.version, 40), description: text(rawManifest.description, 300), source: galleryFolders.get(rawManifest) === id && Object.hasOwn(installed, id) ? 'gallery' : 'manifest', enabled: entry.effective_enabled === true && !legacy, can_toggle: entry.can_toggle !== false && !legacy, panel, nav, capabilities: [...new Set(caps)], permissions, settings_schema: Array.isArray(entry.settings_schema) ? entry.settings_schema : [], theme, tts, sidecar, legacy_injection: legacy, warnings });
1092
1170
  }
1093
1171
  return { protocol_version: 1, manifests };
1094
1172
  }