@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,26 @@
1
+ /**
2
+ * GET/POST `/api/config` — `src/api.rs:56-70,190-372`.
3
+ *
4
+ * POST is a two-phase apply: the HOST validates (model charset/length, url,
5
+ * numeric caps, effort availability) and then hands an accepted patch to the
6
+ * engine seam (`RuntimeAdapter.configure`). The api_key takes a third path: it
7
+ * goes only into `process.env[api_key_env]` — never into a response, a store or
8
+ * a log line.
9
+ */
10
+ import type { Hono } from "hono";
11
+ import type { RouteTable } from "../routes.js";
12
+ import { type Deps } from "./common.js";
13
+ import type { RuntimeAdapter } from "../runtime-adapter.js";
14
+ /**
15
+ * W742 §1: is any live instance still driving UNSETTLED workers? Both endpoints
16
+ * that swap the engine profile (`POST /api/config` here and `POST
17
+ * /api/providers/default`) must refuse while background work is in flight,
18
+ * because recomposing an instance disposes the old one — which aborts its workers
19
+ * and drops their registry rows, i.e. a silent kill the caller never sees.
20
+ *
21
+ * This is the UP-FRONT half of the fix; the registry's `rebuildDeferred` is the
22
+ * second half, because a worker can also appear between this check and the bump
23
+ * (a turn in another session may spawn one).
24
+ */
25
+ export declare function workersInFlight(runtime: RuntimeAdapter): boolean;
26
+ export declare function registerConfig(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,161 @@
1
+ /**
2
+ * GET/POST `/api/config` — `src/api.rs:56-70,190-372`.
3
+ *
4
+ * POST is a two-phase apply: the HOST validates (model charset/length, url,
5
+ * numeric caps, effort availability) and then hands an accepted patch to the
6
+ * engine seam (`RuntimeAdapter.configure`). The api_key takes a third path: it
7
+ * goes only into `process.env[api_key_env]` — never into a response, a store or
8
+ * a log line.
9
+ */
10
+ import { EngineError } from "../runtime-adapter.js";
11
+ import { configView } from "./config-shape.js";
12
+ import { failJson, numField, readJsonBody, strField } from "./common.js";
13
+ import { validateModelName } from "../store/validate.js";
14
+ import { MIN_STEPS } from "../config.js";
15
+ const U32_MAX = 4_294_967_295;
16
+ /**
17
+ * W742 §1: is any live instance still driving UNSETTLED workers? Both endpoints
18
+ * that swap the engine profile (`POST /api/config` here and `POST
19
+ * /api/providers/default`) must refuse while background work is in flight,
20
+ * because recomposing an instance disposes the old one — which aborts its workers
21
+ * and drops their registry rows, i.e. a silent kill the caller never sees.
22
+ *
23
+ * This is the UP-FRONT half of the fix; the registry's `rebuildDeferred` is the
24
+ * second half, because a worker can also appear between this check and the bump
25
+ * (a turn in another session may spawn one).
26
+ */
27
+ export function workersInFlight(runtime) {
28
+ return runtime.workerSessions().some((row) => row.status === "RUNNING");
29
+ }
30
+ /** `reasoning_effort` handling: ""/"off" clears; free strings pass through. */
31
+ function effortPatch(patch, raw) {
32
+ if (raw === undefined)
33
+ return;
34
+ const v = raw.trim();
35
+ patch.reasoning_effort = v === "" || v.toLowerCase() === "off" ? null : v;
36
+ }
37
+ /** Validate + fold the optional numeric fields into the patch. */
38
+ function numericPatch(c, body, patch) {
39
+ const maxOut = numField(c, body, "max_output_tokens");
40
+ if (!maxOut.ok)
41
+ return maxOut.response;
42
+ if (maxOut.value !== undefined) {
43
+ if (maxOut.value > U32_MAX)
44
+ return failJson(c, 400, "max_output_tokens must be <= u32::MAX");
45
+ patch.max_output_tokens = maxOut.value === 0 ? null : Math.trunc(maxOut.value);
46
+ }
47
+ const ctxWindow = numField(c, body, "context_window");
48
+ if (!ctxWindow.ok)
49
+ return ctxWindow.response;
50
+ if (ctxWindow.value !== undefined)
51
+ patch.context_window = Math.trunc(ctxWindow.value);
52
+ const steps = numField(c, body, "max_steps");
53
+ if (!steps.ok)
54
+ return steps.response;
55
+ if (steps.value !== undefined) {
56
+ if (steps.value === 0)
57
+ return failJson(c, 400, "max_steps must be >= 1");
58
+ patch.max_steps = Math.max(Math.trunc(steps.value), MIN_STEPS);
59
+ }
60
+ return null;
61
+ }
62
+ export function registerConfig(app, deps, table) {
63
+ const get = table.get("get_config");
64
+ app.on(get.method, get.honoPath, (c) => c.json(configView(deps)));
65
+ const post = table.get("post_config");
66
+ app.on(post.method, post.honoPath, async (c) => {
67
+ if (deps.runtime.isBusy())
68
+ return failJson(c, 409, "turn in progress; config applies between turns");
69
+ if (workersInFlight(deps.runtime))
70
+ return failJson(c, 409, "a worker is running; config applies between turns");
71
+ const read = await readJsonBody(c);
72
+ if (!read.ok)
73
+ return read.response;
74
+ const body = read.body;
75
+ const model = strField(c, body, "model");
76
+ if (!model.ok)
77
+ return model.response;
78
+ const effort = strField(c, body, "reasoning_effort");
79
+ if (!effort.ok)
80
+ return effort.response;
81
+ const baseUrl = strField(c, body, "base_url");
82
+ if (!baseUrl.ok)
83
+ return baseUrl.response;
84
+ const apiKey = strField(c, body, "api_key");
85
+ if (!apiKey.ok)
86
+ return apiKey.response;
87
+ const systemPrompt = strField(c, body, "system_prompt");
88
+ if (!systemPrompt.ok)
89
+ return systemPrompt.response;
90
+ const patch = {};
91
+ const askedModel = (model.value ?? "").trim();
92
+ if (askedModel !== "") {
93
+ const bad = validateModelName(askedModel);
94
+ if (bad !== null)
95
+ return failJson(c, 400, bad);
96
+ patch.model = askedModel;
97
+ }
98
+ effortPatch(patch, effort.value);
99
+ if (patch.reasoning_effort != null && !isReasoningCapable(deps, patch.model ?? deps.runtime.profile().model)) {
100
+ return failJson(c, 400, `model '${patch.model ?? deps.runtime.profile().model}' is not a reasoning model; reasoning_effort is unavailable`);
101
+ }
102
+ if (baseUrl.value !== undefined) {
103
+ if (baseUrl.value !== "" && !isHttp(baseUrl.value))
104
+ return failJson(c, 400, "base_url must be an http:// or https:// URL");
105
+ if (baseUrl.value !== "")
106
+ patch.base_url = baseUrl.value;
107
+ }
108
+ const numericFailure = numericPatch(c, body, patch);
109
+ if (numericFailure !== null)
110
+ return numericFailure;
111
+ const nextSystemPrompt = systemPrompt.value === undefined ? undefined : systemPrompt.value.trim() === "" ? null : systemPrompt.value;
112
+ if (nextSystemPrompt !== undefined)
113
+ patch.system_prompt = nextSystemPrompt ?? "";
114
+ if (apiKey.value !== undefined && apiKey.value !== "") {
115
+ process.env[deps.config.apiKeyEnv] = apiKey.value;
116
+ }
117
+ // W815-3 + N2: validate EVERYTHING first, then commit the two host-side
118
+ // overrides and the engine patch together inside the shared hot-apply queue.
119
+ // A rejected configure restores the previous overrides (W815-3: base_url may
120
+ // no longer stick while the patch that carried it was refused) and two
121
+ // concurrent writers can no longer roll each other back.
122
+ return deps.applyQueue.run(async () => {
123
+ const previousBaseUrl = deps.settings.baseUrlOverride();
124
+ const previousSystemPrompt = deps.settings.systemPromptOverride();
125
+ if (baseUrl.value !== undefined)
126
+ deps.settings.setBaseUrlOverride(baseUrl.value);
127
+ if (nextSystemPrompt !== undefined)
128
+ deps.settings.setSystemPromptOverride(nextSystemPrompt ?? "");
129
+ try {
130
+ await deps.runtime.configure(patch);
131
+ }
132
+ catch (e) {
133
+ if (baseUrl.value !== undefined)
134
+ deps.settings.setBaseUrlOverride(previousBaseUrl ?? "");
135
+ if (nextSystemPrompt !== undefined)
136
+ deps.settings.setSystemPromptOverride(previousSystemPrompt ?? "");
137
+ const message = e instanceof EngineError ? e.message : String(e);
138
+ return failJson(c, 500, `compose failed: ${message}`);
139
+ }
140
+ return c.json(configView(deps));
141
+ });
142
+ });
143
+ return [get.id, post.id];
144
+ }
145
+ function isHttp(url) {
146
+ return url.startsWith("http://") || url.startsWith("https://");
147
+ }
148
+ /**
149
+ * Reasoning availability: a model the providers store lists WITHOUT any
150
+ * reasoning effort is not reasoning-capable; an unknown id is (the contract's
151
+ * "custom endpoint friendly" rule, `src/main.rs:135-137`).
152
+ */
153
+ function isReasoningCapable(deps, model) {
154
+ for (const p of deps.providers.rows()) {
155
+ for (const m of p.models) {
156
+ if (m.id === model)
157
+ return m.reasoning_efforts.length > 0;
158
+ }
159
+ }
160
+ return true;
161
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `GET /api/sessions/{id}/context` response shaping (W725).
3
+ *
4
+ * The only transformation applied here is the wire-size guard the contract
5
+ * freezes: a single entry longer than [MAX_ENTRY_CHARS] is cut and MARKED, and
6
+ * the top-level `truncated` reports whether anything was cut — a silently
7
+ * shortened prompt would make the viewer lie about what the model saw.
8
+ *
9
+ * `context` reuses the statusline's existing口径 (`context_usage`): the REAL
10
+ * prompt-token count once a usage frame has been seen (`estimated:false`), else
11
+ * the session log's character estimate (`estimated:true`). There is no second
12
+ * accounting path for this endpoint.
13
+ */
14
+ import type { SessionContextView } from "../runtime-adapter.js";
15
+ /** Per-entry character cap (contract): one entry never travels longer. */
16
+ export declare const MAX_ENTRY_CHARS = 20000;
17
+ /** The four fields of the statusline's `context_usage` this endpoint reports. */
18
+ export interface ContextUsage {
19
+ used: number;
20
+ window: number;
21
+ ratio: number;
22
+ estimated: boolean;
23
+ }
24
+ export interface ContextPayloadInput {
25
+ session: string;
26
+ view: SessionContextView;
27
+ usage: ContextUsage;
28
+ }
29
+ /**
30
+ * The frozen 200 body. `counts` describes the payload as SENT (post-cut), so a
31
+ * client can reconcile every number with what it received.
32
+ */
33
+ export declare function contextPayload(input: ContextPayloadInput): Record<string, unknown>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * `GET /api/sessions/{id}/context` response shaping (W725).
3
+ *
4
+ * The only transformation applied here is the wire-size guard the contract
5
+ * freezes: a single entry longer than [MAX_ENTRY_CHARS] is cut and MARKED, and
6
+ * the top-level `truncated` reports whether anything was cut — a silently
7
+ * shortened prompt would make the viewer lie about what the model saw.
8
+ *
9
+ * `context` reuses the statusline's existing口径 (`context_usage`): the REAL
10
+ * prompt-token count once a usage frame has been seen (`estimated:false`), else
11
+ * the session log's character estimate (`estimated:true`). There is no second
12
+ * accounting path for this endpoint.
13
+ */
14
+ /** Per-entry character cap (contract): one entry never travels longer. */
15
+ export const MAX_ENTRY_CHARS = 20_000;
16
+ /** Cut one entry to the cap; `truncated` says whether anything was dropped. */
17
+ function cut(text) {
18
+ if (text.length <= MAX_ENTRY_CHARS)
19
+ return { text, truncated: false };
20
+ return { text: text.slice(0, MAX_ENTRY_CHARS), truncated: true };
21
+ }
22
+ function messageJson(message) {
23
+ const body = cut(message.content);
24
+ return {
25
+ role: message.role,
26
+ content: body.text,
27
+ ...(message.tool_name === undefined ? {} : { tool_name: message.tool_name }),
28
+ ...(message.tool_call_id === undefined ? {} : { tool_call_id: message.tool_call_id }),
29
+ ...(body.truncated ? { truncated: true } : {}),
30
+ };
31
+ }
32
+ /**
33
+ * The frozen 200 body. `counts` describes the payload as SENT (post-cut), so a
34
+ * client can reconcile every number with what it received.
35
+ */
36
+ export function contextPayload(input) {
37
+ const system = cut(input.view.system);
38
+ const messages = input.view.messages.map(messageJson);
39
+ return {
40
+ ok: true,
41
+ session: input.session,
42
+ model: input.view.model,
43
+ system: system.text,
44
+ tools: input.view.tools.map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters })),
45
+ messages,
46
+ counts: {
47
+ system_chars: system.text.length,
48
+ tool_count: input.view.tools.length,
49
+ message_count: messages.length,
50
+ },
51
+ context: {
52
+ used: input.usage.used,
53
+ window: input.usage.window,
54
+ ratio: input.usage.ratio,
55
+ estimated: input.usage.estimated,
56
+ },
57
+ truncated: system.truncated || messages.some((message) => message["truncated"] === true),
58
+ };
59
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Dialog endpoints — `GET /api/events` (SSE), `POST /api/turn`,
3
+ * `POST /api/cancel`, `POST /api/clear`.
4
+ *
5
+ * W513 session-scoped behaviour:
6
+ * - `/api/turn` targets `{session}` (default = active session) and NEVER 409s
7
+ * because of a busy session: a running turn takes the input as an
8
+ * interjection injected at its next step boundary, and the response says so
9
+ * (`{ok:true, injected:true, turn}`). A 409 survives only for the atomic
10
+ * re-check race, and only for the target session;
11
+ * - `/api/cancel` and `/api/clear` take the same optional `{session}`;
12
+ * - `/api/events` streams every session by default (the envelope carries
13
+ * `session`), and `?session=<id>` (repeatable) narrows the server side.
14
+ *
15
+ * `/api/turn` returns immediately; everything else travels over SSE. The SSE
16
+ * stream never closes on overflow: a slow subscriber gets ONE `status:lagged`
17
+ * frame (with the session and the dropped count) and keeps consuming.
18
+ */
19
+ import type { Hono } from "hono";
20
+ import type { RouteTable } from "../routes.js";
21
+ import { type Deps } from "./common.js";
22
+ export declare function registerDialog(app: Hono, deps: Deps, table: RouteTable): string[];
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Dialog endpoints — `GET /api/events` (SSE), `POST /api/turn`,
3
+ * `POST /api/cancel`, `POST /api/clear`.
4
+ *
5
+ * W513 session-scoped behaviour:
6
+ * - `/api/turn` targets `{session}` (default = active session) and NEVER 409s
7
+ * because of a busy session: a running turn takes the input as an
8
+ * interjection injected at its next step boundary, and the response says so
9
+ * (`{ok:true, injected:true, turn}`). A 409 survives only for the atomic
10
+ * re-check race, and only for the target session;
11
+ * - `/api/cancel` and `/api/clear` take the same optional `{session}`;
12
+ * - `/api/events` streams every session by default (the envelope carries
13
+ * `session`), and `?session=<id>` (repeatable) narrows the server side.
14
+ *
15
+ * `/api/turn` returns immediately; everything else travels over SSE. The SSE
16
+ * stream never closes on overflow: a slow subscriber gets ONE `status:lagged`
17
+ * frame (with the session and the dropped count) and keeps consuming.
18
+ */
19
+ import { join } from "node:path";
20
+ import { streamSSE } from "hono/streaming";
21
+ import { TurnBusyError } from "@celestea/runtime";
22
+ import { ATTACHMENTS_DIRNAME, createAttachmentStore } from "@celestea/tools";
23
+ import { CapacityError } from "../runtime-adapter.js";
24
+ import { activeSession, capacityJson, errorOnly, failJson, readJsonBody, storeFail, strField } from "./common.js";
25
+ function registerEvents(app, deps, table) {
26
+ const events = table.get("get_events");
27
+ app.on(events.method, events.honoPath, (c) => {
28
+ const asked = c.req.queries("session") ?? [];
29
+ const response = streamSSE(c, async (stream) => {
30
+ const sub = deps.bus.subscribe(asked.length === 0 ? {} : { sessions: asked });
31
+ stream.onAbort(() => sub.close());
32
+ try {
33
+ for (;;) {
34
+ const frame = await sub.next();
35
+ if (frame === null)
36
+ break;
37
+ await stream.writeSSE({ event: frame.event, data: JSON.stringify(frame.envelope) });
38
+ }
39
+ }
40
+ finally {
41
+ sub.close();
42
+ }
43
+ });
44
+ response.headers.set("cache-control", "no-cache");
45
+ return response;
46
+ });
47
+ return events.id;
48
+ }
49
+ /** W866: the engine-memory worker session prefix (`worker:<sid>`). */
50
+ const WORKER_SESSION_PREFIX = "worker:";
51
+ /**
52
+ * W866: the inner session id of an engine-memory worker target, else null.
53
+ *
54
+ * A worker conversation lives in its owner session's `WorkerRegistry`, not on
55
+ * disk: `sessions.require("worker:…")` can only 404 it. Recognising the prefix
56
+ * HERE is what lets a USER message take the worker route instead of the
57
+ * filesystem-session route (parity with the DSH-side `worker:<sid>` ids the UI
58
+ * already lists and renders).
59
+ */
60
+ function workerSidOf(asked) {
61
+ if (typeof asked !== "string" || !asked.startsWith(WORKER_SESSION_PREFIX))
62
+ return null;
63
+ const sid = asked.slice(WORKER_SESSION_PREFIX.length);
64
+ return sid === "" ? null : sid;
65
+ }
66
+ /**
67
+ * W866: deliver one USER message to an engine-memory worker session.
68
+ *
69
+ * The delivery itself is the SAME `send_message` tool the model calls (the
70
+ * worker driver serialises mailbox arrivals into its own turns), so the user
71
+ * path and the model path cannot drift. Two deliberate refusals before that:
72
+ * images (the mailbox carries text only) and unknown/settled workers (a message
73
+ * queued for a worker nobody drives would report success and never be processed).
74
+ */
75
+ async function workerTurn(c, deps, sid, text, rawAttachments) {
76
+ if (Array.isArray(rawAttachments) && rawAttachments.length > 0) {
77
+ return failJson(c, 400, "worker sessions accept text only");
78
+ }
79
+ if (text === "")
80
+ return errorOnly(c, 400, "input must not be empty");
81
+ const id = WORKER_SESSION_PREFIX + sid;
82
+ const row = deps.runtime.workerSessions().find((w) => w.id === id);
83
+ if (row === undefined)
84
+ return failJson(c, 404, `unknown session '${id}'`);
85
+ const out = await deps.runtime.workerSend({ target: sid, content: text });
86
+ if (out["ok"] !== true) {
87
+ return failJson(c, 404, String(out["error"] ?? `unknown session '${id}'`));
88
+ }
89
+ // `status`/`state` let the client say whether the worker is still RUNNING
90
+ // (idle/in-turn) or settled — the one fact the delivery envelope does not carry.
91
+ return c.json({
92
+ ok: true,
93
+ delivered: true,
94
+ injected: false,
95
+ pending: 0,
96
+ placement: "context",
97
+ worker: sid,
98
+ status: row.status ?? "",
99
+ state: row.state ?? "",
100
+ });
101
+ }
102
+ /** The turn target: an explicit `{session}`, else the active session. */
103
+ function turnTarget(c, deps, asked) {
104
+ if (asked === undefined || asked === "")
105
+ return { ok: true, value: activeSession(deps) };
106
+ const required = deps.sessions.require(asked);
107
+ if (!required.ok)
108
+ return required;
109
+ return { ok: true, value: required.value.id };
110
+ }
111
+ /** W804: the inline attachments one POST /api/turn may carry (section 7.4 P0). */
112
+ const MAX_TURN_ATTACHMENTS = 20;
113
+ /**
114
+ * Decode the optional inline base64 `attachments` and store them under
115
+ * `<session-dir>/attachments/`. P0 adds NO upload endpoint: the bytes ride the
116
+ * turn body and the session log only ever sees the returned references.
117
+ */
118
+ async function storeTurnAttachments(c, deps, session, raw) {
119
+ if (raw === undefined || raw === null)
120
+ return { ok: true, refs: [] };
121
+ if (!Array.isArray(raw))
122
+ return { ok: false, response: errorOnly(c, 400, "attachments must be an array") };
123
+ if (raw.length === 0)
124
+ return { ok: true, refs: [] };
125
+ if (raw.length > MAX_TURN_ATTACHMENTS) {
126
+ return { ok: false, response: errorOnly(c, 400, `too many attachments (max ${MAX_TURN_ATTACHMENTS})`) };
127
+ }
128
+ const resolved = session === null ? null : deps.sessions.resolve(session);
129
+ const dir = resolved !== null && resolved.ok ? resolved.value.dir : null;
130
+ if (dir === null)
131
+ return { ok: false, response: errorOnly(c, 400, "attachments require a resolvable session directory") };
132
+ const store = createAttachmentStore(join(dir, ATTACHMENTS_DIRNAME));
133
+ const refs = [];
134
+ for (const item of raw) {
135
+ const rec = (typeof item === "object" && item !== null ? item : {});
136
+ const data = rec["data"];
137
+ const name = typeof rec["name"] === "string" ? rec["name"] : undefined;
138
+ if (typeof data !== "string" || data.trim() === "") {
139
+ return { ok: false, response: errorOnly(c, 400, "each attachment needs a non-empty base64 data string") };
140
+ }
141
+ const bytes = Buffer.from(data, "base64");
142
+ if (bytes.length === 0)
143
+ return { ok: false, response: errorOnly(c, 400, "attachment data decoded to zero bytes") };
144
+ try {
145
+ refs.push(await store.put({ bytes, ...(name === undefined ? {} : { name }) }));
146
+ }
147
+ catch (e) {
148
+ const message = e instanceof Error ? e.message : String(e);
149
+ return { ok: false, response: errorOnly(c, 400, `attachment rejected: ${message}`) };
150
+ }
151
+ }
152
+ return { ok: true, refs };
153
+ }
154
+ function registerTurn(app, deps, table) {
155
+ const turn = table.get("post_turn");
156
+ app.on(turn.method, turn.honoPath, async (c) => {
157
+ const read = await readJsonBody(c);
158
+ if (!read.ok)
159
+ return read.response;
160
+ const input = strField(c, read.body, "input");
161
+ const asked = strField(c, read.body, "session");
162
+ for (const field of [input, asked])
163
+ if (!field.ok)
164
+ return field.response;
165
+ // W847: resolve + validate the delivery lane BEFORE anything is written, so
166
+ // an illegal mode never leaves an attachment (or a stored turn) behind. An
167
+ // omitted mode is "steer" — byte-identical to the pre-W847 request.
168
+ const requestedMode = read.body["mode"];
169
+ if (requestedMode !== undefined && requestedMode !== null && requestedMode !== "steer" && requestedMode !== "queue") {
170
+ return failJson(c, 400, 'invalid mode: ' + String(requestedMode) + ' (expected "steer" or "queue")');
171
+ }
172
+ const mode = requestedMode === "queue" ? "queue" : "steer";
173
+ const text = (input.ok ? (input.value ?? "") : "").trim();
174
+ // W866: an engine-memory worker session (`worker:<sid>`) is NOT a filesystem
175
+ // session — `turnTarget` could only 404 it, and `startTurn`/`inject` would
176
+ // compose a ghost runtime for the id (the W833 bug). A user message to a
177
+ // worker takes the worker route before any of that.
178
+ const workerSid = workerSidOf(asked.ok ? asked.value : undefined);
179
+ if (workerSid !== null)
180
+ return workerTurn(c, deps, workerSid, text, read.body["attachments"]);
181
+ const target = turnTarget(c, deps, asked.ok ? asked.value : undefined);
182
+ if (!target.ok)
183
+ return storeFail(c, target);
184
+ const session = target.value;
185
+ // W804: decode + store the optional inline image attachments BEFORE the
186
+ // empty-input check — a turn may legitimately carry images and no text.
187
+ // W815-4: a busy session REFUSES image attachments (steering lanes carry
188
+ // text only), so refuse BEFORE `storeTurnAttachments` writes anything under
189
+ // <session>/attachments/ — the old order wrote up to 20 * 4 MiB and only then
190
+ // 409'd, leaking every byte for a turn that was never started.
191
+ const rawAttachments = read.body["attachments"];
192
+ if (deps.runtime.isBusy(session) && Array.isArray(rawAttachments) && rawAttachments.length > 0) {
193
+ return busyAttachmentError(c);
194
+ }
195
+ const stored = await storeTurnAttachments(c, deps, session, rawAttachments);
196
+ if (!stored.ok)
197
+ return stored.response;
198
+ const attachments = stored.refs;
199
+ if (text === "" && attachments.length === 0)
200
+ return errorOnly(c, 400, "input must not be empty");
201
+ if (deps.runtime.isBusy(session)) {
202
+ // W513 steering lanes carry TEXT only: an image must never be silently
203
+ // dropped, so a busy session refuses rather than pretends.
204
+ if (attachments.length > 0)
205
+ return busyAttachmentError(c);
206
+ return injectInto(c, deps, text, session, mode);
207
+ }
208
+ try {
209
+ const started = await deps.runtime.startTurn({
210
+ input: text,
211
+ session,
212
+ ...(attachments.length === 0 ? {} : { attachments }),
213
+ });
214
+ // W515 §2: the turn's own input IS the context the model sees first.
215
+ return c.json({ turn: started.turn, status: "started", placement: started.placement ?? "context" }, 202);
216
+ }
217
+ catch (e) {
218
+ if (e instanceof TurnBusyError) {
219
+ if (attachments.length > 0)
220
+ return busyAttachmentError(c);
221
+ return injectInto(c, deps, text, session, mode);
222
+ }
223
+ if (e instanceof CapacityError)
224
+ return capacityJson(c, e);
225
+ return failJson(c, 500, e instanceof Error ? e.message : String(e));
226
+ }
227
+ });
228
+ return turn.id;
229
+ }
230
+ /** W804: the running-turn refusal (steering cannot carry an image). */
231
+ function busyAttachmentError(c) {
232
+ return failJson(c, 409, "attachments cannot be injected into a running turn; send them when the turn finishes");
233
+ }
234
+ /**
235
+ * W513/W847: the session is busy -> the input joins a lane. `mode` = "steer"
236
+ * injects into the RUNNING turn at its next step boundary; "queue" parks it for
237
+ * the next turn start. The response shape is unchanged (the caller reads
238
+ * `placement`, not `injected`, to render the terminal state).
239
+ */
240
+ function injectInto(c, deps, text, session, mode) {
241
+ const out = deps.runtime.inject({ input: text, session, mode });
242
+ return c.json({ ok: true, injected: out.injected, turn: out.turn, pending: out.pending, placement: out.placement, duplicate: out.duplicate });
243
+ }
244
+ async function bodySession(c, deps) {
245
+ const read = await readJsonBody(c, false);
246
+ if (!read.ok)
247
+ return { ok: false, response: read.response };
248
+ const asked = strField(c, read.body, "session");
249
+ if (!asked.ok)
250
+ return { ok: false, response: asked.response };
251
+ const target = turnTarget(c, deps, asked.value);
252
+ if (!target.ok)
253
+ return { ok: false, response: storeFail(c, target) };
254
+ return { ok: true, session: target.value };
255
+ }
256
+ function registerCancel(app, deps, table) {
257
+ const cancel = table.get("post_cancel");
258
+ app.on(cancel.method, cancel.honoPath, async (c) => {
259
+ const target = await bodySession(c, deps);
260
+ if (!target.ok)
261
+ return target.response;
262
+ return c.json({ ok: true, cancelled: deps.runtime.cancel(target.session) });
263
+ });
264
+ return cancel.id;
265
+ }
266
+ function registerClear(app, deps, table) {
267
+ const clear = table.get("post_clear");
268
+ app.on(clear.method, clear.honoPath, async (c) => {
269
+ const target = await bodySession(c, deps);
270
+ if (!target.ok)
271
+ return target.response;
272
+ const session = target.session;
273
+ try {
274
+ await deps.runtime.clear(session);
275
+ }
276
+ catch (e) {
277
+ if (e instanceof TurnBusyError)
278
+ return failJson(c, 409, "a turn is already running");
279
+ return failJson(c, 500, e instanceof Error ? e.message : String(e));
280
+ }
281
+ if (session !== null) {
282
+ const resolved = deps.sessions.resolve(session);
283
+ if (resolved.ok)
284
+ deps.sessions.truncate(resolved.value);
285
+ }
286
+ return c.json({ ok: true, cleared: true, session });
287
+ });
288
+ return clear.id;
289
+ }
290
+ export function registerDialog(app, deps, table) {
291
+ return [registerEvents(app, deps, table), registerTurn(app, deps, table), registerCancel(app, deps, table), registerClear(app, deps, table)];
292
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `POST /api/exec` (iteration G2) — run a shell command NOW, with no model in
3
+ * the loop. `/run <cmd>` and `!<cmd>` are the UI's callers.
4
+ *
5
+ * Three rules from `docs/iteration-g-workbench.md` §1:
6
+ * 1. it reuses the run_shell execution path — the SAME `selectSandboxDetailed`
7
+ * policy, `sessionSandboxConfig` (workdir/root), `sanitizedEnv` allowlist
8
+ * and output cap the engine's `run_shell` uses, so isolation cannot fork;
9
+ * 2. it passes the PERMISSION gate: a preset that denies `run_shell`, or a
10
+ * sandbox policy that refuses to execute, is a structured refusal — never a
11
+ * silent run;
12
+ * 3. it is NOT a tool: nothing here touches the model-visible tool face.
13
+ *
14
+ * The `sandbox` block carries ONLY the contract fields (provider / net_isolated
15
+ * / tmp_private / seccomp + optional cpu_sec), never host diagnostics.
16
+ */
17
+ import type { Hono } from "hono";
18
+ import type { RouteTable } from "../routes.js";
19
+ import { type Deps } from "./common.js";
20
+ /** The structured refusal when this session's preset denies the shell. */
21
+ export declare const SHELL_DENIED_CODE = "shell_denied";
22
+ export declare function registerExec(app: Hono, deps: Deps, table: RouteTable): string[];