@geminixiang/mikan 1.0.0-beta.4 → 1.0.0-beta.41

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 (661) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/README.md +99 -36
  3. package/dist/adapter.d.ts +11 -1
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +18 -1
  6. package/dist/adapter.js.map +1 -1
  7. package/dist/adapters/discord/bot.d.ts +6 -7
  8. package/dist/adapters/discord/bot.d.ts.map +1 -1
  9. package/dist/adapters/discord/bot.js +88 -122
  10. package/dist/adapters/discord/bot.js.map +1 -1
  11. package/dist/adapters/discord/components.d.ts +33 -0
  12. package/dist/adapters/discord/components.d.ts.map +1 -0
  13. package/dist/adapters/discord/components.js +32 -0
  14. package/dist/adapters/discord/components.js.map +1 -0
  15. package/dist/adapters/discord/context.d.ts +3 -1
  16. package/dist/adapters/discord/context.d.ts.map +1 -1
  17. package/dist/adapters/discord/context.js +66 -217
  18. package/dist/adapters/discord/context.js.map +1 -1
  19. package/dist/adapters/discord/format.d.ts +3 -0
  20. package/dist/adapters/discord/format.d.ts.map +1 -0
  21. package/dist/adapters/discord/format.js +86 -0
  22. package/dist/adapters/discord/format.js.map +1 -0
  23. package/dist/adapters/github/bot.d.ts +89 -0
  24. package/dist/adapters/github/bot.d.ts.map +1 -0
  25. package/dist/adapters/github/bot.js +653 -0
  26. package/dist/adapters/github/bot.js.map +1 -0
  27. package/dist/adapters/github/client.d.ts +122 -0
  28. package/dist/adapters/github/client.d.ts.map +1 -0
  29. package/dist/adapters/github/client.js +314 -0
  30. package/dist/adapters/github/client.js.map +1 -0
  31. package/dist/adapters/github/cloudbuild.d.ts +19 -0
  32. package/dist/adapters/github/cloudbuild.d.ts.map +1 -0
  33. package/dist/adapters/github/cloudbuild.js +71 -0
  34. package/dist/adapters/github/cloudbuild.js.map +1 -0
  35. package/dist/adapters/github/context.d.ts +12 -0
  36. package/dist/adapters/github/context.d.ts.map +1 -0
  37. package/dist/adapters/github/context.js +101 -0
  38. package/dist/adapters/github/context.js.map +1 -0
  39. package/dist/adapters/github/gcp-auth.d.ts +28 -0
  40. package/dist/adapters/github/gcp-auth.d.ts.map +1 -0
  41. package/dist/adapters/github/gcp-auth.js +196 -0
  42. package/dist/adapters/github/gcp-auth.js.map +1 -0
  43. package/dist/adapters/github/github-ops.d.ts +91 -0
  44. package/dist/adapters/github/github-ops.d.ts.map +1 -0
  45. package/dist/adapters/github/github-ops.js +406 -0
  46. package/dist/adapters/github/github-ops.js.map +1 -0
  47. package/dist/adapters/github/ids.d.ts +34 -0
  48. package/dist/adapters/github/ids.d.ts.map +1 -0
  49. package/dist/adapters/github/ids.js +57 -0
  50. package/dist/adapters/github/ids.js.map +1 -0
  51. package/dist/adapters/github/repo.d.ts +33 -0
  52. package/dist/adapters/github/repo.d.ts.map +1 -0
  53. package/dist/adapters/github/repo.js +141 -0
  54. package/dist/adapters/github/repo.js.map +1 -0
  55. package/dist/adapters/github/tool-pack.d.ts +14 -0
  56. package/dist/adapters/github/tool-pack.d.ts.map +1 -0
  57. package/dist/adapters/github/tool-pack.js +56 -0
  58. package/dist/adapters/github/tool-pack.js.map +1 -0
  59. package/dist/adapters/github/tools/checks.d.ts +19 -0
  60. package/dist/adapters/github/tools/checks.d.ts.map +1 -0
  61. package/dist/adapters/github/tools/checks.js +105 -0
  62. package/dist/adapters/github/tools/checks.js.map +1 -0
  63. package/dist/adapters/github/tools/issue.d.ts +21 -0
  64. package/dist/adapters/github/tools/issue.d.ts.map +1 -0
  65. package/dist/adapters/github/tools/issue.js +48 -0
  66. package/dist/adapters/github/tools/issue.js.map +1 -0
  67. package/dist/adapters/github/tools/pr.d.ts +22 -0
  68. package/dist/adapters/github/tools/pr.d.ts.map +1 -0
  69. package/dist/adapters/github/tools/pr.js +48 -0
  70. package/dist/adapters/github/tools/pr.js.map +1 -0
  71. package/dist/adapters/github/tools/read.d.ts +21 -0
  72. package/dist/adapters/github/tools/read.d.ts.map +1 -0
  73. package/dist/adapters/github/tools/read.js +141 -0
  74. package/dist/adapters/github/tools/read.js.map +1 -0
  75. package/dist/adapters/github/tools/review-reply.d.ts +17 -0
  76. package/dist/adapters/github/tools/review-reply.d.ts.map +1 -0
  77. package/dist/adapters/github/tools/review-reply.js +38 -0
  78. package/dist/adapters/github/tools/review-reply.js.map +1 -0
  79. package/dist/adapters/github/tools/sync.d.ts +17 -0
  80. package/dist/adapters/github/tools/sync.d.ts.map +1 -0
  81. package/dist/adapters/github/tools/sync.js +34 -0
  82. package/dist/adapters/github/tools/sync.js.map +1 -0
  83. package/dist/adapters/github/types.d.ts +373 -0
  84. package/dist/adapters/github/types.d.ts.map +1 -0
  85. package/dist/adapters/github/types.js +2 -0
  86. package/dist/adapters/github/types.js.map +1 -0
  87. package/dist/adapters/intake.d.ts +16 -5
  88. package/dist/adapters/intake.d.ts.map +1 -1
  89. package/dist/adapters/intake.js +86 -23
  90. package/dist/adapters/intake.js.map +1 -1
  91. package/dist/adapters/markdown-tables.d.ts +37 -0
  92. package/dist/adapters/markdown-tables.d.ts.map +1 -0
  93. package/dist/adapters/markdown-tables.js +164 -0
  94. package/dist/adapters/markdown-tables.js.map +1 -0
  95. package/dist/adapters/progressive-renderer.d.ts +7 -0
  96. package/dist/adapters/progressive-renderer.d.ts.map +1 -0
  97. package/dist/adapters/progressive-renderer.js +440 -0
  98. package/dist/adapters/progressive-renderer.js.map +1 -0
  99. package/dist/adapters/shared.d.ts +23 -10
  100. package/dist/adapters/shared.d.ts.map +1 -1
  101. package/dist/adapters/shared.js +60 -22
  102. package/dist/adapters/shared.js.map +1 -1
  103. package/dist/adapters/slack/assistant.d.ts +101 -0
  104. package/dist/adapters/slack/assistant.d.ts.map +1 -0
  105. package/dist/adapters/slack/assistant.js +204 -0
  106. package/dist/adapters/slack/assistant.js.map +1 -0
  107. package/dist/adapters/slack/blocks.d.ts +14 -0
  108. package/dist/adapters/slack/blocks.d.ts.map +1 -1
  109. package/dist/adapters/slack/blocks.js +105 -147
  110. package/dist/adapters/slack/blocks.js.map +1 -1
  111. package/dist/adapters/slack/bot.d.ts +95 -13
  112. package/dist/adapters/slack/bot.d.ts.map +1 -1
  113. package/dist/adapters/slack/bot.js +545 -211
  114. package/dist/adapters/slack/bot.js.map +1 -1
  115. package/dist/adapters/slack/context.d.ts +3 -1
  116. package/dist/adapters/slack/context.d.ts.map +1 -1
  117. package/dist/adapters/slack/context.js +11 -17
  118. package/dist/adapters/slack/context.js.map +1 -1
  119. package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
  120. package/dist/adapters/slack/response-lifecycle.js +197 -323
  121. package/dist/adapters/slack/response-lifecycle.js.map +1 -1
  122. package/dist/adapters/slack/session.d.ts.map +1 -1
  123. package/dist/adapters/slack/session.js +7 -11
  124. package/dist/adapters/slack/session.js.map +1 -1
  125. package/dist/adapters/slack/stream-limits.d.ts +35 -0
  126. package/dist/adapters/slack/stream-limits.d.ts.map +1 -0
  127. package/dist/adapters/slack/stream-limits.js +56 -0
  128. package/dist/adapters/slack/stream-limits.js.map +1 -0
  129. package/dist/adapters/slack/tool-pack.d.ts +11 -0
  130. package/dist/adapters/slack/tool-pack.d.ts.map +1 -0
  131. package/dist/adapters/slack/tool-pack.js +45 -0
  132. package/dist/adapters/slack/tool-pack.js.map +1 -0
  133. package/dist/adapters/slack/tools/attach.js +1 -1
  134. package/dist/adapters/slack/tools/attach.js.map +1 -1
  135. package/dist/adapters/slack/tools/blockkit.d.ts +14 -0
  136. package/dist/adapters/slack/tools/blockkit.d.ts.map +1 -0
  137. package/dist/adapters/slack/tools/blockkit.js +106 -0
  138. package/dist/adapters/slack/tools/blockkit.js.map +1 -0
  139. package/dist/adapters/slack/types.d.ts +40 -2
  140. package/dist/adapters/slack/types.d.ts.map +1 -1
  141. package/dist/adapters/slack/types.js.map +1 -1
  142. package/dist/adapters/telegram/bot.d.ts +8 -9
  143. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  144. package/dist/adapters/telegram/bot.js +115 -163
  145. package/dist/adapters/telegram/bot.js.map +1 -1
  146. package/dist/adapters/telegram/context.d.ts +2 -0
  147. package/dist/adapters/telegram/context.d.ts.map +1 -1
  148. package/dist/adapters/telegram/context.js +53 -188
  149. package/dist/adapters/telegram/context.js.map +1 -1
  150. package/dist/adapters/types.d.ts +132 -12
  151. package/dist/adapters/types.d.ts.map +1 -1
  152. package/dist/adapters/types.js.map +1 -1
  153. package/dist/agent-events.d.ts +3 -2
  154. package/dist/agent-events.d.ts.map +1 -1
  155. package/dist/agent-events.js +19 -1
  156. package/dist/agent-events.js.map +1 -1
  157. package/dist/agent.d.ts +14 -19
  158. package/dist/agent.d.ts.map +1 -1
  159. package/dist/agent.js +841 -298
  160. package/dist/agent.js.map +1 -1
  161. package/dist/cli/arg-grammar.d.ts +25 -0
  162. package/dist/cli/arg-grammar.d.ts.map +1 -0
  163. package/dist/cli/arg-grammar.js +61 -0
  164. package/dist/cli/arg-grammar.js.map +1 -0
  165. package/dist/cli/boot.d.ts +5 -0
  166. package/dist/cli/boot.d.ts.map +1 -0
  167. package/dist/cli/boot.js +117 -0
  168. package/dist/cli/boot.js.map +1 -0
  169. package/dist/cli/download.d.ts.map +1 -0
  170. package/dist/{download.js → cli/download.js} +2 -0
  171. package/dist/cli/download.js.map +1 -0
  172. package/dist/cli/ext-dev.d.ts +2 -0
  173. package/dist/cli/ext-dev.d.ts.map +1 -0
  174. package/dist/cli/ext-dev.js +188 -0
  175. package/dist/cli/ext-dev.js.map +1 -0
  176. package/dist/cli/ext.d.ts +2 -0
  177. package/dist/cli/ext.d.ts.map +1 -0
  178. package/dist/cli/ext.js +324 -0
  179. package/dist/cli/ext.js.map +1 -0
  180. package/dist/cli/office.d.ts +2 -0
  181. package/dist/cli/office.d.ts.map +1 -0
  182. package/dist/cli/office.js +91 -0
  183. package/dist/cli/office.js.map +1 -0
  184. package/dist/cli/types.d.ts +16 -0
  185. package/dist/cli/types.d.ts.map +1 -0
  186. package/dist/cli/types.js +2 -0
  187. package/dist/cli/types.js.map +1 -0
  188. package/dist/commands/admin.d.ts.map +1 -1
  189. package/dist/commands/admin.js +3 -2
  190. package/dist/commands/admin.js.map +1 -1
  191. package/dist/commands/auto-reply.d.ts.map +1 -1
  192. package/dist/commands/auto-reply.js +5 -9
  193. package/dist/commands/auto-reply.js.map +1 -1
  194. package/dist/commands/extensions.d.ts +18 -6
  195. package/dist/commands/extensions.d.ts.map +1 -1
  196. package/dist/commands/extensions.js +64 -14
  197. package/dist/commands/extensions.js.map +1 -1
  198. package/dist/commands/login.d.ts +2 -1
  199. package/dist/commands/login.d.ts.map +1 -1
  200. package/dist/commands/login.js +40 -5
  201. package/dist/commands/login.js.map +1 -1
  202. package/dist/commands/manifest.d.ts +37 -0
  203. package/dist/commands/manifest.d.ts.map +1 -0
  204. package/dist/commands/manifest.js +151 -0
  205. package/dist/commands/manifest.js.map +1 -0
  206. package/dist/commands/model.d.ts +2 -3
  207. package/dist/commands/model.d.ts.map +1 -1
  208. package/dist/commands/model.js +63 -24
  209. package/dist/commands/model.js.map +1 -1
  210. package/dist/commands/new.d.ts.map +1 -1
  211. package/dist/commands/new.js +15 -3
  212. package/dist/commands/new.js.map +1 -1
  213. package/dist/commands/registry.d.ts +2 -2
  214. package/dist/commands/registry.d.ts.map +1 -1
  215. package/dist/commands/registry.js +1 -2
  216. package/dist/commands/registry.js.map +1 -1
  217. package/dist/commands/sandbox.d.ts.map +1 -1
  218. package/dist/commands/sandbox.js +83 -33
  219. package/dist/commands/sandbox.js.map +1 -1
  220. package/dist/commands/session-view.d.ts.map +1 -1
  221. package/dist/commands/session-view.js +6 -3
  222. package/dist/commands/session-view.js.map +1 -1
  223. package/dist/commands/types.d.ts +79 -7
  224. package/dist/commands/types.d.ts.map +1 -1
  225. package/dist/commands/types.js.map +1 -1
  226. package/dist/config.d.ts +23 -8
  227. package/dist/config.d.ts.map +1 -1
  228. package/dist/config.js +222 -135
  229. package/dist/config.js.map +1 -1
  230. package/dist/env-manifest.d.ts +30 -0
  231. package/dist/env-manifest.d.ts.map +1 -0
  232. package/dist/env-manifest.js +233 -0
  233. package/dist/env-manifest.js.map +1 -0
  234. package/dist/events.d.ts.map +1 -1
  235. package/dist/events.js +20 -81
  236. package/dist/events.js.map +1 -1
  237. package/dist/execution-resolver.d.ts +7 -8
  238. package/dist/execution-resolver.d.ts.map +1 -1
  239. package/dist/execution-resolver.js +106 -110
  240. package/dist/execution-resolver.js.map +1 -1
  241. package/dist/extension-schedules.d.ts +31 -0
  242. package/dist/extension-schedules.d.ts.map +1 -0
  243. package/dist/extension-schedules.js +229 -0
  244. package/dist/extension-schedules.js.map +1 -0
  245. package/dist/harness/auth.d.ts +2 -1
  246. package/dist/harness/auth.d.ts.map +1 -1
  247. package/dist/harness/auth.js +11 -5
  248. package/dist/harness/auth.js.map +1 -1
  249. package/dist/harness/event-format.d.ts +27 -0
  250. package/dist/harness/event-format.d.ts.map +1 -0
  251. package/dist/harness/event-format.js +160 -0
  252. package/dist/harness/event-format.js.map +1 -0
  253. package/dist/harness/extensions/loader.d.ts +27 -49
  254. package/dist/harness/extensions/loader.d.ts.map +1 -1
  255. package/dist/harness/extensions/loader.js +486 -130
  256. package/dist/harness/extensions/loader.js.map +1 -1
  257. package/dist/harness/extensions/registry.d.ts +107 -2
  258. package/dist/harness/extensions/registry.d.ts.map +1 -1
  259. package/dist/harness/extensions/registry.js +316 -0
  260. package/dist/harness/extensions/registry.js.map +1 -1
  261. package/dist/harness/extensions/types.d.ts +482 -42
  262. package/dist/harness/extensions/types.d.ts.map +1 -1
  263. package/dist/harness/extensions/types.js.map +1 -1
  264. package/dist/harness/index.d.ts +18 -8
  265. package/dist/harness/index.d.ts.map +1 -1
  266. package/dist/harness/index.js +7 -4
  267. package/dist/harness/index.js.map +1 -1
  268. package/dist/harness/models.d.ts +7 -4
  269. package/dist/harness/models.d.ts.map +1 -1
  270. package/dist/harness/models.js +46 -24
  271. package/dist/harness/models.js.map +1 -1
  272. package/dist/harness/runner.d.ts +50 -54
  273. package/dist/harness/runner.d.ts.map +1 -1
  274. package/dist/harness/runner.js +232 -46
  275. package/dist/harness/runner.js.map +1 -1
  276. package/dist/harness/session-store.d.ts +11 -5
  277. package/dist/harness/session-store.d.ts.map +1 -1
  278. package/dist/harness/session-store.js +141 -36
  279. package/dist/harness/session-store.js.map +1 -1
  280. package/dist/harness/settings.d.ts +2 -20
  281. package/dist/harness/settings.d.ts.map +1 -1
  282. package/dist/harness/settings.js +12 -1
  283. package/dist/harness/settings.js.map +1 -1
  284. package/dist/harness/skills.d.ts +10 -29
  285. package/dist/harness/skills.d.ts.map +1 -1
  286. package/dist/harness/skills.js +48 -6
  287. package/dist/harness/skills.js.map +1 -1
  288. package/dist/harness/subagent-profiles.d.ts +8 -0
  289. package/dist/harness/subagent-profiles.d.ts.map +1 -0
  290. package/dist/harness/subagent-profiles.js +299 -0
  291. package/dist/harness/subagent-profiles.js.map +1 -0
  292. package/dist/harness/subagent-runner.d.ts +45 -0
  293. package/dist/harness/subagent-runner.d.ts.map +1 -0
  294. package/dist/harness/subagent-runner.js +680 -0
  295. package/dist/harness/subagent-runner.js.map +1 -0
  296. package/dist/harness/types.d.ts +267 -1
  297. package/dist/harness/types.d.ts.map +1 -1
  298. package/dist/harness/types.js.map +1 -1
  299. package/dist/harness/usage.d.ts +6 -0
  300. package/dist/harness/usage.d.ts.map +1 -0
  301. package/dist/harness/usage.js +32 -0
  302. package/dist/harness/usage.js.map +1 -0
  303. package/dist/index.d.ts +36 -8
  304. package/dist/index.d.ts.map +1 -1
  305. package/dist/index.js +22 -6
  306. package/dist/index.js.map +1 -1
  307. package/dist/main.js +391 -127
  308. package/dist/main.js.map +1 -1
  309. package/dist/observability/instrument.js +7 -5
  310. package/dist/observability/instrument.js.map +1 -1
  311. package/dist/observability/sentry.d.ts +2 -0
  312. package/dist/observability/sentry.d.ts.map +1 -1
  313. package/dist/observability/sentry.js +10 -2
  314. package/dist/observability/sentry.js.map +1 -1
  315. package/dist/office/address.d.ts +31 -0
  316. package/dist/office/address.d.ts.map +1 -0
  317. package/dist/office/address.js +113 -0
  318. package/dist/office/address.js.map +1 -0
  319. package/dist/office/index.d.ts +11 -0
  320. package/dist/office/index.d.ts.map +1 -0
  321. package/dist/office/index.js +11 -0
  322. package/dist/office/index.js.map +1 -0
  323. package/dist/office/layout.d.ts +27 -0
  324. package/dist/office/layout.d.ts.map +1 -0
  325. package/dist/office/layout.js +97 -0
  326. package/dist/office/layout.js.map +1 -0
  327. package/dist/office/migration.d.ts +44 -0
  328. package/dist/office/migration.d.ts.map +1 -0
  329. package/dist/office/migration.js +255 -0
  330. package/dist/office/migration.js.map +1 -0
  331. package/dist/office/registry.d.ts +75 -0
  332. package/dist/office/registry.d.ts.map +1 -0
  333. package/dist/office/registry.js +646 -0
  334. package/dist/office/registry.js.map +1 -0
  335. package/dist/office/types.d.ts +71 -0
  336. package/dist/office/types.d.ts.map +1 -0
  337. package/dist/office/types.js +2 -0
  338. package/dist/office/types.js.map +1 -0
  339. package/dist/packages/admin.d.ts +17 -0
  340. package/dist/packages/admin.d.ts.map +1 -0
  341. package/dist/packages/admin.js +98 -0
  342. package/dist/packages/admin.js.map +1 -0
  343. package/dist/packages/index.d.ts +6 -0
  344. package/dist/packages/index.d.ts.map +1 -0
  345. package/dist/packages/index.js +6 -0
  346. package/dist/packages/index.js.map +1 -0
  347. package/dist/packages/inspect.d.ts +11 -0
  348. package/dist/packages/inspect.d.ts.map +1 -0
  349. package/dist/packages/inspect.js +98 -0
  350. package/dist/packages/inspect.js.map +1 -0
  351. package/dist/packages/materialize.d.ts +40 -0
  352. package/dist/packages/materialize.d.ts.map +1 -0
  353. package/dist/packages/materialize.js +297 -0
  354. package/dist/packages/materialize.js.map +1 -0
  355. package/dist/packages/resolve.d.ts +25 -0
  356. package/dist/packages/resolve.d.ts.map +1 -0
  357. package/dist/packages/resolve.js +208 -0
  358. package/dist/packages/resolve.js.map +1 -0
  359. package/dist/packages/source.d.ts +39 -0
  360. package/dist/packages/source.d.ts.map +1 -0
  361. package/dist/packages/source.js +189 -0
  362. package/dist/packages/source.js.map +1 -0
  363. package/dist/packages/types.d.ts +104 -0
  364. package/dist/packages/types.d.ts.map +1 -0
  365. package/dist/packages/types.js +2 -0
  366. package/dist/packages/types.js.map +1 -0
  367. package/dist/provisioner.d.ts +66 -0
  368. package/dist/provisioner.d.ts.map +1 -1
  369. package/dist/provisioner.js +335 -13
  370. package/dist/provisioner.js.map +1 -1
  371. package/dist/runtime/conversation-runtime.d.ts +1 -1
  372. package/dist/runtime/conversation-runtime.d.ts.map +1 -1
  373. package/dist/runtime/conversation-runtime.js +562 -118
  374. package/dist/runtime/conversation-runtime.js.map +1 -1
  375. package/dist/runtime/session-lifecycle.d.ts +38 -0
  376. package/dist/runtime/session-lifecycle.d.ts.map +1 -0
  377. package/dist/runtime/session-lifecycle.js +193 -0
  378. package/dist/runtime/session-lifecycle.js.map +1 -0
  379. package/dist/runtime/types.d.ts +66 -7
  380. package/dist/runtime/types.d.ts.map +1 -1
  381. package/dist/runtime/types.js.map +1 -1
  382. package/dist/sandbox/cloudflare.d.ts +3 -0
  383. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  384. package/dist/sandbox/cloudflare.js +13 -1
  385. package/dist/sandbox/cloudflare.js.map +1 -1
  386. package/dist/sandbox/container.d.ts +4 -0
  387. package/dist/sandbox/container.d.ts.map +1 -1
  388. package/dist/sandbox/container.js +13 -3
  389. package/dist/sandbox/container.js.map +1 -1
  390. package/dist/sandbox/firecracker.d.ts +3 -0
  391. package/dist/sandbox/firecracker.d.ts.map +1 -1
  392. package/dist/sandbox/firecracker.js +13 -2
  393. package/dist/sandbox/firecracker.js.map +1 -1
  394. package/dist/sandbox/gondolin.d.ts +57 -0
  395. package/dist/sandbox/gondolin.d.ts.map +1 -0
  396. package/dist/sandbox/gondolin.js +764 -0
  397. package/dist/sandbox/gondolin.js.map +1 -0
  398. package/dist/sandbox/host.d.ts +3 -0
  399. package/dist/sandbox/host.d.ts.map +1 -1
  400. package/dist/sandbox/host.js +18 -3
  401. package/dist/sandbox/host.js.map +1 -1
  402. package/dist/sandbox/identity.d.ts +44 -0
  403. package/dist/sandbox/identity.d.ts.map +1 -0
  404. package/dist/sandbox/identity.js +68 -0
  405. package/dist/sandbox/identity.js.map +1 -0
  406. package/dist/sandbox/image.d.ts.map +1 -1
  407. package/dist/sandbox/image.js +2 -0
  408. package/dist/sandbox/image.js.map +1 -1
  409. package/dist/sandbox/index.d.ts +17 -4
  410. package/dist/sandbox/index.d.ts.map +1 -1
  411. package/dist/sandbox/index.js +37 -3
  412. package/dist/sandbox/index.js.map +1 -1
  413. package/dist/sandbox/types.d.ts +65 -1
  414. package/dist/sandbox/types.d.ts.map +1 -1
  415. package/dist/sandbox/types.js.map +1 -1
  416. package/dist/sandbox/utils.d.ts +14 -0
  417. package/dist/sandbox/utils.d.ts.map +1 -1
  418. package/dist/sandbox/utils.js +68 -1
  419. package/dist/sandbox/utils.js.map +1 -1
  420. package/dist/sessions/chat-history-sync.d.ts +25 -0
  421. package/dist/sessions/chat-history-sync.d.ts.map +1 -0
  422. package/dist/sessions/{agent-memory-file-manager.js → chat-history-sync.js} +135 -131
  423. package/dist/sessions/chat-history-sync.js.map +1 -0
  424. package/dist/sessions/conversation-log.d.ts +9 -0
  425. package/dist/sessions/conversation-log.d.ts.map +1 -0
  426. package/dist/sessions/conversation-log.js +50 -0
  427. package/dist/sessions/conversation-log.js.map +1 -0
  428. package/dist/sessions/history-line.d.ts +29 -0
  429. package/dist/sessions/history-line.d.ts.map +1 -0
  430. package/dist/sessions/history-line.js +45 -0
  431. package/dist/sessions/history-line.js.map +1 -0
  432. package/dist/sessions/rotation.d.ts +2 -0
  433. package/dist/sessions/rotation.d.ts.map +1 -0
  434. package/dist/sessions/rotation.js +39 -0
  435. package/dist/sessions/rotation.js.map +1 -0
  436. package/dist/sessions/session-key.d.ts +33 -0
  437. package/dist/sessions/session-key.d.ts.map +1 -0
  438. package/dist/sessions/session-key.js +115 -0
  439. package/dist/sessions/session-key.js.map +1 -0
  440. package/dist/sessions/store.d.ts +15 -7
  441. package/dist/sessions/store.d.ts.map +1 -1
  442. package/dist/sessions/store.js +127 -36
  443. package/dist/sessions/store.js.map +1 -1
  444. package/dist/sessions/types.d.ts +19 -5
  445. package/dist/sessions/types.d.ts.map +1 -1
  446. package/dist/sessions/types.js.map +1 -1
  447. package/dist/settings-mutation.d.ts +40 -0
  448. package/dist/settings-mutation.d.ts.map +1 -0
  449. package/dist/settings-mutation.js +61 -0
  450. package/dist/settings-mutation.js.map +1 -0
  451. package/dist/subagent-progress.d.ts +39 -0
  452. package/dist/subagent-progress.d.ts.map +1 -0
  453. package/dist/subagent-progress.js +225 -0
  454. package/dist/subagent-progress.js.map +1 -0
  455. package/dist/tools/bash.d.ts +3 -1
  456. package/dist/tools/bash.d.ts.map +1 -1
  457. package/dist/tools/bash.js +26 -18
  458. package/dist/tools/bash.js.map +1 -1
  459. package/dist/tools/edit.d.ts.map +1 -1
  460. package/dist/tools/edit.js +14 -13
  461. package/dist/tools/edit.js.map +1 -1
  462. package/dist/tools/event.d.ts +1 -1
  463. package/dist/tools/event.d.ts.map +1 -1
  464. package/dist/tools/event.js +58 -45
  465. package/dist/tools/event.js.map +1 -1
  466. package/dist/tools/generate-image.d.ts +23 -0
  467. package/dist/tools/generate-image.d.ts.map +1 -0
  468. package/dist/tools/generate-image.js +65 -0
  469. package/dist/tools/generate-image.js.map +1 -0
  470. package/dist/tools/host-fn-tool.d.ts +21 -0
  471. package/dist/tools/host-fn-tool.d.ts.map +1 -0
  472. package/dist/tools/host-fn-tool.js +32 -0
  473. package/dist/tools/host-fn-tool.js.map +1 -0
  474. package/dist/tools/index.d.ts +19 -2
  475. package/dist/tools/index.d.ts.map +1 -1
  476. package/dist/tools/index.js +25 -2
  477. package/dist/tools/index.js.map +1 -1
  478. package/dist/tools/react.d.ts +15 -0
  479. package/dist/tools/react.d.ts.map +1 -0
  480. package/dist/tools/react.js +29 -0
  481. package/dist/tools/react.js.map +1 -0
  482. package/dist/tools/read.d.ts +1 -0
  483. package/dist/tools/read.d.ts.map +1 -1
  484. package/dist/tools/read.js +39 -33
  485. package/dist/tools/read.js.map +1 -1
  486. package/dist/tools/sandbox.d.ts +2 -2
  487. package/dist/tools/sandbox.d.ts.map +1 -1
  488. package/dist/tools/sandbox.js +14 -9
  489. package/dist/tools/sandbox.js.map +1 -1
  490. package/dist/tools/subagent-slots.d.ts +28 -0
  491. package/dist/tools/subagent-slots.d.ts.map +1 -0
  492. package/dist/tools/subagent-slots.js +72 -0
  493. package/dist/tools/subagent-slots.js.map +1 -0
  494. package/dist/tools/subagent.d.ts +61 -0
  495. package/dist/tools/subagent.d.ts.map +1 -0
  496. package/dist/tools/subagent.js +434 -0
  497. package/dist/tools/subagent.js.map +1 -0
  498. package/dist/tools/types.d.ts +40 -32
  499. package/dist/tools/types.d.ts.map +1 -1
  500. package/dist/tools/types.js.map +1 -1
  501. package/dist/tools/write.d.ts.map +1 -1
  502. package/dist/tools/write.js +2 -8
  503. package/dist/tools/write.js.map +1 -1
  504. package/dist/trigger.d.ts +2 -1
  505. package/dist/trigger.d.ts.map +1 -1
  506. package/dist/trigger.js +6 -9
  507. package/dist/trigger.js.map +1 -1
  508. package/dist/types.d.ts +411 -59
  509. package/dist/types.d.ts.map +1 -1
  510. package/dist/types.js +2 -2
  511. package/dist/types.js.map +1 -1
  512. package/dist/utils/file-guards.d.ts +9 -0
  513. package/dist/utils/file-guards.d.ts.map +1 -1
  514. package/dist/utils/file-guards.js +44 -1
  515. package/dist/utils/file-guards.js.map +1 -1
  516. package/dist/vault/disabled.d.ts +9 -0
  517. package/dist/vault/disabled.d.ts.map +1 -0
  518. package/dist/vault/disabled.js +25 -0
  519. package/dist/vault/disabled.js.map +1 -0
  520. package/dist/vault/index.d.ts +45 -2
  521. package/dist/vault/index.d.ts.map +1 -1
  522. package/dist/vault/index.js +127 -36
  523. package/dist/vault/index.js.map +1 -1
  524. package/dist/vault/types.d.ts +6 -2
  525. package/dist/vault/types.d.ts.map +1 -1
  526. package/dist/vault/types.js.map +1 -1
  527. package/dist/web/admin/portal.d.ts +31 -3
  528. package/dist/web/admin/portal.d.ts.map +1 -1
  529. package/dist/web/admin/portal.js +850 -345
  530. package/dist/web/admin/portal.js.map +1 -1
  531. package/dist/web/admin/provider-models.js +1 -1
  532. package/dist/web/admin/provider-models.js.map +1 -1
  533. package/dist/web/admin/types.d.ts +23 -3
  534. package/dist/web/admin/types.d.ts.map +1 -1
  535. package/dist/web/admin/types.js.map +1 -1
  536. package/dist/web/login/oauth.d.ts +2 -3
  537. package/dist/web/login/oauth.d.ts.map +1 -1
  538. package/dist/web/login/oauth.js +1 -32
  539. package/dist/web/login/oauth.js.map +1 -1
  540. package/dist/web/login/portal.d.ts +1 -1
  541. package/dist/web/login/portal.d.ts.map +1 -1
  542. package/dist/web/login/portal.js +13 -36
  543. package/dist/web/login/portal.js.map +1 -1
  544. package/dist/web/login/types.d.ts +0 -11
  545. package/dist/web/login/types.d.ts.map +1 -1
  546. package/dist/web/login/types.js.map +1 -1
  547. package/dist/web/portal-shell.d.ts +21 -0
  548. package/dist/web/portal-shell.d.ts.map +1 -0
  549. package/dist/{portal-shell.js → web/portal-shell.js} +67 -2
  550. package/dist/web/portal-shell.js.map +1 -0
  551. package/dist/web/server.d.ts +3 -2
  552. package/dist/web/server.d.ts.map +1 -1
  553. package/dist/web/server.js +11 -4
  554. package/dist/web/server.js.map +1 -1
  555. package/dist/web/session-view/portal.d.ts +1 -1
  556. package/dist/web/session-view/portal.d.ts.map +1 -1
  557. package/dist/web/session-view/portal.js +37 -28
  558. package/dist/web/session-view/portal.js.map +1 -1
  559. package/dist/web/session-view/service.d.ts +1 -1
  560. package/dist/web/session-view/service.d.ts.map +1 -1
  561. package/dist/web/session-view/service.js +61 -18
  562. package/dist/web/session-view/service.js.map +1 -1
  563. package/dist/web/session-view/types.d.ts +0 -3
  564. package/dist/web/session-view/types.d.ts.map +1 -1
  565. package/dist/web/session-view/types.js.map +1 -1
  566. package/dist/web/token-store.js +1 -1
  567. package/dist/web/token-store.js.map +1 -1
  568. package/dist/workspace-projection/index.d.ts +10 -0
  569. package/dist/workspace-projection/index.d.ts.map +1 -0
  570. package/dist/workspace-projection/index.js +120 -0
  571. package/dist/workspace-projection/index.js.map +1 -0
  572. package/dist/workspace-projection/types.d.ts +16 -0
  573. package/dist/workspace-projection/types.d.ts.map +1 -0
  574. package/dist/workspace-projection/types.js +2 -0
  575. package/dist/workspace-projection/types.js.map +1 -0
  576. package/package.json +68 -37
  577. package/dist/adapters/streaming.d.ts +0 -18
  578. package/dist/adapters/streaming.d.ts.map +0 -1
  579. package/dist/adapters/streaming.js +0 -44
  580. package/dist/adapters/streaming.js.map +0 -1
  581. package/dist/adapters/telegram/html.d.ts +0 -3
  582. package/dist/adapters/telegram/html.d.ts.map +0 -1
  583. package/dist/adapters/telegram/html.js +0 -98
  584. package/dist/adapters/telegram/html.js.map +0 -1
  585. package/dist/commands/parse.d.ts +0 -7
  586. package/dist/commands/parse.d.ts.map +0 -1
  587. package/dist/commands/parse.js +0 -14
  588. package/dist/commands/parse.js.map +0 -1
  589. package/dist/download.d.ts.map +0 -1
  590. package/dist/download.js.map +0 -1
  591. package/dist/model-registry.d.ts +0 -4
  592. package/dist/model-registry.d.ts.map +0 -1
  593. package/dist/model-registry.js +0 -7
  594. package/dist/model-registry.js.map +0 -1
  595. package/dist/portal-shell.d.ts +0 -4
  596. package/dist/portal-shell.d.ts.map +0 -1
  597. package/dist/portal-shell.js.map +0 -1
  598. package/dist/runtime/agent-run-controller.d.ts +0 -31
  599. package/dist/runtime/agent-run-controller.d.ts.map +0 -1
  600. package/dist/runtime/agent-run-controller.js +0 -195
  601. package/dist/runtime/agent-run-controller.js.map +0 -1
  602. package/dist/sandbox/path-context.d.ts +0 -3
  603. package/dist/sandbox/path-context.d.ts.map +0 -1
  604. package/dist/sandbox/path-context.js +0 -20
  605. package/dist/sandbox/path-context.js.map +0 -1
  606. package/dist/sessions/agent-memory-file-manager.d.ts +0 -19
  607. package/dist/sessions/agent-memory-file-manager.d.ts.map +0 -1
  608. package/dist/sessions/agent-memory-file-manager.js.map +0 -1
  609. package/dist/sessions/metadata.d.ts +0 -3
  610. package/dist/sessions/metadata.d.ts.map +0 -1
  611. package/dist/sessions/metadata.js +0 -11
  612. package/dist/sessions/metadata.js.map +0 -1
  613. package/dist/sessions/policy.d.ts +0 -6
  614. package/dist/sessions/policy.d.ts.map +0 -1
  615. package/dist/sessions/policy.js +0 -23
  616. package/dist/sessions/policy.js.map +0 -1
  617. package/dist/store.d.ts +0 -46
  618. package/dist/store.d.ts.map +0 -1
  619. package/dist/store.js +0 -177
  620. package/dist/store.js.map +0 -1
  621. package/dist/tools/truncate.d.ts +0 -33
  622. package/dist/tools/truncate.d.ts.map +0 -1
  623. package/dist/tools/truncate.js +0 -184
  624. package/dist/tools/truncate.js.map +0 -1
  625. package/dist/utils/date.d.ts +0 -10
  626. package/dist/utils/date.d.ts.map +0 -1
  627. package/dist/utils/date.js +0 -23
  628. package/dist/utils/date.js.map +0 -1
  629. package/dist/utils/env.d.ts +0 -3
  630. package/dist/utils/env.d.ts.map +0 -1
  631. package/dist/utils/env.js +0 -12
  632. package/dist/utils/env.js.map +0 -1
  633. package/dist/utils/fs-atomic.d.ts +0 -10
  634. package/dist/utils/fs-atomic.d.ts.map +0 -1
  635. package/dist/utils/fs-atomic.js +0 -45
  636. package/dist/utils/fs-atomic.js.map +0 -1
  637. package/dist/utils/html.d.ts +0 -2
  638. package/dist/utils/html.d.ts.map +0 -1
  639. package/dist/utils/html.js +0 -4
  640. package/dist/utils/html.js.map +0 -1
  641. package/dist/utils/http-body.d.ts +0 -10
  642. package/dist/utils/http-body.d.ts.map +0 -1
  643. package/dist/utils/http-body.js +0 -34
  644. package/dist/utils/http-body.js.map +0 -1
  645. package/dist/vault/routing.d.ts +0 -3
  646. package/dist/vault/routing.d.ts.map +0 -1
  647. package/dist/vault/routing.js +0 -13
  648. package/dist/vault/routing.js.map +0 -1
  649. package/dist/web/agent-events/portal.d.ts +0 -3
  650. package/dist/web/agent-events/portal.d.ts.map +0 -1
  651. package/dist/web/agent-events/portal.js +0 -18
  652. package/dist/web/agent-events/portal.js.map +0 -1
  653. package/dist/web/request.d.ts +0 -3
  654. package/dist/web/request.d.ts.map +0 -1
  655. package/dist/web/request.js +0 -13
  656. package/dist/web/request.js.map +0 -1
  657. package/dist/web/session-view/command.d.ts +0 -4
  658. package/dist/web/session-view/command.d.ts.map +0 -1
  659. package/dist/web/session-view/command.js +0 -7
  660. package/dist/web/session-view/command.js.map +0 -1
  661. /package/dist/{download.d.ts → cli/download.d.ts} +0 -0
