@celestea/studio 2.7.1

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 (240) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app.d.ts +111 -0
  3. package/dist/app.js +219 -0
  4. package/dist/auth/api-token.d.ts +76 -0
  5. package/dist/auth/api-token.js +172 -0
  6. package/dist/auth/htpasswd.d.ts +22 -0
  7. package/dist/auth/htpasswd.js +61 -0
  8. package/dist/auth/index.d.ts +14 -0
  9. package/dist/auth/index.js +14 -0
  10. package/dist/auth/page.d.ts +19 -0
  11. package/dist/auth/page.js +73 -0
  12. package/dist/auth/rate-limit.d.ts +27 -0
  13. package/dist/auth/rate-limit.js +47 -0
  14. package/dist/auth/token.d.ts +49 -0
  15. package/dist/auth/token.js +112 -0
  16. package/dist/config.d.ts +78 -0
  17. package/dist/config.js +72 -0
  18. package/dist/deployment.d.ts +92 -0
  19. package/dist/deployment.js +154 -0
  20. package/dist/fake-runtime-adapter.d.ts +37 -0
  21. package/dist/fake-runtime-adapter.js +276 -0
  22. package/dist/handlers/auth.d.ts +24 -0
  23. package/dist/handlers/auth.js +155 -0
  24. package/dist/handlers/common.d.ts +76 -0
  25. package/dist/handlers/common.js +127 -0
  26. package/dist/handlers/config-shape.d.ts +76 -0
  27. package/dist/handlers/config-shape.js +227 -0
  28. package/dist/handlers/config.d.ts +26 -0
  29. package/dist/handlers/config.js +161 -0
  30. package/dist/handlers/context-shape.d.ts +33 -0
  31. package/dist/handlers/context-shape.js +59 -0
  32. package/dist/handlers/dialog.d.ts +22 -0
  33. package/dist/handlers/dialog.js +292 -0
  34. package/dist/handlers/exec.d.ts +22 -0
  35. package/dist/handlers/exec.js +131 -0
  36. package/dist/handlers/fs-read.d.ts +63 -0
  37. package/dist/handlers/fs-read.js +159 -0
  38. package/dist/handlers/fs.d.ts +57 -0
  39. package/dist/handlers/fs.js +153 -0
  40. package/dist/handlers/grants-shape.d.ts +45 -0
  41. package/dist/handlers/grants-shape.js +130 -0
  42. package/dist/handlers/grants.d.ts +21 -0
  43. package/dist/handlers/grants.js +267 -0
  44. package/dist/handlers/health.d.ts +37 -0
  45. package/dist/handlers/health.js +139 -0
  46. package/dist/handlers/index.d.ts +40 -0
  47. package/dist/handlers/index.js +85 -0
  48. package/dist/handlers/permissions.d.ts +9 -0
  49. package/dist/handlers/permissions.js +183 -0
  50. package/dist/handlers/plugins.d.ts +18 -0
  51. package/dist/handlers/plugins.js +23 -0
  52. package/dist/handlers/prompts.d.ts +12 -0
  53. package/dist/handlers/prompts.js +120 -0
  54. package/dist/handlers/providers.d.ts +12 -0
  55. package/dist/handlers/providers.js +178 -0
  56. package/dist/handlers/questions.d.ts +23 -0
  57. package/dist/handlers/questions.js +115 -0
  58. package/dist/handlers/session-model.d.ts +38 -0
  59. package/dist/handlers/session-model.js +105 -0
  60. package/dist/handlers/session-move.d.ts +22 -0
  61. package/dist/handlers/session-move.js +188 -0
  62. package/dist/handlers/session-tools.d.ts +19 -0
  63. package/dist/handlers/session-tools.js +75 -0
  64. package/dist/handlers/sessions.d.ts +32 -0
  65. package/dist/handlers/sessions.js +242 -0
  66. package/dist/handlers/usage.d.ts +26 -0
  67. package/dist/handlers/usage.js +77 -0
  68. package/dist/handlers/worker.d.ts +12 -0
  69. package/dist/handlers/worker.js +82 -0
  70. package/dist/handlers/workspaces.d.ts +12 -0
  71. package/dist/handlers/workspaces.js +88 -0
  72. package/dist/harness.test-util.d.ts +101 -0
  73. package/dist/harness.test-util.js +176 -0
  74. package/dist/index.d.ts +33 -0
  75. package/dist/index.js +33 -0
  76. package/dist/main.d.ts +13 -0
  77. package/dist/main.js +35 -0
  78. package/dist/plugins.d.ts +105 -0
  79. package/dist/plugins.js +97 -0
  80. package/dist/question-registry.d.ts +124 -0
  81. package/dist/question-registry.js +212 -0
  82. package/dist/question-rows.d.ts +32 -0
  83. package/dist/question-rows.js +43 -0
  84. package/dist/replay/compare.d.ts +47 -0
  85. package/dist/replay/compare.js +89 -0
  86. package/dist/replay/e2e-replay.d.ts +46 -0
  87. package/dist/replay/e2e-replay.js +91 -0
  88. package/dist/replay/expect-compact.d.ts +31 -0
  89. package/dist/replay/expect-compact.js +92 -0
  90. package/dist/replay/fixtures.d.ts +52 -0
  91. package/dist/replay/fixtures.js +53 -0
  92. package/dist/replay/host.d.ts +32 -0
  93. package/dist/replay/host.js +62 -0
  94. package/dist/replay/index.d.ts +16 -0
  95. package/dist/replay/index.js +16 -0
  96. package/dist/replay/probes.d.ts +55 -0
  97. package/dist/replay/probes.js +225 -0
  98. package/dist/replay/report.d.ts +13 -0
  99. package/dist/replay/report.js +103 -0
  100. package/dist/replay/session-e2e.d.ts +44 -0
  101. package/dist/replay/session-e2e.js +91 -0
  102. package/dist/routes.d.ts +47 -0
  103. package/dist/routes.js +59 -0
  104. package/dist/runtime/attachments-llm.d.ts +13 -0
  105. package/dist/runtime/attachments-llm.js +37 -0
  106. package/dist/runtime/boot-recovery.d.ts +32 -0
  107. package/dist/runtime/boot-recovery.js +71 -0
  108. package/dist/runtime/context-snapshot.d.ts +59 -0
  109. package/dist/runtime/context-snapshot.js +113 -0
  110. package/dist/runtime/engine-grants.d.ts +121 -0
  111. package/dist/runtime/engine-grants.js +338 -0
  112. package/dist/runtime/engine-permissions.d.ts +29 -0
  113. package/dist/runtime/engine-permissions.js +92 -0
  114. package/dist/runtime/engine-plugins.d.ts +189 -0
  115. package/dist/runtime/engine-plugins.js +292 -0
  116. package/dist/runtime/engine-profile.d.ts +28 -0
  117. package/dist/runtime/engine-profile.js +88 -0
  118. package/dist/runtime/engine-session.d.ts +9 -0
  119. package/dist/runtime/engine-session.js +9 -0
  120. package/dist/runtime/fallback-contract.d.ts +32 -0
  121. package/dist/runtime/fallback-contract.js +10 -0
  122. package/dist/runtime/fallback-host.d.ts +134 -0
  123. package/dist/runtime/fallback-host.js +299 -0
  124. package/dist/runtime/host-autowake.d.ts +79 -0
  125. package/dist/runtime/host-autowake.js +86 -0
  126. package/dist/runtime/image-downgrade.d.ts +49 -0
  127. package/dist/runtime/image-downgrade.js +90 -0
  128. package/dist/runtime/inbox-message.d.ts +11 -0
  129. package/dist/runtime/inbox-message.js +20 -0
  130. package/dist/runtime/index.d.ts +17 -0
  131. package/dist/runtime/index.js +17 -0
  132. package/dist/runtime/ledger-view.d.ts +40 -0
  133. package/dist/runtime/ledger-view.js +56 -0
  134. package/dist/runtime/llm-assembly.d.ts +66 -0
  135. package/dist/runtime/llm-assembly.js +114 -0
  136. package/dist/runtime/offline-llm.d.ts +71 -0
  137. package/dist/runtime/offline-llm.js +132 -0
  138. package/dist/runtime/provider-target.d.ts +10 -0
  139. package/dist/runtime/provider-target.js +10 -0
  140. package/dist/runtime/question-host.d.ts +33 -0
  141. package/dist/runtime/question-host.js +41 -0
  142. package/dist/runtime/question-view.d.ts +48 -0
  143. package/dist/runtime/question-view.js +76 -0
  144. package/dist/runtime/real-runtime-adapter.d.ts +95 -0
  145. package/dist/runtime/real-runtime-adapter.js +553 -0
  146. package/dist/runtime/recovery-audit.d.ts +81 -0
  147. package/dist/runtime/recovery-audit.js +95 -0
  148. package/dist/runtime/recovery-view.d.ts +36 -0
  149. package/dist/runtime/recovery-view.js +63 -0
  150. package/dist/runtime/session-compose.d.ts +235 -0
  151. package/dist/runtime/session-compose.js +350 -0
  152. package/dist/runtime/session-grants.d.ts +36 -0
  153. package/dist/runtime/session-grants.js +69 -0
  154. package/dist/runtime/session-lifecycle.d.ts +40 -0
  155. package/dist/runtime/session-lifecycle.js +86 -0
  156. package/dist/runtime/session-publisher.d.ts +40 -0
  157. package/dist/runtime/session-publisher.js +48 -0
  158. package/dist/runtime/session-release.d.ts +53 -0
  159. package/dist/runtime/session-release.js +80 -0
  160. package/dist/runtime/test-util.d.ts +78 -0
  161. package/dist/runtime/test-util.js +162 -0
  162. package/dist/runtime/watchdog-view.d.ts +33 -0
  163. package/dist/runtime/watchdog-view.js +45 -0
  164. package/dist/runtime/worker-bridge.d.ts +76 -0
  165. package/dist/runtime/worker-bridge.js +156 -0
  166. package/dist/runtime/worker-live.d.ts +22 -0
  167. package/dist/runtime/worker-live.js +25 -0
  168. package/dist/runtime/worker-recovery.d.ts +35 -0
  169. package/dist/runtime/worker-recovery.js +64 -0
  170. package/dist/runtime/worker-table.d.ts +58 -0
  171. package/dist/runtime/worker-table.js +70 -0
  172. package/dist/runtime-adapter.d.ts +421 -0
  173. package/dist/runtime-adapter.js +76 -0
  174. package/dist/serial-queue.d.ts +16 -0
  175. package/dist/serial-queue.js +22 -0
  176. package/dist/server.d.ts +44 -0
  177. package/dist/server.js +119 -0
  178. package/dist/settings.d.ts +23 -0
  179. package/dist/settings.js +31 -0
  180. package/dist/sse.d.ts +67 -0
  181. package/dist/sse.js +186 -0
  182. package/dist/static.d.ts +20 -0
  183. package/dist/static.js +120 -0
  184. package/dist/store/builtin-sections.d.ts +52 -0
  185. package/dist/store/builtin-sections.js +118 -0
  186. package/dist/store/celestea-home.d.ts +12 -0
  187. package/dist/store/celestea-home.js +11 -0
  188. package/dist/store/fs-json.d.ts +51 -0
  189. package/dist/store/fs-json.js +119 -0
  190. package/dist/store/grants-audit.d.ts +84 -0
  191. package/dist/store/grants-audit.js +109 -0
  192. package/dist/store/grants-service.d.ts +44 -0
  193. package/dist/store/grants-service.js +41 -0
  194. package/dist/store/grants-tokens.d.ts +62 -0
  195. package/dist/store/grants-tokens.js +105 -0
  196. package/dist/store/grants.d.ts +113 -0
  197. package/dist/store/grants.js +238 -0
  198. package/dist/store/index.d.ts +36 -0
  199. package/dist/store/index.js +36 -0
  200. package/dist/store/mode.d.ts +35 -0
  201. package/dist/store/mode.js +41 -0
  202. package/dist/store/permissions.d.ts +61 -0
  203. package/dist/store/permissions.js +139 -0
  204. package/dist/store/prompts-compose.d.ts +56 -0
  205. package/dist/store/prompts-compose.js +62 -0
  206. package/dist/store/prompts-template.d.ts +26 -0
  207. package/dist/store/prompts-template.js +99 -0
  208. package/dist/store/prompts.d.ts +142 -0
  209. package/dist/store/prompts.js +273 -0
  210. package/dist/store/provider-probe.d.ts +81 -0
  211. package/dist/store/provider-probe.js +154 -0
  212. package/dist/store/providers.d.ts +97 -0
  213. package/dist/store/providers.js +219 -0
  214. package/dist/store/result.d.ts +29 -0
  215. package/dist/store/result.js +30 -0
  216. package/dist/store/session-id.d.ts +119 -0
  217. package/dist/store/session-id.js +220 -0
  218. package/dist/store/session-meta.d.ts +41 -0
  219. package/dist/store/session-meta.js +66 -0
  220. package/dist/store/session-ops.d.ts +109 -0
  221. package/dist/store/session-ops.js +264 -0
  222. package/dist/store/session-tools.d.ts +42 -0
  223. package/dist/store/session-tools.js +69 -0
  224. package/dist/store/sessions.d.ts +162 -0
  225. package/dist/store/sessions.js +307 -0
  226. package/dist/store/validate.d.ts +13 -0
  227. package/dist/store/validate.js +38 -0
  228. package/dist/store/workspaces.d.ts +67 -0
  229. package/dist/store/workspaces.js +228 -0
  230. package/dist/user-questions.d.ts +58 -0
  231. package/dist/user-questions.js +157 -0
  232. package/dist/version.d.ts +14 -0
  233. package/dist/version.js +52 -0
  234. package/package.json +38 -0
  235. package/webdist/assets/index-Bngs7gUm.js +98 -0
  236. package/webdist/assets/index-Bu6ci_rN.css +1 -0
  237. package/webdist/assets/katex-CCbS2qAY.js +257 -0
  238. package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
  239. package/webdist/build-meta.json +7 -0
  240. package/webdist/index.html +261 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Handler registry — the ONLY place that knows every endpoint group.
