@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,242 @@
1
+ /**
2
+ * Session endpoints, part 1: list / create / transcript / activate / context.
3
+ *
4
+ * `GET /api/sessions` merges two sources: session directories across every
5
+ * registered workspace, plus the engine's in-memory worker sessions
6
+ * (`worker:<sid>`, pseudo-workspace "engine", `kind:"worker"`), sorted by id.
7
+ * W513: every row carries `kind` (`session` | `worker`) and `busy` (that
8
+ * session's own turn slot), and worker rows carry `wid` / `status` / `state` /
9
+ * `host_session`, so the UI can list and open them.
10
+ *
11
+ * `POST /api/sessions/{id}/activate` is "open this view + make sure the session
12
+ * HAS a runtime": it composes the instance on demand, persists the active
13
+ * session as a view preference, and NEVER returns 409 — a session that is
14
+ * already running is perfectly fine (that is the point of session independence).
15
+ *
16
+ * W791 (P1): `POST /api/sessions/{id}/mode` switches the session's working mode
17
+ * (the W729 `session.json.mode`, rewritten through the same writer) at a turn
18
+ * boundary — see `registerMode`.
19
+ *
20
+ * W791 (B): `GET /api/sessions?archived=1` lists the ARCHIVED sessions (the
21
+ * `<ws>/.celestea-archived/` rows `list()` skips); the default body is unchanged.
22
+ *
23
+ * W725: `GET /api/sessions/{id}/context` is the read-only "what does the model
24
+ * actually see" snapshot. The body is assembled by the ENGINE (the agent loop's
25
+ * own `buildRequest`, reached through `runtime.sessionContext`) and the usage
26
+ * block is the statusline's existing `context_usage`口径 — this handler adds
27
+ * only the 20k-per-entry wire guard.
28
+ */
29
+ import { CapacityError, EngineError } from "../runtime-adapter.js";
30
+ import { readSessionMeta, writeSessionMeta } from "../store/session-meta.js";
31
+ import { DEFAULT_SESSION_MODE, parseMode, validateMode } from "../store/mode.js";
32
+ import { validateModelName } from "../store/validate.js";
33
+ import { capacityJson, failJson, readJsonBody, strField, storeFail } from "./common.js";
34
+ import { contextPayload } from "./context-shape.js";
35
+ function workerRows(deps) {
36
+ return deps.runtime.workerSessions();
37
+ }
38
+ /** W513: `busy` is the session's OWN turn slot, never a process-wide flag. */
39
+ function withBusy(deps, row) {
40
+ return row.kind === "worker" ? row : { ...row, busy: deps.runtime.isBusy(row.id) };
41
+ }
42
+ /**
43
+ * W791 (B2): `?archived=1` (or `true`) selects the ARCHIVED listing.
44
+ *
45
+ * The default listing is untouched — archived sessions still live in a hidden
46
+ * sibling directory and are therefore absent from it, and the rows below are the
47
+ * only place an `archived` key ever appears. `0`, `false`, an empty value or an
48
+ * unrecognised value all read as "the default listing", so a client can poll the
49
+ * parameter without inventing a second default.
50
+ */
51
+ function wantsArchived(raw) {
52
+ if (raw === undefined)
53
+ return false;
54
+ const value = raw.trim().toLowerCase();
55
+ return value === "1" || value === "true";
56
+ }
57
+ function registerList(app, deps, table) {
58
+ const route = table.get("get_sessions");
59
+ app.on(route.method, route.honoPath, (c) => {
60
+ // W791: the ARCHIVED source answers with `listArchived()` only — archived
61
+ // sessions are filesystem rows, never worker rows, and none of them can be
62
+ // active or busy. W794: not because archiving REFUSES the active session (it
63
+ // no longer does) but because archiving CLEARS the marker: a moved session
64
+ // cannot be the active one, so `active_session` and the rows stay consistent.
65
+ const rows = wantsArchived(c.req.query("archived"))
66
+ ? deps.sessions.listArchived()
67
+ : deps.sessions.list(workerRows(deps)).map((row) => withBusy(deps, row));
68
+ return c.json({ sessions: rows, active_session: deps.workspaces.activeSession() });
69
+ });
70
+ return route.id;
71
+ }
72
+ function registerCreate(app, deps, table) {
73
+ const route = table.get("post_sessions");
74
+ app.on(route.method, route.honoPath, async (c) => {
75
+ const read = await readJsonBody(c);
76
+ if (!read.ok)
77
+ return read.response;
78
+ const title = strField(c, read.body, "title");
79
+ const workspace = strField(c, read.body, "workspace");
80
+ const model = strField(c, read.body, "model");
81
+ const prompt = strField(c, read.body, "prompt");
82
+ const mode = strField(c, read.body, "mode");
83
+ for (const f of [title, workspace, model, prompt, mode])
84
+ if (!f.ok)
85
+ return f.response;
86
+ const res = deps.sessions.create({
87
+ title: title.ok ? (title.value ?? "") : "",
88
+ workspace: workspace.ok ? workspace.value : undefined,
89
+ model: model.ok ? model.value : undefined,
90
+ prompt: prompt.ok ? prompt.value : undefined,
91
+ mode: mode.ok ? mode.value : undefined,
92
+ });
93
+ if (!res.ok)
94
+ return storeFail(c, res);
95
+ return c.json({ ok: true, id: res.value });
96
+ });
97
+ return route.id;
98
+ }
99
+ function registerMessages(app, deps, table) {
100
+ const route = table.get("get_session_messages");
101
+ app.on(route.method, route.honoPath, (c) => {
102
+ const id = c.req.param("id") ?? "";
103
+ if (id.startsWith("worker:")) {
104
+ const messages = deps.runtime.workerMessages(id);
105
+ if (messages === null)
106
+ return failJson(c, 404, `unknown session '${id}'`);
107
+ return c.json({ ok: true, session: id, messages });
108
+ }
109
+ const resolved = deps.sessions.require(id);
110
+ if (!resolved.ok)
111
+ return storeFail(c, resolved);
112
+ return c.json({ ok: true, session: id, messages: deps.sessions.messages(resolved.value) });
113
+ });
114
+ return route.id;
115
+ }
116
+ /** The session-level model override problem, or null when it is usable. */
117
+ function invalidSessionModel(dir) {
118
+ const model = readSessionMeta(dir)?.model;
119
+ if (model === undefined || model === "")
120
+ return null;
121
+ return validateModelName(model);
122
+ }
123
+ function registerActivate(app, deps, table) {
124
+ const route = table.get("post_session_activate");
125
+ app.on(route.method, route.honoPath, (c) => {
126
+ const id = c.req.param("id") ?? "";
127
+ const resolved = deps.sessions.require(id);
128
+ if (!resolved.ok)
129
+ return storeFail(c, resolved);
130
+ const bad = invalidSessionModel(resolved.value.dir);
131
+ if (bad !== null)
132
+ return failJson(c, 400, `invalid session model: ${bad}`);
133
+ let info;
134
+ try {
135
+ info = deps.runtime.ensureSession(resolved.value.id);
136
+ }
137
+ catch (e) {
138
+ if (e instanceof CapacityError)
139
+ return capacityJson(c, e);
140
+ return failJson(c, 500, `compose failed: ${e instanceof EngineError ? e.message : String(e)}`);
141
+ }
142
+ const saved = deps.workspaces.setActiveSession(resolved.value.id);
143
+ if (!saved.ok)
144
+ return failJson(c, 500, `cannot persist active session: ${saved.error}`);
145
+ return c.json({ ok: true, active_session: resolved.value.id, runtime: info.runtime, busy: info.busy, rebuilt: info.rebuilt });
146
+ });
147
+ return route.id;
148
+ }
149
+ /**
150
+ * GET /api/sessions/{id}/context (W725) — the engine's model-visible context.
151
+ *
152
+ * A session with no live instance is composed on demand (the same `entryFor`
153
+ * path activate and a turn use), so the endpoint works on a cold session and
154
+ * never drives a turn.
155
+ */
156
+ function registerContext(app, deps, table) {
157
+ const route = table.get("get_session_context");
158
+ app.on(route.method, route.honoPath, (c) => {
159
+ const resolved = deps.sessions.require(c.req.param("id") ?? "");
160
+ if (!resolved.ok)
161
+ return storeFail(c, resolved);
162
+ const session = resolved.value.id;
163
+ try {
164
+ const view = deps.runtime.sessionContext(session);
165
+ return c.json(contextPayload({ session, view, usage: contextUsageOf(deps, session) }));
166
+ }
167
+ catch (e) {
168
+ if (e instanceof CapacityError)
169
+ return capacityJson(c, e);
170
+ return failJson(c, 500, `context snapshot failed: ${e instanceof EngineError ? e.message : String(e)}`);
171
+ }
172
+ });
173
+ return route.id;
174
+ }
175
+ /**
176
+ * POST /api/sessions/{id}/mode (W791, P1 — `docs/modes-standard-vs-execution.md`
177
+ * §3.1/§5.2 #6, U8): switch the session's WORKING MODE at a turn boundary.
178
+ *
179
+ * Three disciplines, all inherited from existing endpoints on purpose:
180
+ * - the busy guard is `/compact`'s, in semantics AND in shape (409, and the
181
+ * wording is the same sentence with this action's verb) — a mode is a
182
+ * property of the generation, so it may not change inside a running turn;
183
+ * - the write path is W729's `session.json` writer (the other keys are kept),
184
+ * so a switch is the SAME operation `POST /api/sessions {mode}` performs;
185
+ * - the effect is W516's: the session's instance is dropped and the next turn
186
+ * recomposes it, which is what makes the response's `effective:"next_turn"`
187
+ * a fact rather than a promise. Sessions other than this one are untouched.
188
+ *
189
+ * TS-only (U8): the retired backend has no such endpoint, so the contract registers
190
+ * it under `tsOnlyRoutes` and the frontend gates on
191
+ * `capabilities.session_mode_tools`.
192
+ */
193
+ function registerMode(app, deps, table) {
194
+ const route = table.get("post_session_mode");
195
+ app.on(route.method, route.honoPath, async (c) => {
196
+ // W815-5: resolve to the CANONICAL id BEFORE the busy guard. `require` trims
197
+ // and sanitizes the raw path segment, so `%2F`/`%20` used to produce an id
198
+ // whose isBusy() lookup never matched the canonical instance (guard bypass).
199
+ const resolved = deps.sessions.require(c.req.param("id") ?? "");
200
+ if (!resolved.ok)
201
+ return storeFail(c, resolved);
202
+ const session = resolved.value.id;
203
+ if (deps.runtime.isBusy(session))
204
+ return failJson(c, 409, "turn 进行中,无法切换模式");
205
+ const read = await readJsonBody(c);
206
+ if (!read.ok)
207
+ return read.response;
208
+ const mode = strField(c, read.body, "mode");
209
+ if (!mode.ok)
210
+ return mode.response;
211
+ if (mode.value === undefined)
212
+ return failJson(c, 422, "field 'mode' must be a string");
213
+ const bad = validateMode(mode.value);
214
+ if (bad !== null)
215
+ return failJson(c, 400, bad);
216
+ try {
217
+ // W729 write path, key-preserving: title / model / prompt survive the switch.
218
+ writeSessionMeta(resolved.value.dir, { ...(readSessionMeta(resolved.value.dir) ?? {}), mode: parseMode(mode.value) ?? DEFAULT_SESSION_MODE });
219
+ }
220
+ catch (e) {
221
+ return failJson(c, 500, `meta write failed: ${String(e)}`);
222
+ }
223
+ deps.runtime.invalidateSession?.(session);
224
+ return c.json({ ok: true, session, mode: mode.value, effective: "next_turn" });
225
+ });
226
+ return route.id;
227
+ }
228
+ /** The statusline's context口径 (W263) with the `method` discriminator dropped. */
229
+ function contextUsageOf(deps, session) {
230
+ const usage = deps.runtime.statusline(session).context_usage;
231
+ return { used: usage.used, window: usage.window, ratio: usage.ratio, estimated: usage.estimated };
232
+ }
233
+ export function registerSessions(app, deps, table) {
234
+ return [
235
+ registerList(app, deps, table),
236
+ registerCreate(app, deps, table),
237
+ registerMessages(app, deps, table),
238
+ registerActivate(app, deps, table),
239
+ registerContext(app, deps, table),
240
+ registerMode(app, deps, table),
241
+ ];
242
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `GET /api/usage/ledger` — the aggregate view of the usage ledger (E §3.2.4, W785 P1 ①).
3
+ *
4
+ * The endpoint is a thin, honest SHAPE over the engine seam: the runtime adapter
5
+ * owns the ledger file and the folding (`queryLedger`), this handler only reads
6
+ * the query string, rejects a malformed one, and forwards. There is no host-side
7
+ * aggregation and no cache, so "what the endpoint says" and "what the ledger
8
+ * holds" cannot drift.
9
+ *
10
+ * Two `ok:false` answers are NOT client errors and stay HTTP 200, because the
11
+ * request was understood and the answer is "there is no ledger here":
12
+ * - `usage ledger unavailable` — the adapter has no ledger capability at all
13
+ * (an embedded/fake adapter; the `/api/health` style of degradation);
14
+ * - `usage ledger disabled` — the real adapter, `CELESTEA_USAGE_LEDGER=off`.
15
+ * The `error` field is registered optional in `contracts/endpoints.json`, which is
16
+ * exactly this case.
17
+ *
18
+ * A MALFORMED query is a 422 like every other handler (`field '<name>' must be …`,
19
+ * `handlers/common.ts` style): `since`/`until` are SECONDS and must be integers
20
+ * (`ts` of a ledger row), `group_by` must be one of session|turn|model|day.
21
+ */
22
+ import type { Hono } from "hono";
23
+ import type { RouteTable } from "../routes.js";
24
+ import { type Deps } from "./common.js";
25
+ /** The one aggregate endpoint, in contract order. */
26
+ export declare function registerUsage(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `GET /api/usage/ledger` — the aggregate view of the usage ledger (E §3.2.4, W785 P1 ①).
3
+ *
4
+ * The endpoint is a thin, honest SHAPE over the engine seam: the runtime adapter
5
+ * owns the ledger file and the folding (`queryLedger`), this handler only reads
6
+ * the query string, rejects a malformed one, and forwards. There is no host-side
7
+ * aggregation and no cache, so "what the endpoint says" and "what the ledger
8
+ * holds" cannot drift.
9
+ *
10
+ * Two `ok:false` answers are NOT client errors and stay HTTP 200, because the
11
+ * request was understood and the answer is "there is no ledger here":
12
+ * - `usage ledger unavailable` — the adapter has no ledger capability at all
13
+ * (an embedded/fake adapter; the `/api/health` style of degradation);
14
+ * - `usage ledger disabled` — the real adapter, `CELESTEA_USAGE_LEDGER=off`.
15
+ * The `error` field is registered optional in `contracts/endpoints.json`, which is
16
+ * exactly this case.
17
+ *
18
+ * A MALFORMED query is a 422 like every other handler (`field '<name>' must be …`,
19
+ * `handlers/common.ts` style): `since`/`until` are SECONDS and must be integers
20
+ * (`ts` of a ledger row), `group_by` must be one of session|turn|model|day.
21
+ */
22
+ import { LEDGER_GROUP_BY_VALUES, DEFAULT_LEDGER_GROUP_BY } from "@celestea/runtime";
23
+ import { errText } from "../store/result.js";
24
+ import { failJson } from "./common.js";
25
+ /** `since`/`until` are epoch SECONDS (the `ts` of a row), never ISO strings. */
26
+ function intQuery(c, name) {
27
+ const raw = (c.req.query(name) ?? "").trim();
28
+ if (raw === "")
29
+ return { ok: true, value: null };
30
+ if (!/^-?\d+$/.test(raw))
31
+ return { ok: false, error: `field '${name}' must be an integer` };
32
+ const value = Number.parseInt(raw, 10);
33
+ if (!Number.isSafeInteger(value))
34
+ return { ok: false, error: `field '${name}' must be an integer` };
35
+ return { ok: true, value };
36
+ }
37
+ /** The whole query string; an empty value is the same as an absent one. */
38
+ function readQuery(c) {
39
+ const session = (c.req.query("session") ?? "").trim();
40
+ const groupBy = (c.req.query("group_by") ?? "").trim();
41
+ if (groupBy !== "" && !LEDGER_GROUP_BY_VALUES.includes(groupBy)) {
42
+ return { ok: false, error: `field 'group_by' must be one of ${LEDGER_GROUP_BY_VALUES.join(", ")}` };
43
+ }
44
+ const since = intQuery(c, "since");
45
+ if (!since.ok)
46
+ return { ok: false, error: since.error };
47
+ const until = intQuery(c, "until");
48
+ if (!until.ok)
49
+ return { ok: false, error: until.error };
50
+ const q = { group_by: groupBy === "" ? DEFAULT_LEDGER_GROUP_BY : groupBy };
51
+ if (session !== "")
52
+ q.session = session;
53
+ if (since.value !== null)
54
+ q.since = since.value;
55
+ if (until.value !== null)
56
+ q.until = until.value;
57
+ return { ok: true, value: q };
58
+ }
59
+ /** The one aggregate endpoint, in contract order. */
60
+ export function registerUsage(app, deps, table) {
61
+ const route = table.get("get_usage_ledger");
62
+ app.on(route.method, route.honoPath, (c) => {
63
+ const read = readQuery(c);
64
+ if (!read.ok)
65
+ return failJson(c, 422, read.error);
66
+ const view = deps.runtime.usageLedger;
67
+ if (view === undefined)
68
+ return c.json({ ok: false, error: "usage ledger unavailable" });
69
+ try {
70
+ return c.json(view.call(deps.runtime, read.value));
71
+ }
72
+ catch (e) {
73
+ return failJson(c, 500, errText(e));
74
+ }
75
+ });
76
+ return [route.id];
77
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Worker orchestration — `src/api.rs:437-503`.
3
+ *
4
+ * The three endpoints proxy the engine's worker tools so the HTTP surface and
5
+ * the agent tool surface cannot drift. A tool-level refusal (`{ok:false,…}`) is
6
+ * still HTTP 200; only a HARD dispatch failure is 502, and a tool that returns
7
+ * no value at all is 500.
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 registerWorker(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Worker orchestration — `src/api.rs:437-503`.
3
+ *
4
+ * The three endpoints proxy the engine's worker tools so the HTTP surface and
5
+ * the agent tool surface cannot drift. A tool-level refusal (`{ok:false,…}`) is
6
+ * still HTTP 200; only a HARD dispatch failure is 502, and a tool that returns
7
+ * no value at all is 500.
8
+ */
9
+ import { failJson, readJsonBody, strField, storeFail } from "./common.js";
10
+ function registerSpawn(app, deps, table) {
11
+ const route = table.get("post_worker_spawn");
12
+ app.on(route.method, route.honoPath, async (c) => {
13
+ const read = await readJsonBody(c);
14
+ if (!read.ok)
15
+ return read.response;
16
+ const wid = strField(c, read.body, "wid");
17
+ const brief = strField(c, read.body, "brief");
18
+ const title = strField(c, read.body, "title");
19
+ const model = strField(c, read.body, "model");
20
+ const reportTo = strField(c, read.body, "report_to");
21
+ const session = strField(c, read.body, "session");
22
+ for (const f of [wid, brief, title, model, reportTo, session])
23
+ if (!f.ok)
24
+ return f.response;
25
+ if ((wid.ok ? wid.value : undefined) === undefined || (brief.ok ? brief.value : undefined) === undefined) {
26
+ return failJson(c, 422, "fields 'wid' and 'brief' are required");
27
+ }
28
+ // W833 (R3 B7 / W816 F3): an explicit session must RESOLVE before a worker
29
+ // is composed against it. Passing an unknown id used to compose an in-memory
30
+ // ghost instance (live slot + autowake loop + worker row) whose receipt no
31
+ // one could ever receive. Omitted/empty session keeps the detached default.
32
+ if (session.ok && typeof session.value === "string" && session.value !== "") {
33
+ const resolved = deps.sessions.require(session.value);
34
+ if (!resolved.ok)
35
+ return storeFail(c, resolved);
36
+ }
37
+ const out = await deps.runtime.workerSpawn({
38
+ wid: wid.ok ? wid.value : "",
39
+ brief: brief.ok ? brief.value : "",
40
+ title: title.ok ? title.value : undefined,
41
+ model: model.ok ? model.value : undefined,
42
+ report_to: reportTo.ok ? reportTo.value : undefined,
43
+ session: session.ok ? (session.value ?? null) : null,
44
+ });
45
+ if (out.ok)
46
+ return c.json({ ok: true, sessionId: out.sessionId, title: out.title, wid: out.wid });
47
+ if (out.error === undefined && out.value === undefined)
48
+ return failJson(c, 500, "tool returned no value");
49
+ return failJson(c, 502, out.error ?? "worker spawn failed", out.value === undefined ? undefined : { value: out.value });
50
+ });
51
+ return route.id;
52
+ }
53
+ function registerSend(app, deps, table) {
54
+ const route = table.get("post_worker_send");
55
+ app.on(route.method, route.honoPath, async (c) => {
56
+ const read = await readJsonBody(c);
57
+ if (!read.ok)
58
+ return read.response;
59
+ const target = strField(c, read.body, "target");
60
+ const content = strField(c, read.body, "content");
61
+ for (const f of [target, content])
62
+ if (!f.ok)
63
+ return f.response;
64
+ if ((target.ok ? target.value : undefined) === undefined || (content.ok ? content.value : undefined) === undefined) {
65
+ return failJson(c, 422, "fields 'target' and 'content' are required");
66
+ }
67
+ const out = await deps.runtime.workerSend({ target: target.ok ? target.value : "", content: content.ok ? content.value : "" });
68
+ return c.json(out);
69
+ });
70
+ return route.id;
71
+ }
72
+ function registerStatus(app, deps, table) {
73
+ const route = table.get("get_worker_status");
74
+ app.on(route.method, route.honoPath, (c) => {
75
+ const wid = c.req.query("wid");
76
+ return c.json(deps.runtime.workerStatus(wid === undefined || wid === "" ? undefined : wid));
77
+ });
78
+ return route.id;
79
+ }
80
+ export function registerWorker(app, deps, table) {
81
+ return [registerSpawn(app, deps, table), registerSend(app, deps, table), registerStatus(app, deps, table)];
82
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Workspace registry endpoints — `src/workspaces.rs:790-966`.
3
+ *
4
+ * `POST /api/workspaces` registers a folder by absolute path (the key is the
5
+ * folder basename and is never stored); `{name}/rename` really renames the
6
+ * USER folder and returns the whole registry view; `{name}/delete` only
7
+ * deregisters and never touches the folder.
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 registerWorkspaces(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Workspace registry endpoints — `src/workspaces.rs:790-966`.
3
+ *
4
+ * `POST /api/workspaces` registers a folder by absolute path (the key is the
5
+ * folder basename and is never stored); `{name}/rename` really renames the
6
+ * USER folder and returns the whole registry view; `{name}/delete` only
7
+ * deregisters and never touches the folder.
8
+ */
9
+ import { validateWorkspaceName } from "../store/session-id.js";
10
+ import { failJson, readJsonBody, strArrayField, strField, storeFail } from "./common.js";
11
+ /** Busy guard: renaming a workspace that hosts the ACTIVE session re-composes. */
12
+ function activeInWorkspace(deps, name) {
13
+ const active = deps.workspaces.activeSession();
14
+ return active !== null && active.split("/")[0] === name;
15
+ }
16
+ function registerList(app, deps, table) {
17
+ const route = table.get("get_workspaces");
18
+ app.on(route.method, route.honoPath, (c) => c.json(deps.workspaces.view()));
19
+ return route.id;
20
+ }
21
+ function registerCreate(app, deps, table) {
22
+ const route = table.get("post_workspaces");
23
+ app.on(route.method, route.honoPath, async (c) => {
24
+ const read = await readJsonBody(c);
25
+ if (!read.ok)
26
+ return read.response;
27
+ const path = strField(c, read.body, "path");
28
+ if (!path.ok)
29
+ return path.response;
30
+ const res = deps.workspaces.register(path.value ?? "");
31
+ if (!res.ok)
32
+ return storeFail(c, res);
33
+ return c.json({ ok: true, name: res.value });
34
+ });
35
+ return route.id;
36
+ }
37
+ function registerRename(app, deps, table) {
38
+ const route = table.get("post_workspace_rename");
39
+ app.on(route.method, route.honoPath, async (c) => {
40
+ const name = c.req.param("name") ?? "";
41
+ if (activeInWorkspace(deps, name) && deps.runtime.isBusy()) {
42
+ return failJson(c, 409, "turn in progress; rename applies between turns");
43
+ }
44
+ const read = await readJsonBody(c);
45
+ if (!read.ok)
46
+ return read.response;
47
+ const newName = strField(c, read.body, "new_name");
48
+ if (!newName.ok)
49
+ return newName.response;
50
+ const checked = validateWorkspaceName(newName.value ?? "");
51
+ if (!checked.ok)
52
+ return failJson(c, 400, checked.error);
53
+ const res = deps.workspaces.renameWorkspace(name, checked.name);
54
+ if (!res.ok)
55
+ return storeFail(c, res);
56
+ return c.json(deps.workspaces.view());
57
+ });
58
+ return route.id;
59
+ }
60
+ function registerDelete(app, deps, table) {
61
+ const route = table.get("post_workspace_delete");
62
+ app.on(route.method, route.honoPath, (c) => {
63
+ const res = deps.workspaces.deregister(c.req.param("name") ?? "");
64
+ if (!res.ok)
65
+ return storeFail(c, res);
66
+ return c.json({ ok: true });
67
+ });
68
+ return route.id;
69
+ }
70
+ function registerBatchDelete(app, deps, table) {
71
+ const route = table.get("post_workspaces_batch_delete");
72
+ app.on(route.method, route.honoPath, async (c) => {
73
+ const read = await readJsonBody(c);
74
+ if (!read.ok)
75
+ return read.response;
76
+ const names = strArrayField(c, read.body, "names");
77
+ if (!names.ok)
78
+ return names.response;
79
+ if (names.value === undefined)
80
+ return failJson(c, 422, "field 'names' must be an array of strings");
81
+ const out = deps.workspaces.batchDelete(names.value);
82
+ return c.json({ ok: true, deleted: out.deleted, failed: out.failed });
83
+ });
84
+ return route.id;
85
+ }
86
+ export function registerWorkspaces(app, deps, table) {
87
+ return [registerList(app, deps, table), registerCreate(app, deps, table), registerRename(app, deps, table), registerDelete(app, deps, table), registerBatchDelete(app, deps, table)];
88
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Shared test harness for the Studio HTTP layer.
3
+ *
4
+ * Every test gets a THROWAWAY data directory (workspaces/providers/prompts +
5
+ * one registered workspace holding one session) and a throwaway static root, so
6
+ * no test can read or write a production data file, and the fake runtime adapter
7
+ * keeps the engine seam deterministic.
8
+ */
9
+ import type { Hono } from "hono";
10
+ import { type StudioApp, type StudioAppOptions } from "./app.js";
11
+ import { type FakeRuntimeAdapter } from "./fake-runtime-adapter.js";
12
+ import { type StudioPaths } from "./config.js";
13
+ import type { EngineFactory } from "./plugins.js";
14
+ import type { RuntimeAdapter } from "./runtime-adapter.js";
15
+ /**
16
+ * A fake adapter that always reports ITS BUSY SLOT as taken: every session is
17
+ * busy, so the process-wide guards (config / prompts / providers) 409 while
18
+ * `/api/turn` takes the interjection path and activate reports `busy:true`.
19
+ * A `Proxy` is used because spreading a class instance would drop its methods.
20
+ */
21
+ export declare function busyRuntime(base?: FakeRuntimeAdapter): FakeRuntimeAdapter;
22
+ export declare const FIXED_NOW = 1700000000000;
23
+ export declare const FIXED_TS = "1700000000.0";
24
+ export interface StudioHarness {
25
+ app: Hono;
26
+ studio: StudioApp;
27
+ runtime: RuntimeAdapter;
28
+ /** Throwaway root of all data files + the static build. */
29
+ root: string;
30
+ /** Registered workspace folder (empty by default). */
31
+ workspace: string;
32
+ staticRoot: string;
33
+ cleanup(): void;
34
+ }
35
+ export interface HarnessOptions extends Omit<StudioAppOptions, "runtime"> {
36
+ /** Tests inject a concrete adapter (the fake by default). */
37
+ runtime?: RuntimeAdapter;
38
+ /**
39
+ * Real-engine path: a factory over the composed stores (the adapter it builds
40
+ * is `studio.services.runtime`, which is what [StudioHarness.runtime] exposes).
41
+ */
42
+ engineFactory?: EngineFactory;
43
+ /** Files planted before the app composes (e.g. a providers.json secret). */
44
+ files?: Record<string, unknown>;
45
+ /**
46
+ * W787: files planted VERBATIM (a TSV worker table, a JSONL log) — unlike
47
+ * `files`, the value is written as-is instead of JSON-encoded.
48
+ */
49
+ rawFiles?: Record<string, string>;
50
+ /**
51
+ * Path overrides merged over the throwaway root (W767: the auth password file
52
+ * and secret file, which must point at a test-controlled location).
53
+ */
54
+ paths?: Partial<StudioPaths>;
55
+ /** Create a session dir in the workspace holding `log` lines. */
56
+ session?: {
57
+ name: string;
58
+ log?: string;
59
+ meta?: Record<string, string>;
60
+ };
61
+ }
62
+ export declare function makeHarness(opts?: HarnessOptions): StudioHarness;
63
+ /**
64
+ * W864: the built-in `full-access` preset now also opens every path
65
+ * (`allPaths`), so a test that wants to observe the PATH/grants boundary must
66
+ * pin a baseline that is path-limited. This plants exactly that — network on,
67
+ * workspace writable, `allPaths` off — as a custom preset in <data dir> and
68
+ * selects it for the session, through the same two files the engine reads
69
+ * (`store/permissions.ts`, `runtime/engine-permissions.ts`).
70
+ */
71
+ export declare const PATH_ONLY_PRESET: {
72
+ id: string;
73
+ label: string;
74
+ network: boolean;
75
+ workspaceWritable: boolean;
76
+ toolRootsWritable: boolean;
77
+ writeRoots: never[];
78
+ allPaths: boolean;
79
+ unsandboxed: boolean;
80
+ toolDeny: never[];
81
+ };
82
+ export declare function pinPathOnly(h: StudioHarness, name?: string): void;
83
+ export declare function jsonRequest(method: string, body?: unknown): RequestInit;
84
+ /**
85
+ * W516 grant helpers: a grant needs a one-shot token from the same-origin
86
+ * token endpoint, so every test drives the same two-step handshake the UI does.
87
+ */
88
+ export declare function grantToken(h: StudioHarness, id: string, cap: string, scope: Record<string, unknown>): Promise<string>;
89
+ /** POST a grant (token minted automatically unless one is passed / `null`). */
90
+ export declare function grant(h: StudioHarness, id: string, body: Record<string, unknown>, token?: string | null): Promise<{
91
+ status: number;
92
+ body: Record<string, unknown>;
93
+ }>;
94
+ /** The local (authoritative) grants audit channel of a harness data dir. */
95
+ export declare function auditLines(h: StudioHarness): Array<Record<string, unknown>>;
96
+ export declare function getJson(app: Hono, path: string, init?: RequestInit): Promise<{
97
+ status: number;
98
+ body: Record<string, unknown>;
99
+ }>;
100
+ /** W885: file permission bits are meaningful (false on Windows; W883 E10). */
101
+ export declare const FILE_MODES_MEANINGFUL: boolean;