@@ -1,15 +1,17 @@
1
- import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
2
- import { basename, join, resolve as pathResolve, sep as pathSep } from "path";
3
- import { MikanModels, SessionStore } from "../../harness/index.js";
4
- import { loadConversationAutoReplyConfig, loadGlobalSettings, resolveConversationSettings, saveConversationAutoReplyConfig, updateConversationSettings, updateGlobalSettings, } from "../../config.js";
5
- import { escapeHtml } from "../../utils/html.js";
6
- import { readRawBody } from "../../utils/http-body.js";
7
- import { renderPortalShell } from "../../portal-shell.js";
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, join, resolve as pathResolve, sep as pathSep } from "node:path";
3
+ import { defaultExtensionDirs, listInstalledExtensions, MikanModels, parseFrontmatter, SessionStore, } from "../../harness/index.js";
4
+ import { loadConversationAutoReplyConfig, loadConversationWorkspaceOverride, loadGlobalSettings, resolveConversationSettings, saveConversationAutoReplyConfig, } from "../../config.js";
5
+ import { applyConversationSettings, applyConversationWorkspacePolicy, applyGlobalSettings, applyGlobalWorkspacePolicy, } from "../../settings-mutation.js";
6
+ import { addPackage, inspectConversationPackages, refreshPackage, removePackage, } from "../../packages/index.js";
7
+ import { escapeHtml, readJsonBody, renderPortalShell } from "../portal-shell.js";
8
8
  import { resolveExistingSessionFile } from "../session-view/service.js";