3
+ *
4
+ * Each `registerXxx` returns the contract ids it bound, and `app.ts` asserts
5
+ * the union equals the frozen 47. A route can therefore never be silently
6
+ * dropped: adding an endpoint to `contracts/endpoints.json` without a handler
7
+ * fails at startup with the missing id.
8
+ *
9
+ * Module map:
10
+ * common.ts error/body/field helpers shared by every handler
11
+ * config-shape.ts the /api/config body assembled from live stores
12
+ * health.ts GET /api/health | /api/status | /api/tools
13
+ * dialog.ts GET /api/events (SSE) | POST /api/turn | /api/cancel | /api/clear
14
+ * config.ts GET+POST /api/config
15
+ * sessions.ts GET+POST /api/sessions | {id}/messages | {id}/activate | {id}/context
16
+ * context-shape.ts the context snapshot body + the 20k-per-entry wire guard
17
+ * session-move.ts {id}/rename | {id}/branch | {id}/compact | archive | unarchive | batch-*
18
+ * workspaces.ts /api/workspaces (+rename/delete/batch-delete)
19
+ * fs.ts GET /api/fs/browse | GET /api/fs/list
20
+ * exec.ts G2: POST /api/exec (immediate shell, permission-gated)
21
+ * providers.ts /api/providers (+delete/test/models fetch/default)
22
+ * prompts.ts /api/prompts (+delete/default)
23
+ * worker.ts /api/worker/spawn | send | status
24
+ * grants.ts GET+POST+DELETE /api/sessions/{id}/grants | grants/confirm-token
25
+ * questions.ts W783: GET /api/questions | POST /api/questions/{id}/answer
26
+ * usage.ts W785: GET /api/usage/ledger (the ledger's aggregate view)
27
+ * permissions.ts W9: /api/permissions/presets (+{id}) | /api/sessions/{id}/permission
28
+ * session-tools.ts W860: GET+PUT /api/sessions/{id}/tools (the session's disabled list)
29
+ * session-model.ts W870: PUT /api/sessions/{id}/model (the session-level model switch)
30
+ * plugins.ts W860: GET /api/plugins (the host startup plugin inventory)
31
+ * auth.ts W767: GET /login | POST /auth/login | GET /auth/check
32
+ *
33
+ * W725: the context endpoint (44th) lives in sessions.ts; its shaping is in
34
+ * context-shape.ts.
35
+ */
36
+ import { registerAuth } from "./auth.js";
37
+ import { registerConfig } from "./config.js";
38
+ import { registerDialog } from "./dialog.js";
39
+ import { registerExec } from "./exec.js";
40
+ import { registerFs } from "./fs.js";
41
+ import { registerGrants } from "./grants.js";
42
+ import { registerHealth } from "./health.js";
43
+ import { registerPermissions } from "./permissions.js";
44
+ import { registerPlugins } from "./plugins.js";
45
+ import { registerPrompts } from "./prompts.js";
46
+ import { registerProviders } from "./providers.js";
47
+ import { registerQuestions } from "./questions.js";
48
+ import { registerSessionMoves } from "./session-move.js";
49
+ import { registerSessionModel } from "./session-model.js";
50
+ import { registerSessionTools } from "./session-tools.js";
51
+ import { registerSessions } from "./sessions.js";
52
+ import { registerUsage } from "./usage.js";
53
+ import { registerWorker } from "./worker.js";
54
+ import { registerWorkspaces } from "./workspaces.js";
55
+ export function registerHandlers(app, deps, table) {
56
+ return [
57
+ ...registerHealth(app, deps, table),
58
+ ...registerDialog(app, deps, table),
59
+ ...registerConfig(app, deps, table),
60
+ ...registerSessions(app, deps, table),
61
+ ...registerSessionMoves(app, deps, table),
62
+ ...registerWorkspaces(app, deps, table),
63
+ ...registerFs(app, deps, table),
64
+ // G2: immediate execution for the UI (/run, !cmd) — no model in the loop.
65
+ ...registerExec(app, deps, table),
66
+ ...registerProviders(app, deps, table),
67
+ ...registerPrompts(app, deps, table),
68
+ ...registerWorker(app, deps, table),
69
+ ...registerGrants(app, deps, table),
70
+ // W9: permission presets (custom CRUD + a session's chosen preset).
71
+ ...registerPermissions(app, deps, table),
72
+ // W783: the user-question answer + pending-list endpoints (47 -> 49).
73
+ ...registerQuestions(app, deps, table),
74
+ // W785 (E-P1, capability 3): the usage ledger's aggregate view (49 -> 50).
75
+ ...registerUsage(app, deps, table),
76
+ // W767: Studio's OWN login-cookie gate (page + login + nginx auth_request).
77
+ ...registerAuth(app, deps, table),
78
+ // W860: session-level tool switches (57 -> 59) + the host plugin inventory (59 -> 60).
79
+ ...registerSessionTools(app, deps, table),
80
+ ...registerPlugins(app, deps, table),
81
+ // W870: the session-scoped model switch (60 -> 61) — the statusline picker's
82
+ // target; POST /api/config keeps meaning "the global default".
83
+ registerSessionModel(app, deps, table),
84
+ ];
85
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The six permission endpoints (W9): custom-preset CRUD plus a session's chosen
3
+ * preset. Storage and resolution live in store/permissions.ts and
4
+ * runtime/engine-permissions.ts; this module is the HTTP face only.
5
+ */
6
+ import type { Hono } from "hono";
7
+ import type { RouteTable } from "../routes.js";
8
+ import { type Deps } from "./common.js";
9
+ export declare function registerPermissions(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The six permission endpoints (W9): custom-preset CRUD plus a session's chosen
3
+ * preset. Storage and resolution live in store/permissions.ts and
4
+ * runtime/engine-permissions.ts; this module is the HTTP face only.
5
+ */
6
+ import { effectivePermissionOf, permissionDataDir } from "../runtime/engine-permissions.js";
7
+ import { validatePermissionPreset } from "../runtime/engine-grants.js";
8
+ import { BUILTIN_PRESETS, isBuiltinPresetId, maxPermissionId, readPermissionsFile, readSessionPermission, writePermissionsFile, writeSessionPermission, } from "../store/permissions.js";
9
+ import { nowSec } from "../store/grants-service.js";
10
+ import { errText } from "../store/result.js";
11
+ import { failJson, readJsonBody, strField, storeFail } from "./common.js";
12
+ function presetBody(p) {
13
+ return {
14
+ id: p.id,
15
+ label: p.label,
16
+ network: p.network,
17
+ workspaceWritable: p.workspaceWritable,
18
+ toolRootsWritable: p.toolRootsWritable,
19
+ writeRoots: [...p.writeRoots],
20
+ allPaths: p.allPaths,
21
+ unsandboxed: p.unsandboxed,
22
+ toolDeny: [...p.toolDeny],
23
+ };
24
+ }
25
+ /** W864: the resolved baseline, as the two session-permission endpoints report it. */
26
+ function effectiveBody(b) {
27
+ return {
28
+ network: b.network,
29
+ workspaceWritable: b.workspaceWritable,
30
+ toolRootsWritable: b.toolRootsWritable,
31
+ writeRoots: [...b.writeRoots],
32
+ allPaths: b.allPaths,
33
+ unsandboxed: b.unsandboxed,
34
+ toolDeny: [...b.toolDeny],
35
+ };
36
+ }
37
+ function registerListPresets(app, deps, table) {
38
+ const route = table.get("get_permissions_presets");
39
+ app.on(route.method, route.honoPath, (c) => {
40
+ const read = readPermissionsFile(permissionDataDir(deps.grants.env));
41
+ return c.json({
42
+ ok: true,
43
+ builtin: BUILTIN_PRESETS.map(presetBody),
44
+ custom: read.presets.map(presetBody),
45
+ max: maxPermissionId(deps.grants.env),
46
+ ...(read.warnings.length === 0 ? {} : { warnings: read.warnings }),
47
+ });
48
+ });
49
+ return route.id;
50
+ }
51
+ function registerCreatePreset(app, deps, table) {
52
+ const route = table.get("post_permissions_presets");
53
+ app.on(route.method, route.honoPath, async (c) => {
54
+ const body = await readJsonBody(c);
55
+ if (!body.ok)
56
+ return body.response;
57
+ const dataDir = permissionDataDir(deps.grants.env);
58
+ const read = readPermissionsFile(dataDir);
59
+ const validated = validatePermissionPreset(body.body["preset"], deps.grants.env, read.presets.map((p) => p.id));
60
+ if (!validated.ok)
61
+ return failJson(c, validated.conflict === true ? 409 : 422, validated.error);
62
+ try {
63
+ writePermissionsFile(dataDir, [...read.presets, validated.preset], nowSec(deps.grants));
64
+ }
65
+ catch (e) {
66
+ return failJson(c, 500, "cannot persist permissions: " + errText(e));
67
+ }
68
+ return c.json({ ok: true, preset: presetBody(validated.preset) });
69
+ });
70
+ return route.id;
71
+ }
72
+ function registerUpdatePreset(app, deps, table) {
73
+ const route = table.get("put_permissions_preset");
74
+ app.on(route.method, route.honoPath, async (c) => {
75
+ const id = c.req.param("id") ?? "";
76
+ if (isBuiltinPresetId(id))
77
+ return failJson(c, 409, "'" + id + "' is a built-in preset");
78
+ const body = await readJsonBody(c);
79
+ if (!body.ok)
80
+ return body.response;
81
+ const dataDir = permissionDataDir(deps.grants.env);
82
+ const read = readPermissionsFile(dataDir);
83
+ if (!read.presets.some((p) => p.id === id))
84
+ return failJson(c, 404, "no custom preset '" + id + "'");
85
+ const validated = validatePermissionPreset(body.body["preset"], deps.grants.env, []);
86
+ if (!validated.ok)
87
+ return failJson(c, 422, validated.error);
88
+ if (validated.preset.id !== id)
89
+ return failJson(c, 422, "preset id must not change (" + id + ")");
90
+ const next = read.presets.map((p) => (p.id === id ? validated.preset : p));
91
+ try {
92
+ writePermissionsFile(dataDir, next, nowSec(deps.grants));
93
+ }
94
+ catch (e) {
95
+ return failJson(c, 500, "cannot persist permissions: " + errText(e));
96
+ }
97
+ return c.json({ ok: true, preset: presetBody(validated.preset) });
98
+ });
99
+ return route.id;
100
+ }
101
+ function registerDeletePreset(app, deps, table) {
102
+ const route = table.get("delete_permissions_preset");
103
+ app.on(route.method, route.honoPath, (c) => {
104
+ const id = c.req.param("id") ?? "";
105
+ if (isBuiltinPresetId(id))
106
+ return failJson(c, 409, "'" + id + "' is a built-in preset");
107
+ const dataDir = permissionDataDir(deps.grants.env);
108
+ const read = readPermissionsFile(dataDir);
109
+ if (!read.presets.some((p) => p.id === id))
110
+ return failJson(c, 404, "no custom preset '" + id + "'");
111
+ try {
112
+ writePermissionsFile(dataDir, read.presets.filter((p) => p.id !== id), nowSec(deps.grants));
113
+ }
114
+ catch (e) {
115
+ return failJson(c, 500, "cannot persist permissions: " + errText(e));
116
+ }
117
+ return c.json({ ok: true, deleted: id });
118
+ });
119
+ return route.id;
120
+ }
121
+ function registerGetSessionPermission(app, deps, table) {
122
+ const route = table.get("get_session_permission");
123
+ app.on(route.method, route.honoPath, (c) => {
124
+ const resolved = deps.sessions.require(c.req.param("id") ?? "");
125
+ if (!resolved.ok)
126
+ return storeFail(c, resolved);
127
+ const read = readSessionPermission(resolved.value.dir, resolved.value.id);
128
+ const baseline = effectivePermissionOf(resolved.value.dir, resolved.value.id, deps.grants.env);
129
+ const warnings = [
130
+ ...(read.error === undefined ? [] : [read.error]),
131
+ ...baseline.warnings,
132
+ ];
133
+ return c.json({
134
+ ok: true,
135
+ session: resolved.value.id,
136
+ preset: read.preset ?? baseline.preset,
137
+ effective: effectiveBody(baseline),
138
+ ...(warnings.length === 0 ? {} : { warnings }),
139
+ });
140
+ });
141
+ return route.id;
142
+ }
143
+ function registerPutSessionPermission(app, deps, table) {
144
+ const route = table.get("put_session_permission");
145
+ app.on(route.method, route.honoPath, async (c) => {
146
+ const resolved = deps.sessions.require(c.req.param("id") ?? "");
147
+ if (!resolved.ok)
148
+ return storeFail(c, resolved);
149
+ const body = await readJsonBody(c);
150
+ if (!body.ok)
151
+ return body.response;
152
+ const preset = strField(c, body.body, "preset");
153
+ if (!preset.ok)
154
+ return preset.response;
155
+ if (preset.value === undefined || preset.value === "")
156
+ return failJson(c, 422, "field 'preset' is required");
157
+ const read = readPermissionsFile(permissionDataDir(deps.grants.env));
158
+ if (!isBuiltinPresetId(preset.value) && !read.presets.some((p) => p.id === preset.value)) {
159
+ return failJson(c, 422, "unknown preset '" + preset.value + "'");
160
+ }
161
+ try {
162
+ writeSessionPermission(resolved.value.dir, resolved.value.id, preset.value, nowSec(deps.grants));
163
+ }
164
+ catch (e) {
165
+ return failJson(c, 500, "cannot persist permission: " + errText(e));
166
+ }
167
+ // W9: recompose the session at the next boundary (same hook grants use).
168
+ deps.runtime.invalidateSession?.(resolved.value.id);
169
+ const baseline = effectivePermissionOf(resolved.value.dir, resolved.value.id, deps.grants.env);
170
+ return c.json({ ok: true, preset: preset.value, effective: effectiveBody(baseline) });
171
+ });
172
+ return route.id;
173
+ }
174
+ export function registerPermissions(app, deps, table) {
175
+ return [
176
+ registerListPresets(app, deps, table),
177
+ registerCreatePreset(app, deps, table),
178
+ registerUpdatePreset(app, deps, table),
179
+ registerDeletePreset(app, deps, table),
180
+ registerGetSessionPermission(app, deps, table),
181
+ registerPutSessionPermission(app, deps, table),
182
+ ];
183
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * W860: the host plugin inventory — `GET /api/plugins`.
3
+ *
4
+ * The names are NOT a constant transcribed here: `composeStudio` records what it
5
+ * actually mounted (`StudioServices.hostPluginNames`, built with `pluginNames`
6
+ * over the very `storePlugins` / `hostPlugins` arrays it mounts), and this
7
+ * handler reads that record. Adding a plugin to `plugins.ts` therefore shows up
8
+ * here automatically — there is no second list to forget.
9
+ *
10
+ * Honest boundary (also stated in the contract note): this covers the HOST
11
+ * startup layer only — the store plugins (workspaces, sessions, session-ops,
12
+ * providers, prompts) and the host singletons (bus, runtime, settings).
13
+ * Plugins the ENGINE mounts while composing a session are not listed.
14
+ */
15
+ import type { Hono } from "hono";
16
+ import type { RouteTable } from "../routes.js";
17
+ import type { Deps } from "./common.js";
18
+ export declare function registerPlugins(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * W860: the host plugin inventory — `GET /api/plugins`.
3
+ *
4
+ * The names are NOT a constant transcribed here: `composeStudio` records what it
5
+ * actually mounted (`StudioServices.hostPluginNames`, built with `pluginNames`
6
+ * over the very `storePlugins` / `hostPlugins` arrays it mounts), and this
7
+ * handler reads that record. Adding a plugin to `plugins.ts` therefore shows up
8
+ * here automatically — there is no second list to forget.
9
+ *
10
+ * Honest boundary (also stated in the contract note): this covers the HOST
11
+ * startup layer only — the store plugins (workspaces, sessions, session-ops,
12
+ * providers, prompts) and the host singletons (bus, runtime, settings).
13
+ * Plugins the ENGINE mounts while composing a session are not listed.
14
+ */
15
+ /** One row of the inventory; `hot` is about runtime mounting, not this value. */
16
+ function pluginRows(deps) {
17
+ return deps.hostPluginNames.map((name) => ({ name, layer: "host", hot: false }));
18
+ }
19
+ export function registerPlugins(app, deps, table) {
20
+ const route = table.get("get_plugins");
21
+ app.on(route.method, route.honoPath, (c) => c.json({ ok: true, plugins: pluginRows(deps) }));
22
+ return [route.id];
23
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Prompt registry endpoints — `src/prompts.rs:714-875`.
3
+ *
4
+ * Scope resolution: `workspace` absent or blank = global; an unknown workspace
5
+ * is 404. The three write endpoints share ONE order: 409 guard (nothing is
6
+ * written) -> persist -> hot apply -> restore the previous file when the apply
7
+ * fails, so a failed compose can never leave the registry ahead of the engine.
8
+ */
9
+ import type { Hono } from "hono";
10
+ import type { RouteTable } from "../routes.js";
11
+ import { type Deps } from "./common.js";
12
+ export declare function registerPrompts(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Prompt registry endpoints — `src/prompts.rs:714-875`.
3
+ *
4
+ * Scope resolution: `workspace` absent or blank = global; an unknown workspace
5
+ * is 404. The three write endpoints share ONE order: 409 guard (nothing is
6
+ * written) -> persist -> hot apply -> restore the previous file when the apply
7
+ * fails, so a failed compose can never leave the registry ahead of the engine.
8
+ */
9
+ import { EngineError } from "../runtime-adapter.js";
10
+ import { activePromptBinding, assembleSystemPromptFor } from "./config-shape.js";
11
+ import { DEFAULT_SESSION_MODE } from "../store/mode.js";
12
+ import { failJson, readJsonBody, strField, storeFail } from "./common.js";
13
+ function resolveScope(deps, workspace) {
14
+ const name = (workspace ?? "").trim();
15
+ if (name === "")
16
+ return { scope: deps.prompts.scopeGlobal() };
17
+ const path = deps.workspaces.workspacePath(name);
18
+ if (path === undefined)
19
+ return { error: `unknown workspace '${name}'` };
20
+ return { scope: deps.prompts.scopeWorkspace(name, path) };
21
+ }
22
+ /**
23
+ * 409 -> persist -> hot apply -> rollback; returns the success body.
24
+ *
25
+ * W815-N2: the whole snapshot/mutate/configure/rollback window runs inside the
26
+ * shared serial queue, so a concurrent write cannot re-snapshot in the middle
27
+ * and have its success rolled back by an earlier failure.
28
+ */
29
+ async function hotApply(deps, scope, mutate) {
30
+ if (deps.runtime.isBusy())
31
+ return { ok: false, status: 409, error: "turn in progress; prompt applies between turns" };
32
+ return deps.applyQueue.run(async () => {
33
+ const snapshot = deps.prompts.snapshot(scope);
34
+ const res = mutate();
35
+ if (!res.ok)
36
+ return { ok: false, status: res.status, error: res.error };
37
+ try {
38
+ // W729: a prompt hot-apply primes the BASE generation, so it stays on the
39
+ // DEFAULT mode and can never inherit the active session's mode variant.
40
+ await deps.runtime.configure({ system_prompt: assembleSystemPromptFor(deps, null, DEFAULT_SESSION_MODE) });
41
+ }
42
+ catch (e) {
43
+ // W815-1: a null snapshot means the scope file did not exist before this
44
+ // mutate; restoring must therefore REMOVE the file (PromptsStore.restore).
45
+ deps.prompts.restore(scope, snapshot);
46
+ return { ok: false, status: 500, error: `compose failed: ${e instanceof EngineError ? e.message : String(e)}` };
47
+ }
48
+ return { ok: true, body: { ok: true, ...res.value } };
49
+ });
50
+ }
51
+ function registerList(app, deps, table) {
52
+ const route = table.get("get_prompts");
53
+ app.on(route.method, route.honoPath, (c) => {
54
+ const resolved = resolveScope(deps, c.req.query("workspace"));
55
+ if (resolved.scope === undefined)
56
+ return failJson(c, 404, resolved.error ?? "unknown workspace");
57
+ return c.json(deps.prompts.list(resolved.scope, activePromptBinding(deps)));
58
+ });
59
+ return route.id;
60
+ }
61
+ function registerUpsert(app, deps, table) {
62
+ const route = table.get("post_prompts");
63
+ app.on(route.method, route.honoPath, async (c) => {
64
+ const read = await readJsonBody(c);
65
+ if (!read.ok)
66
+ return read.response;
67
+ const workspace = strField(c, read.body, "workspace");
68
+ const id = strField(c, read.body, "id");
69
+ const name = strField(c, read.body, "name");
70
+ for (const f of [workspace, id, name])
71
+ if (!f.ok)
72
+ return f.response;
73
+ const resolved = resolveScope(deps, workspace.ok ? workspace.value : undefined);
74
+ if (resolved.scope === undefined)
75
+ return failJson(c, 404, resolved.error ?? "unknown workspace");
76
+ const overrides = read.body["section_overrides"];
77
+ // W815-6: a PRESENT but wrongly-typed field is a 400, never a silent
78
+ // "absent" that would replace the stored overrides with an empty map. Only
79
+ // absent/null falls back to the store's default.
80
+ if (overrides !== undefined && overrides !== null && !isStringMap(overrides)) {
81
+ return failJson(c, 400, "section_overrides must be an object of string templates");
82
+ }
83
+ const out = await hotApply(deps, resolved.scope, () => deps.prompts.upsert(resolved.scope, {
84
+ id: (id.ok ? id.value : "") ?? "",
85
+ name: (name.ok ? name.value : "") ?? "",
86
+ section_overrides: isStringMap(overrides) ? overrides : undefined,
87
+ is_default: typeof read.body["is_default"] === "boolean" ? read.body["is_default"] : undefined,
88
+ }));
89
+ if (!out.ok)
90
+ return failJson(c, out.status, out.error);
91
+ return c.json(out.body);
92
+ });
93
+ return route.id;
94
+ }
95
+ function isStringMap(v) {
96
+ return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string");
97
+ }
98
+ function registerScopedWrite(app, deps, table, id) {
99
+ const route = table.get(id);
100
+ app.on(route.method, route.honoPath, async (c) => {
101
+ const read = await readJsonBody(c, false);
102
+ if (!read.ok)
103
+ return read.response;
104
+ const workspace = strField(c, read.body, "workspace");
105
+ if (!workspace.ok)
106
+ return workspace.response;
107
+ const resolved = resolveScope(deps, workspace.value);
108
+ if (resolved.scope === undefined)
109
+ return failJson(c, 404, resolved.error ?? "unknown workspace");
110
+ const promptId = c.req.param("id") ?? "";
111
+ const out = await hotApply(deps, resolved.scope, () => id === "post_prompts_delete" ? deps.prompts.remove(resolved.scope, promptId) : deps.prompts.setDefault(resolved.scope, promptId));
112
+ if (!out.ok)
113
+ return failJson(c, out.status, out.error);
114
+ return c.json(out.body);
115
+ });
116
+ return route.id;
117
+ }
118
+ export function registerPrompts(app, deps, table) {
119
+ return [registerList(app, deps, table), registerUpsert(app, deps, table), registerScopedWrite(app, deps, table, "post_prompts_delete"), registerScopedWrite(app, deps, table, "post_prompts_default")];
120
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Provider endpoints — `src/providers.rs:673-797`.
3
+ *
4
+ * Every response is a `public_view`: the `api_key` KEY does not exist in the
5
+ * shape at all. `/api/providers/test` accepts a full inline overlay that is
6
+ * never persisted; `/models/fetch` resolves the row from the store and may
7
+ * borrow the engine's own key for a same-origin keyless provider.
8
+ */
9
+ import type { Hono } from "hono";
10
+ import type { RouteTable } from "../routes.js";
11
+ import { type Deps } from "./common.js";
12
+ export declare function registerProviders(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Provider endpoints — `src/providers.rs:673-797`.
3
+ *
4
+ * Every response is a `public_view`: the `api_key` KEY does not exist in the
5
+ * shape at all. `/api/providers/test` accepts a full inline overlay that is
6
+ * never persisted; `/models/fetch` resolves the row from the store and may
7
+ * borrow the engine's own key for a same-origin keyless provider.
8
+ */
9
+ import { EngineError } from "../runtime-adapter.js";
10
+ import { probeModels, testProvider } from "../store/provider-probe.js";
11
+ import { REQUEST_FORMATS } from "../store/providers.js";
12
+ import { isHttpUrl } from "../store/validate.js";
13
+ import { baseUrlOf } from "./config-shape.js";
14
+ // W742 §1: the same "background work in flight" guard `POST /api/config` uses.
15
+ import { workersInFlight } from "./config.js";
16
+ import { failJson, readJsonBody, strField, storeFail } from "./common.js";
17
+ function probeOptions(deps) {
18
+ return { engineBaseUrl: baseUrlOf(deps), engineKey: process.env[deps.config.apiKeyEnv] ?? null };
19
+ }
20
+ /** Validate a candidate the way the retired backend validated `ProviderReq` before probing. */
21
+ function candidateError(candidate) {
22
+ if (candidate.base_url.trim() === "")
23
+ return "base_url is required";
24
+ if (!isHttpUrl(candidate.base_url))
25
+ return "base_url must be an http:// or https:// URL";
26
+ if (!REQUEST_FORMATS.includes(candidate.request_format)) {
27
+ return `invalid request_format '${candidate.request_format}': expected chat_completions | responses | anthropic_messages`;
28
+ }
29
+ return null;
30
+ }
31
+ function asCandidate(body, stored) {
32
+ const fmt = typeof body["request_format"] === "string" ? body["request_format"] : (stored?.request_format ?? "chat_completions");
33
+ return {
34
+ id: stored?.id ?? "__inline__",
35
+ base_url: typeof body["base_url"] === "string" ? body["base_url"] : (stored?.base_url ?? ""),
36
+ request_format: fmt,
37
+ api_key: typeof body["api_key"] === "string" ? body["api_key"] : (stored?.api_key ?? null),
38
+ };
39
+ }
40
+ function registerList(app, deps, table) {
41
+ const route = table.get("get_providers");
42
+ app.on(route.method, route.honoPath, (c) => c.json(deps.providers.response()));
43
+ return route.id;
44
+ }
45
+ function registerUpsert(app, deps, table) {
46
+ const route = table.get("post_providers");
47
+ app.on(route.method, route.honoPath, async (c) => {
48
+ const read = await readJsonBody(c);
49
+ if (!read.ok)
50
+ return read.response;
51
+ const body = read.body;
52
+ const models = body["models"];
53
+ // W815-7: a PRESENT but wrongly-typed `models` is a 400. It used to fall
54
+ // through as `undefined`, which the store reads as "absent" and therefore
55
+ // CLEARS the list — only an absent/null field may clear (docs/pitfalls P1b).
56
+ if (models !== undefined && models !== null && !Array.isArray(models)) {
57
+ return failJson(c, 400, "models must be an array");
58
+ }
59
+ const res = deps.providers.upsert({
60
+ id: typeof body["id"] === "string" ? body["id"] : undefined,
61
+ name: typeof body["name"] === "string" ? body["name"] : undefined,
62
+ note: typeof body["note"] === "string" ? body["note"] : undefined,
63
+ base_url: typeof body["base_url"] === "string" ? body["base_url"] : undefined,
64
+ request_format: typeof body["request_format"] === "string" ? body["request_format"] : undefined,
65
+ api_key: typeof body["api_key"] === "string" ? body["api_key"] : null,
66
+ models: Array.isArray(models) ? models : undefined,
67
+ });
68
+ if (!res.ok)
69
+ return storeFail(c, res);
70
+ return c.json(res.value);
71
+ });
72
+ return route.id;
73
+ }
74
+ function registerDelete(app, deps, table) {
75
+ const route = table.get("post_provider_delete");
76
+ app.on(route.method, route.honoPath, (c) => {
77
+ const res = deps.providers.remove(c.req.param("id") ?? "");
78
+ if (!res.ok)
79
+ return storeFail(c, res);
80
+ return c.json({ ok: true });
81
+ });
82
+ return route.id;
83
+ }
84
+ function registerTest(app, deps, table) {
85
+ const route = table.get("post_provider_test");
86
+ app.on(route.method, route.honoPath, async (c) => {
87
+ const read = await readJsonBody(c, false);
88
+ if (!read.ok)
89
+ return read.response;
90
+ const id = strField(c, read.body, "id");
91
+ if (!id.ok)
92
+ return id.response;
93
+ const stored = id.value === undefined ? undefined : deps.providers.find(id.value);
94
+ const candidate = asCandidate(read.body, stored);
95
+ const bad = candidateError(candidate);
96
+ if (bad !== null)
97
+ return failJson(c, 400, bad);
98
+ const out = await testProvider(candidate, probeOptions(deps));
99
+ if (!out.ok)
100
+ return c.json({ ok: false, error: out.error });
101
+ return c.json({ ok: true, latency_ms: out.latency_ms, model_count: out.model_count });
102
+ });
103
+ return route.id;
104
+ }
105
+ function registerModelsFetch(app, deps, table) {
106
+ const route = table.get("post_provider_models_fetch");
107
+ app.on(route.method, route.honoPath, async (c) => {
108
+ const id = c.req.param("id") ?? "";
109
+ const stored = deps.providers.find(id);
110
+ if (stored === undefined)
111
+ return failJson(c, 404, `unknown provider '${id}'`);
112
+ const out = await probeModels({ id: stored.id, base_url: stored.base_url, request_format: stored.request_format, api_key: stored.api_key }, probeOptions(deps));
113
+ if (!out.ok)
114
+ return c.json({ ok: false, error: out.error });
115
+ return c.json({ ok: true, models: out.models });
116
+ });
117
+ return route.id;
118
+ }
119
+ /**
120
+ * POST /api/providers/default — make (provider, model) the default pair.
121
+ *
122
+ * W750: `provider_id` is the OPTIONAL disambiguator. Model ids are not unique
123
+ * across providers (production: `deepseek-flash` is listed by both the gateway
124
+ * and 「基元」), so "model id only" cannot express "switch to THAT provider" —
125
+ * the plain `find` below would keep picking the first provider that happens to
126
+ * list the id. When `provider_id` is given the model must be one of that
127
+ * provider's models (otherwise nothing is applied: 400/404, no partial write);
128
+ * when it is absent the historical first-lister behaviour is unchanged.
129
+ */
130
+ function registerDefault(app, deps, table) {
131
+ const route = table.get("post_provider_default");
132
+ app.on(route.method, route.honoPath, async (c) => {
133
+ const read = await readJsonBody(c);
134
+ if (!read.ok)
135
+ return read.response;
136
+ const model = strField(c, read.body, "model");
137
+ if (!model.ok)
138
+ return model.response;
139
+ const providerId = strField(c, read.body, "provider_id");
140
+ if (!providerId.ok)
141
+ return providerId.response;
142
+ const wanted = (model.value ?? "").trim();
143
+ if (wanted === "")
144
+ return failJson(c, 400, "model must not be empty");
145
+ if (deps.runtime.isBusy())
146
+ return failJson(c, 409, "a turn is running; provider default applies between turns");
147
+ if (workersInFlight(deps.runtime))
148
+ return failJson(c, 409, "a worker is running; provider default applies between turns");
149
+ const askedProvider = (providerId.value ?? "").trim();
150
+ let owner;
151
+ if (askedProvider !== "") {
152
+ owner = deps.providers.rows().find((p) => p.id === askedProvider);
153
+ if (owner === undefined)
154
+ return failJson(c, 404, `unknown provider '${askedProvider}'`);
155
+ if (!owner.models.some((m) => m.id === wanted)) {
156
+ return failJson(c, 400, `provider '${askedProvider}' does not list model '${wanted}'`);
157
+ }
158
+ }
159
+ else {
160
+ owner = deps.providers.rows().find((p) => p.models.some((m) => m.id === wanted));
161
+ }
162
+ const patch = owner !== undefined && owner.request_format === "chat_completions" && owner.base_url !== "" ? { model: wanted, base_url: owner.base_url } : { model: wanted };
163
+ try {
164
+ await deps.runtime.configure(patch);
165
+ }
166
+ catch (e) {
167
+ return failJson(c, 500, e instanceof EngineError ? e.message : String(e));
168
+ }
169
+ const saved = deps.providers.setDefaultModel(wanted);
170
+ if (!saved.ok)
171
+ return storeFail(c, saved);
172
+ return c.json(deps.providers.response());
173
+ });
174
+ return route.id;
175
+ }
176
+ export function registerProviders(app, deps, table) {
177
+ return [registerList(app, deps, table), registerUpsert(app, deps, table), registerDelete(app, deps, table), registerTest(app, deps, table), registerModelsFetch(app, deps, table), registerDefault(app, deps, table)];
178
+ }