9
9
  import { PRODUCT_NAME } from "../../platform-messages.js";
10
- import { resolveActorVaultKey } from "../../vault/routing.js";
10
+ import { credentialAuthorizationKey } from "../../sandbox/identity.js";
11
+ import { resolveWorkspaceProjection } from "../../workspace-projection/index.js";
11
12
  import { sharedVaultKey } from "../../vault/index.js";
12
13
  import { modelKey, resolveAdminModelAccessStatuses } from "./provider-models.js";
14
+ import { assertPlatformName, createOfficeAddress, listRegisteredOffices, sameOffice, } from "../../office/index.js";
13
15
  // ── Handler ────────────────────────────────────────────────────────────────────
14
16
  export async function handleAdminRequest(req, res, url, services) {
15
17
  if (!url.pathname.startsWith("/admin"))
@@ -87,8 +89,16 @@ async function routeApiRequest(req, res, url, services) {
87
89
  serveSkillFile(res, url, services, token);
88
90
  return;
89
91
  }
92
+ if (url.pathname === "/admin/api/packages") {
93
+ await servePackagesList(res, url, services, token);
94
+ return;
95
+ }
96
+ if (url.pathname === "/admin/api/extension-secrets") {
97
+ serveExtensionSecretsList(res, url, services, token);
98
+ return;
99
+ }
90
100
  if (url.pathname === "/admin/api/events") {
91
- serveEventsList(res, services);
101
+ await serveEventsList(res, services);
92
102
  return;
93
103
  }
94
104
  if (url.pathname === "/admin/api/events/file") {
@@ -96,7 +106,7 @@ async function routeApiRequest(req, res, url, services) {
96
106
  return;
97
107
  }
98
108
  if (url.pathname === "/admin/api/conversations/events") {
99
- serveConversationEventsList(res, url, services, token);
109
+ await serveConversationEventsList(res, url, services, token);
100
110
  return;
101
111
  }
102
112
  jsonRes(res, 404, { error: "Not found" });
@@ -106,7 +116,7 @@ async function routeApiRequest(req, res, url, services) {
106
116
  jsonRes(res, 405, { error: "Method not allowed" });
107
117
  return;
108
118
  }
109
- const body = await readJsonBody(req, res);
119
+ const body = await readJsonBody(req, res, 32 * 1024);
110
120
  if (!body)
111
121
  return;
112
122
  const rawToken = typeof body.token === "string" ? body.token : "";
@@ -140,44 +150,65 @@ async function routeApiRequest(req, res, url, services) {
140
150
  return;
141
151
  }
142
152
  if (url.pathname === "/admin/api/conversations/events/delete") {
143
- serveConversationEventDelete(res, body, services, token);
153
+ await serveConversationEventDelete(res, body, services, token);
154
+ return;
155
+ }
156
+ if (url.pathname === "/admin/api/packages/mutate") {
157
+ servePackageMutation(res, body, services, token);
158
+ return;
159
+ }
160
+ if (url.pathname === "/admin/api/extension-secrets/mutate") {
161
+ serveExtensionSecretMutation(res, body, services);
144
162
  return;
145
163
  }
146
164
  if (url.pathname === "/admin/api/settings/model") {
147
- serveGlobalModelUpdate(res, body);
165
+ serveGlobalModelUpdate(res, body, services);
166
+ return;
167
+ }
168
+ if (url.pathname === "/admin/api/settings/workspace") {
169
+ serveGlobalWorkspaceUpdate(res, body, services);
148
170
  return;
149
171
  }
150
172
  if (url.pathname === "/admin/api/settings/sandbox") {
151
- serveGlobalSandboxUpdate(res, body);
173
+ serveGlobalSandboxUpdate(res, body, services);
152
174
  return;
153
175
  }
154
176
  if (url.pathname === "/admin/api/settings/slack") {
155
- serveGlobalSlackUpdate(res, body);
177
+ serveGlobalSlackUpdate(res, body, services);
156
178
  return;
157
179
  }
158
180
  jsonRes(res, 404, { error: "Not found" });
159
181
  }
160
- // ── Scope helpers ──────────────────────────────────────────────────────────────
161
- function resolveConversationId(requested, token) {
162
- if (!requested)
163
- return { conversationId: token.conversationId };
164
- if (requested === token.conversationId)
165
- return { conversationId: requested };
166
- if (requested.includes("/") || requested.includes("..")) {
167
- return { conversationId: requested, error: "Invalid conversationId." };
168
- }
169
- return { conversationId: requested };
182
+ /**
183
+ * Admin scope is a full office address. The platform defaults to the token's
184
+ * (an admin invoked from Slack browses Slack offices); cross-platform targets
185
+ * name theirs explicitly. Identity validation is the address factory's.
186
+ */
187
+ export function resolveConversationScope(requestedId, requestedPlatform, token) {
188
+ const fallback = createOfficeAddress(token.platform, token.conversationId);
189
+ try {
190
+ const address = createOfficeAddress(requestedPlatform ? assertPlatformName(requestedPlatform) : token.platform, requestedId || token.conversationId);
191
+ return { address, conversationId: address.conversationId };
192
+ }
193
+ catch {
194
+ return {
195
+ address: fallback,
196
+ conversationId: fallback.conversationId,
197
+ error: "Invalid conversation scope.",
198
+ };
199
+ }
170
200
  }
171
201
  function resolveTargetConversation(body, token) {
172
202
  const requested = typeof body.conversationId === "string" ? body.conversationId.trim() : "";
173
- return resolveConversationId(requested, token);
203
+ const platform = typeof body.platform === "string" ? body.platform.trim() : "";
204
+ return resolveConversationScope(requested, platform, token);
174
205
  }
175
- function requireAdminWorkingDir(res, services) {
176
- if (!services.workingDir) {
206
+ function requireAdminWorkspace(res, services) {
207
+ if (!services.workspace) {
177
208
  jsonRes(res, 503, { error: "Working directory not available" });
178
209
  return null;
179
210
  }
180
- return services.workingDir;
211
+ return services.workspace;
181
212
  }
182
213
  // ── API handlers ───────────────────────────────────────────────────────────────
183
214
  function serveMe(res, token) {
@@ -189,30 +220,20 @@ function serveMe(res, token) {
189
220
  expiresAt: token.expiresAt,
190
221
  });
191
222
  }
192
- const SETTINGS_FILES = new Set(["settings.json", "auto-reply", "auto-reply.disabled"]);
193
- function listConversationDirs(workingDir) {
194
- if (!existsSync(workingDir))
195
- return [];
196
- const skip = new Set(["vaults", "skills", "events", "node_modules", ".git"]);
197
- return readdirSync(workingDir, { withFileTypes: true })
198
- .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && !skip.has(entry.name))
199
- .map((entry) => entry.name)
200
- .filter((name) => {
201
- const dir = join(workingDir, name);
202
- try {
203
- const items = readdirSync(dir);
204
- // Conversation settings migrated to the state dir, so a sessions/
205
- // subdir is the primary marker for existing conversation dirs.
206
- return items.some((item) => item === "sessions" || SETTINGS_FILES.has(item) || item.endsWith(".jsonl"));
207
- }
208
- catch {
209
- return false;
210
- }
211
- })
212
- .toSorted((a, b) => a.localeCompare(b));
223
+ /**
224
+ * Office directories are office-key named and not reversible to raw ids, so
225
+ * enumeration reads the office registry — the durable raw-id ↔ office
226
+ * mapping — instead of scanning the workspace. Offices whose directory
227
+ * disappeared are filtered out.
228
+ */
229
+ function listAdminOffices(workspace) {
230
+ return listRegisteredOffices(workspace.stateDir)
231
+ .filter((office) => existsSync(workspace.office(office).dir))
232
+ .map((office) => createOfficeAddress(office.platform, office.conversationId))
233
+ .toSorted((a, b) => a.platform.localeCompare(b.platform) || a.conversationId.localeCompare(b.conversationId));
213
234
  }
214
- function conversationLastActivity(workingDir, conversationId) {
215
- const dir = join(workingDir, conversationId);
235
+ function conversationLastActivity(workspace, office) {
236
+ const dir = workspace.office(office).dir;
216
237
  if (!existsSync(dir))
217
238
  return null;
218
239
  let latest = 0;
@@ -245,48 +266,43 @@ function conversationLastActivity(workingDir, conversationId) {
245
266
  visit(dir, 0);
246
267
  return latest > 0 ? latest : null;
247
268
  }
248
- function conversationDisplayLabel(services, conversationId) {
249
- for (const [platform, bot] of Object.entries(services.botsByPlatform ?? {})) {
250
- const channel = bot?.getMessagingInfo().channels.find((c) => c.id === conversationId);
251
- if (channel)
252
- return `${platform}:#${channel.name}:${conversationId}`;
253
- }
254
- return conversationId;
269
+ function conversationDisplayLabel(services, office) {
270
+ const bot = services.botsByPlatform?.[office.platform];
271
+ const channel = bot?.getMessagingInfo().channels.find((c) => c.id === office.conversationId);
272
+ if (channel)
273
+ return `${office.platform}:#${channel.name}:${office.conversationId}`;
274
+ return `${office.platform}:${office.conversationId}`;
255
275
  }
256
276
  function serveConversationsList(res, services) {
257
- const workingDir = requireAdminWorkingDir(res, services);
258
- if (!workingDir)
259
- return;
260
- const ids = listConversationDirs(workingDir);
261
- const runningKeys = new Set(services.runtime?.getRunningSessions().map((s) => s.sessionKey) ?? []);
262
- const conversations = ids.map((conversationId) => {
263
- const lastActivity = conversationLastActivity(workingDir, conversationId);
264
- const running = Array.from(runningKeys).some((key) => key === conversationId || key.startsWith(`${conversationId}:`));
265
- return {
266
- conversationId,
267
- label: conversationDisplayLabel(services, conversationId),
268
- running,
269
- lastActivityAt: lastActivity,
270
- };
271
- });
277
+ const workspace = requireAdminWorkspace(res, services);
278
+ if (!workspace)
279
+ return;
280
+ const running = services.runtime?.getRunningSessions() ?? [];
281
+ const conversations = listAdminOffices(workspace).map((office) => ({
282
+ platform: office.platform,
283
+ conversationId: office.conversationId,
284
+ label: conversationDisplayLabel(services, office),
285
+ running: running.some((session) => sameOffice(session.address, office)),
286
+ lastActivityAt: conversationLastActivity(workspace, office),
287
+ }));
272
288
  jsonRes(res, 200, { conversations });
273
289
  }
274
290
  function serveSessionUsage(res, services) {
275
- const workingDir = requireAdminWorkingDir(res, services);
276
- if (!workingDir)
291
+ const workspace = requireAdminWorkspace(res, services);
292
+ if (!workspace)
277
293
  return;
278
- const rows = listConversationDirs(workingDir)
279
- .flatMap((conversationId) => listConversationSessionUsage(workingDir, conversationId, conversationDisplayLabel(services, conversationId)))
294
+ const rows = listAdminOffices(workspace)
295
+ .flatMap((office) => listConversationSessionUsage(workspace, office, conversationDisplayLabel(services, office)))
280
296
  .toSorted((a, b) => b.total - a.total)
281
297
  .slice(0, 20);
282
298
  jsonRes(res, 200, { sessions: rows });
283
299
  }
284
- function listConversationSessionUsage(workingDir, conversationId, label) {
285
- const sessionDir = join(workingDir, conversationId, "sessions");
300
+ function listConversationSessionUsage(workspace, office, label) {
301
+ const sessionDir = workspace.office(office).sessionsDir;
286
302
  try {
287
303
  return readdirSync(sessionDir, { withFileTypes: true })
288
304
  .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
289
- .flatMap((entry) => readSessionUsage(join(sessionDir, entry.name), conversationId, label));
305
+ .flatMap((entry) => readSessionUsage(join(sessionDir, entry.name), office.conversationId, label));
290
306
  }
291
307
  catch {
292
308
  return [];
@@ -346,11 +362,14 @@ function emptyBucket(date) {
346
362
  }
347
363
  /** Per-conversation daily token usage over the last N days (N clamped to 1..7). */
348
364
  function serveConversationUsage(res, url, services) {
349
- const workingDir = requireAdminWorkingDir(res, services);
350
- if (!workingDir)
351
- return;
352
- const conversationId = url.searchParams.get("conversationId") ?? "";
353
- if (!conversationId || !listConversationDirs(workingDir).includes(conversationId)) {
365
+ const workspace = requireAdminWorkspace(res, services);
366
+ if (!workspace)
367
+ return;
368
+ const conversationId = (url.searchParams.get("conversationId") ?? "").trim();
369
+ const platformParam = (url.searchParams.get("platform") ?? "").trim();
370
+ const office = listAdminOffices(workspace).find((candidate) => candidate.conversationId === conversationId &&
371
+ (!platformParam || candidate.platform === platformParam));
372
+ if (!office) {
354
373
  jsonRes(res, 400, { error: "Unknown conversationId" });
355
374
  return;
356
375
  }
@@ -369,7 +388,7 @@ function serveConversationUsage(res, url, services) {
369
388
  const cutoff = new Date(today);
370
389
  cutoff.setDate(today.getDate() - (days - 1));
371
390
  const flags = { hasOlder: false };
372
- const sessionDir = join(workingDir, conversationId, "sessions");
391
+ const sessionDir = workspace.office(office).sessionsDir;
373
392
  try {
374
393
  for (const entry of readdirSync(sessionDir, { withFileTypes: true })) {
375
394
  if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
@@ -391,8 +410,9 @@ function serveConversationUsage(res, url, services) {
391
410
  return sum;
392
411
  }, emptyBucket(""));
393
412
  jsonRes(res, 200, {
413
+ platform: office.platform,
394
414
  conversationId,
395
- label: conversationDisplayLabel(services, conversationId),
415
+ label: conversationDisplayLabel(services, office),
396
416
  days,
397
417
  hasOlder: flags.hasOlder,
398
418
  buckets: series,
@@ -444,19 +464,21 @@ function accumulateSessionUsageByDay(sessionFile, cutoff, buckets, flags) {
444
464
  }
445
465
  }
446
466
  function serveConversationState(res, url, services, token) {
447
- const workingDir = requireAdminWorkingDir(res, services);
448
- if (!workingDir)
467
+ const workspace = requireAdminWorkspace(res, services);
468
+ if (!workspace)
449
469
  return;
450
- const requested = url.searchParams.get("conversationId")?.trim() ?? "";
451
- const conversationId = requested || token.conversationId;
452
- if (conversationId.includes("/") || conversationId.includes("..")) {
453
- jsonRes(res, 400, { error: "Invalid conversationId" });
470
+ const scope = resolveConversationFromQuery(url, token);
471
+ if (scope.error) {
472
+ jsonRes(res, 400, { error: scope.error });
454
473
  return;
455
474
  }
456
- const dir = join(workingDir, conversationId);
475
+ const conversationId = scope.conversationId;
476
+ const office = workspace.office(scope.address);
457
477
  const globalConfig = loadGlobalSettings();
458
- const conversationConfig = resolveConversationSettings(dir);
459
- const autoReply = loadConversationAutoReplyConfig(dir);
478
+ const conversationConfig = resolveConversationSettings(office);
479
+ const conversationWorkspace = resolveWorkspaceProjection(office);
480
+ const globalWorkspaceSettings = globalConfig.sandbox?.workspace;
481
+ const autoReply = loadConversationAutoReplyConfig(office.dir);
460
482
  jsonRes(res, 200, {
461
483
  conversationId,
462
484
  provider: conversationConfig.provider,
@@ -465,8 +487,11 @@ function serveConversationState(res, url, services, token) {
465
487
  globalProvider: globalConfig.provider,
466
488
  globalModel: globalConfig.model,
467
489
  globalThinkingLevel: globalConfig.thinkingLevel,
468
- sandboxImageWorkspaceMount: conversationConfig.sandboxImageWorkspaceMount ?? null,
469
- globalSandboxImageWorkspaceMount: globalConfig.sandboxImageWorkspaceMount ?? null,
490
+ workspaceDoorPolicy: conversationWorkspace.doorPolicy,
491
+ workspaceLayout: conversationWorkspace.layout,
492
+ workspaceOverride: doorPolicyChoiceKey(loadConversationWorkspaceOverride(office)),
493
+ globalWorkspaceDoorPolicy: globalWorkspaceSettings?.doorPolicy ?? "isolated",
494
+ globalWorkspaceLayout: globalWorkspaceSettings?.layout ?? "conversation",
470
495
  autoReplyEnabled: autoReply.enabled,
471
496
  autoReplyRules: autoReply.rules,
472
497
  slack: {
@@ -482,12 +507,13 @@ function serveGlobalSettings(res) {
482
507
  provider: config.provider,
483
508
  model: config.model,
484
509
  thinkingLevel: config.thinkingLevel,
485
- sandboxCpus: config.sandboxCpus ?? null,
486
- sandboxMemory: config.sandboxMemory ?? null,
487
- sandboxBoostCpus: config.sandboxBoostCpus ?? null,
488
- sandboxBoostMemory: config.sandboxBoostMemory ?? null,
489
- sandboxImageWorkspaceMount: config.sandboxImageWorkspaceMount ?? null,
490
- defaultSharedVault: config.defaultSharedVault ?? null,
510
+ sandboxCpus: config.sandbox?.cpus ?? null,
511
+ sandboxMemory: config.sandbox?.memory ?? null,
512
+ sandboxBoostCpus: config.sandbox?.boost?.cpus ?? null,
513
+ sandboxBoostMemory: config.sandbox?.boost?.memory ?? null,
514
+ workspaceDoorPolicy: config.sandbox?.workspace?.doorPolicy ?? "isolated",
515
+ workspaceLayout: config.sandbox?.workspace?.layout ?? "conversation",
516
+ defaultSharedVault: config.sandbox?.defaultSharedVault ?? null,
491
517
  slack: {
492
518
  replyMode: config.slack?.replyMode ?? "top-level",
493
519
  },
@@ -518,7 +544,7 @@ async function serveModelsList(res) {
518
544
  jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
519
545
  }
520
546
  }
521
- const VALID_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh"]);
547
+ const VALID_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
522
548
  function serveConversationModelUpdate(res, body, services, token) {
523
549
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
524
550
  const model = typeof body.model === "string" ? body.model.trim() : "";
@@ -534,30 +560,55 @@ function serveConversationModelUpdate(res, body, services, token) {
534
560
  jsonRes(res, 403, { error: scope.error });
535
561
  return;
536
562
  }
537
- const workingDir = requireAdminWorkingDir(res, services);
538
- if (!workingDir)
563
+ const workspace = requireAdminWorkspace(res, services);
564
+ if (!workspace)
539
565
  return;
540
- const dir = join(workingDir, scope.conversationId);
541
566
  try {
542
- updateConversationSettings(dir, {
567
+ const result = applyConversationSettings(services.runtime, workspace.office(scope.address), {
543
568
  provider,
544
569
  model,
545
570
  ...(thinkingLevel ? { thinkingLevel } : {}),
546
571
  });
547
- let runtimeSwitched = null;
548
- if (services.runtime) {
549
- runtimeSwitched = services.runtime.switchConversationModel(scope.conversationId, provider, model);
572
+ if (!result.ok) {
573
+ jsonRes(res, 409, {
574
+ error: "Conversation has a running job; retry after it finishes (or /stop it).",
575
+ });
576
+ return;
550
577
  }
551
- jsonRes(res, 200, { ok: true, runtimeSwitched });
578
+ jsonRes(res, 200, { ok: true, runtimeSwitched: result.runtimeSwitched });
552
579
  }
553
580
  catch (err) {
554
581
  jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
555
582
  }
556
583
  }
584
+ /** Wire values for a door-policy selection; "default" clears the office's override. */
585
+ function parseDoorPolicyChoice(value) {
586
+ switch (value) {
587
+ case "default":
588
+ return { choice: null };
589
+ case "isolated":
590
+ return { choice: { doorPolicy: "isolated" } };
591
+ case "trusted-shared-support":
592
+ return { choice: { doorPolicy: "trusted", layout: "shared-support" } };
593
+ case "trusted-full":
594
+ return { choice: { doorPolicy: "trusted", layout: "full" } };
595
+ default:
596
+ return undefined;
597
+ }
598
+ }
599
+ function doorPolicyChoiceKey(choice) {
600
+ if (!choice)
601
+ return "default";
602
+ if (choice.doorPolicy === "isolated")
603
+ return "isolated";
604
+ return choice.layout === "full" ? "trusted-full" : "trusted-shared-support";
605
+ }
557
606
  function serveConversationSandboxUpdate(res, body, services, token) {
558
- const workspaceMount = body.workspaceMount;
559
- if (workspaceMount !== "private" && workspaceMount !== "full") {
560
- jsonRes(res, 400, { error: "workspaceMount must be 'private' or 'full'" });
607
+ const parsed = parseDoorPolicyChoice(body.doorPolicy);
608
+ if (!parsed) {
609
+ jsonRes(res, 400, {
610
+ error: "doorPolicy must be 'default', 'isolated', 'trusted-shared-support', or 'trusted-full'",
611
+ });
561
612
  return;
562
613
  }
563
614
  const scope = resolveTargetConversation(body, token);
@@ -565,18 +616,37 @@ function serveConversationSandboxUpdate(res, body, services, token) {
565
616
  jsonRes(res, 403, { error: scope.error });
566
617
  return;
567
618
  }
568
- const workingDir = requireAdminWorkingDir(res, services);
569
- if (!workingDir)
619
+ const workspace = requireAdminWorkspace(res, services);
620
+ if (!workspace)
570
621
  return;
571
- const dir = join(workingDir, scope.conversationId);
572
622
  try {
573
- updateConversationSettings(dir, { sandboxImageWorkspaceMount: workspaceMount });
623
+ const result = applyConversationWorkspacePolicy(services.runtime, workspace.office(scope.address), parsed.choice);
624
+ if (!result.ok) {
625
+ jsonRes(res, 409, { error: "Conversation is busy; retry when the current run finishes" });
626
+ return;
627
+ }
574
628
  jsonRes(res, 200, { ok: true });
575
629
  }
576
630
  catch (err) {
577
631
  jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
578
632
  }
579
633
  }
634
+ function serveGlobalWorkspaceUpdate(res, body, services) {
635
+ const parsed = parseDoorPolicyChoice(body.doorPolicy);
636
+ if (!parsed) {
637
+ jsonRes(res, 400, {
638
+ error: "doorPolicy must be 'default', 'isolated', 'trusted-shared-support', or 'trusted-full'",
639
+ });
640
+ return;
641
+ }
642
+ try {
643
+ const result = applyGlobalWorkspacePolicy(services.runtime, parsed.choice);
644
+ jsonRes(res, 200, { ok: true, staleConversations: result.staleConversations.length });
645
+ }
646
+ catch (err) {
647
+ jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
648
+ }
649
+ }
580
650
  function serveConversationSlackUpdate(res, body, services, token) {
581
651
  const replyMode = body.replyMode;
582
652
  if (replyMode !== "top-level" && replyMode !== "thread") {
@@ -588,12 +658,13 @@ function serveConversationSlackUpdate(res, body, services, token) {
588
658
  jsonRes(res, 403, { error: scope.error });
589
659
  return;
590
660
  }
591
- const workingDir = requireAdminWorkingDir(res, services);
592
- if (!workingDir)
661
+ const workspace = requireAdminWorkspace(res, services);
662
+ if (!workspace)
593
663
  return;
594
- const dir = join(workingDir, scope.conversationId);
595
664
  try {
596
- updateConversationSettings(dir, { slack: { replyMode } });
665
+ applyConversationSettings(services.runtime, workspace.office(scope.address), {
666
+ slack: { replyMode },
667
+ });
597
668
  jsonRes(res, 200, { ok: true });
598
669
  }
599
670
  catch (err) {
@@ -613,10 +684,10 @@ function serveConversationAutoReplyUpdate(res, body, services, token) {
613
684
  jsonRes(res, 403, { error: scope.error });
614
685
  return;
615
686
  }
616
- const workingDir = requireAdminWorkingDir(res, services);
617
- if (!workingDir)
687
+ const workspace = requireAdminWorkspace(res, services);
688
+ if (!workspace)
618
689
  return;
619
- const dir = join(workingDir, scope.conversationId);
690
+ const dir = workspace.office(scope.address).dir;
620
691
  try {
621
692
  const existing = loadConversationAutoReplyConfig(dir);
622
693
  saveConversationAutoReplyConfig(dir, {
@@ -635,8 +706,8 @@ function serveConversationSessionLink(res, body, services, token) {
635
706
  jsonRes(res, 403, { error: scope.error });
636
707
  return;
637
708
  }
638
- const workingDir = requireAdminWorkingDir(res, services);
639
- if (!workingDir)
709
+ const workspace = requireAdminWorkspace(res, services);
710
+ if (!workspace)
640
711
  return;
641
712
  if (!services.sessionViewTokenStore) {
642
713
  jsonRes(res, 503, { error: "Session view token store not available" });
@@ -648,7 +719,7 @@ function serveConversationSessionLink(res, body, services, token) {
648
719
  });
649
720
  return;
650
721
  }
651
- const sessionFile = resolveExistingSessionFile(workingDir, scope.conversationId, scope.conversationId);
722
+ const sessionFile = resolveExistingSessionFile(workspace.office(scope.address).dir, scope.conversationId);
652
723
  if (!sessionFile) {
653
724
  jsonRes(res, 404, { error: "No session file found for this conversation" });
654
725
  return;
@@ -688,7 +759,10 @@ function serveConversationLoginLink(res, body, services, token) {
688
759
  }
689
760
  else {
690
761
  try {
691
- vaultId = resolveActorVaultKey(services.sandbox, token.platformUserId, scope.conversationId);
762
+ vaultId = credentialAuthorizationKey(services.sandbox, {
763
+ userId: token.platformUserId,
764
+ address: scope.address,
765
+ });
692
766
  }
693
767
  catch (err) {
694
768
  jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
@@ -704,7 +778,7 @@ function serveConversationLoginLink(res, body, services, token) {
704
778
  jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
705
779
  }
706
780
  }
707
- function serveGlobalModelUpdate(res, body) {
781
+ function serveGlobalModelUpdate(res, body, services) {
708
782
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
709
783
  const model = typeof body.model === "string" ? body.model.trim() : "";
710
784
  const thinkingLevel = typeof body.thinkingLevel === "string" && VALID_THINKING_LEVELS.has(body.thinkingLevel)
@@ -715,55 +789,57 @@ function serveGlobalModelUpdate(res, body) {
715
789
  return;
716
790
  }
717
791
  try {
718
- updateGlobalSettings({
792
+ const result = applyGlobalSettings(services.runtime, {
719
793
  provider,
720
794
  model,
721
795
  ...(thinkingLevel ? { thinkingLevel } : {}),
722
796
  });
723
- jsonRes(res, 200, { ok: true });
797
+ jsonRes(res, 200, {
798
+ ok: true,
799
+ staleConversations: result.staleConversations.map((office) => `${office.platform}:${office.conversationId}`),
800
+ });
724
801
  }
725
802
  catch (err) {
726
803
  jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
727
804
  }
728
805
  }
729
- function serveGlobalSlackUpdate(res, body) {
806
+ function serveGlobalSlackUpdate(res, body, services) {
730
807
  const replyMode = body.replyMode;
731
808
  if (replyMode !== "top-level" && replyMode !== "thread") {
732
809
  jsonRes(res, 400, { error: "replyMode must be 'top-level' or 'thread'" });
733
810
  return;
734
811
  }
735
812
  try {
736
- updateGlobalSettings({ slack: { replyMode } });
813
+ applyGlobalSettings(services.runtime, { slack: { replyMode } });
737
814
  jsonRes(res, 200, { ok: true });
738
815
  }
739
816
  catch (err) {
740
817
  jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
741
818
  }
742
819
  }
743
- function serveGlobalSandboxUpdate(res, body) {
820
+ function serveGlobalSandboxUpdate(res, body, services) {
744
821
  const cpus = typeof body.cpus === "string" ? body.cpus.trim() : "";
745
822
  const memory = typeof body.memory === "string" ? body.memory.trim() : "";
746
823
  const boostCpus = typeof body.boostCpus === "string" ? body.boostCpus.trim() : "";
747
824
  const boostMemory = typeof body.boostMemory === "string" ? body.boostMemory.trim() : "";
748
- const workspaceMount = body.workspaceMount;
749
- const validMount = workspaceMount === "private" || workspaceMount === "full";
750
- const update = {};
751
- if (cpus)
752
- update.sandboxCpus = cpus;
753
- if (memory)
754
- update.sandboxMemory = memory;
755
- if (boostCpus)
756
- update.sandboxBoostCpus = boostCpus;
757
- if (boostMemory)
758
- update.sandboxBoostMemory = boostMemory;
759
- if (validMount)
760
- update.sandboxImageWorkspaceMount = workspaceMount;
825
+ const update = {
826
+ ...(cpus ? { cpus } : {}),
827
+ ...(memory ? { memory } : {}),
828
+ ...(boostCpus || boostMemory
829
+ ? {
830
+ boost: {
831
+ ...(boostCpus ? { cpus: boostCpus } : {}),
832
+ ...(boostMemory ? { memory: boostMemory } : {}),
833
+ },
834
+ }
835
+ : {}),
836
+ };
761
837
  if (Object.keys(update).length === 0) {
762
838
  jsonRes(res, 400, { error: "No valid sandbox fields provided" });
763
839
  return;
764
840
  }
765
841
  try {
766
- updateGlobalSettings(update);
842
+ applyGlobalSettings(services.runtime, { sandbox: update });
767
843
  jsonRes(res, 200, { ok: true });
768
844
  }
769
845
  catch (err) {
@@ -784,9 +860,9 @@ function isWorkspacePathAllowed(rel) {
784
860
  if (rel === "")
785
861
  return true;
786
862
  const segments = rel.split("/").filter(Boolean);
787
- if (segments.length === 0)
788
- return true;
789
863
  const first = segments[0];
864
+ if (first === undefined)
865
+ return true;
790
866
  if (segments.length === 1) {
791
867
  return WORKSPACE_TOP_DIRS.has(first) || WORKSPACE_TOP_FILES.has(first);
792
868
  }
@@ -794,7 +870,8 @@ function isWorkspacePathAllowed(rel) {
794
870
  }
795
871
  function resolveConversationFromQuery(url, token) {
796
872
  const requested = (url.searchParams.get("conversationId") ?? "").trim();
797
- return resolveConversationId(requested, token);
873
+ const platform = (url.searchParams.get("platform") ?? "").trim();
874
+ return resolveConversationScope(requested, platform, token);
798
875
  }
799
876
  function safeJoinUnderRoot(rootDir, relative) {
800
877
  if (relative.startsWith("/") || relative.includes("\0")) {
@@ -880,10 +957,10 @@ function serveWorkspaceTree(res, url, services, token) {
880
957
  jsonRes(res, 403, { error: scope.error });
881
958
  return;
882
959
  }
883
- const workingDir = requireAdminWorkingDir(res, services);
884
- if (!workingDir)
960
+ const workspace = requireAdminWorkspace(res, services);
961
+ if (!workspace)
885
962
  return;
886
- const convDir = join(workingDir, scope.conversationId);
963
+ const convDir = workspace.office(scope.address).dir;
887
964
  if (!existsSync(convDir)) {
888
965
  jsonRes(res, 200, { conversationId: scope.conversationId, tree: null });
889
966
  return;
@@ -910,7 +987,7 @@ function looksTextual(buf) {
910
987
  const limit = Math.min(buf.length, BINARY_PROBE_BYTES);
911
988
  for (let i = 0; i < limit; i++) {
912
989
  const byte = buf[i];
913
- if (byte === 0)
990
+ if (byte === undefined || byte === 0)
914
991
  return false;
915
992
  if (byte < 9)
916
993
  return false;
@@ -974,8 +1051,8 @@ function serveWorkspaceFile(res, url, services, token) {
974
1051
  jsonRes(res, 403, { error: scope.error });
975
1052
  return;
976
1053
  }
977
- const workingDir = requireAdminWorkingDir(res, services);
978
- if (!workingDir)
1054
+ const workspace = requireAdminWorkspace(res, services);
1055
+ if (!workspace)
979
1056
  return;
980
1057
  const requestedPath = (url.searchParams.get("path") ?? "").trim();
981
1058
  if (!requestedPath) {
@@ -986,7 +1063,7 @@ function serveWorkspaceFile(res, url, services, token) {
986
1063
  jsonRes(res, 403, { error: "Workspace path is not exposed" });
987
1064
  return;
988
1065
  }
989
- const convDir = join(workingDir, scope.conversationId);
1066
+ const convDir = workspace.office(scope.address).dir;
990
1067
  const safe = safeJoinUnderRoot(convDir, requestedPath);
991
1068
  if (safe.error) {
992
1069
  jsonRes(res, 400, { error: safe.error });
@@ -994,7 +1071,12 @@ function serveWorkspaceFile(res, url, services, token) {
994
1071
  }
995
1072
  servePreviewFile(res, safe.absolute, { path: requestedPath }, "File not found");
996
1073
  }
997
- function parseSkillFrontmatter(filePath) {
1074
+ /**
1075
+ * Read skill metadata via the harness frontmatter parser (the owning module).
1076
+ * The portal historically accepted frontmatter keys case-insensitively, so
1077
+ * the lookup — not the parser — preserves that leniency.
1078
+ */
1079
+ function readSkillMeta(filePath) {
998
1080
  let text;
999
1081
  try {
1000
1082
  text = readFileSync(filePath, "utf-8");
@@ -1002,31 +1084,18 @@ function parseSkillFrontmatter(filePath) {
1002
1084
  catch {
1003
1085
  return {};
1004
1086
  }
1005
- if (!text.startsWith("---"))
1006
- return {};
1007
- const end = text.indexOf("\n---", 3);
1008
- if (end < 0)
1009
- return {};
1010
- const block = text.slice(3, end);
1087
+ const { values } = parseFrontmatter(text);
1011
1088
  const out = {};
1012
- for (const line of block.split("\n")) {
1013
- const colon = line.indexOf(":");
1014
- if (colon < 0)
1015
- continue;
1016
- const key = line.slice(0, colon).trim().toLowerCase();
1017
- let value = line.slice(colon + 1).trim();
1018
- if ((value.startsWith('"') && value.endsWith('"')) ||
1019
- (value.startsWith("'") && value.endsWith("'"))) {
1020
- value = value.slice(1, -1);
1021
- }
1022
- if (key === "name")
1089
+ for (const [key, value] of Object.entries(values)) {
1090
+ const normalized = key.toLowerCase();
1091
+ if (normalized === "name")
1023
1092
  out.name = value;
1024
- if (key === "description")
1093
+ if (normalized === "description")
1025
1094
  out.description = value;
1026
1095
  }
1027
1096
  return out;
1028
1097
  }
1029
- function readSkillsFromDir(skillsDir, source) {
1098
+ export function readSkillsFromDir(skillsDir, source) {
1030
1099
  if (!existsSync(skillsDir))
1031
1100
  return [];
1032
1101
  const out = [];
@@ -1043,7 +1112,7 @@ function readSkillsFromDir(skillsDir, source) {
1043
1112
  const skillMd = join(skillsDir, entry.name, "SKILL.md");
1044
1113
  if (!existsSync(skillMd))
1045
1114
  continue;
1046
- const meta = parseSkillFrontmatter(skillMd);
1115
+ const meta = readSkillMeta(skillMd);
1047
1116
  out.push({
1048
1117
  name: meta.name ?? entry.name,
1049
1118
  description: meta.description ?? "",
@@ -1054,17 +1123,157 @@ function readSkillsFromDir(skillsDir, source) {
1054
1123
  }
1055
1124
  return out.toSorted((a, b) => a.name.localeCompare(b.name));
1056
1125
  }
1126
+ /** Inventory of both scopes' declared packages for the selected conversation. */
1127
+ async function servePackagesList(res, url, services, token) {
1128
+ const scope = resolveConversationFromQuery(url, token);
1129
+ if (scope.error) {
1130
+ jsonRes(res, 403, { error: scope.error });
1131
+ return;
1132
+ }
1133
+ const workspace = requireAdminWorkspace(res, services);
1134
+ if (!workspace)
1135
+ return;
1136
+ try {
1137
+ const inventory = await inspectConversationPackages({
1138
+ office: workspace.office(scope.address),
1139
+ });
1140
+ jsonRes(res, 200, { conversationId: scope.conversationId, ...inventory });
1141
+ }
1142
+ catch (err) {
1143
+ jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
1144
+ }
1145
+ }
1146
+ const EXTENSION_SLUG_PATTERN = /^[a-z0-9_-]{1,64}$/;
1147
+ const EXTENSION_SECRET_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
1148
+ /**
1149
+ * Declared + provisioned secrets for every extension visible to the selected
1150
+ * conversation (global scope plus the conversation's own installs, deduped by
1151
+ * slug — the vault is keyed by slug, so two scopes share one secret set).
1152
+ * Values never leave the server; only key names and provisioning status do.
1153
+ */
1154
+ function serveExtensionSecretsList(res, url, services, token) {
1155
+ const scope = resolveConversationFromQuery(url, token);
1156
+ if (scope.error) {
1157
+ jsonRes(res, 403, { error: scope.error });
1158
+ return;
1159
+ }
1160
+ const workspace = requireAdminWorkspace(res, services);
1161
+ if (!workspace)
1162
+ return;
1163
+ const infosBySlug = new Map();
1164
+ for (const info of listInstalledExtensions(defaultExtensionDirs(scope.address, workspace.stateDir))) {
1165
+ infosBySlug.set(info.slug, info);
1166
+ }
1167
+ const extensions = [...infosBySlug.values()]
1168
+ .toSorted((left, right) => left.slug.localeCompare(right.slug))
1169
+ .map((info) => {
1170
+ const provisioned = Object.keys(services.vaultManager.resolve(`extensions/${info.slug}`)?.env ?? {}).toSorted();
1171
+ const declaredKeys = new Set(info.secrets.map((secret) => secret.key));
1172
+ return {
1173
+ slug: info.slug,
1174
+ name: info.name,
1175
+ declared: info.secrets.map((secret) => ({
1176
+ ...secret,
1177
+ provisioned: provisioned.includes(secret.key),
1178
+ })),
1179
+ extraProvisioned: provisioned.filter((key) => !declaredKeys.has(key)),
1180
+ };
1181
+ });
1182
+ jsonRes(res, 200, { extensions });
1183
+ }
1184
+ /** Set or delete one extension secret. Values are write-only through here. */
1185
+ function serveExtensionSecretMutation(res, body, services) {
1186
+ const action = body.action;
1187
+ if (action !== "set" && action !== "delete") {
1188
+ jsonRes(res, 400, { error: "action must be 'set' or 'delete'" });
1189
+ return;
1190
+ }
1191
+ const slug = typeof body.slug === "string" ? body.slug : "";
1192
+ if (!EXTENSION_SLUG_PATTERN.test(slug)) {
1193
+ jsonRes(res, 400, { error: "invalid extension slug" });
1194
+ return;
1195
+ }
1196
+ const key = typeof body.key === "string" ? body.key : "";
1197
+ if (!EXTENSION_SECRET_KEY_PATTERN.test(key)) {
1198
+ jsonRes(res, 400, { error: "invalid secret key (expected an env-style name)" });
1199
+ return;
1200
+ }
1201
+ const vaultKey = `extensions/${slug}`;
1202
+ if (action === "set") {
1203
+ const value = typeof body.value === "string" ? body.value : "";
1204
+ if (!value) {
1205
+ jsonRes(res, 400, { error: "value is required" });
1206
+ return;
1207
+ }
1208
+ services.vaultManager.upsertEnv(vaultKey, { [key]: value });
1209
+ }
1210
+ else if (!services.vaultManager.deleteEnvKey(vaultKey, key)) {
1211
+ jsonRes(res, 404, { error: "Not provisioned." });
1212
+ return;
1213
+ }
1214
+ const provisioned = Object.keys(services.vaultManager.resolve(vaultKey)?.env ?? {}).toSorted();
1215
+ jsonRes(res, 200, { ok: true, provisioned });
1216
+ }
1217
+ /**
1218
+ * Add / remove / refresh, in one route because they share validation and all
1219
+ * three answer with the freshly re-read inventory: the panel never has to
1220
+ * guess what the write did.
1221
+ */
1222
+ function servePackageMutation(res, body, services, token) {
1223
+ const action = body.action;
1224
+ if (action !== "add" && action !== "remove" && action !== "refresh") {
1225
+ jsonRes(res, 400, { error: "action must be 'add', 'remove', or 'refresh'" });
1226
+ return;
1227
+ }
1228
+ const packageScope = body.scope === "global" ? "global" : "conversation";
1229
+ const source = typeof body.source === "string" ? body.source.trim() : "";
1230
+ if (!source) {
1231
+ jsonRes(res, 400, { error: "source is required" });
1232
+ return;
1233
+ }
1234
+ const scope = resolveTargetConversation(body, token);
1235
+ if (scope.error) {
1236
+ jsonRes(res, 403, { error: scope.error });
1237
+ return;
1238
+ }
1239
+ const workspace = requireAdminWorkspace(res, services);
1240
+ if (!workspace)
1241
+ return;
1242
+ const context = {
1243
+ office: workspace.office(scope.address),
1244
+ runtime: services.runtime,
1245
+ };
1246
+ try {
1247
+ if (action === "add") {
1248
+ const result = addPackage(packageScope, source, context);
1249
+ jsonRes(res, 200, { ok: true, source: result.source, dir: result.dir });
1250
+ return;
1251
+ }
1252
+ if (action === "refresh") {
1253
+ const result = refreshPackage(packageScope, source, context);
1254
+ jsonRes(res, 200, { ok: true, source: result.source, dir: result.dir });
1255
+ return;
1256
+ }
1257
+ const removed = removePackage(packageScope, source, context);
1258
+ jsonRes(res, removed ? 200 : 404, removed ? { ok: true } : { error: "Not declared here." });
1259
+ }
1260
+ catch (err) {
1261
+ // Fetch/validation failures are the admin's problem to fix, not a server
1262
+ // fault: report them as a bad request with git's own message.
1263
+ jsonRes(res, 400, { error: err instanceof Error ? err.message : String(err) });
1264
+ }
1265
+ }
1057
1266
  function serveSkillsList(res, url, services, token) {
1058
1267
  const scope = resolveConversationFromQuery(url, token);
1059
1268
  if (scope.error) {
1060
1269
  jsonRes(res, 403, { error: scope.error });
1061
1270
  return;
1062
1271
  }
1063
- const workingDir = requireAdminWorkingDir(res, services);
1064
- if (!workingDir)
1272
+ const workspace = requireAdminWorkspace(res, services);
1273
+ if (!workspace)
1065
1274
  return;
1066
- const global = readSkillsFromDir(join(workingDir, "skills"), "global");
1067
- const conversation = readSkillsFromDir(join(workingDir, scope.conversationId, "skills"), "conversation");
1275
+ const global = readSkillsFromDir(workspace.skillsDir, "global");
1276
+ const conversation = readSkillsFromDir(workspace.office(scope.address).skillsDir, "conversation");
1068
1277
  jsonRes(res, 200, {
1069
1278
  conversationId: scope.conversationId,
1070
1279
  skills: [...global, ...conversation],
@@ -1076,8 +1285,8 @@ function serveSkillFile(res, url, services, token) {
1076
1285
  jsonRes(res, 403, { error: scope.error });
1077
1286
  return;
1078
1287
  }
1079
- const workingDir = requireAdminWorkingDir(res, services);
1080
- if (!workingDir)
1288
+ const workspace = requireAdminWorkspace(res, services);
1289
+ if (!workspace)
1081
1290
  return;
1082
1291
  const source = (url.searchParams.get("source") ?? "").trim();
1083
1292
  const directory = (url.searchParams.get("directory") ?? "").trim();
@@ -1092,9 +1301,7 @@ function serveSkillFile(res, url, services, token) {
1092
1301
  jsonRes(res, 400, { error: "Invalid skill directory" });
1093
1302
  return;
1094
1303
  }
1095
- const skillsRoot = source === "global"
1096
- ? join(workingDir, "skills")
1097
- : join(workingDir, scope.conversationId, "skills");
1304
+ const skillsRoot = source === "global" ? workspace.skillsDir : workspace.office(scope.address).skillsDir;
1098
1305
  const safe = safeJoinUnderRoot(skillsRoot, join(directory, "SKILL.md"));
1099
1306
  if (safe.error) {
1100
1307
  jsonRes(res, 400, { error: safe.error });
@@ -1104,87 +1311,65 @@ function serveSkillFile(res, url, services, token) {
1104
1311
  }
1105
1312
  // ── Events ─────────────────────────────────────────────────────────────────────
1106
1313
  const EVENTS_FILE_MAX_BYTES = 64 * 1024;
1107
- function listAllEvents(workingDir) {
1108
- const dir = join(workingDir, "events");
1109
- if (!existsSync(dir))
1110
- return [];
1111
- let entries;
1112
- try {
1113
- entries = readdirSync(dir, { withFileTypes: true });
1114
- }
1115
- catch {
1116
- return [];
1117
- }
1118
- return entries
1119
- .filter((e) => e.isFile() && e.name.endsWith(".json"))
1120
- .map((e) => {
1121
- const filePath = join(dir, e.name);
1122
- let stats;
1123
- try {
1124
- stats = statSync(filePath);
1125
- }
1126
- catch {
1127
- return null;
1128
- }
1129
- let parsed = null;
1130
- try {
1131
- parsed = JSON.parse(readFileSync(filePath, "utf-8"));
1132
- }
1133
- catch {
1134
- // Keep entry; just omit parsed fields.
1135
- }
1136
- const meta = parsed && typeof parsed === "object" ? parsed : {};
1137
- // events.ts accepts `channelId` as a legacy alias for `conversationId`.
1138
- const conversationId = typeof meta.conversationId === "string"
1139
- ? meta.conversationId
1140
- : typeof meta.channelId === "string"
1141
- ? meta.channelId
1142
- : null;
1314
+ /**
1315
+ * List events through the owning store, whose payloads are validated by the
1316
+ * event-format module (files that fail validation stay visible with a null
1317
+ * payload so operators can delete them).
1318
+ */
1319
+ export async function listAllEvents(store) {
1320
+ const entries = await store.list();
1321
+ return entries.map((entry) => {
1322
+ const payload = entry.payload;
1143
1323
  return {
1144
- name: e.name,
1145
- size: stats.size,
1146
- mtimeMs: stats.mtimeMs,
1147
- type: typeof meta.type === "string" ? meta.type : null,
1148
- platform: typeof meta.platform === "string" ? meta.platform : null,
1149
- conversationId,
1150
- text: typeof meta.text === "string" ? meta.text : null,
1151
- at: typeof meta.at === "string" ? meta.at : null,
1152
- schedule: typeof meta.schedule === "string" ? meta.schedule : null,
1153
- timezone: typeof meta.timezone === "string" ? meta.timezone : null,
1324
+ name: entry.filename,
1325
+ size: entry.size,
1326
+ mtimeMs: entry.mtimeMs,
1327
+ type: payload?.type ?? null,
1328
+ platform: payload?.platform ?? null,
1329
+ conversationId: payload?.conversationId ?? null,
1330
+ text: payload?.text ?? null,
1331
+ at: payload?.type === "one-shot" ? payload.at : null,
1332
+ schedule: payload?.type === "periodic" ? payload.schedule : null,
1333
+ timezone: payload?.type === "periodic" ? payload.timezone : null,
1154
1334
  };
1155
- })
1156
- .filter((e) => e !== null)
1157
- .toSorted((a, b) => a.name.localeCompare(b.name));
1335
+ });
1158
1336
  }
1159
- function serveEventsList(res, services) {
1160
- const workingDir = requireAdminWorkingDir(res, services);
1161
- if (!workingDir)
1337
+ function requireAdminEventStore(res, services) {
1338
+ if (!services.eventStore) {
1339
+ jsonRes(res, 503, { error: "Working directory not available" });
1340
+ return null;
1341
+ }
1342
+ return services.eventStore;
1343
+ }
1344
+ async function serveEventsList(res, services) {
1345
+ const store = requireAdminEventStore(res, services);
1346
+ if (!store)
1162
1347
  return;
1163
- jsonRes(res, 200, { events: listAllEvents(workingDir) });
1348
+ jsonRes(res, 200, { events: await listAllEvents(store) });
1164
1349
  }
1165
1350
  /** Per-conversation listing — filter all events by conversationId match. */
1166
- function serveConversationEventsList(res, url, services, token) {
1351
+ async function serveConversationEventsList(res, url, services, token) {
1167
1352
  const scope = resolveConversationFromQuery(url, token);
1168
1353
  if (scope.error) {
1169
1354
  jsonRes(res, 403, { error: scope.error });
1170
1355
  return;
1171
1356
  }
1172
- const workingDir = requireAdminWorkingDir(res, services);
1173
- if (!workingDir)
1357
+ const store = requireAdminEventStore(res, services);
1358
+ if (!store)
1174
1359
  return;
1175
- const events = listAllEvents(workingDir).filter((e) => e.conversationId === scope.conversationId);
1360
+ const events = (await listAllEvents(store)).filter((e) => e.conversationId === scope.conversationId);
1176
1361
  jsonRes(res, 200, { conversationId: scope.conversationId, events });
1177
1362
  }
1178
1363
  function serveEventsFile(res, url, services) {
1179
- const workingDir = requireAdminWorkingDir(res, services);
1180
- if (!workingDir)
1364
+ const workspace = requireAdminWorkspace(res, services);
1365
+ if (!workspace)
1181
1366
  return;
1182
1367
  const name = (url.searchParams.get("name") ?? "").trim();
1183
1368
  if (!name || name.includes("/") || name.includes("\\") || name.includes("..")) {
1184
1369
  jsonRes(res, 400, { error: "Invalid name" });
1185
1370
  return;
1186
1371
  }
1187
- const filePath = join(workingDir, "events", name);
1372
+ const filePath = join(workspace.eventsDir, name);
1188
1373
  let stats;
1189
1374
  try {
1190
1375
  stats = statSync(filePath);
@@ -1212,7 +1397,7 @@ function serveEventsFile(res, url, services) {
1212
1397
  jsonRes(res, 200, { name, content: raw });
1213
1398
  }
1214
1399
  /** Delete a single event file scoped to the caller's conversation. */
1215
- function serveConversationEventDelete(res, body, services, token) {
1400
+ async function serveConversationEventDelete(res, body, services, token) {
1216
1401
  const scope = resolveTargetConversation(body, token);
1217
1402
  if (scope.error) {
1218
1403
  jsonRes(res, 403, { error: scope.error });
@@ -1223,38 +1408,24 @@ function serveConversationEventDelete(res, body, services, token) {
1223
1408
  jsonRes(res, 400, { error: "Invalid name" });
1224
1409
  return;
1225
1410
  }
1226
- const workingDir = requireAdminWorkingDir(res, services);
1227
- if (!workingDir)
1411
+ const store = requireAdminEventStore(res, services);
1412
+ if (!store)
1228
1413
  return;
1229
- const filePath = join(workingDir, "events", name);
1230
- let raw;
1414
+ let payload;
1231
1415
  try {
1232
- raw = readFileSync(filePath, "utf-8");
1416
+ payload = (await store.read(name)).payload;
1233
1417
  }
1234
1418
  catch {
1235
1419
  jsonRes(res, 404, { error: "Event not found" });
1236
1420
  return;
1237
1421
  }
1238
- let parsed = {};
1239
- try {
1240
- const j = JSON.parse(raw);
1241
- if (j && typeof j === "object")
1242
- parsed = j;
1243
- }
1244
- catch {
1245
- // Malformed events cannot be associated with a conversation below.
1246
- }
1247
- const eventConvId = typeof parsed.conversationId === "string"
1248
- ? parsed.conversationId
1249
- : typeof parsed.channelId === "string"
1250
- ? parsed.channelId
1251
- : null;
1252
- if (eventConvId !== scope.conversationId) {
1422
+ // The store normalizes the legacy `channelId` alias into `conversationId`.
1423
+ if (payload?.conversationId !== scope.conversationId) {
1253
1424
  jsonRes(res, 403, { error: "Event does not belong to this conversation." });
1254
1425
  return;
1255
1426
  }
1256
1427
  try {
1257
- rmSync(filePath, { force: true });
1428
+ await store.delete(name);
1258
1429
  jsonRes(res, 200, { ok: true });
1259
1430
  }
1260
1431
  catch (err) {
@@ -1269,18 +1440,6 @@ function jsonRes(res, status, body) {
1269
1440
  });
1270
1441
  res.end(JSON.stringify(body));
1271
1442
  }
1272
- async function readJsonBody(req, res) {
1273
- const data = await readRawBody(req, res, 32 * 1024);
1274
- if (data === null)
1275
- return null;
1276
- try {
1277
- return JSON.parse(data);
1278
- }
1279
- catch {
1280
- jsonRes(res, 400, { error: "Invalid JSON" });
1281
- return null;
1282
- }
1283
- }
1284
1443
  const esc = escapeHtml;
1285
1444
  // ── HTML ───────────────────────────────────────────────────────────────────────
1286
1445
  function renderAdminPage(token) {
@@ -1330,6 +1489,25 @@ function renderAdminPage(token) {
1330
1489
  </div>
1331
1490
  </section>
1332
1491
 
1492
+ <section class="card sect" id="sect-packages" data-section="packages">
1493
+ <header class="sect-head">
1494
+ <div>
1495
+ <p class="eyebrow">Extensions</p>
1496
+ <h2 class="card-title">此對話的 extension 套件</h2>
1497
+ </div>
1498
+ <button class="refresh-btn" onclick="loadPackages()">↻</button>
1499
+ </header>
1500
+ <div class="pkg-add">
1501
+ <input id="pkg-conv-url" class="pkg-input" type="text" spellcheck="false"
1502
+ placeholder="github:owner/repo 或 https://github.com/owner/repo.git" />
1503
+ <input id="pkg-conv-ref" class="pkg-input pkg-input-ref" type="text" spellcheck="false"
1504
+ placeholder="tag / branch / commit(可留空)" />
1505
+ <button class="primary-action-btn" onclick="addPackage('conversation')">Add</button>
1506
+ </div>
1507
+ <div id="pkg-conv-msg" class="pkg-msg" style="display:none"></div>
1508
+ <div id="pkg-conv-content"><div class="loading-msg">Loading…</div></div>
1509
+ </section>
1510
+
1333
1511
  <section class="card sect" id="sect-vault" data-section="vault">
1334
1512
  <header class="sect-head">
1335
1513
  <div>
@@ -1407,6 +1585,37 @@ function renderAdminPage(token) {
1407
1585
  <div id="global-settings-content"><div class="loading-msg">Loading…</div></div>
1408
1586
  </section>
1409
1587
 
1588
+ <section class="card sect">
1589
+ <header class="sect-head">
1590
+ <div>
1591
+ <p class="eyebrow">Global Extensions</p>
1592
+ <h2 class="card-title">所有對話都會載入的 extension 套件</h2>
1593
+ </div>
1594
+ <button class="refresh-btn" onclick="loadPackages()">↻</button>
1595
+ </header>
1596
+ <div class="pkg-add">
1597
+ <input id="pkg-global-url" class="pkg-input" type="text" spellcheck="false"
1598
+ placeholder="github:owner/repo 或 https://github.com/owner/repo.git" />
1599
+ <input id="pkg-global-ref" class="pkg-input pkg-input-ref" type="text" spellcheck="false"
1600
+ placeholder="tag / branch / commit(可留空)" />
1601
+ <button class="primary-action-btn" onclick="addPackage('global')">Add</button>
1602
+ </div>
1603
+ <div id="pkg-global-msg" class="pkg-msg" style="display:none"></div>
1604
+ <div id="pkg-global-content"><div class="loading-msg">Loading…</div></div>
1605
+ </section>
1606
+
1607
+ <section class="card sect">
1608
+ <header class="sect-head">
1609
+ <div>
1610
+ <p class="eyebrow">Extension Secrets</p>
1611
+ <h2 class="card-title">Extension 密鑰(依 slug 全域共用)</h2>
1612
+ </div>
1613
+ <button class="refresh-btn" onclick="loadExtensionSecrets()">↻</button>
1614
+ </header>
1615
+ <div id="ext-secrets-msg" class="pkg-msg" style="display:none"></div>
1616
+ <div id="ext-secrets-content"><div class="loading-msg">Loading…</div></div>
1617
+ </section>
1618
+
1410
1619
  <section class="card sect">
1411
1620
  <header class="sect-head">
1412
1621
  <div>
@@ -1431,8 +1640,23 @@ function renderAdminPage(token) {
1431
1640
  </div>`;
1432
1641
  const script = `
1433
1642
  const adminToken = ${JSON.stringify(token.token)};
1434
- const defaultConversationId = ${JSON.stringify(token.conversationId)};
1435
- let activeConversationId = defaultConversationId;
1643
+ // Conversation ids never contain ":" (session-key grammar), so
1644
+ // "platform:id" is a safe composite scope key for the UI.
1645
+ const defaultConversationKey = ${JSON.stringify(`${token.platform}:${token.conversationId}`)};
1646
+ let activeConversationKey = defaultConversationKey;
1647
+ function scopeOf(key) {
1648
+ const sep = key.indexOf(':');
1649
+ return { platform: key.slice(0, sep), conversationId: key.slice(sep + 1) };
1650
+ }
1651
+ function scopeQuery() {
1652
+ const scope = scopeOf(activeConversationKey);
1653
+ return 'conversationId=' + encodeURIComponent(scope.conversationId) +
1654
+ '&platform=' + encodeURIComponent(scope.platform);
1655
+ }
1656
+ function scopeBody() {
1657
+ const scope = scopeOf(activeConversationKey);
1658
+ return { conversationId: scope.conversationId, platform: scope.platform };
1659
+ }
1436
1660
  let availableModels = [];
1437
1661
  let modelsLoaded = false;
1438
1662
 
@@ -1540,9 +1764,10 @@ function renderAdminPage(token) {
1540
1764
  try {
1541
1765
  const data = await apiGet('/admin/api/conversations');
1542
1766
  sel.innerHTML = data.conversations.map((c) => {
1767
+ const key = c.platform + ':' + c.conversationId;
1543
1768
  const label = (c.label || c.conversationId) + (c.running ? ' (running)' : '');
1544
- const selected = c.conversationId === defaultConversationId ? ' selected' : '';
1545
- return '<option value="' + escAttr(c.conversationId) + '"' + selected + '>' + escHtml(label) + '</option>';
1769
+ const selected = key === defaultConversationKey ? ' selected' : '';
1770
+ return '<option value="' + escAttr(key) + '"' + selected + '>' + escHtml(label) + '</option>';
1546
1771
  }).join('');
1547
1772
  sel.addEventListener('change', () => setActiveConversation(sel.value));
1548
1773
  } catch (err) {
@@ -1550,14 +1775,15 @@ function renderAdminPage(token) {
1550
1775
  }
1551
1776
  }
1552
1777
 
1553
- function setActiveConversation(id) {
1554
- activeConversationId = id;
1778
+ function setActiveConversation(key) {
1779
+ activeConversationKey = key;
1555
1780
  const sel = document.getElementById('conv-switcher');
1556
- if (sel && sel.value !== id) sel.value = id;
1781
+ if (sel && sel.value !== key) sel.value = key;
1557
1782
  // Reset all conversation sections.
1558
1783
  loadSettings();
1559
1784
  loadWorkspace();
1560
1785
  loadSkills();
1786
+ loadPackages();
1561
1787
  loadConversationEvents();
1562
1788
  openLogin(true);
1563
1789
  openSessionView(true);
@@ -1570,7 +1796,7 @@ function renderAdminPage(token) {
1570
1796
  container.innerHTML = '<div class="loading-msg">Loading…</div>';
1571
1797
  if (!modelsLoaded) await loadModels();
1572
1798
  try {
1573
- const data = await apiGet('/admin/api/conversation-state?conversationId=' + encodeURIComponent(activeConversationId));
1799
+ const data = await apiGet('/admin/api/conversation-state?' + scopeQuery());
1574
1800
  container.innerHTML = renderSettings(data);
1575
1801
  } catch (err) {
1576
1802
  container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
@@ -1578,14 +1804,10 @@ function renderAdminPage(token) {
1578
1804
  }
1579
1805
 
1580
1806
  function renderSettings(data) {
1581
- const thinking = ['off','minimal','low','medium','high','xhigh'];
1807
+ const thinking = ['off','minimal','low','medium','high','xhigh','max'];
1582
1808
  const thinkingOpts = thinking.map((t) =>
1583
1809
  '<option value="' + t + '"' + (data.thinkingLevel === t ? ' selected' : '') + '>' + t + '</option>'
1584
1810
  ).join('');
1585
- const mounts = ['private','full'];
1586
- const mountOpts = mounts.map((m) =>
1587
- '<option value="' + m + '"' + (data.sandboxImageWorkspaceMount === m ? ' selected' : '') + '>' + m + '</option>'
1588
- ).join('');
1589
1811
  const rulesText = (data.autoReplyRules || []).join('\\n');
1590
1812
  const replyModes = ['top-level','thread'];
1591
1813
  const replyModeOpts = replyModes.map((m) =>
@@ -1594,7 +1816,15 @@ function renderAdminPage(token) {
1594
1816
  const globalReplyMode = (data.slack && data.slack.globalReplyMode) || 'top-level';
1595
1817
  const globalModel = [data.globalProvider, data.globalModel].filter(Boolean).join('/');
1596
1818
  const globalModelLabel = globalModel + (data.globalThinkingLevel ? ':' + data.globalThinkingLevel : '');
1597
- const globalMount = data.globalSandboxImageWorkspaceMount || 'private';
1819
+ const doorPolicyChoices = [
1820
+ ['default', 'Global default (' + data.globalWorkspaceDoorPolicy + ' / ' + data.globalWorkspaceLayout + ')'],
1821
+ ['isolated', 'isolated — own office only'],
1822
+ ['trusted-shared-support', 'trusted / shared-support — office + shared memory, skills, events'],
1823
+ ['trusted-full', 'trusted / full — entire workspace'],
1824
+ ];
1825
+ const doorPolicyOpts = doorPolicyChoices.map(([value, label]) =>
1826
+ '<option value="' + value + '"' + ((data.workspaceOverride || 'default') === value ? ' selected' : '') + '>' + escHtml(label) + '</option>'
1827
+ ).join('');
1598
1828
  return [
1599
1829
  '<div class="config-grid">',
1600
1830
  '<div class="config-block">',
@@ -1613,10 +1843,11 @@ function renderAdminPage(token) {
1613
1843
  '<div id="auto-save-result" class="inline-result" style="display:none"></div>',
1614
1844
  '</div>',
1615
1845
  '<div class="config-block">',
1616
- '<h3 class="card-subtitle">Workspace mount</h3>',
1617
- '<div class="config-row"><label>Mode</label><select id="m-mount">' + mountOpts + '</select></div>',
1618
- '<p class="muted-note">Global default: ' + escHtml(globalMount) + '</p>',
1619
- '<button class="primary-action-btn" onclick="saveMount(this)">Save mount</button>',
1846
+ '<h3 class="card-subtitle">Office data policy</h3>',
1847
+ '<div class="config-row"><label>Door policy</label><select id="m-door-policy">' + doorPolicyOpts + '</select></div>',
1848
+ '<p class="muted-note">Effective: ' + escHtml(data.workspaceDoorPolicy + ' / ' + data.workspaceLayout) + '</p>',
1849
+ '<p class="muted-note">Changing the policy rebuilds the office sandbox container with the new mounts on its next message; the container contents are preserved.</p>',
1850
+ '<button class="primary-action-btn" onclick="saveDoorPolicy(this)">Save door policy</button>',
1620
1851
  '<div id="mount-save-result" class="inline-result" style="display:none"></div>',
1621
1852
  '</div>',
1622
1853
  '<div class="config-block">',
@@ -1644,12 +1875,10 @@ function renderAdminPage(token) {
1644
1875
  btn.disabled = true; btn.textContent = 'Saving…'; result.style.display = 'none';
1645
1876
  try {
1646
1877
  const data = await apiPost('/admin/api/conversations/model', {
1647
- conversationId: activeConversationId, provider, model, thinkingLevel,
1878
+ ...scopeBody(), provider, model, thinkingLevel,
1648
1879
  });
1649
1880
  result.style.display = 'block'; result.className = 'inline-result ok';
1650
- result.textContent = data.runtimeSwitched === false
1651
- ? 'Saved — running session pinned; new model applies on next start.'
1652
- : 'Saved ✓';
1881
+ result.textContent = 'Saved ✓';
1653
1882
  } catch (err) {
1654
1883
  result.style.display = 'block'; result.className = 'inline-result err';
1655
1884
  result.textContent = err.message;
@@ -1665,7 +1894,7 @@ function renderAdminPage(token) {
1665
1894
  btn.disabled = true; btn.textContent = 'Saving…'; result.style.display = 'none';
1666
1895
  try {
1667
1896
  await apiPost('/admin/api/conversations/auto-reply', {
1668
- conversationId: activeConversationId, enabled, rules,
1897
+ ...scopeBody(), enabled, rules,
1669
1898
  });
1670
1899
  result.style.display = 'block'; result.className = 'inline-result ok'; result.textContent = 'Saved ✓';
1671
1900
  } catch (err) {
@@ -1675,35 +1904,36 @@ function renderAdminPage(token) {
1675
1904
  }
1676
1905
  }
1677
1906
 
1678
- async function saveMount(btn) {
1679
- const workspaceMount = document.getElementById('m-mount').value;
1680
- const result = document.getElementById('mount-save-result');
1907
+ async function saveSlack(btn) {
1908
+ const replyMode = document.getElementById('m-slack-reply-mode').value;
1909
+ const result = document.getElementById('slack-save-result');
1681
1910
  btn.disabled = true; btn.textContent = 'Saving…'; result.style.display = 'none';
1682
1911
  try {
1683
- await apiPost('/admin/api/conversations/sandbox', {
1684
- conversationId: activeConversationId, workspaceMount,
1912
+ await apiPost('/admin/api/conversations/slack', {
1913
+ ...scopeBody(), replyMode,
1685
1914
  });
1686
1915
  result.style.display = 'block'; result.className = 'inline-result ok'; result.textContent = 'Saved ✓';
1687
1916
  } catch (err) {
1688
1917
  result.style.display = 'block'; result.className = 'inline-result err'; result.textContent = err.message;
1689
1918
  } finally {
1690
- btn.disabled = false; btn.textContent = 'Save mount';
1919
+ btn.disabled = false; btn.textContent = 'Save Slack';
1691
1920
  }
1692
1921
  }
1693
1922
 
1694
- async function saveSlack(btn) {
1695
- const replyMode = document.getElementById('m-slack-reply-mode').value;
1696
- const result = document.getElementById('slack-save-result');
1923
+ async function saveDoorPolicy(btn) {
1924
+ const doorPolicy = document.getElementById('m-door-policy').value;
1925
+ const result = document.getElementById('mount-save-result');
1697
1926
  btn.disabled = true; btn.textContent = 'Saving…'; result.style.display = 'none';
1698
1927
  try {
1699
- await apiPost('/admin/api/conversations/slack', {
1700
- conversationId: activeConversationId, replyMode,
1928
+ await apiPost('/admin/api/conversations/sandbox', {
1929
+ ...scopeBody(), doorPolicy,
1701
1930
  });
1702
1931
  result.style.display = 'block'; result.className = 'inline-result ok'; result.textContent = 'Saved ✓';
1932
+ loadSettings();
1703
1933
  } catch (err) {
1704
1934
  result.style.display = 'block'; result.className = 'inline-result err'; result.textContent = err.message;
1705
1935
  } finally {
1706
- btn.disabled = false; btn.textContent = 'Save Slack';
1936
+ btn.disabled = false; btn.textContent = 'Save door policy';
1707
1937
  }
1708
1938
  }
1709
1939
 
@@ -1715,7 +1945,7 @@ function renderAdminPage(token) {
1715
1945
  treeEl.innerHTML = '<div class="loading-msg">Loading…</div>';
1716
1946
  previewEl.innerHTML = '<div class="placeholder-msg">Click a file to preview</div>';
1717
1947
  try {
1718
- const data = await apiGet('/admin/api/workspace/tree?conversationId=' + encodeURIComponent(activeConversationId));
1948
+ const data = await apiGet('/admin/api/workspace/tree?' + scopeQuery());
1719
1949
  if (!data.tree) {
1720
1950
  treeEl.innerHTML = '<div class="empty-state">No files</div>';
1721
1951
  return;
@@ -1755,7 +1985,7 @@ function renderAdminPage(token) {
1755
1985
  const previewEl = document.getElementById('workspace-preview');
1756
1986
  previewEl.innerHTML = '<div class="loading-msg">Loading ' + escHtml(path) + '…</div>';
1757
1987
  try {
1758
- const data = await apiGet('/admin/api/workspace/file?conversationId=' + encodeURIComponent(activeConversationId) + '&path=' + encodeURIComponent(path));
1988
+ const data = await apiGet('/admin/api/workspace/file?' + scopeQuery() + '&path=' + encodeURIComponent(path));
1759
1989
  renderPreviewFileResult(previewEl, path, data);
1760
1990
  } catch (err) {
1761
1991
  previewEl.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
@@ -1764,13 +1994,200 @@ function renderAdminPage(token) {
1764
1994
 
1765
1995
  // ── Skills ───────────────────────────────────────────────────────────────────
1766
1996
 
1997
+ function packageMessage(scope, text, kind) {
1998
+ const el = document.getElementById(scope === 'global' ? 'pkg-global-msg' : 'pkg-conv-msg');
1999
+ if (!text) { el.style.display = 'none'; return; }
2000
+ el.className = 'pkg-msg pkg-msg-' + kind;
2001
+ el.textContent = text;
2002
+ el.style.display = 'block';
2003
+ }
2004
+
2005
+ function renderPackageList(container, rows, scope) {
2006
+ if (rows.length === 0) {
2007
+ container.innerHTML = '<div class="empty-state">尚未加入任何套件</div>';
2008
+ return;
2009
+ }
2010
+ container.innerHTML = '<div class="pkg-list">' + rows.map((p) => {
2011
+ const provides = []
2012
+ .concat(p.extensions.map((s) => 'ext: ' + s))
2013
+ .concat(p.skills.map((s) => 'skill: ' + s));
2014
+ const status = p.error
2015
+ ? '<span class="pkg-badge pkg-badge-err">' + escHtml(p.error) + '</span>'
2016
+ : p.shadowed
2017
+ ? '<span class="pkg-badge pkg-badge-warn">被此對話的同名套件覆蓋,不會載入</span>'
2018
+ // "downloaded", not "ready": this status means the checkout is on
2019
+ // disk and was inspected, which is not the same as activated. The
2020
+ // extension loads on the conversation's next harness instance.
2021
+ : '<span class="pkg-badge pkg-badge-ok">已下載</span>'
2022
+ + '<span class="pkg-badge pkg-badge-warn">需 /pi-new 才會載入</span>';
2023
+ return '<div class="pkg-row">' +
2024
+ '<div class="pkg-row-main">' +
2025
+ '<code class="pkg-source">' + escHtml(p.source) + '</code>' + status +
2026
+ '</div>' +
2027
+ (provides.length > 0
2028
+ ? '<div class="pkg-provides">' + provides.map((t) => '<span class="pkg-chip">' + escHtml(t) + '</span>').join('') + '</div>'
2029
+ : '<div class="pkg-provides pkg-provides-empty">此套件沒有提供 extension 或 skill</div>') +
2030
+ '<div class="pkg-actions">' +
2031
+ '<button class="pkg-btn" data-pkg-action="refresh" data-pkg-scope="' + scope + '" data-pkg-source="' + escAttr(p.source) + '">Update</button>' +
2032
+ '<button class="pkg-btn pkg-btn-danger" data-pkg-action="remove" data-pkg-scope="' + scope + '" data-pkg-source="' + escAttr(p.source) + '">Remove</button>' +
2033
+ '</div>' +
2034
+ '</div>';
2035
+ }).join('') + '</div>';
2036
+ }
2037
+
2038
+ async function loadPackages() {
2039
+ const convEl = document.getElementById('pkg-conv-content');
2040
+ const globalEl = document.getElementById('pkg-global-content');
2041
+ if (convEl) convEl.innerHTML = '<div class="loading-msg">Loading…</div>';
2042
+ if (globalEl) globalEl.innerHTML = '<div class="loading-msg">Loading…</div>';
2043
+ try {
2044
+ const data = await apiGet('/admin/api/packages?' + scopeQuery());
2045
+ if (convEl) renderPackageList(convEl, data.conversation, 'conversation');
2046
+ if (globalEl) renderPackageList(globalEl, data.global, 'global');
2047
+ } catch (err) {
2048
+ if (convEl) convEl.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
2049
+ if (globalEl) globalEl.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
2050
+ }
2051
+ }
2052
+
2053
+ async function mutatePackage(scope, action, source) {
2054
+ packageMessage(scope, action === 'remove' ? '移除中…' : '取得中…', 'busy');
2055
+ try {
2056
+ const result = await apiPost('/admin/api/packages/mutate', {
2057
+ action: action,
2058
+ scope: scope,
2059
+ source: source,
2060
+ ...scopeBody(),
2061
+ });
2062
+ packageMessage(
2063
+ scope,
2064
+ action === 'remove'
2065
+ ? '已移除。對話輸入 /pi-new 生效。'
2066
+ : '完成:' + result.source + '(對話輸入 /pi-new 生效)',
2067
+ 'ok',
2068
+ );
2069
+ await loadPackages();
2070
+ } catch (err) {
2071
+ // Fetch and validation failures land here, next to the input that
2072
+ // caused them, rather than turning into a silently missing feature.
2073
+ packageMessage(scope, err.message, 'err');
2074
+ }
2075
+ }
2076
+
2077
+ async function addPackage(scope) {
2078
+ const urlEl = document.getElementById(scope === 'global' ? 'pkg-global-url' : 'pkg-conv-url');
2079
+ const refEl = document.getElementById(scope === 'global' ? 'pkg-global-ref' : 'pkg-conv-ref');
2080
+ const url = urlEl.value.trim();
2081
+ const ref = refEl.value.trim();
2082
+ if (!url) { packageMessage(scope, '請填入 git URL', 'err'); return; }
2083
+ // Assembled here so nobody has to hand-write the @ref form, whose
2084
+ // ambiguity with git@host:owner/repo is the one sharp edge in the syntax.
2085
+ await mutatePackage(scope, 'add', ref ? url + '@' + ref : url);
2086
+ urlEl.value = '';
2087
+ refEl.value = '';
2088
+ }
2089
+
2090
+ document.addEventListener('click', (event) => {
2091
+ const btn = event.target.closest('[data-pkg-action]');
2092
+ if (!btn) return;
2093
+ void mutatePackage(btn.dataset.pkgScope, btn.dataset.pkgAction, btn.dataset.pkgSource);
2094
+ });
2095
+
2096
+ // ── Extension secrets ───────────────────────────────────────────────────────
2097
+
2098
+ function extSecretsMessage(text, kind) {
2099
+ const el = document.getElementById('ext-secrets-msg');
2100
+ if (!el) return;
2101
+ el.style.display = 'block';
2102
+ el.className = 'pkg-msg pkg-msg-' + kind;
2103
+ el.textContent = text;
2104
+ }
2105
+
2106
+ function renderSecretRow(slug, key, meta) {
2107
+ return '<div class="pkg-row">' +
2108
+ '<div class="pkg-row-main"><span class="pkg-source">' + escHtml(key) + '</span>' +
2109
+ (meta.required ? '<span class="pkg-badge pkg-badge-warn">required</span>' : '') +
2110
+ (meta.provisioned
2111
+ ? '<span class="pkg-badge pkg-badge-ok">provisioned</span>'
2112
+ : '<span class="pkg-badge pkg-badge-err">missing</span>') +
2113
+ '</div>' +
2114
+ (meta.description ? '<div class="skill-desc">' + escHtml(meta.description) + '</div>' : '') +
2115
+ '<div class="pkg-actions">' +
2116
+ '<button class="pkg-btn" data-secret-action="set" data-secret-slug="' + escAttr(slug) + '" data-secret-key="' + escAttr(key) + '">Set</button>' +
2117
+ (meta.provisioned
2118
+ ? '<button class="pkg-btn pkg-btn-danger" data-secret-action="delete" data-secret-slug="' + escAttr(slug) + '" data-secret-key="' + escAttr(key) + '">Delete</button>'
2119
+ : '') +
2120
+ '</div>' +
2121
+ '</div>';
2122
+ }
2123
+
2124
+ async function loadExtensionSecrets() {
2125
+ const container = document.getElementById('ext-secrets-content');
2126
+ if (!container) return;
2127
+ container.innerHTML = '<div class="loading-msg">Loading…</div>';
2128
+ try {
2129
+ const data = await apiGet('/admin/api/extension-secrets?' + scopeQuery());
2130
+ if (data.extensions.length === 0) {
2131
+ container.innerHTML = '<div class="empty-state">No extensions installed</div>';
2132
+ return;
2133
+ }
2134
+ container.innerHTML = data.extensions.map((ext) => {
2135
+ const declared = ext.declared.map((s) => renderSecretRow(ext.slug, s.key, s)).join('');
2136
+ const extras = ext.extraProvisioned
2137
+ .map((key) => renderSecretRow(ext.slug, key, { provisioned: true }))
2138
+ .join('');
2139
+ return '<div class="pkg-group">' +
2140
+ '<div class="pkg-group-head"><strong>' + escHtml(ext.name) + '</strong>' +
2141
+ '<span class="pkg-source">' + escHtml(ext.slug) + '</span>' +
2142
+ '<button class="pkg-btn" data-secret-action="add" data-secret-slug="' + escAttr(ext.slug) + '">Add key</button>' +
2143
+ '</div>' +
2144
+ '<div class="pkg-list">' +
2145
+ (declared + extras || '<div class="pkg-provides-empty">No secrets declared</div>') +
2146
+ '</div>' +
2147
+ '</div>';
2148
+ }).join('');
2149
+ } catch (err) {
2150
+ container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
2151
+ }
2152
+ }
2153
+
2154
+ document.addEventListener('click', async (event) => {
2155
+ const btn = event.target.closest('[data-secret-action]');
2156
+ if (!btn) return;
2157
+ const slug = btn.dataset.secretSlug;
2158
+ const action = btn.dataset.secretAction;
2159
+ let key = btn.dataset.secretKey;
2160
+ if (action === 'add') {
2161
+ key = (prompt('Secret key for ' + slug + '(e.g. SLACK_BOT_TOKEN)') || '').trim();
2162
+ if (!key) return;
2163
+ }
2164
+ if (action === 'delete' && !confirm('Delete ' + key + ' from ' + slug + '?')) return;
2165
+ let value;
2166
+ if (action !== 'delete') {
2167
+ value = prompt('Value for ' + key + '(' + slug + ')');
2168
+ if (!value) return;
2169
+ }
2170
+ try {
2171
+ await apiPost('/admin/api/extension-secrets/mutate', {
2172
+ action: action === 'delete' ? 'delete' : 'set',
2173
+ slug: slug,
2174
+ key: key,
2175
+ value: value,
2176
+ });
2177
+ extSecretsMessage('已更新。對話輸入 /pi-new 生效。', 'ok');
2178
+ await loadExtensionSecrets();
2179
+ } catch (err) {
2180
+ extSecretsMessage(err.message, 'err');
2181
+ }
2182
+ });
2183
+
1767
2184
  async function loadSkills() {
1768
2185
  const container = document.getElementById('skills-content');
1769
2186
  const previewEl = document.getElementById('skills-preview');
1770
2187
  container.innerHTML = '<div class="loading-msg">Loading…</div>';
1771
2188
  if (previewEl) previewEl.innerHTML = '<div class="placeholder-msg">Click a skill to preview SKILL.md</div>';
1772
2189
  try {
1773
- const data = await apiGet('/admin/api/skills?conversationId=' + encodeURIComponent(activeConversationId));
2190
+ const data = await apiGet('/admin/api/skills?' + scopeQuery());
1774
2191
  if (data.skills.length === 0) {
1775
2192
  container.innerHTML = '<div class="empty-state">No skills available</div>';
1776
2193
  return;
@@ -1796,7 +2213,7 @@ function renderAdminPage(token) {
1796
2213
  }
1797
2214
  previewEl.innerHTML = '<div class="loading-msg">Loading ' + escHtml(name || directory) + '…</div>';
1798
2215
  try {
1799
- const data = await apiGet('/admin/api/skills/file?conversationId=' + encodeURIComponent(activeConversationId) + '&source=' + encodeURIComponent(source) + '&directory=' + encodeURIComponent(directory));
2216
+ const data = await apiGet('/admin/api/skills/file?' + scopeQuery() + '&source=' + encodeURIComponent(source) + '&directory=' + encodeURIComponent(directory));
1800
2217
  renderPreviewFileResult(previewEl, source + '/' + directory + '/SKILL.md', data);
1801
2218
  } catch (err) {
1802
2219
  previewEl.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
@@ -1817,7 +2234,7 @@ function renderAdminPage(token) {
1817
2234
  if (silent) { frame.removeAttribute('src'); frame.style.display = 'none'; result.style.display = 'none'; return; }
1818
2235
  result.style.display = 'block'; result.className = 'link-result loading'; result.textContent = 'Generating link…';
1819
2236
  try {
1820
- const data = await apiPost('/admin/api/conversations/login-link', { conversationId: activeConversationId });
2237
+ const data = await apiPost('/admin/api/conversations/login-link', scopeBody());
1821
2238
  result.className = 'link-result ok';
1822
2239
  result.innerHTML =
1823
2240
  '<span class="link-vault">vault: <code>' + escHtml(data.vaultId) + '</code></span>' +
@@ -1838,7 +2255,7 @@ function renderAdminPage(token) {
1838
2255
  if (silent) { frame.removeAttribute('src'); frame.style.display = 'none'; result.style.display = 'none'; return; }
1839
2256
  result.style.display = 'block'; result.className = 'link-result loading'; result.textContent = 'Generating link…';
1840
2257
  try {
1841
- const data = await apiPost('/admin/api/conversations/session-link', { conversationId: activeConversationId });
2258
+ const data = await apiPost('/admin/api/conversations/session-link', scopeBody());
1842
2259
  result.className = 'link-result ok';
1843
2260
  result.innerHTML =
1844
2261
  '<a href="' + escAttr(data.url) + '" target="_blank" rel="noopener">' + escHtml(data.url) + '</a>' +
@@ -1857,7 +2274,7 @@ function renderAdminPage(token) {
1857
2274
  if (!container) return;
1858
2275
  container.innerHTML = '<div class="loading-msg">Loading…</div>';
1859
2276
  try {
1860
- const data = await apiGet('/admin/api/conversations/events?conversationId=' + encodeURIComponent(activeConversationId));
2277
+ const data = await apiGet('/admin/api/conversations/events?' + scopeQuery());
1861
2278
  if (data.events.length === 0) {
1862
2279
  container.innerHTML = '<div class="empty-state">沒有關聯此對話的 event</div>';
1863
2280
  return;
@@ -1908,7 +2325,7 @@ function renderAdminPage(token) {
1908
2325
  btn.disabled = true; btn.textContent = 'Deleting…';
1909
2326
  try {
1910
2327
  await apiPost('/admin/api/conversations/events/delete', {
1911
- conversationId: activeConversationId, name,
2328
+ ...scopeBody(), name,
1912
2329
  });
1913
2330
  await loadConversationEvents();
1914
2331
  } catch (err) {
@@ -1928,6 +2345,7 @@ function renderAdminPage(token) {
1928
2345
  loadGlobalSettings();
1929
2346
  loadGlobalSkills();
1930
2347
  loadEvents();
2348
+ loadExtensionSecrets();
1931
2349
  }
1932
2350
 
1933
2351
  async function loadAllConversations() {
@@ -1996,12 +2414,13 @@ function renderAdminPage(token) {
1996
2414
  timelineConvLoaded = true;
1997
2415
  return;
1998
2416
  }
1999
- const want = prev || defaultConversationId;
2000
- sel.innerHTML = data.conversations.map((c) =>
2001
- '<option value="' + escAttr(c.conversationId) + '"' +
2002
- (c.conversationId === want ? ' selected' : '') + '>' +
2003
- escHtml(c.label || c.conversationId) + '</option>'
2004
- ).join('');
2417
+ const want = prev || defaultConversationKey;
2418
+ sel.innerHTML = data.conversations.map((c) => {
2419
+ const key = c.platform + ':' + c.conversationId;
2420
+ return '<option value="' + escAttr(key) + '"' +
2421
+ (key === want ? ' selected' : '') + '>' +
2422
+ escHtml(c.label || c.conversationId) + '</option>';
2423
+ }).join('');
2005
2424
  timelineConvLoaded = true;
2006
2425
  }
2007
2426
 
@@ -2024,8 +2443,10 @@ function renderAdminPage(token) {
2024
2443
  return;
2025
2444
  }
2026
2445
  container.innerHTML = '<div class="loading-msg">Loading…</div>';
2446
+ const convScope = scopeOf(conv);
2027
2447
  const data = await apiGet('/admin/api/conversation-usage?conversationId=' +
2028
- encodeURIComponent(conv));
2448
+ encodeURIComponent(convScope.conversationId) +
2449
+ '&platform=' + encodeURIComponent(convScope.platform));
2029
2450
  timelineData = data;
2030
2451
  container.innerHTML = renderUsageTimeline(data);
2031
2452
  } catch (err) {
@@ -2130,18 +2551,25 @@ function renderAdminPage(token) {
2130
2551
  }
2131
2552
 
2132
2553
  function renderGlobalSettings(data) {
2133
- const thinking = ['off','minimal','low','medium','high','xhigh'];
2554
+ const thinking = ['off','minimal','low','medium','high','xhigh','max'];
2134
2555
  const thinkingOpts = thinking.map((t) =>
2135
2556
  '<option value="' + t + '"' + (data.thinkingLevel === t ? ' selected' : '') + '>' + t + '</option>'
2136
2557
  ).join('');
2137
- const mounts = ['private','full'];
2138
- const mountOpts = mounts.map((m) =>
2139
- '<option value="' + m + '"' + (data.sandboxImageWorkspaceMount === m ? ' selected' : '') + '>' + m + '</option>'
2140
- ).join('');
2141
2558
  const replyModes = ['top-level','thread'];
2142
2559
  const replyModeOpts = replyModes.map((m) =>
2143
2560
  '<option value="' + m + '"' + (((data.slack && data.slack.replyMode) || 'top-level') === m ? ' selected' : '') + '>' + m + '</option>'
2144
2561
  ).join('');
2562
+ const gDoorKey = data.workspaceDoorPolicy === 'isolated'
2563
+ ? 'isolated'
2564
+ : (data.workspaceLayout === 'full' ? 'trusted-full' : 'trusted-shared-support');
2565
+ const gDoorPolicyChoices = [
2566
+ ['isolated', 'isolated — own office only (built-in default)'],
2567
+ ['trusted-shared-support', 'trusted / shared-support — office + shared memory, skills, events'],
2568
+ ['trusted-full', 'trusted / full — entire workspace'],
2569
+ ];
2570
+ const gDoorPolicyOpts = gDoorPolicyChoices.map(([value, label]) =>
2571
+ '<option value="' + value + '"' + (gDoorKey === value ? ' selected' : '') + '>' + escHtml(label) + '</option>'
2572
+ ).join('');
2145
2573
  return [
2146
2574
  '<div class="config-grid">',
2147
2575
  '<div class="config-block">',
@@ -2157,10 +2585,16 @@ function renderAdminPage(token) {
2157
2585
  '<div class="config-row"><label>Memory</label><input id="g-mem" placeholder="1g" value="' + escAttr(data.sandboxMemory || '') + '"></div>',
2158
2586
  '<div class="config-row"><label>Boost CPUs</label><input id="g-bcpus" placeholder="2" value="' + escAttr(data.sandboxBoostCpus || '') + '"></div>',
2159
2587
  '<div class="config-row"><label>Boost Mem</label><input id="g-bmem" placeholder="4g" value="' + escAttr(data.sandboxBoostMemory || '') + '"></div>',
2160
- '<div class="config-row"><label>Mount</label><select id="g-mount">' + mountOpts + '</select></div>',
2161
2588
  '<button class="primary-action-btn" onclick="saveGlobalSandbox(this)">Save sandbox</button>',
2162
2589
  '<div id="g-sandbox-result" class="inline-result" style="display:none"></div>',
2163
2590
  '</div>',
2591
+ '<div class="config-block">',
2592
+ '<h3 class="card-subtitle">Office door policy</h3>',
2593
+ '<div class="config-row"><label>Default</label><select id="g-door-policy">' + gDoorPolicyOpts + '</select></div>',
2594
+ '<p class="muted-note">Applies to offices without their own policy. Affected offices rebuild their sandbox container with the new mounts on the next message; container contents are preserved.</p>',
2595
+ '<button class="primary-action-btn" onclick="saveGlobalWorkspace(this)">Save door policy</button>',
2596
+ '<div id="g-workspace-result" class="inline-result" style="display:none"></div>',
2597
+ '</div>',
2164
2598
  '<div class="config-block">',
2165
2599
  '<h3 class="card-subtitle">Slack</h3>',
2166
2600
  '<div class="config-row"><label>Reply mode</label><select id="g-slack-reply-mode">' + replyModeOpts + '</select></div>',
@@ -2197,11 +2631,10 @@ function renderAdminPage(token) {
2197
2631
  const memory = document.getElementById('g-mem').value.trim();
2198
2632
  const boostCpus = document.getElementById('g-bcpus').value.trim();
2199
2633
  const boostMemory = document.getElementById('g-bmem').value.trim();
2200
- const workspaceMount = document.getElementById('g-mount').value;
2201
2634
  const result = document.getElementById('g-sandbox-result');
2202
2635
  btn.disabled = true; btn.textContent = 'Saving…'; result.style.display = 'none';
2203
2636
  try {
2204
- await apiPost('/admin/api/settings/sandbox', { cpus, memory, boostCpus, boostMemory, workspaceMount });
2637
+ await apiPost('/admin/api/settings/sandbox', { cpus, memory, boostCpus, boostMemory });
2205
2638
  result.style.display = 'block'; result.className = 'inline-result ok'; result.textContent = 'Saved ✓';
2206
2639
  } catch (err) {
2207
2640
  result.style.display = 'block'; result.className = 'inline-result err'; result.textContent = err.message;
@@ -2210,6 +2643,23 @@ function renderAdminPage(token) {
2210
2643
  }
2211
2644
  }
2212
2645
 
2646
+ async function saveGlobalWorkspace(btn) {
2647
+ const doorPolicy = document.getElementById('g-door-policy').value;
2648
+ const result = document.getElementById('g-workspace-result');
2649
+ btn.disabled = true; btn.textContent = 'Saving…'; result.style.display = 'none';
2650
+ try {
2651
+ const data = await apiPost('/admin/api/settings/workspace', { doorPolicy });
2652
+ result.style.display = 'block'; result.className = 'inline-result ok';
2653
+ result.textContent = data.staleConversations > 0
2654
+ ? 'Saved ✓ (' + data.staleConversations + ' busy conversation(s) refresh after their current run)'
2655
+ : 'Saved ✓';
2656
+ } catch (err) {
2657
+ result.style.display = 'block'; result.className = 'inline-result err'; result.textContent = err.message;
2658
+ } finally {
2659
+ btn.disabled = false; btn.textContent = 'Save door policy';
2660
+ }
2661
+ }
2662
+
2213
2663
  async function saveGlobalSlack(btn) {
2214
2664
  const replyMode = document.getElementById('g-slack-reply-mode').value;
2215
2665
  const result = document.getElementById('g-slack-result');
@@ -2229,7 +2679,7 @@ function renderAdminPage(token) {
2229
2679
  container.innerHTML = '<div class="loading-msg">Loading…</div>';
2230
2680
  try {
2231
2681
  // Reuse skills endpoint scoped to a conversation that doesn't have any of its own; the global half is what we want.
2232
- const data = await apiGet('/admin/api/skills?conversationId=' + encodeURIComponent(activeConversationId));
2682
+ const data = await apiGet('/admin/api/skills?' + scopeQuery());
2233
2683
  const globals = data.skills.filter((s) => s.source === 'global');
2234
2684
  if (globals.length === 0) {
2235
2685
  container.innerHTML = '<div class="empty-state">No global skills</div>';
@@ -2251,6 +2701,7 @@ function renderAdminPage(token) {
2251
2701
  loadSettings();
2252
2702
  loadWorkspace();
2253
2703
  loadSkills();
2704
+ loadPackages();
2254
2705
  loadConversationEvents();
2255
2706
  });
2256
2707
  `;
@@ -2433,6 +2884,60 @@ const adminViewStyles = `
2433
2884
  .skill-source-conversation { background: rgba(217,119,6,0.1); color: var(--accent); }
2434
2885
  .skill-desc { color: var(--muted); font-size: 0.82rem; margin-top: 4px; line-height: 1.5; }
2435
2886
 
2887
+ /* ── Packages ───────────────────────────────────────────────────────── */
2888
+
2889
+ .pkg-add { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
2890
+ .pkg-input {
2891
+ flex: 1 1 240px; min-width: 0; padding: 8px 10px;
2892
+ border: 1px solid var(--border); border-radius: 8px; background: var(--card);
2893
+ font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.8rem; color: var(--text);
2894
+ }
2895
+ .pkg-input-ref { flex: 0 1 200px; }
2896
+ .pkg-msg {
2897
+ padding: 8px 12px; border-radius: 8px; margin-bottom: 12px;
2898
+ font-size: 0.82rem; line-height: 1.5; word-break: break-word;
2899
+ }
2900
+ .pkg-msg-ok { background: rgba(22,163,74,0.1); color: #15803d; }
2901
+ .pkg-msg-err { background: rgba(220,38,38,0.1); color: #b91c1c; }
2902
+ .pkg-msg-busy { background: rgba(0,0,0,0.05); color: var(--muted); }
2903
+ .pkg-list { display: flex; flex-direction: column; gap: 8px; }
2904
+ .pkg-row {
2905
+ padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px;
2906
+ background: rgba(0,0,0,0.02);
2907
+ }
2908
+ .pkg-row-main { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
2909
+ .pkg-source {
2910
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
2911
+ font-size: 0.78rem; color: var(--text); word-break: break-all;
2912
+ }
2913
+ .pkg-badge {
2914
+ padding: 1px 8px; border-radius: 999px; font-size: 0.7rem;
2915
+ font-weight: 600; letter-spacing: 0.03em;
2916
+ }
2917
+ .pkg-badge-ok { background: rgba(22,163,74,0.1); color: #15803d; }
2918
+ .pkg-badge-warn { background: rgba(217,119,6,0.12); color: var(--accent); }
2919
+ .pkg-badge-err { background: rgba(220,38,38,0.1); color: #b91c1c; }
2920
+ .pkg-provides { margin-top: 6px; display: flex; gap: 6px; flex-wrap: wrap; }
2921
+ .pkg-provides-empty { color: var(--subtle); font-size: 0.78rem; }
2922
+ .pkg-chip {
2923
+ padding: 1px 8px; border-radius: 6px; background: rgba(59,130,246,0.1);
2924
+ color: #1d4ed8; font-size: 0.72rem;
2925
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
2926
+ }
2927
+ .pkg-actions { margin-top: 8px; display: flex; gap: 8px; }
2928
+ .pkg-btn {
2929
+ padding: 4px 10px; border: 1px solid var(--border); border-radius: 7px;
2930
+ background: var(--card); color: var(--text); font-size: 0.76rem; cursor: pointer;
2931
+ }
2932
+ .pkg-btn:hover { background: rgba(0,0,0,0.05); }
2933
+ .pkg-btn-danger { color: #b91c1c; }
2934
+ .pkg-group { margin-bottom: 16px; }
2935
+ .pkg-group-head {
2936
+ display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
2937
+ font-size: 0.85rem;
2938
+ }
2939
+ .pkg-group-head .pkg-btn { margin-left: auto; }
2940
+
2436
2941
  /* ── Events ─────────────────────────────────────────────────────────── */
2437
2942
 
2438
2943
  .events-list { display: flex; flex-direction: column; gap: 8px; }